diff --git a/sui/api/request.go b/sui/api/request.go index d5ca7eb7..c84159a4 100644 --- a/sui/api/request.go +++ b/sui/api/request.go @@ -133,14 +133,14 @@ func (r *Request) Render() (string, int, error) { if c.Data != "" { err = r.Request.ExecStringMerge(data, c.Data) if err != nil { - return "", 500, fmt.Errorf("data error, please re-complie the page %s", err.Error()) + return "", 500, fmt.Errorf("data error, please re-complie the page. %s", err.Error()) } } if c.Global != "" { global, err := r.Request.ExecString(c.Global) if err != nil { - return "", 500, fmt.Errorf("global data error, please re-complie the page %s", err.Error()) + return "", 500, fmt.Errorf("global data error, please re-complie the page. %s", err.Error()) } data["$global"] = global } diff --git a/sui/core/build.go b/sui/core/build.go index 83ad12e9..153b0adb 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -341,7 +341,10 @@ func (page *Page) parseProps(from *goquery.Selection, to *goquery.Selection, ext exp := dataTokens.MatchString(attr.Val) prop := PageProp{Key: attr.Key, Val: attr.Val, Trans: trans, Exp: exp} page.props[attr.Key] = prop - to.SetAttr(attr.Key, attr.Val) + + // Set the prop + key := fmt.Sprintf("prop:%s", attr.Key) + to.SetAttr(key, attr.Val) } if extra != nil && len(extra) > 0 { diff --git a/sui/core/compile.go b/sui/core/compile.go index 3624c373..d16138fd 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -142,14 +142,10 @@ func (page *Page) CompileAsComponent(ctx *BuildContext, option *BuildOption) (st return "", warnings, err } - // Page Components - components := []string{} + rawComponents := "{}" if ctx != nil && ctx.components != nil && len(ctx.components) > 0 { - for route := range ctx.components { - components = append(components, route) - } + rawComponents, _ = jsoniter.MarshalToString(ctx.components) } - rawComponents, _ := jsoniter.MarshalToString(components) if body.Children().Length() == 0 { return "", warnings, fmt.Errorf("page %s as component should have one root element", page.Route) diff --git a/sui/core/injections.go b/sui/core/injections.go index 99efe181..3b3cd5cc 100644 --- a/sui/core/injections.go +++ b/sui/core/injections.go @@ -42,7 +42,46 @@ const libsuisource = ` function __sui_props(elm) { this.Get = function (key) { - return elm && elm.getAttribute(key); + if (!elm || typeof elm.getAttribute !== "function") { + return null; + } + const k = "prop:" + key; + const v = elm.getAttribute(k); + const json = elm.getAttribute("json-attr-prop:" + key) === "true"; + if (json) { + try { + return JSON.parse(v); + } catch (e) { + return null; + } + } + return v; + } + + this.List = function () { + const props = {}; + if (!elm || typeof elm.getAttribute !== "function") { + return props; + } + + const attrs = elm.attributes; + for (let i = 0; i < attrs.length; i++) { + const attr = attrs[i]; + if (attr.name.startsWith("prop:")) { + const k = attr.name.replace("prop:", ""); + const json = elm.getAttribute("json-attr-prop:" + k) === "true"; + if (json) { + try { + props[k] = JSON.parse(attr.value); + } catch (e) { + props[k] = null; + } + continue; + } + props[k] = attr.value; + } + } + return props; } } @@ -123,6 +162,10 @@ const libsuisource = ` this.SetJSON = function (key, value) { elm.setAttribute("json:" + key, JSON.stringify(value)); } + + this.GetData = function () { + return this.GetJSON("__component_data") || {}; + } } ` @@ -184,6 +227,7 @@ const compEventScriptTmpl = ` const componentInitScriptTmpl = ` this.root = %s; this.store = new __sui_store(this.root); + this.props = new __sui_props(this.root); ` // Inject code diff --git a/sui/core/jit.go b/sui/core/jit.go index 26787220..185b4d5d 100644 --- a/sui/core/jit.go +++ b/sui/core/jit.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/PuerkitoBio/goquery" + "github.com/hashicorp/go-multierror" jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/application" ) @@ -19,6 +20,7 @@ type JitComponent struct { html string scripts []ScriptNode styles []StyleNode + imports map[string]string buildOption *BuildOption } @@ -39,6 +41,7 @@ var chComp = make(chan *componentData, 1) var reScripts = regexp.MustCompile(`]*name="scripts"[^>]*>(.*?)`) var reStyles = regexp.MustCompile(`]*name="styles"[^>]*>(.*?)`) var reOption = regexp.MustCompile(`]*name="option"[^>]*>(.*?)`) +var reImports = regexp.MustCompile(`]*name="imports"[^>]*>(.*?)`) func init() { go componentWriter() @@ -47,119 +50,222 @@ func init() { // parseComponent parse the component func (parser *TemplateParser) parseJitComponent(sel *goquery.Selection) { parser.parsed(sel) - comp, props, slots, children, err := parser.getComponent(sel) + comp, err := parser.getJitComponent(sel) if err != nil { parser.errors = append(parser.errors, err) setError(sel, err) return } - html, _, err := parser.RenderComponent(comp, props, slots, children) + comsel, err := parser.newJitComponentSel(sel, comp) if err != nil { parser.errors = append(parser.errors, err) setError(sel, err) return } - - // fmt.Println(sel.Nodes[0].Attr) - sel.SetHtml(html) + parser.parseElementComponent(comsel) + sel.ReplaceWithSelection(comsel) } -// 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 +func (parser *TemplateParser) newJitComponentSel(sel *goquery.Selection, comp *JitComponent) (*goquery.Selection, error) { - fmt.Println("---", comp.route, "----------------") - fmt.Println(html) - fmt.Println("-------------------------------") - fmt.Println() - html = replaceRandVar(html, Data(props)) - fmt.Println("---- after replaceRandVar ----------------") - fmt.Println() - fmt.Println(html) - fmt.Println("-------------------------------") - fmt.Println() - - 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) + ns := Namespace(comp.route, parser.sequence+1, comp.buildOption.ScriptMinify) cn := ComponentName(comp.route, comp.buildOption.ScriptMinify) - - sel, err := NewDocumentString(`` + html + ``) - if err != nil { - return "", "", err + props := map[string]string{ + "s:ns": ns, + "s:cn": cn, + "s:ready": cn + "()", + } + data := Data{} + for _, attr := range sel.Nodes[0].Attr { + if attr.Key == "is" || attr.Key == "s:jit" { + continue + } + props[attr.Key] = attr.Val } - root := sel.Find("body") + // JSON props + for key, val := range props { + var v interface{} = val + if props[fmt.Sprintf("json-attr-%s", key)] == "true" { + fmt.Println("json-attr-", key) + err := jsoniter.UnmarshalFromString(val, &v) + if err != nil { + v = fmt.Sprintf(`"%s"`, val) + } + } + data[key] = v + } + + doc, err := NewDocumentString(comp.html) + if err != nil { + return nil, fmt.Errorf("Component %s failed to load, please recompile the component. %s", comp.route, err.Error()) + } + compSel := doc.Find("body").Children().First() + data.replaceNodeUse(propTokens, compSel.Nodes[0]) + for key, val := range props { + if strings.HasPrefix(key, "s:") || strings.HasPrefix(key, "json-attr-") || key == "parsed" { + compSel.SetAttr(key, val) + continue + } + compSel.SetAttr(fmt.Sprintf("prop:%s", key), val) + } // Replace the slots - slots.Each(func(i int, s *goquery.Selection) { - name := s.AttrOr("name", "") - if name == "" { - return - } + slots := sel.Find("slot") + if slots.Length() > 0 { + for i := 0; i < slots.Length(); i++ { + name := slots.Eq(i).AttrOr("name", "") + if name == "" { + continue + } - // Find the slot - slotSel := root.Find(name) - if slotSel.Length() == 0 { - return + slots.Eq(i).Remove() + slotSel := compSel.Find(name) + if slotSel.Length() == 0 { + continue + } + slotSel.ReplaceWithSelection(slots.Eq(i).Contents()) } - - // Replace the slot - slotSel.ReplaceWithSelection(s.Contents()) - }) + } // Replace the children - children.Find("slot").Remove() - root.Find("children").ReplaceWithSelection(children) + children := sel.Contents() + compSel.Find("children").ReplaceWithSelection(children) + return compSel, nil +} + +func (parser *TemplateParser) getJitComponent(sel *goquery.Selection) (*JitComponent, error) { + is := sel.AttrOr("is", "") + if is == "" { + return nil, fmt.Errorf("Component route is required") - // Replace the props - if locale != nil { - locale.replaceVars(Data(props)) } - compParser.locale = locale - compParser.BindEvent(root, ns, cn) - compParser.RenderSelection(root) - - if compParser.scripts != nil { - for _, script := range compParser.scripts { - script.Namespace = ns - parser.scripts = append(parser.scripts, script) - } + if parser.option == nil { + parser.option = &ParserOption{Debug: true, DisableCache: false} } - if comp.scripts != nil { - for _, script := range comp.scripts { - script.Namespace = ns - parser.scripts = append(parser.scripts, script) - } + // Load the component + if comp, has := Components[is]; has && parser.option.Debug == false && parser.option.DisableCache == false { + return comp, nil } - if comp.styles != nil { - for _, style := range comp.styles { - style.Namespace = ns - parser.styles = append(parser.styles, style) - } + file := filepath.Join(string(os.PathSeparator), "public", parser.option.Root, is+".jit") + if exist, _ := application.App.Exists(file); !exist { + return nil, fmt.Errorf("Component %s file not found, please recompile the component", is) } - // Update sequence - parser.sequence = compParser.sequence + parser.sequence - first := root.Children().First() - first.SetAttr("s:ns", ns) - first.SetAttr("s:cn", cn) - first.SetAttr("s:ready", cn+"()") - - html, err = root.Html() + source, err := application.App.Read(file) if err != nil { - return "", "", err + return nil, fmt.Errorf("Component %s failed to load, please recompile the component", is) } - return html, ns, nil + + // Get the scripts + var scriptnodes []ScriptNode = []ScriptNode{} + var stylenodes []StyleNode = []StyleNode{} + var imports map[string]string = map[string]string{} + var buildOption *BuildOption = &BuildOption{} + + source, scriptnodes, err = parser.getScriptNodes(source) + if err != nil { + return nil, fmt.Errorf("Component %s failed to load, please recompile the component. %s", is, err.Error()) + } + + source, stylenodes, err = parser.getStyleNodes(source) + if err != nil { + return nil, fmt.Errorf("Component %s failed to load, please recompile the component. %s", is, err.Error()) + } + + source, imports, err = parser.getImports(source) + if err != nil { + return nil, fmt.Errorf("Component %s failed to load, please recompile the component. %s", is, err.Error()) + } + + source, buildOption, err = parser.getBuildOption(source) + if err != nil { + return nil, fmt.Errorf("Component %s failed to load, please recompile the component. %s", is, err.Error()) + } + + comp := &JitComponent{ + file: file, + route: is, + html: string(source), + scripts: scriptnodes, + styles: stylenodes, + imports: imports, + buildOption: buildOption, + } + + // Save the component to the cache + chComp <- &componentData{is, comp, saveComponent} + return comp, nil +} + +func (parser *TemplateParser) getImports(source []byte) ([]byte, map[string]string, error) { + imports := map[string]string{} + source = reImports.ReplaceAllFunc(source, func(raw []byte) []byte { + raw = reImports.ReplaceAll(raw, []byte("$1")) + err := jsoniter.Unmarshal(raw, &imports) + if err != nil { + return nil + } + return []byte{} + }) + return source, imports, nil +} + +func (parser *TemplateParser) getBuildOption(source []byte) ([]byte, *BuildOption, error) { + var buildOption BuildOption + rawOption := []byte{} + source = reOption.ReplaceAllFunc(source, func(raw []byte) []byte { + rawOption = reOption.ReplaceAll(raw, []byte("$1")) + return []byte{} + }) + + if rawOption != nil { + err := jsoniter.Unmarshal(rawOption, &buildOption) + if err != nil { + return source, nil, err + } + } + return source, &buildOption, nil +} + +func (parser *TemplateParser) getStyleNodes(source []byte) ([]byte, []StyleNode, error) { + var errs error + nodes := []StyleNode{} + source = reStyles.ReplaceAllFunc(source, func(raw []byte) []byte { + raw = reStyles.ReplaceAll(raw, []byte("$1")) + var stylenodes []StyleNode + err := jsoniter.Unmarshal(raw, &stylenodes) + if err != nil { + errs = multierror.Append(errs, err) + return nil + } + nodes = append(nodes, stylenodes...) + return []byte{} + }) + return source, nodes, errs +} + +func (parser *TemplateParser) getScriptNodes(source []byte) ([]byte, []ScriptNode, error) { + var errs error + nodes := []ScriptNode{} + + // Get the scripts + source = reScripts.ReplaceAllFunc(source, func(raw []byte) []byte { + raw = reScripts.ReplaceAll(raw, []byte("$1")) + var scripts []ScriptNode + err := jsoniter.Unmarshal(raw, &scripts) + if err != nil { + errs = multierror.Append(errs, err) + return nil + } + nodes = append(nodes, scripts...) + return []byte{} + }) + return source, nodes, errs } func (parser *TemplateParser) filterScripts(parent string, scripts []ScriptNode) []ScriptNode { @@ -201,29 +307,6 @@ func (parser *TemplateParser) addStyles(sel *goquery.Selection, styles []StyleNo } } -func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponent, map[string]interface{}, *goquery.Selection, *goquery.Selection, error) { - - slots := sel.Find("slot") - children := sel.Contents() - - props, err := parser.componentProps(sel) - if err != nil { - return nil, nil, slots, children, err - } - - file, route, err := parser.componentFile(sel, props) - if err != nil { - return nil, props, slots, children, err - } - - comp, err := getComponent(route, file, parser.disableCache()) - if err != nil { - return nil, props, slots, children, err - } - - return comp, props, slots, children, nil -} - // isJitComponent check if the selection is a component func (parser *TemplateParser) isJitComponent(sel *goquery.Selection) bool { _, exist := sel.Attr("s:jit") @@ -231,113 +314,6 @@ func (parser *TemplateParser) isJitComponent(sel *goquery.Selection) bool { return exist && is != "" } -func (parser *TemplateParser) componentProps(sel *goquery.Selection) (map[string]interface{}, error) { - - parentProps := map[string]string{} - props := map[string]string{} - parent := sel.AttrOr("s:parent", "") - parentSel := sel.Parents().Find(fmt.Sprintf(`[s\:ns="%s"]`, parent)) - - if parentSel != nil && parentSel.Length() > 0 { - for _, attr := range parentSel.Nodes[0].Attr { - - if !strings.HasPrefix(attr.Key, "s:prop") { - continue - } - key := strings.TrimPrefix(attr.Key, "s:prop:") - parentProps[key] = attr.Val - } - } - - 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 - } - - if strings.HasPrefix(attr.Key, "...$props") { - data := Data{"$props": parentProps} - values, _, err := data.Exec(fmt.Sprintf("{{ %s }}", strings.TrimPrefix(attr.Key, "..."))) - if err != nil { - return map[string]interface{}{}, err - } - if values == nil { - continue - } - - if _, ok := values.(map[string]string); ok { - for key, val := range values.(map[string]string) { - props[key] = val - } - } - continue - } - - props[attr.Key] = attr.Val - } - - return parser.parseComponentProps(props) -} - -func (parser *TemplateParser) parseComponentProps(props map[string]string) (map[string]interface{}, error) { - result := map[string]interface{}{} - for key, val := range props { - if strings.HasPrefix(key, "...") { - values, _, err := parser.data.Exec(fmt.Sprintf("{{ %s }}", strings.TrimPrefix(key, "..."))) - if err != nil { - return nil, err - } - - if values == nil { - continue - } - - if _, ok := values.(map[string]interface{}); ok { - for k := range values.(map[string]interface{}) { - result[k] = k - } - } - continue - } - - result[key] = val - } - return result, nil -} - -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") - } - - data := Data{"$props": props} - route, _ = data.ReplaceUse(dataTokens, route) - route, _ = parser.data.Replace(route) - file := filepath.Join(string(os.PathSeparator), "public", parser.option.Root, route+".jit") - return file, route, nil -} - -func (locale *Locale) replaceVars(data Data) { - if locale.Keys != nil { - for key, val := range locale.Keys { - locale.Keys[key] = replaceRandVar(val, data) - } - } - - if locale.Messages != nil { - for key, val := range locale.Messages { - locale.Messages[key] = replaceRandVar(val, data) - } - } -} - func componentWriter() { for { select { @@ -351,110 +327,3 @@ func componentWriter() { } } } - -func getComponent(route string, file string, disableCache ...bool) (*JitComponent, error) { - - noCache := false - if len(disableCache) > 0 && disableCache[0] { - noCache = true - } - - if comp, ok := Components[file]; ok && !noCache { - return comp, nil - } - - comp, err := readComponent(route, file) - if err != nil { - return nil, err - } - - // Save the component to the cache - chComp <- &componentData{file, comp, saveComponent} - return comp, nil -} - -// readComponent read the JIT component -func readComponent(route string, file string) (*JitComponent, error) { - content, err := application.App.Read(file) - if err != nil { - return nil, err - } - - // 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 -} - -func replaceRandVar(value string, data Data) string { - - return propTokens.ReplaceAllStringFunc(value, func(exp string) string { - if strings.HasPrefix(exp, "[{") && strings.HasSuffix(exp, "}]") { - matches := propTokens.FindAllStringSubmatch(exp, -1) - if len(matches) > 0 { - identifiers, err := data.Identifiers(matches[0][1]) - if err != nil { - return err.Error() - } - for _, identifier := range identifiers { - exp = strings.ReplaceAll(exp, identifier.Value, strings.ReplaceAll(identifier.Value, "$props.", "")) - } - } - - } - res := data.ExecString(exp) - if res.Error != nil { - return res.Error.Error() - } - return res.Value - }) -} diff --git a/sui/core/parser.go b/sui/core/parser.go index df18eb51..28f2b04f 100644 --- a/sui/core/parser.go +++ b/sui/core/parser.go @@ -256,16 +256,17 @@ func (parser *TemplateParser) parseElementNode(sel *goquery.Selection) { func (parser *TemplateParser) parseElementComponent(sel *goquery.Selection) { - sel.SetAttr("parsed", "true") + parser.parsed(sel) com := sel.AttrOr("s:cn", "") props := map[string]interface{}{} for _, attr := range sel.Nodes[0].Attr { - if !strings.HasPrefix(attr.Key, "s:") && attr.Key != "parsed" { + if strings.HasPrefix(attr.Key, "prop:") { var val any = attr.Val + var key = strings.TrimPrefix(attr.Key, "prop:") if _, exist := sel.Attr("json-attr-" + attr.Key); exist { val = ValueJSON(attr.Val) } - props[attr.Key] = val + props[key] = val } } @@ -299,6 +300,11 @@ func (parser *TemplateParser) parseElementComponent(sel *goquery.Selection) { compParser.data[k] = v } } + raw, err := jsoniter.MarshalToString(data) + if err != nil { + raw = fmt.Sprintf(`"%s"`, err.Error()) + } + sel.SetAttr("json:__component_data", raw) } err = compParser.RenderSelection(sel) @@ -307,6 +313,7 @@ func (parser *TemplateParser) parseElementComponent(sel *goquery.Selection) { setError(sel, err) } parser.sequence = compParser.sequence + 1 + } func (parser *TemplateParser) clone(script *Script) *TemplateParser { diff --git a/sui/core/request.go b/sui/core/request.go index 78163b5f..62a8c8d9 100644 --- a/sui/core/request.go +++ b/sui/core/request.go @@ -126,7 +126,11 @@ func (r *Request) ExecString(data string) (Data, error) { if err != nil { return nil, err } - r.Exec(res) + err = r.Exec(res) + if err != nil { + return nil, err + } + return res, nil } @@ -288,7 +292,11 @@ func (r *Request) call(p interface{}) (interface{}, error) { // Call the backend script if r.Script != nil && strings.HasPrefix(processName, "@") { method := processName[1:] - return r.Script.Call(r, method, processArgs...) + v, err := r.Script.Call(r, method, processArgs...) + if err != nil { + return nil, fmt.Errorf("backend script %s %s, please check the script", method, err.Error()) + } + return v, nil } process, err := process.Of(processName, processArgs...) diff --git a/sui/core/script.go b/sui/core/script.go index 3f1f015d..725ff788 100644 --- a/sui/core/script.go +++ b/sui/core/script.go @@ -54,11 +54,11 @@ func scriptWriter() { // LoadScript load the script func LoadScript(file string) (*Script, error) { - if script, has := Scripts[file]; has { + base := strings.TrimSuffix(strings.TrimSuffix(file, ".sui"), ".jit") + if script, has := Scripts[base]; has { return script, nil } - base := strings.TrimSuffix(file, ".sui") file = base + ".ts" if exist, _ := application.App.Exists(file); !exist { file = base + ".js" @@ -79,7 +79,7 @@ func LoadScript(file string) (*Script, error) { } script := &Script{Script: v8script} - chScript <- &scriptData{file, script, saveScript} + chScript <- &scriptData{base, script, saveScript} return script, nil }