diff --git a/sui/api/compile_test.go b/sui/api/compile_test.go
new file mode 100644
index 00000000..60f599c4
--- /dev/null
+++ b/sui/api/compile_test.go
@@ -0,0 +1,43 @@
+package api
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/yaoapp/yao/sui/core"
+)
+
+func TestCompile(t *testing.T) {
+ prepare(t)
+ defer clean()
+ loadTestSui(t)
+
+ page := testPage(t)
+ html, err := page.Compile(&core.BuildOption{KeepPageTag: false})
+ if err != nil {
+ t.Fatalf("Compile error: %v", err)
+ }
+ assert.Contains(t, html, `Link2`)
+ assert.Contains(t, html, `Link`)
+ assert.Contains(t, html, "input.data")
+}
+
+func testPage(t *testing.T) *core.Page {
+
+ sui := core.SUIs["demo"]
+ if sui == nil {
+ t.Fatal("SUI demo not found")
+ }
+
+ tmpl, err := sui.GetTemplate("tech-blue")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ page, err := tmpl.Page("/index")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ return page.Get()
+}
diff --git a/sui/api/process_test.go b/sui/api/process_test.go
index fd441249..8602520c 100644
--- a/sui/api/process_test.go
+++ b/sui/api/process_test.go
@@ -251,9 +251,9 @@ func TestPageTree(t *testing.T) {
}
assert.IsType(t, []*core.PageTreeNode{}, res)
- assert.Equal(t, 5, len(res.([]*core.PageTreeNode)))
+ assert.Equal(t, 6, len(res.([]*core.PageTreeNode)))
assert.Equal(t, "error", res.([]*core.PageTreeNode)[0].Name)
- assert.Equal(t, "index", res.([]*core.PageTreeNode)[1].Name)
+ assert.Equal(t, "footer", res.([]*core.PageTreeNode)[1].Name)
}
func TestPageGet(t *testing.T) {
@@ -273,7 +273,7 @@ func TestPageGet(t *testing.T) {
pages := res.([]core.IPage)
assert.IsType(t, []core.IPage{}, pages)
- assert.Equal(t, 8, len(pages))
+ assert.Equal(t, 9, len(pages))
for _, page := range pages {
assert.IsType(t, &local.Page{}, page)
}
diff --git a/sui/api/sui_test.go b/sui/api/sui_test.go
index ff7c8a15..0983b244 100644
--- a/sui/api/sui_test.go
+++ b/sui/api/sui_test.go
@@ -35,6 +35,16 @@ func prepare(t *testing.T) {
test.Prepare(t, config.Conf, "YAO_TEST_BUILDER_APPLICATION")
}
+func loadTestSui(t *testing.T) {
+ prepare(t)
+ defer clean()
+
+ _, err := loadFile("suis/demo.sui.yao", "demo")
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
func clean() {
test.Clean()
}
diff --git a/sui/core/build.go b/sui/core/build.go
index f9740285..2b6af2c0 100644
--- a/sui/core/build.go
+++ b/sui/core/build.go
@@ -1,12 +1,18 @@
package core
import (
+ "bufio"
"fmt"
+ "regexp"
"strings"
"github.com/PuerkitoBio/goquery"
+ "github.com/yaoapp/kun/log"
)
+var slotRe = regexp.MustCompile(`\[\{([^\}]+)\}\]`)
+var cssRe = regexp.MustCompile(`([\.a-z0-9A-Z# ]+)\{`)
+
// Build is the struct for the public
func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error) {
@@ -22,6 +28,12 @@ func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error
warnings = append(warnings, err.Error())
}
+ // Append the nested html
+ err = page.parse(doc, option, warnings)
+ if err != nil {
+ warnings = append(warnings, err.Error())
+ }
+
// Add Style
style, err := page.BuildStyle(option)
if err != nil {
@@ -38,6 +50,138 @@ func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error
return doc, warnings, nil
}
+// BuildForImport build the page for import
+func (page *Page) BuildForImport(option *BuildOption, slots map[string]interface{}) (string, string, string, []string, error) {
+ warnings := []string{}
+ html, err := page.BuildHTML(option)
+ if err != nil {
+ warnings = append(warnings, err.Error())
+ }
+
+ // Add Style & Script & Warning
+ doc, err := NewDocument([]byte(html))
+ if err != nil {
+ warnings = append(warnings, err.Error())
+ }
+
+ // Append the nested html
+ err = page.parse(doc, option, warnings)
+ if err != nil {
+ warnings = append(warnings, err.Error())
+ }
+
+ // Add Style
+ style, err := page.BuildStyle(option)
+ if err != nil {
+ warnings = append(warnings, err.Error())
+ }
+
+ script, err := page.BuildScript(option)
+ if err != nil {
+ warnings = append(warnings, err.Error())
+ }
+
+ body := doc.Selection.Find("body")
+
+ if body.Length() > 1 {
+ body.SetHtml("
" + html + "
")
+ }
+
+ body.Children().First().SetAttr("s:ns", option.Namespace)
+ body.Children().First().SetAttr("s:ready", option.Namespace+"()")
+ html, err = body.Html()
+ if err != nil {
+ return "", "", "", warnings, err
+ }
+
+ // Replace the slots
+ html, _ = Data(slots).ReplaceUse(slotRe, html)
+ return html, style, script, warnings, nil
+}
+
+func (page *Page) parse(doc *goquery.Document, option *BuildOption, warnings []string) error {
+ pages := doc.Find("page")
+ sui := SUIs[page.SuiID]
+ if sui == nil {
+ return fmt.Errorf("SUI %s not found", page.SuiID)
+ }
+
+ tmpl, err := sui.GetTemplate(page.TemplateID)
+ if err != nil {
+ return err
+ }
+
+ for idx, node := range pages.Nodes {
+ sel := goquery.NewDocumentFromNode(node)
+ name, has := sel.Attr("is")
+ if !has {
+ msg := fmt.Sprintf("Page %s/%s/%s: page tag must have an is attribute", page.SuiID, page.TemplateID, page.Route)
+ sel.ReplaceWith(fmt.Sprintf("", msg))
+ log.Warn(msg)
+ continue
+ }
+
+ ipage, err := tmpl.Page(name)
+ if err != nil {
+ sel.ReplaceWith(fmt.Sprintf("", err.Error()))
+ log.Warn("Page %s/%s/%s: %s", page.SuiID, page.TemplateID, page.Route, err.Error())
+ continue
+ }
+
+ err = ipage.Load()
+ if err != nil {
+ sel.ReplaceWith(fmt.Sprintf("", err.Error()))
+ log.Warn("Page %s/%s/%s: %s", page.SuiID, page.TemplateID, page.Route, err.Error())
+ continue
+ }
+
+ slots := map[string]interface{}{}
+ for _, slot := range sel.Find("slot").Nodes {
+ slotSel := goquery.NewDocumentFromNode(slot)
+ slotName, has := slotSel.Attr("is")
+ if !has {
+ continue
+ }
+ slotHTML, err := slotSel.Html()
+ if err != nil {
+ continue
+ }
+ slots[slotName] = strings.TrimSpace(slotHTML)
+ }
+
+ namespace := fmt.Sprintf("__page_%s_%d", strings.ReplaceAll(name, "/", "_"), idx)
+ html, style, script, warns, err := ipage.Get().BuildForImport(&BuildOption{
+ SSR: option.SSR,
+ AssetRoot: option.AssetRoot,
+ IgnoreAssetRoot: option.IgnoreAssetRoot,
+ KeepPageTag: option.KeepPageTag,
+ IgnoreDocument: true,
+ Namespace: namespace,
+ }, slots)
+
+ if err != nil {
+ sel.ReplaceWith(fmt.Sprintf("", err.Error()))
+ log.Warn("Page %s/%s/%s: %s", page.SuiID, page.TemplateID, page.Route, err.Error())
+ continue
+ }
+
+ if warns != nil {
+ warnings = append(warnings, warns...)
+ }
+
+ sel.SetAttr("s:ns", namespace)
+ sel.SetAttr("s:ready", namespace+"()")
+
+ if option.KeepPageTag {
+ sel.SetHtml(fmt.Sprintf("\n%s\n%s\n%s\n", style, addTabToEachLine(html), script))
+ continue
+ }
+ sel.ReplaceWithHtml(fmt.Sprintf("\n%s\n%s\n%s\n", style, html, script))
+
+ }
+ return nil
+}
+
// BuildHTML build the html
func (page *Page) BuildHTML(option *BuildOption) (string, error) {
@@ -73,6 +217,12 @@ func (page *Page) BuildStyle(option *BuildOption) (string, error) {
code = strings.ReplaceAll(page.Codes.CSS.Code, "@assets", option.AssetRoot)
}
+ if option.Namespace != "" {
+ code = cssRe.ReplaceAllStringFunc(code, func(css string) string {
+ return fmt.Sprintf("[s:ns=%s] %s", option.Namespace, css)
+ })
+ }
+
res, err := page.CompileCSS([]byte(code), false)
if err != nil {
return "", err
@@ -94,7 +244,11 @@ func (page *Page) BuildScript(option *BuildOption) (string, error) {
return "", err
}
- return fmt.Sprintf("\n", res), nil
+ if option.Namespace == "" {
+ return fmt.Sprintf("\n", res), nil
+ }
+
+ return fmt.Sprintf("\n", option.Namespace, addTabToEachLine(string(res))), nil
}
code := page.Codes.JS.Code
@@ -107,5 +261,26 @@ func (page *Page) BuildScript(option *BuildOption) (string, error) {
return "", err
}
- return fmt.Sprintf("\n", res), nil
+ if option.Namespace == "" {
+ return fmt.Sprintf("\n", res), nil
+ }
+ return fmt.Sprintf("\n", option.Namespace, addTabToEachLine(string(res))), nil
+}
+
+func addTabToEachLine(input string, prefix ...string) string {
+ var lines []string
+
+ space := " "
+ if len(prefix) > 0 {
+ space = prefix[0]
+ }
+
+ scanner := bufio.NewScanner(strings.NewReader(input))
+ for scanner.Scan() {
+ line := scanner.Text()
+ lineWithTab := space + line
+ lines = append(lines, lineWithTab)
+ }
+
+ return strings.Join(lines, "\n")
}
diff --git a/sui/core/compile_test.go b/sui/core/compile_test.go
deleted file mode 100644
index d87e3d14..00000000
--- a/sui/core/compile_test.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package core
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestCompile(t *testing.T) {
- prepare(t)
- defer clean()
-
- page := testPage(t)
- html, err := page.Compile(&BuildOption{})
- if err != nil {
- t.Fatalf("Compile error: %v", err)
- }
-
- assert.Contains(t, html, "input.data")
-}
diff --git a/sui/core/data.go b/sui/core/data.go
index b9413694..e94359d8 100644
--- a/sui/core/data.go
+++ b/sui/core/data.go
@@ -27,6 +27,7 @@ var options = []expr.Option{
// New create a new expression
func (data Data) New(stmt string) (*vm.Program, error) {
stmt = strings.TrimSpace(strings.TrimRight(strings.TrimLeft(stmt, "{{ "), "}}"))
+ stmt = strings.TrimSpace(strings.TrimRight(strings.TrimLeft(stmt, "[{ "), "}]"))
return expr.Compile(stmt, append([]expr.Option{expr.Env(data)}, options...)...)
}
@@ -71,6 +72,20 @@ func (data Data) Replace(value string) (string, bool) {
return res, hasStmt
}
+// ReplaceUse replace the statement use the regexp
+func (data Data) ReplaceUse(re *regexp.Regexp, value string) (string, bool) {
+ hasStmt := false
+ res := re.ReplaceAllStringFunc(value, func(stmt string) string {
+ hasStmt = true
+ res, err := data.ExecString(stmt)
+ if err != nil {
+ log.Warn("Replace %s: %s", stmt, err)
+ }
+ return res
+ })
+ return res, hasStmt
+}
+
func _process(args ...any) (interface{}, error) {
if len(args) < 1 {
diff --git a/sui/core/page_test.go b/sui/core/page_test.go
index ad7b9f76..38e124a7 100644
--- a/sui/core/page_test.go
+++ b/sui/core/page_test.go
@@ -120,6 +120,18 @@ func testPage(t *testing.T) *Page {
class="text-blue-600 p-2" />
+
+ Import Page
+
+ Link
+ Item
+
+
+
+ Link2
+ Item2
+
+
`,
},
DATA: Source{
diff --git a/sui/core/types.go b/sui/core/types.go
index 4c396548..b5fd56cc 100644
--- a/sui/core/types.go
+++ b/sui/core/types.go
@@ -141,6 +141,8 @@ type BuildOption struct {
AssetRoot string `json:"asset_root,omitempty"`
IgnoreAssetRoot bool `json:"ignore_asset_root,omitempty"`
IgnoreDocument bool `json:"ignore_document,omitempty"`
+ KeepPageTag bool `json:"keep_page_tag,omitempty"`
+ Namespace string `json:"namespace,omitempty"`
}
// Request is the struct for the request
diff --git a/sui/storages/local/page_test.go b/sui/storages/local/page_test.go
index 92799101..9c82706a 100644
--- a/sui/storages/local/page_test.go
+++ b/sui/storages/local/page_test.go
@@ -60,21 +60,21 @@ func TestTemplatePageTree(t *testing.T) {
t.Fatalf("Pages error: %v", err)
}
- assert.Equal(t, 5, len(pages))
+ assert.Equal(t, 6, len(pages))
assert.Equal(t, "error", pages[0].Name)
assert.Equal(t, true, pages[0].IsDir)
assert.Equal(t, "error", pages[0].Children[0].Name)
assert.Equal(t, "/error", pages[0].Children[0].IPage.(*Page).Route)
assert.Equal(t, "error", pages[0].Children[0].IPage.(*Page).Name)
- assert.Equal(t, "index", pages[1].Name)
+ assert.Equal(t, "index", pages[2].Name)
assert.Equal(t, true, pages[1].IsDir)
- assert.Equal(t, "[invite]", pages[1].Children[0].Name)
- assert.Equal(t, true, pages[1].Children[0].IsDir)
- assert.Equal(t, "/index/[invite]", pages[1].Children[0].Children[0].IPage.(*Page).Route)
- assert.Equal(t, "[invite]", pages[1].Children[0].Children[0].IPage.(*Page).Name)
- assert.Equal(t, "/index", pages[1].Children[1].IPage.(*Page).Route)
- assert.Equal(t, "index", pages[1].Children[1].IPage.(*Page).Name)
+ assert.Equal(t, "[invite]", pages[2].Children[0].Name)
+ assert.Equal(t, true, pages[2].Children[0].IsDir)
+ assert.Equal(t, "/index/[invite]", pages[2].Children[0].Children[0].IPage.(*Page).Route)
+ assert.Equal(t, "[invite]", pages[2].Children[0].Children[0].IPage.(*Page).Name)
+ assert.Equal(t, "/index", pages[2].Children[1].IPage.(*Page).Route)
+ assert.Equal(t, "index", pages[2].Children[1].IPage.(*Page).Name)
}