From ffb858b76f037f5548d61962d053da5a8595693e Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 8 Jul 2024 10:31:35 +0800 Subject: [PATCH] refactor: Update parsing logic for JIT components --- sui/core/build.go | 9 ++- sui/core/compile.go | 129 ++++++++++++++++++++--------- sui/core/jit.go | 144 +++++++++++++++++++++++++++++---- sui/core/parser.go | 6 ++ sui/core/types.go | 5 +- sui/storages/local/build.go | 5 ++ sui/storages/local/template.go | 13 +++ 7 files changed, 251 insertions(+), 60 deletions(-) diff --git a/sui/core/build.go b/sui/core/build.go index 5fc84288..68c45886 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -40,7 +40,7 @@ func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Docume ctx.sequence++ - namespace := Namespace(page.Name, ctx.sequence, option.ScriptMinify) + namespace := Namespace(page.Route, ctx.sequence, option.ScriptMinify) page.namespace = namespace html, err := page.BuildHTML(option) @@ -367,6 +367,11 @@ func (page *Page) BuildStyles(ctx *BuildContext, option *BuildOption, component // BuildScripts build the scripts for the page func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component string, namespace string) ([]ScriptNode, error) { + ispage := component == "__page" + if ispage { + component = ComponentName(page.Route, option.ScriptMinify) + } + scripts := []ScriptNode{} if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" { return scripts, nil @@ -416,7 +421,7 @@ func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component code := string(source) parent := "body" - if component != "__page" { + if !ispage { parent = "head" code = fmt.Sprintf("function %s(){\n%s\n}\n", component, addTabToEachLine(code)) } diff --git a/sui/core/compile.go b/sui/core/compile.go index 40c20117..5feebe7c 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -1,10 +1,12 @@ package core import ( + "fmt" "regexp" "strings" "github.com/evanw/esbuild/pkg/api" + jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/runtime/transform" "github.com/yaoapp/kun/log" ) @@ -93,44 +95,6 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, error return html, nil } -// HTML return the html of the script -func (script ScriptNode) HTML() string { - - attrs := []string{ - "s:ns=\"" + script.Namespace + "\"", - "s:cn=\"" + script.Component + "\"", - } - if script.Attrs != nil { - for _, attr := range script.Attrs { - attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"") - } - } - // Inline Script - if script.Source == "" { - return "" - } - return "" -} - -// HTML return the html of the style node -func (style StyleNode) HTML() string { - attrs := []string{ - "s:ns=\"" + style.Namespace + "\"", - "s:cn=\"" + style.Component + "\"", - } - if style.Attrs != nil { - for _, attr := range style.Attrs { - attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"") - } - } - // Inline Style - if style.Source == "" { - return "" - } - return "" - -} - // CompileAsComponent compile the page as component func (page *Page) CompileAsComponent(ctx *BuildContext, option *BuildOption) (string, error) { @@ -148,8 +112,34 @@ func (page *Page) CompileAsComponent(ctx *BuildContext, option *BuildOption) (st } } - page.ReplaceDocument(doc) - return doc.Find("body").Html() + body := doc.Find("body") + rawScripts, err := jsoniter.MarshalToString(ctx.scripts) + if err != nil { + return "", err + } + + rawStyles, err := jsoniter.MarshalToString(ctx.styles) + if err != nil { + return "", err + } + + rawOption, err := jsoniter.MarshalToString(option) + if err != nil { + return "", err + } + + if body.Children().Length() == 0 { + return "", fmt.Errorf("page %s as component should have one root element", page.Route) + } + + if body.Children().Length() > 1 { + return "", fmt.Errorf("page %s as component should have only one root element", page.Route) + } + + body.Children().First().AppendHtml(fmt.Sprintf(``+"\n", rawScripts)) + body.Children().First().AppendHtml(fmt.Sprintf(``+"\n", rawStyles)) + body.Children().First().AppendHtml(fmt.Sprintf(``+"\n", rawOption)) + return body.Html() } // CompileJS compile the javascript @@ -212,3 +202,62 @@ func (page *Page) CompileCSS(source []byte, minify bool) ([]byte, error) { func (page *Page) CompileHTML(source []byte, minify bool) ([]byte, error) { return source, nil } + +// HTML return the html of the script +func (script ScriptNode) HTML() string { + + attrs := []string{ + "s:ns=\"" + script.Namespace + "\"", + "s:cn=\"" + script.Component + "\"", + } + if script.Attrs != nil { + for _, attr := range script.Attrs { + attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"") + } + } + // Inline Script + if script.Source == "" { + return "" + } + return "" +} + +// ComponentHTML return the html of the script +func (script ScriptNode) ComponentHTML(ns string) string { + + attrs := []string{ + "s:ns=\"" + ns + "\"", + "s:cn=\"" + script.Component + "\"", + } + if script.Attrs != nil { + for _, attr := range script.Attrs { + attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"") + } + } + // Inline Script + if script.Source == "" { + return "" + } + + source := fmt.Sprintf(`function %s(){%s};`, script.Component, script.Source) + return "" +} + +// HTML return the html of the style node +func (style StyleNode) HTML() string { + attrs := []string{ + "s:ns=\"" + style.Namespace + "\"", + "s:cn=\"" + style.Component + "\"", + } + if style.Attrs != nil { + for _, attr := range style.Attrs { + attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"") + } + } + // Inline Style + if style.Source == "" { + return "" + } + return "" + +} diff --git a/sui/core/jit.go b/sui/core/jit.go index 8daa74e2..5b05487d 100644 --- a/sui/core/jit.go +++ b/sui/core/jit.go @@ -4,18 +4,23 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "time" "github.com/PuerkitoBio/goquery" + jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/application" ) // JitComponent component type JitComponent struct { - html string - scripts []ScriptNode - styles []StyleNode + file string + route string + html string + scripts []ScriptNode + styles []StyleNode + buildOption *BuildOption } const ( @@ -32,6 +37,9 @@ type componentData struct { // Components loaded JIT components var Components = map[string]*JitComponent{} var chComp = make(chan *componentData, 1) +var reScripts = regexp.MustCompile(`]*name="scripts"[^>]*>(.*?)`) +var reStyles = regexp.MustCompile(`]*name="styles"[^>]*>(.*?)`) +var reOption = regexp.MustCompile(`]*name="option"[^>]*>(.*?)`) func init() { go componentWriter() @@ -47,17 +55,18 @@ func (parser *TemplateParser) parseComponent(sel *goquery.Selection) { return } - html, err := parser.RenderComponent(comp, props, slots) + html, _, err := parser.RenderComponent(comp, props, slots) if err != nil { parser.errors = append(parser.errors, err) setError(sel, err) return } + sel.SetHtml(html) } // RenderComponent render the component -func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[string]interface{}, slots *goquery.Selection) (string, error) { +func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[string]interface{}, slots *goquery.Selection) (string, string, error) { html := comp.html randvar := fmt.Sprintf("__%s_$props", time.Now().Format("20060102150405")) html = slotRe.ReplaceAllStringFunc(html, func(exp string) string { @@ -77,7 +86,7 @@ func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[stri sel, err := NewDocumentString(`` + html + ``) if err != nil { - return "", err + return "", "", err } root := sel.Find("body") @@ -103,15 +112,65 @@ func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[stri compParser := NewTemplateParser(data, &option) // Parse the node + ns := Namespace(comp.route, compParser.sequence, comp.buildOption.ScriptMinify) + cn := ComponentName(comp.route, comp.buildOption.ScriptMinify) compParser.parseNode(root.Get(0)) for sel, nodes := range compParser.replace { sel.ReplaceWithNodes(nodes...) delete(parser.replace, sel) } + if comp.scripts != nil { + for _, script := range comp.scripts { + script.Namespace = ns + parser.scripts = append(parser.scripts, script) + } + } + + if comp.styles != nil { + for _, style := range comp.styles { + style.Namespace = ns + parser.styles = append(parser.styles, style) + } + } + // Update sequence parser.sequence = compParser.sequence + parser.sequence - return root.Html() + first := root.Children().First() + first.SetAttr("s:ns", ns) + first.SetAttr("s:cn", cn) + first.SetAttr("s:ready", cn+"()") + html, err = root.Html() + if err != nil { + return "", "", err + } + return html, ns, nil +} + +func (parser *TemplateParser) addScripts(sel *goquery.Selection, scripts []ScriptNode) { + if scripts == nil { + return + } + for _, script := range scripts { + query := fmt.Sprintf(`script[s\:cn="%s"]`, script.Component) + if sel.Find(query).Length() > 0 { + continue + } + sel.AppendHtml(script.ComponentHTML(script.Namespace)) + } +} + +func (parser *TemplateParser) addStyles(sel *goquery.Selection, styles []StyleNode) { + if styles == nil { + return + } + for _, style := range styles { + query := fmt.Sprintf(`style[s\:cn="%s"]`, style.Component) + if sel.Find(query).Length() > 0 { + continue + } + sel.Append(style.HTML()) + } } func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponent, map[string]interface{}, *goquery.Selection, error) { @@ -123,12 +182,12 @@ func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponen return nil, nil, slots, err } - file, err := parser.componentFile(sel, props) + file, route, err := parser.componentFile(sel, props) if err != nil { return nil, props, slots, err } - comp, err := getComponent(file, parser.disableCache()) + comp, err := getComponent(route, file, parser.disableCache()) if err != nil { return nil, props, slots, err } @@ -225,10 +284,10 @@ func (parser *TemplateParser) parseComponentProps(props map[string]string) (map[ return result, nil } -func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[string]interface{}) (string, error) { +func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[string]interface{}) (string, string, error) { route := sel.AttrOr("is", "") if route == "" { - return "", fmt.Errorf("Component route is required") + return "", "", fmt.Errorf("Component route is required") } data := Data{"$props": props} @@ -236,7 +295,7 @@ func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[st route, _ = parser.data.Replace(route) root := sel.AttrOr("s:root", "/") file := filepath.Join(string(os.PathSeparator), "public", root, route+".jit") - return file, nil + return file, route, nil } func componentWriter() { @@ -253,7 +312,7 @@ func componentWriter() { } } -func getComponent(file string, disableCache ...bool) (*JitComponent, error) { +func getComponent(route string, file string, disableCache ...bool) (*JitComponent, error) { noCache := false if len(disableCache) > 0 && disableCache[0] { @@ -264,7 +323,7 @@ func getComponent(file string, disableCache ...bool) (*JitComponent, error) { return comp, nil } - comp, err := readComponent(file) + comp, err := readComponent(route, file) if err != nil { return nil, err } @@ -275,10 +334,63 @@ func getComponent(file string, disableCache ...bool) (*JitComponent, error) { } // readComponent read the JIT component -func readComponent(file string) (*JitComponent, error) { +func readComponent(route string, file string) (*JitComponent, error) { content, err := application.App.Read(file) if err != nil { return nil, err } - return &JitComponent{html: string(content)}, nil + + // Get the scripts + html := string(content) + rawScripts := "" + rawStyles := "" + rawOption := "" + html = reScripts.ReplaceAllStringFunc(html, func(exp string) string { + rawScripts = reScripts.ReplaceAllString(exp, "$1") + return "" + }) + + html = reStyles.ReplaceAllStringFunc(html, func(exp string) string { + rawStyles = reStyles.ReplaceAllString(exp, "$1") + return "" + }) + + html = reOption.ReplaceAllStringFunc(html, func(exp string) string { + rawOption = reOption.ReplaceAllString(exp, "$1") + return "" + }) + + scripts := []ScriptNode{} + styles := []StyleNode{} + buildOption := BuildOption{} + + if rawScripts != "" { + err := jsoniter.UnmarshalFromString(rawScripts, &scripts) + if err != nil { + return nil, err + } + } + + if rawStyles != "" { + err := jsoniter.UnmarshalFromString(rawStyles, &styles) + if err != nil { + return nil, err + } + } + + if rawOption != "" { + err := jsoniter.UnmarshalFromString(rawOption, &buildOption) + if err != nil { + return nil, err + } + } + + return &JitComponent{ + file: file, + route: route, + html: html, + scripts: scripts, + styles: styles, + buildOption: &buildOption, + }, nil } diff --git a/sui/core/parser.go b/sui/core/parser.go index 57ad5706..795d3179 100644 --- a/sui/core/parser.go +++ b/sui/core/parser.go @@ -26,6 +26,8 @@ type TemplateParser struct { option *ParserOption // parser option locale *Locale // locale context *ParserContext // parser context + scripts []ScriptNode // scripts + styles []StyleNode // styles } // ParserContext parser context for the template @@ -171,6 +173,8 @@ func NewTemplateParser(data Data, option *ParserOption) *TemplateParser { errors: []error{}, replace: map[*goquery.Selection][]*html.Node{}, option: option, + scripts: []ScriptNode{}, + styles: []StyleNode{}, } } @@ -202,6 +206,8 @@ func (parser *TemplateParser) Render(html string) (string, error) { head := doc.Find("head") if head.Length() > 0 { head.AppendHtml(headInjectionScript()) + parser.addScripts(head, parser.scripts) + parser.addStyles(head, parser.styles) } // Append the data to the body diff --git a/sui/core/types.go b/sui/core/types.go index cf2e5d14..5c1b11f8 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -191,8 +191,9 @@ type Template struct { // TemplateScirpts is the struct for the template scripts type TemplateScirpts struct { - BeforeBuild []*TemplateScript `json:"before:build,omitempty"` // Run before build - AfterBuild []*TemplateScript `json:"after:build,omitempty"` // Run after build + BeforeBuild []*TemplateScript `json:"before:build,omitempty"` // Run before build + AfterBuild []*TemplateScript `json:"after:build,omitempty"` // Run after build + BuildComplete []*TemplateScript `json:"build:complete,omitempty"` // Run build complete } // TemplateScript is the struct for the template script diff --git a/sui/storages/local/build.go b/sui/storages/local/build.go index ce111749..f1d739e0 100644 --- a/sui/storages/local/build.go +++ b/sui/storages/local/build.go @@ -16,6 +16,11 @@ import ( // Build the template func (tmpl *Template) Build(option *core.BuildOption) error { var err error + defer func() { + if option.ExecScripts { + tmpl.ExecBuildCompleteScripts() + } + }() // Execute the build before hook if option.ExecScripts { diff --git a/sui/storages/local/template.go b/sui/storages/local/template.go index 835d3457..871c6e75 100644 --- a/sui/storages/local/template.go +++ b/sui/storages/local/template.go @@ -79,6 +79,14 @@ func (tmpl *Template) Reload() error { return nil } +// ExecBuildCompleteScripts execute the build complete scripts +func (tmpl *Template) ExecBuildCompleteScripts() []core.TemplateScirptResult { + if tmpl.Scripts == nil || len(tmpl.Scripts.BuildComplete) == 0 { + return nil + } + return tmpl.ExecScripts(tmpl.Scripts.BuildComplete) +} + // ExecBeforeBuildScripts execute the before build scripts func (tmpl *Template) ExecBeforeBuildScripts() []core.TemplateScirptResult { if tmpl.Scripts == nil || len(tmpl.Scripts.BeforeBuild) == 0 { @@ -97,7 +105,12 @@ func (tmpl *Template) ExecAfterBuildScripts() []core.TemplateScirptResult { // ExecScripts execute the scripts func (tmpl *Template) ExecScripts(scripts []*core.TemplateScript) []core.TemplateScirptResult { + results := []core.TemplateScirptResult{} + if scripts == nil { + return results + } + for _, script := range scripts { switch script.Type { case "command":