Merge pull request #512 from trheyi/main
[add] sui import page support (dev)
This commit is contained in:
commit
7202f108d0
9 changed files with 270 additions and 33 deletions
43
sui/api/compile_test.go
Normal file
43
sui/api/compile_test.go
Normal file
|
|
@ -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, `<a href="Link2">Link2</a>`)
|
||||||
|
assert.Contains(t, html, `<a href="Link">Link</a>`)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
@ -251,9 +251,9 @@ func TestPageTree(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.IsType(t, []*core.PageTreeNode{}, res)
|
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, "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) {
|
func TestPageGet(t *testing.T) {
|
||||||
|
|
@ -273,7 +273,7 @@ func TestPageGet(t *testing.T) {
|
||||||
|
|
||||||
pages := res.([]core.IPage)
|
pages := res.([]core.IPage)
|
||||||
assert.IsType(t, []core.IPage{}, pages)
|
assert.IsType(t, []core.IPage{}, pages)
|
||||||
assert.Equal(t, 8, len(pages))
|
assert.Equal(t, 9, len(pages))
|
||||||
for _, page := range pages {
|
for _, page := range pages {
|
||||||
assert.IsType(t, &local.Page{}, page)
|
assert.IsType(t, &local.Page{}, page)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,16 @@ func prepare(t *testing.T) {
|
||||||
test.Prepare(t, config.Conf, "YAO_TEST_BUILDER_APPLICATION")
|
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() {
|
func clean() {
|
||||||
test.Clean()
|
test.Clean()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"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
|
// Build is the struct for the public
|
||||||
func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error) {
|
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())
|
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
|
// Add Style
|
||||||
style, err := page.BuildStyle(option)
|
style, err := page.BuildStyle(option)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -38,6 +50,138 @@ func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error
|
||||||
return doc, warnings, nil
|
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("<div>" + html + "</div>")
|
||||||
|
}
|
||||||
|
|
||||||
|
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("<!-- %s -->", msg))
|
||||||
|
log.Warn(msg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ipage, err := tmpl.Page(name)
|
||||||
|
if err != nil {
|
||||||
|
sel.ReplaceWith(fmt.Sprintf("<!-- %s -->", 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("<!-- %s -->", 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("<!-- %s -->", 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
|
// BuildHTML build the html
|
||||||
func (page *Page) BuildHTML(option *BuildOption) (string, error) {
|
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)
|
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)
|
res, err := page.CompileCSS([]byte(code), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
@ -94,9 +244,13 @@ func (page *Page) BuildScript(option *BuildOption) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if option.Namespace == "" {
|
||||||
return fmt.Sprintf("<script>\n%s\n</script>\n", res), nil
|
return fmt.Sprintf("<script>\n%s\n</script>\n", res), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("<script>\nfunction %s(){\n%s\n}\n</script>\n", option.Namespace, addTabToEachLine(string(res))), nil
|
||||||
|
}
|
||||||
|
|
||||||
code := page.Codes.JS.Code
|
code := page.Codes.JS.Code
|
||||||
if !option.IgnoreAssetRoot {
|
if !option.IgnoreAssetRoot {
|
||||||
code = strings.ReplaceAll(page.Codes.JS.Code, "@assets", option.AssetRoot)
|
code = strings.ReplaceAll(page.Codes.JS.Code, "@assets", option.AssetRoot)
|
||||||
|
|
@ -107,5 +261,26 @@ func (page *Page) BuildScript(option *BuildOption) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if option.Namespace == "" {
|
||||||
return fmt.Sprintf("<script>\n%s\n</script>\n", res), nil
|
return fmt.Sprintf("<script>\n%s\n</script>\n", res), nil
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("<script>\nfunction %s(){\n%s\n}\n</script>\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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
|
|
@ -27,6 +27,7 @@ var options = []expr.Option{
|
||||||
// New create a new expression
|
// New create a new expression
|
||||||
func (data Data) New(stmt string) (*vm.Program, error) {
|
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, "{{ "), "}}"))
|
||||||
|
stmt = strings.TrimSpace(strings.TrimRight(strings.TrimLeft(stmt, "[{ "), "}]"))
|
||||||
return expr.Compile(stmt, append([]expr.Option{expr.Env(data)}, options...)...)
|
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
|
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) {
|
func _process(args ...any) (interface{}, error) {
|
||||||
|
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,18 @@ func testPage(t *testing.T) *Page {
|
||||||
class="text-blue-600 p-2" />
|
class="text-blue-600 p-2" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-10">Import Page</div>
|
||||||
|
<page is="/footer">
|
||||||
|
<slot is="link"> Link </slot>
|
||||||
|
<slot is="item"> Item </slot>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<page is="/footer" no-style>
|
||||||
|
<slot is="link"> Link2 </slot>
|
||||||
|
<slot is="item"> Item2 </slot>
|
||||||
|
</page>
|
||||||
|
|
||||||
</div>`,
|
</div>`,
|
||||||
},
|
},
|
||||||
DATA: Source{
|
DATA: Source{
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,8 @@ type BuildOption struct {
|
||||||
AssetRoot string `json:"asset_root,omitempty"`
|
AssetRoot string `json:"asset_root,omitempty"`
|
||||||
IgnoreAssetRoot bool `json:"ignore_asset_root,omitempty"`
|
IgnoreAssetRoot bool `json:"ignore_asset_root,omitempty"`
|
||||||
IgnoreDocument bool `json:"ignore_document,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
|
// Request is the struct for the request
|
||||||
|
|
|
||||||
|
|
@ -60,21 +60,21 @@ func TestTemplatePageTree(t *testing.T) {
|
||||||
t.Fatalf("Pages error: %v", err)
|
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, "error", pages[0].Name)
|
||||||
assert.Equal(t, true, pages[0].IsDir)
|
assert.Equal(t, true, pages[0].IsDir)
|
||||||
assert.Equal(t, "error", pages[0].Children[0].Name)
|
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).Route)
|
||||||
assert.Equal(t, "error", pages[0].Children[0].IPage.(*Page).Name)
|
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, true, pages[1].IsDir)
|
||||||
assert.Equal(t, "[invite]", pages[1].Children[0].Name)
|
assert.Equal(t, "[invite]", pages[2].Children[0].Name)
|
||||||
assert.Equal(t, true, pages[1].Children[0].IsDir)
|
assert.Equal(t, true, pages[2].Children[0].IsDir)
|
||||||
assert.Equal(t, "/index/[invite]", pages[1].Children[0].Children[0].IPage.(*Page).Route)
|
assert.Equal(t, "/index/[invite]", pages[2].Children[0].Children[0].IPage.(*Page).Route)
|
||||||
assert.Equal(t, "[invite]", pages[1].Children[0].Children[0].IPage.(*Page).Name)
|
assert.Equal(t, "[invite]", pages[2].Children[0].Children[0].IPage.(*Page).Name)
|
||||||
assert.Equal(t, "/index", pages[1].Children[1].IPage.(*Page).Route)
|
assert.Equal(t, "/index", pages[2].Children[1].IPage.(*Page).Route)
|
||||||
assert.Equal(t, "index", pages[1].Children[1].IPage.(*Page).Name)
|
assert.Equal(t, "index", pages[2].Children[1].IPage.(*Page).Name)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue