From 7ea9953b693a2ba46aa2236a1e625ac81dbc8492 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 8 Jul 2024 00:43:14 +0800 Subject: [PATCH] [add] SUI component dynamic loading --- sui/core/jit.go | 247 +++++++++++++++++++++++++++++++++++++++++++++ sui/core/parser.go | 53 ++-------- 2 files changed, 258 insertions(+), 42 deletions(-) create mode 100644 sui/core/jit.go diff --git a/sui/core/jit.go b/sui/core/jit.go new file mode 100644 index 00000000..e9774f80 --- /dev/null +++ b/sui/core/jit.go @@ -0,0 +1,247 @@ +package core + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/PuerkitoBio/goquery" + "github.com/yaoapp/gou/application" +) + +// JitComponent component +type JitComponent struct { + html string + scripts []ScriptNode + styles []StyleNode +} + +const ( + saveComponent uint8 = iota + removeComponent +) + +type componentData struct { + file string + comp *JitComponent + cmd uint8 +} + +// Components loaded JIT components +var Components = map[string]*JitComponent{} +var chComp = make(chan *componentData, 1) + +func init() { + go componentWriter() +} + +// parseComponent parse the component +func (parser *TemplateParser) parseComponent(sel *goquery.Selection) { + comp, props, slots, err := parser.getComponent(sel) + if err != nil { + parser.errors = append(parser.errors, err) + setError(sel, err) + return + } + + 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) { + html := comp.html + randvar := fmt.Sprintf("__%s_$props", time.Now().Format("20060102150405")) + html = slotRe.ReplaceAllStringFunc(html, func(exp string) string { + exp = strings.ReplaceAll(exp, "[{", "{{") + exp = strings.ReplaceAll(exp, "}]", "}}") + exp = strings.ReplaceAll(exp, "$props", randvar) + return exp + }) + + data := Data{} + data[randvar] = props + if parser.data != nil { + for key, val := range parser.data { + data[key] = val + } + } + + sel, err := NewDocumentString(`` + html + ``) + if err != nil { + return "", err + } + + root := sel.Find("body") + + // Replace the slots + slots.Each(func(i int, s *goquery.Selection) { + name := s.AttrOr("name", "") + if name == "" { + return + } + + // Find the slot + slotSel := root.Find(fmt.Sprintf("slot[name='%s']", name)) + if slotSel.Length() == 0 { + return + } + + // Replace the slot + slotSel.ReplaceWithNodes(s.Contents().Nodes...) + }) + + option := *parser.option + compParser := NewTemplateParser(data, &option) + + // Parse the node + compParser.parseNode(root.Get(0)) + for sel, nodes := range compParser.replace { + sel.ReplaceWithNodes(nodes...) + delete(parser.replace, sel) + } + + // Update sequence + parser.sequence = compParser.sequence + parser.sequence + return root.Html() +} + +func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponent, map[string]interface{}, *goquery.Selection, error) { + + slots := sel.Find("slot") + + props, err := parser.componentProps(sel) + if err != nil { + return nil, nil, slots, err + } + + file, err := parser.componentFile(sel, props) + if err != nil { + return nil, props, slots, err + } + + comp, err := getComponent(file, parser.disableCache()) + if err != nil { + return nil, props, slots, err + } + + return comp, props, slots, nil +} + +// isComponent check if the selection is a component +func (parser *TemplateParser) isComponent(sel *goquery.Selection) bool { + _, exist := sel.Attr("s:jit") + is := sel.AttrOr("is", "") + return exist && is != "" +} + +func (parser *TemplateParser) componentProps(sel *goquery.Selection) (map[string]interface{}, error) { + props := map[string]string{} + parentSel := sel.Parent() + if parentSel == nil { + return map[string]interface{}{}, nil + } + + for _, attr := range parentSel.Nodes[0].Attr { + if !strings.HasPrefix(attr.Key, "s:prop") { + continue + } + key := strings.TrimPrefix(attr.Key, "s:prop:") + props[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 }}", val)) + if err != nil { + return nil, err + } + + if values == nil { + continue + } + + for k, v := range values.(map[string]interface{}) { + result[k] = v + } + + continue + } + value, err := parser.data.Exec(val) + if err != nil { + return nil, err + } + result[key] = value + } + return result, nil +} + +func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[string]interface{}) (string, error) { + route := sel.AttrOr("is", "") + if route == "" { + return "", fmt.Errorf("Component route is required") + } + + 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") + return file, nil +} + +func componentWriter() { + for { + select { + case data := <-chComp: + switch data.cmd { + case saveComponent: + Components[data.file] = data.comp + case removeCache: + delete(Components, data.file) + } + } + } +} + +func getComponent(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(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(file string) (*JitComponent, error) { + content, err := application.App.Read(file) + if err != nil { + return nil, err + } + return &JitComponent{html: string(content)}, nil +} diff --git a/sui/core/parser.go b/sui/core/parser.go index 7e6e2d54..e9789bb4 100644 --- a/sui/core/parser.go +++ b/sui/core/parser.go @@ -2,7 +2,6 @@ package core import ( "fmt" - "os" "path/filepath" "strings" @@ -279,8 +278,8 @@ func (parser *TemplateParser) parseElementNode(sel *goquery.Selection) { } // JIT Compile the element - if _, exist := sel.Attr("s:jit"); exist { - parser.parseJitElementNode(sel) + if parser.isComponent(sel) { + parser.parseComponent(sel) } // Parse the attributes @@ -290,45 +289,6 @@ func (parser *TemplateParser) parseElementNode(sel *goquery.Selection) { parser.transElementNode(sel) } -func (parser *TemplateParser) parseJitElementNode(sel *goquery.Selection) { - - is := sel.AttrOr("is", "") - if is == "" { - sel.Remove() - return - } - - parser.parsed(sel) - // Render the JIT component Data - is, _ = parser.data.Replace(is) - root := sel.AttrOr("s:root", "/") - file := filepath.Join(string(os.PathSeparator), "public", root, is+".jit") - - // Should be cached to reduce the file read and unnecessary parsing - content, err := application.App.Read(file) - if err != nil { - log.Error("[parser] %s JIT %s", file, err.Error()) - setError(sel, err) - return - } - sel.ReplaceWith(string(content)) - - // With Properties - - // // copy options - // option := *parser.option - // option.Component = true - // p := NewTemplateParser(parser.data, &option) - // html, err := p.Render(string(content)) - // if err != nil { - // log.Error("[parser] %s JIT %s", file, err.Error()) - // return - // } - - // // Replace the node - // sel.ReplaceWithHtml(html) -} - func (parser *TemplateParser) transElementNode(sel *goquery.Selection) { if parser.locale == nil { return @@ -694,6 +654,11 @@ func (parser *TemplateParser) tidy(s *goquery.Selection) { return } + if node.Data == "s:set" { + child.Remove() + return + } + if node.Type == html.CommentNode { child.Remove() return @@ -748,6 +713,10 @@ func (parser *TemplateParser) debug() bool { return parser.option != nil && parser.option.Debug } +func (parser *TemplateParser) disableCache() bool { + return (parser.option != nil && parser.option.DisableCache) || parser.debug() +} + func (parser *TemplateParser) toArray(value interface{}) ([]interface{}, error) { switch values := value.(type) {