From 276f29d77726b622cc4673da747fcf34989b8a8b Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 25 Jul 2024 17:56:40 +0800 Subject: [PATCH] [feat] SUI support BeforeRender hook for dataset processing, and deprecating the s:set method. --- sui/api/request.go | 15 +++- sui/api/request_test.go | 2 +- sui/core/build.go | 4 +- sui/core/cache.go | 1 + sui/core/compile.go | 8 +- sui/core/context.go | 2 +- sui/core/injections.go | 5 -- sui/core/jit.go | 9 +-- sui/core/parser.go | 160 +++++++++++++++++++++++++++++++--------- sui/core/script.go | 25 +++++++ sui/core/types.go | 2 +- 11 files changed, 174 insertions(+), 59 deletions(-) diff --git a/sui/api/request.go b/sui/api/request.go index e9b27743..af060ba7 100644 --- a/sui/api/request.go +++ b/sui/api/request.go @@ -168,7 +168,8 @@ func (r *Request) Render() (string, int, error) { Route: r.Request.URL.Path, Root: c.Root, Script: c.Script, - Request: true, + Imports: c.Imports, + Request: r.Request, } // Parse the template @@ -250,6 +251,17 @@ func (r *Request) MakeCache() (*core.Cache, int, error) { globalDataSel.Remove() } + var imports map[string]string + importsSel := doc.Find("script[name=imports]") + if importsSel != nil && importsSel.Length() > 0 { + importsRaw := importsSel.Text() + importsSel.Remove() + err := jsoniter.UnmarshalFromString(importsRaw, &imports) + if err != nil { + return nil, 500, fmt.Errorf("imports error, please re-complie the page %s", err.Error()) + } + } + html, err := doc.Html() if err != nil { return nil, 500, fmt.Errorf("parse error, please re-complie the page %s", err.Error()) @@ -274,6 +286,7 @@ func (r *Request) MakeCache() (*core.Cache, int, error) { CacheTime: time.Duration(cacheTime) * time.Second, DataCacheTime: time.Duration(dataCacheTime) * time.Second, Script: script, + Imports: imports, } go core.SetCache(r.File, cache) diff --git a/sui/api/request_test.go b/sui/api/request_test.go index ea8e0936..d08cffb4 100644 --- a/sui/api/request_test.go +++ b/sui/api/request_test.go @@ -71,7 +71,7 @@ func makeParser(route string, t *testing.T) (*core.TemplateParser, string, core. Debug: r.Request.DebugMode(), DisableCache: r.Request.DisableCache(), Route: r.Request.URL.Path, - Request: true, + Request: r.Request, } // Parse the template diff --git a/sui/core/build.go b/sui/core/build.go index 55dc7245..4e8b392c 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -236,7 +236,7 @@ func (page *Page) BuildAsComponent(sel *goquery.Selection, ctx *BuildContext, op ctx.styles = append(ctx.styles, styles...) sel.ReplaceWithSelection(body.Contents()) - ctx.components[page.Route] = true + ctx.components[component] = page.Route return source, nil } @@ -456,7 +456,6 @@ func (page *Page) buildComponents(doc *goquery.Document, ctx *BuildContext, opti return warnings, fmt.Errorf("SUI %s not found", page.SuiID) } - public := sui.GetPublic() tmpl, err := sui.GetTemplate(page.TemplateID) if err != nil { return warnings, err @@ -480,7 +479,6 @@ func (page *Page) buildComponents(doc *goquery.Document, ctx *BuildContext, opti if ctx.isJitComponent(name) { sel.SetAttr("s:jit", "true") sel.SetAttr("s:parent", page.namespace) - sel.SetAttr("s:root", public.Root) ctx.addJitComponent(name) return } diff --git a/sui/core/cache.go b/sui/core/cache.go index c3cc176d..e902be51 100644 --- a/sui/core/cache.go +++ b/sui/core/cache.go @@ -21,6 +21,7 @@ type Cache struct { CacheTime time.Duration DataCacheTime time.Duration Script *Script + Imports map[string]string } const ( diff --git a/sui/core/compile.go b/sui/core/compile.go index 83986c33..0c0e053f 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -93,13 +93,7 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, []str } // Page Components - components := []string{} - if ctx != nil && ctx.components != nil && len(ctx.components) > 0 { - for route := range ctx.components { - components = append(components, route) - } - } - rawComponents, _ := jsoniter.MarshalToString(components) + rawComponents, _ := jsoniter.MarshalToString(ctx.components) body.AppendHtml("\n\n" + `\n\n") page.ReplaceDocument(doc) diff --git a/sui/core/context.go b/sui/core/context.go index f8b78b05..c7c1b483 100644 --- a/sui/core/context.go +++ b/sui/core/context.go @@ -3,7 +3,7 @@ package core // NewBuildContext create a new build context func NewBuildContext(global *GlobalBuildContext) *BuildContext { return &BuildContext{ - components: map[string]bool{}, + components: map[string]string{}, sequence: 1, scripts: []ScriptNode{}, scriptUnique: map[string]bool{}, diff --git a/sui/core/injections.go b/sui/core/injections.go index 8eadefc4..99efe181 100644 --- a/sui/core/injections.go +++ b/sui/core/injections.go @@ -191,16 +191,11 @@ const backendScriptTmpl = ` this.__sui_page = '%s'; this.__sui_constants = {}; this.__sui_helpers = []; -this.__sui_hooks = null; if (typeof Helpers === 'object') { this.__sui_helpers = Object.keys(Helpers); } -if (typeof Hooks === 'function') { - this.__sui_hooks = new Hooks(); -} - if (typeof Constants === 'object') { this.__sui_constants = Constants; } diff --git a/sui/core/jit.go b/sui/core/jit.go index 64737c86..32d3a590 100644 --- a/sui/core/jit.go +++ b/sui/core/jit.go @@ -45,7 +45,7 @@ func init() { } // parseComponent parse the component -func (parser *TemplateParser) parseComponent(sel *goquery.Selection) { +func (parser *TemplateParser) parseJitComponent(sel *goquery.Selection) { parser.parsed(sel) comp, props, slots, children, err := parser.getComponent(sel) if err != nil { @@ -218,8 +218,8 @@ func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponen return comp, props, slots, children, nil } -// isComponent check if the selection is a component -func (parser *TemplateParser) isComponent(sel *goquery.Selection) bool { +// isJitComponent check if the selection is a component +func (parser *TemplateParser) isJitComponent(sel *goquery.Selection) bool { _, exist := sel.Attr("s:jit") is := sel.AttrOr("is", "") return exist && is != "" @@ -314,8 +314,7 @@ func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[st data := Data{"$props": props} route, _ = data.ReplaceUse(slotRe, route) route, _ = parser.data.Replace(route) - root := sel.AttrOr("s:root", "/") - file := filepath.Join(string(os.PathSeparator), "public", root, route+".jit") + file := filepath.Join(string(os.PathSeparator), "public", parser.option.Root, route+".jit") return file, route, nil } diff --git a/sui/core/parser.go b/sui/core/parser.go index bfef4b03..286eba1a 100644 --- a/sui/core/parser.go +++ b/sui/core/parser.go @@ -2,6 +2,8 @@ package core import ( "fmt" + "os" + "path/filepath" "strings" "github.com/PuerkitoBio/goquery" @@ -40,17 +42,18 @@ type Mapping struct { // ParserOption parser option type ParserOption struct { - Component bool `json:"component,omitempty"` - Editor bool `json:"editor,omitempty"` - Preview bool `json:"preview,omitempty"` - Debug bool `json:"debug,omitempty"` - DisableCache bool `json:"disableCache,omitempty"` - Request bool `json:"request,omitempty"` - Route string `json:"route,omitempty"` - Theme any `json:"theme,omitempty"` - Locale any `json:"locale,omitempty"` - Root string `json:"root,omitempty"` - Script *Script `json:"-"` // backend script + Component bool `json:"component,omitempty"` + Editor bool `json:"editor,omitempty"` + Preview bool `json:"preview,omitempty"` + Debug bool `json:"debug,omitempty"` + DisableCache bool `json:"disableCache,omitempty"` + Route string `json:"route,omitempty"` + Theme any `json:"theme,omitempty"` + Locale any `json:"locale,omitempty"` + Root string `json:"root,omitempty"` + Imports map[string]string `json:"imports,omitempty"` + Script *Script `json:"-"` // backend script + Request *Request `json:"request,omitempty"` } var keepWords = map[string]bool{ @@ -99,9 +102,6 @@ func NewTemplateParser(data Data, option *ParserOption) *TemplateParser { // Render parses and renders the HTML template func (parser *TemplateParser) Render(html string) (string, error) { - // Set the locale - parser.locale = parser.Locale() - if !strings.Contains(html, "%s`, html) } @@ -111,13 +111,11 @@ func (parser *TemplateParser) Render(html string) (string, error) { return "", err } - root := doc.Selection.Find("html") - parser.parseNode(root.Nodes[0]) - - // Replace the nodes - for sel, nodes := range parser.replace { - sel.ReplaceWithNodes(nodes...) - delete(parser.replace, sel) + // Set the locale + parser.locale = parser.Locale() + err = parser.RenderSelection(doc.Selection) + if err != nil { + return "", err } // Append the head @@ -150,16 +148,13 @@ func (parser *TemplateParser) Render(html string) (string, error) { parser.addScripts(body, parser.filterScripts("body", parser.scripts)) } - // Fmt - parser.Fmt(doc) - // For editor if parser.option != nil && parser.option.Editor { return doc.Find("body").Html() } // For Request - if parser.option != nil && (parser.option.Request || parser.option.Preview) { + if parser.option != nil && (parser.option.Request != nil || parser.option.Preview) { // Remove the sui-hide attribute doc.Find("[sui-hide]").Remove() parser.tidy(doc.Selection) @@ -170,8 +165,26 @@ func (parser *TemplateParser) Render(html string) (string, error) { return doc.Html() } +// RenderSelection parses and renders the HTML template +func (parser *TemplateParser) RenderSelection(section *goquery.Selection) error { + + if len(section.Nodes) == 0 { + return fmt.Errorf("No nodes found") + } + + parser.parseNode(section.Nodes[0]) + // Replace the nodes + for sel, nodes := range parser.replace { + sel.ReplaceWithNodes(nodes...) + delete(parser.replace, sel) + } + + parser.Fmt(section) + return nil +} + // Fmt formats the HTML template -func (parser *TemplateParser) Fmt(doc *goquery.Document) { +func (parser *TemplateParser) Fmt(doc *goquery.Selection) { if parser.locale != nil { sels := doc.Find(`[s\:trans-fmt]`) sels.Each(func(i int, sel *goquery.Selection) { @@ -194,10 +207,8 @@ func (parser *TemplateParser) parseNode(node *html.Node) { } parser.parseElementNode(sel) - // Skip children if the node is a loop node - if _, exist := sel.Attr("s:for"); exist { - skipChildren = true - } + // Skip children if the node is a loop node态element component or JIT component + skipChildren = parser.hasForStatement(sel) || parser.isElementComponent(sel) || parser.isJitComponent(sel) case html.TextNode: parser.parseTextNode(node) @@ -229,13 +240,86 @@ func (parser *TemplateParser) parseElementNode(sel *goquery.Selection) { parser.setStatementNode(sel) } - // JIT Compile the element - if parser.isComponent(sel) { - parser.parseComponent(sel) - } - // Parse the attributes parser.parseElementAttrs(sel) + + // if the element is a component + if parser.isElementComponent(sel) { + parser.parseElementComponent(sel) + } + + // JIT Compile the element + if parser.isJitComponent(sel) { + parser.parseJitComponent(sel) + } +} + +func (parser *TemplateParser) parseElementComponent(sel *goquery.Selection) { + + sel.SetAttr("parsed", "true") + com := sel.AttrOr("s:cn", "") + props := map[string]string{} + for _, attr := range sel.Nodes[0].Attr { + if !strings.HasPrefix(attr.Key, "s:") && attr.Key != "parsed" { + props[attr.Key] = attr.Val + } + } + + // load the component based on the route + var script *Script + var err error + if parser.option.Imports != nil { + if route, has := parser.option.Imports[com]; has { + file := filepath.Join(string(os.PathSeparator), "public", parser.option.Root, route) + script, err = LoadScript(file) + if err != nil { + parser.errors = append(parser.errors, err) + setError(sel, err) + return + } + } + } + + compParser := parser.clone(script) + + // Call the BeforeRender Hook + if script != nil { + data, err := script.BeforeRender(parser.option.Request, props) + if err != nil { + parser.errors = append(parser.errors, err) + setError(sel, err) + return + } + if data != nil { + for k, v := range data { + compParser.data[k] = v + } + } + } + + err = compParser.RenderSelection(sel) + if err != nil { + parser.errors = append(parser.errors, err) + setError(sel, err) + } + parser.sequence = parser.sequence + 1 +} + +func (parser *TemplateParser) clone(script *Script) *TemplateParser { + var new = *parser + new.data = Data{} + for k, v := range parser.data { + new.data[k] = v + } + new.option.Script = script + return &new +} + +func (parser *TemplateParser) isElementComponent(sel *goquery.Selection) bool { + if comp, exist := sel.Attr("s:cn"); exist && comp != "" && sel.Nodes[0].Data != "script" { + return true + } + return false } func (parser *TemplateParser) transTextNode(node *html.Node) { @@ -486,6 +570,12 @@ func (parser *TemplateParser) parseTextNode(node *html.Node) { } node.Data = res } +func (parser *TemplateParser) hasForStatement(sel *goquery.Selection) bool { + if _, exist := sel.Attr("s:for"); exist { + return true + } + return false +} func (parser *TemplateParser) forStatementNode(sel *goquery.Selection) { diff --git a/sui/core/script.go b/sui/core/script.go index 1e9f60ce..e16dc8d4 100644 --- a/sui/core/script.go +++ b/sui/core/script.go @@ -99,6 +99,31 @@ func (script *Script) Call(r *Request, method string, args ...any) (interface{}, return res, nil } +// BeforeRender the script method +func (script *Script) BeforeRender(r *Request, props map[string]string) (Data, error) { + + ctx, err := script.NewContext(r.Sid, nil) + if err != nil { + return nil, err + } + defer ctx.Close() + + if !ctx.Global().Has("BeforeRender") { + return nil, nil + } + + res, err := ctx.Call("BeforeRender", r, props) + if err != nil { + return nil, err + } + + if data, ok := res.(map[string]interface{}); ok { + return data, nil + } + + return nil, fmt.Errorf("BeforeRender return %v should be Record", res) +} + // ConstantsToString get the constants from the script func (script *Script) ConstantsToString() (string, error) { constants, err := script.Constants() diff --git a/sui/core/types.go b/sui/core/types.go index a5f45a6a..5d0ed685 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -59,7 +59,7 @@ type PageProp struct { // BuildContext is the struct for the build context type BuildContext struct { - components map[string]bool + components map[string]string jitComponents map[string]bool sequence int doc *goquery.Document