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) }