diff --git a/sui/core/build.go b/sui/core/build.go index 0009c90c..f1b32eb8 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -55,7 +55,9 @@ func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Docume doc.Find("body").SetAttr("s:ns", namespace) // Bind the Page events - page.BindEvent(ctx, doc.Selection) + if !option.JitMode { + page.BindEvent(ctx, doc.Selection, "__page", true) + } warnings, err := page.buildComponents(doc, ctx, option) if err != nil { @@ -170,6 +172,9 @@ func (page *Page) BuildAsComponent(sel *goquery.Selection, ctx *BuildContext, op return "", err } + // Bind the component events + page.BindEvent(ctx, doc.Selection, component, false) + body := doc.Selection.Find("body") if body.Children().Length() == 0 { @@ -553,6 +558,11 @@ func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component component = ComponentName(page.Route, option.ScriptMinify) } + arguments := "" + if !ispage { + arguments = "arguments[0]" + } + scripts := []ScriptNode{} if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" { return scripts, nil @@ -566,17 +576,18 @@ func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component var imports []string = nil var source []byte = nil if page.Codes.TS.Code != "" { - source, imports, err = page.CompileTS([]byte(page.Codes.TS.Code), option.ScriptMinify) + code := fmt.Sprintf("this.store = new __sui_store(%s);\n%s", arguments, page.Codes.TS.Code) + source, imports, err = page.CompileTS([]byte(code), option.ScriptMinify) if err != nil { return nil, err } } else if page.Codes.JS.Code != "" { - source, imports, err = page.CompileJS([]byte(page.Codes.JS.Code), option.ScriptMinify) + code := fmt.Sprintf("this.store = new __sui_store(%s);\n%s", arguments, page.Codes.JS.Code) + source, imports, err = page.CompileJS([]byte(code), option.ScriptMinify) if err != nil { return nil, err } - } // Add the script diff --git a/sui/core/compile.go b/sui/core/compile.go index 27cd6735..a83bbf9d 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -58,6 +58,9 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, []str } + // SUI lib + head.AppendHtml("\n\n" + `` + "\n\n") + // Page Config page.Config = page.GetConfig() @@ -111,6 +114,7 @@ func (page *Page) CompileAsComponent(ctx *BuildContext, option *BuildOption) (st opt := *option opt.IgnoreDocument = true opt.WithWrapper = true + opt.JitMode = true doc, warnings, err := page.Build(ctx, &opt) if err != nil { return "", warnings, err @@ -262,6 +266,9 @@ func (script ScriptNode) ComponentHTML(ns string) string { } source := fmt.Sprintf(`function %s(){%s};`, script.Component, script.Source) + if script.Component == "" { + return "" + } return "" } diff --git a/sui/core/event.go b/sui/core/event.go index c959da48..b231de48 100644 --- a/sui/core/event.go +++ b/sui/core/event.go @@ -9,27 +9,48 @@ import ( "golang.org/x/net/html" ) +var eventMatcher = NewAttrPrefixMatcher(`s:on-`) + // BindEvent is a method that binds events to the page. -func (page *Page) BindEvent(ctx *BuildContext, sel *goquery.Selection) { - matcher := NewAttrPrefixMatcher(`s:on-`) - sel.FindMatcher(matcher).Each(func(i int, s *goquery.Selection) { - page.appendEventScript(ctx, s) +func (page *Page) BindEvent(ctx *BuildContext, sel *goquery.Selection, cn string, ispage bool) { + + sel.FindMatcher(eventMatcher).Each(func(i int, s *goquery.Selection) { + if comp, has := s.Attr("is"); has && ctx.isJitComponent(comp) { + return + } + script := GetEventScript(ctx.sequence, s, page.namespace, cn, "event", ispage) + if script != nil { + ctx.scripts = append(ctx.scripts, *script) + ctx.sequence++ + } }) } -func (page *Page) appendEventScript(ctx *BuildContext, sel *goquery.Selection) { +// BindEvent is a method that binds events to the component in just-in-time mode. +func (parser *TemplateParser) BindEvent(sel *goquery.Selection, ns string, cn string) { + sel.FindMatcher(eventMatcher).Each(func(i int, s *goquery.Selection) { + script := GetEventScript(parser.sequence, s, ns, cn, "event-jit", false) + if script != nil { + script.Component = "" + script.Parent = "body" + parser.scripts = append(parser.scripts, *script) + parser.sequence++ + } + }) +} + +// GetEventScript the event script +func GetEventScript(sequence int, sel *goquery.Selection, ns string, cn string, prefix string, ispage bool) *ScriptNode { if len(sel.Nodes) == 0 { - return + return nil } // Page events events := map[string]string{} dataUnique := map[string]string{} jsonUnique := map[string]string{} - id := fmt.Sprintf("event-%d", ctx.sequence) - ctx.sequence++ - + id := fmt.Sprintf("%s-%d", prefix, sequence) for _, attr := range sel.Nodes[0].Attr { if strings.HasPrefix(attr.Key, "s:on-") { @@ -71,15 +92,20 @@ func (page *Page) appendEventScript(ctx *BuildContext, sel *goquery.Selection) { source := "" for name, handler := range events { - source += pageEventInjectScript(id, name, dataRaw, jsonRaw, handler) + "\n" + if ispage { + source += pageEventInjectScript(id, name, dataRaw, jsonRaw, handler) + "\n" + } else { + source += compEventInjectScript(id, name, cn, dataRaw, jsonRaw, handler) + "\n" + } sel.RemoveAttr(fmt.Sprintf("s:on-%s", name)) } - ctx.scripts = append(ctx.scripts, ScriptNode{ - Source: source, - Namespace: page.namespace, - Attrs: []html.Attribute{{Key: "event", Val: id}}, - }) - sel.SetAttr("s:event", id) + + return &ScriptNode{ + Source: source, + Namespace: ns, + Component: cn, + Attrs: []html.Attribute{{Key: "event", Val: id}}, + } } diff --git a/sui/core/injections.go b/sui/core/injections.go index 14eb5447..70375ad8 100644 --- a/sui/core/injections.go +++ b/sui/core/injections.go @@ -2,11 +2,7 @@ package core import "fmt" -const initScriptTmpl = ` - try { - var __sui_data = %s; - } catch (e) { console.log('init data error:', e); } - +const suiLibScript = ` function __sui_event_handler(event, dataKeys, jsonKeys, elm, handler) { const data = {}; @@ -26,10 +22,47 @@ const initScriptTmpl = ` } } }) - + handler && handler(event, data, elm); }; + function __sui_store(elm) { + elm = elm || document.body; + + this.Get = function (key) { + return elm.getAttribute("data:" + key); + } + + this.Set = function (key, value) { + elm.setAttribute("data:" + key, value); + } + + this.GetJSON = function (key) { + const value = elm.getAttribute("json:" + key); + if (value && value != "") { + try { + const res = JSON.parse(value); + return res; + } catch (e) { + const message = e.message || e || "An error occurred"; + console.error(` + "`[SUI] Event Handler Error: ${message}`" + `, elm); + return null; + } + } + return null; + } + + this.SetJSON = function (key, value) { + elm.setAttribute("json:" + key, JSON.stringify(value)); + } + } +` + +const initScriptTmpl = ` + try { + var __sui_data = %s; + } catch (e) { console.log('init data error:', e); } + document.addEventListener("DOMContentLoaded", function () { try { document.querySelectorAll("[s\\:ready]").forEach(function (element) { @@ -71,6 +104,15 @@ const pageEventScriptTmpl = ` }); ` +const compEventScriptTmpl = ` + document.querySelector("[s\\:event=%s]").addEventListener("%s", function (event) { + const dataKeys = %s; + const jsonKeys = %s; + const handler = new %s(this).%s; + __sui_event_handler(event, dataKeys, jsonKeys, this, handler); + }); +` + func bodyInjectionScript(jsonRaw string, debug bool) string { jsPrintData := "" if debug { @@ -86,3 +128,7 @@ func headInjectionScript(jsonRaw string) string { func pageEventInjectScript(eventID, eventName, dataKeys, jsonKeys, handler string) string { return fmt.Sprintf(pageEventScriptTmpl, eventID, eventName, dataKeys, jsonKeys, handler) } + +func compEventInjectScript(eventID, eventName, component, dataKeys, jsonKeys, handler string) string { + return fmt.Sprintf(compEventScriptTmpl, eventID, eventName, dataKeys, jsonKeys, component, handler) +} diff --git a/sui/core/jit.go b/sui/core/jit.go index 73d8e7b6..64737c86 100644 --- a/sui/core/jit.go +++ b/sui/core/jit.go @@ -6,7 +6,6 @@ import ( "path/filepath" "regexp" "strings" - "time" "github.com/PuerkitoBio/goquery" jsoniter "github.com/json-iterator/go" @@ -62,21 +61,24 @@ func (parser *TemplateParser) parseComponent(sel *goquery.Selection) { return } + // fmt.Println(sel.Nodes[0].Attr) sel.SetHtml(html) } // RenderComponent render the component func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[string]interface{}, slots *goquery.Selection, children *goquery.Selection) (string, string, error) { html := comp.html - randvar := fmt.Sprintf("__%s_$props", time.Now().Format("20060102150405")) - html = replaceRandVar(html, randvar) - data := Data{} - data[randvar] = props - if parser.data != nil { - for key, val := range parser.data { - data[key] = val - } - } + + html = replaceRandVar(html, Data(props)) + option := *parser.option + option.Route = comp.route + compParser := NewTemplateParser(parser.data, &option) + compParser.sequence = parser.sequence + 1 + locale := compParser.Locale() + + // Parse the node + ns := Namespace(comp.route, compParser.sequence, comp.buildOption.ScriptMinify) + cn := ComponentName(comp.route, comp.buildOption.ScriptMinify) sel, err := NewDocumentString(`` + html + ``) if err != nil { @@ -106,24 +108,26 @@ func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[stri children.Find("slot").Remove() root.Find("children").ReplaceWithSelection(children) - option := *parser.option - option.Route = comp.route - compParser := NewTemplateParser(data, &option) - locale := compParser.Locale() + // Replace the props if locale != nil { - locale.replaceVars(randvar) + locale.replaceVars(Data(props)) } - compParser.locale = locale - // Parse the node - ns := Namespace(comp.route, compParser.sequence, comp.buildOption.ScriptMinify) - cn := ComponentName(comp.route, comp.buildOption.ScriptMinify) + compParser.locale = locale + compParser.BindEvent(root, ns, cn) compParser.parseNode(root.Get(0)) for sel, nodes := range compParser.replace { sel.ReplaceWithNodes(nodes...) delete(parser.replace, sel) } + if compParser.scripts != nil { + for _, script := range compParser.scripts { + script.Namespace = ns + parser.scripts = append(parser.scripts, script) + } + } + if comp.scripts != nil { for _, script := range comp.scripts { script.Namespace = ns @@ -144,6 +148,7 @@ func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[stri first.SetAttr("s:ns", ns) first.SetAttr("s:cn", cn) first.SetAttr("s:ready", cn+"()") + html, err = root.Html() if err != nil { return "", "", err @@ -151,15 +156,28 @@ func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[stri return html, ns, nil } -func (parser *TemplateParser) addScripts(sel *goquery.Selection, scripts []ScriptNode) { +func (parser *TemplateParser) filterScripts(parent string, scripts []ScriptNode) []ScriptNode { if scripts == nil { - return + return []ScriptNode{} } + filtered := []ScriptNode{} for _, script := range scripts { - query := fmt.Sprintf(`script[s\:cn="%s"]`, script.Component) - if sel.Find(query).Length() > 0 { + if script.Parent != parent { continue } + filtered = append(filtered, script) + } + return filtered +} + +func (parser *TemplateParser) addScripts(sel *goquery.Selection, scripts []ScriptNode) { + for _, script := range scripts { + if script.Component != "" { + query := fmt.Sprintf(`script[s\:cn="%s"]`, script.Component) + if sel.Find(query).Length() > 0 { + continue + } + } sel.AppendHtml(script.ComponentHTML(script.Namespace)) } } @@ -226,6 +244,13 @@ func (parser *TemplateParser) componentProps(sel *goquery.Selection) (map[string } for _, attr := range sel.Nodes[0].Attr { + + // s:on , s:data , s:json + if strings.HasPrefix(attr.Key, "s:on") || strings.HasPrefix(attr.Key, "s:data") || strings.HasPrefix(attr.Key, "s:json") { + props[attr.Key] = attr.Val + continue + } + if strings.HasPrefix(attr.Key, "s:") || attr.Key == "is" { continue } @@ -268,22 +293,13 @@ func (parser *TemplateParser) parseComponentProps(props map[string]string) (map[ } if _, ok := values.(map[string]interface{}); ok { - for k, v := range values.(map[string]interface{}) { - result[k] = v + for k := range values.(map[string]interface{}) { + result[k] = k } } continue } - if strings.HasPrefix(val, "{{") && strings.HasSuffix(val, "}}") { - value, err := parser.data.Exec(val) - if err != nil { - return map[string]interface{}{}, err - } - result[key] = value - continue - } - result[key] = val } return result, nil @@ -303,16 +319,16 @@ func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[st return file, route, nil } -func (locale *Locale) replaceVars(randvar string) { +func (locale *Locale) replaceVars(data Data) { if locale.Keys != nil { for key, val := range locale.Keys { - locale.Keys[key] = replaceRandVar(val, randvar) + locale.Keys[key] = replaceRandVar(val, data) } } if locale.Messages != nil { for key, val := range locale.Messages { - locale.Messages[key] = replaceRandVar(val, randvar) + locale.Messages[key] = replaceRandVar(val, data) } } } @@ -414,11 +430,18 @@ func readComponent(route string, file string) (*JitComponent, error) { }, nil } -func replaceRandVar(html string, randvar string) string { - return slotRe.ReplaceAllStringFunc(html, func(exp string) string { - exp = strings.ReplaceAll(exp, "[{", "{{") - exp = strings.ReplaceAll(exp, "}]", "}}") - exp = strings.ReplaceAll(exp, "$props", randvar) - return exp +func replaceRandVar(value string, data Data) string { + + value = propNewRe.ReplaceAllStringFunc(value, func(exp string) string { + exp = strings.TrimPrefix(exp, "{%") + exp = strings.TrimSuffix(exp, "%}") + res, _ := data.ExecString(fmt.Sprintf("{{ %s }}", exp)) + return res + }) + + data = Data{"$props": data} + return slotRe.ReplaceAllStringFunc(value, func(exp string) string { + res, _ := data.ExecString(exp) + return res }) } diff --git a/sui/core/parser.go b/sui/core/parser.go index 175f987e..a3db3bed 100644 --- a/sui/core/parser.go +++ b/sui/core/parser.go @@ -134,7 +134,7 @@ func (parser *TemplateParser) Render(html string) (string, error) { } head.AppendHtml(headInjectionScript(data)) - parser.addScripts(head, parser.scripts) + parser.addScripts(head, parser.filterScripts("head", parser.scripts)) parser.addStyles(head, parser.styles) } @@ -146,6 +146,7 @@ func (parser *TemplateParser) Render(html string) (string, error) { data, _ = jsoniter.MarshalToString(map[string]string{"error": err.Error()}) } body.AppendHtml(bodyInjectionScript(data, parser.debug())) + parser.addScripts(body, parser.filterScripts("body", parser.scripts)) } // Fmt diff --git a/sui/core/types.go b/sui/core/types.go index 7c81197b..6e21a859 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -253,6 +253,7 @@ type BuildOption struct { AssetRoot string `json:"asset_root,omitempty"` IgnoreAssetRoot bool `json:"ignore_asset_root,omitempty"` IgnoreDocument bool `json:"ignore_document,omitempty"` + JitMode bool `json:"jit_mode,omitempty"` WithWrapper bool `json:"with_wrapper,omitempty"` KeepPageTag bool `json:"keep_page_tag,omitempty"` Namespace string `json:"namespace,omitempty"`