From b6c5c0fd315669a50d8a9c6c6afae91cd152cc0c Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 26 Jul 2024 08:03:23 +0800 Subject: [PATCH 1/2] Refactor SUI core to set attribute values in parseProps function --- sui/core/build.go | 1 + 1 file changed, 1 insertion(+) diff --git a/sui/core/build.go b/sui/core/build.go index 4e8b392c..da0f9f78 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -341,6 +341,7 @@ func (page *Page) parseProps(from *goquery.Selection, to *goquery.Selection, ext exp := stmtRe.Match([]byte(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) } if extra != nil && len(extra) > 0 { From 43e37fbfe496284076cbb24c5ba4a5950f6512c1 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 26 Jul 2024 10:01:41 +0800 Subject: [PATCH 2/2] Optimize expression parsing in SUI --- sui/core/build.go | 8 +-- sui/core/data.go | 149 ++++++++++++++++++++++++++++++++------------- sui/core/jit.go | 12 ++-- sui/core/parser.go | 31 ++++++---- sui/core/script.go | 2 +- sui/core/utils.go | 21 +++++++ 6 files changed, 159 insertions(+), 64 deletions(-) diff --git a/sui/core/build.go b/sui/core/build.go index da0f9f78..b990208f 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -370,13 +370,13 @@ func (page *Page) replacePropsText(text string, data Data) (string, []string) { matched := PropFindAllStringSubmatch(text) for _, match := range matched { stmt := match[1] - val, err := data.ExecString(stmt) - if err != nil { - log.Error("[replaceProps] Replace %s: %s", stmt, err) + val := data.ExecString(stmt) + if val.Error != nil { + log.Error("[replaceProps] Replace %s: %s", stmt, val.Error) continue } - text = strings.ReplaceAll(text, match[0], val) + text = strings.ReplaceAll(text, match[0], val.Value) vars := PropGetVarNames(stmt) for _, v := range vars { if v == "" { diff --git a/sui/core/data.go b/sui/core/data.go index cfcbd216..17aab80e 100644 --- a/sui/core/data.go +++ b/sui/core/data.go @@ -8,9 +8,10 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/expr-lang/expr" + "github.com/expr-lang/expr/ast" "github.com/expr-lang/expr/vm" + jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/process" - "github.com/yaoapp/kun/log" "golang.org/x/net/html" ) @@ -23,6 +24,27 @@ var propVarNameRe = regexp.MustCompile(`(?:\$props\.)?(?:$begin:math:display$'([ // Data data for the template type Data map[string]interface{} +// Identifier the identifier +type Identifier struct { + Value string + Type string +} + +// Visitor the visitor +type Visitor struct { + Identifiers []Identifier +} + +// StringValue the string value +type StringValue struct { + Value string + Stmt string + Data interface{} + JSON bool + Identifiers []Identifier + Error error +} + var options = []expr.Option{ expr.Function("P_", _process), expr.Function("True", _true), @@ -30,6 +52,24 @@ var options = []expr.Option{ expr.AllowUndefinedVariables(), } +// Visit visit the node +func (v *Visitor) Visit(node *ast.Node) { + if n, ok := (*node).(*ast.IdentifierNode); ok { + typ := "unknown" + t := n.Type() + if t != nil { + typ = t.Name() + if typ == "" { + typ = "json" + } + } + v.Identifiers = append(v.Identifiers, Identifier{ + Value: n.Value, + Type: typ, + }) + } +} + // Hash get the hash of the data func (data Data) Hash() string { h := fnv.New64a() @@ -64,76 +104,104 @@ func (data Data) New(stmt string) (*vm.Program, error) { } // Exec exec statement for the template -func (data Data) Exec(stmt string) (interface{}, error) { +func (data Data) Exec(stmt string) (interface{}, []Identifier, error) { program, err := data.New(stmt) if err != nil { - return nil, err + return nil, nil, err } - return expr.Run(program, data) + + node := program.Node() + v := &Visitor{} + ast.Walk(&node, v) + + res, err := expr.Run(program, data) + if err != nil { + return nil, nil, err + } + + return res, v.Identifiers, nil } // ExecString exec statement for the template -func (data Data) ExecString(stmt string) (string, error) { +func (data Data) ExecString(stmt string) StringValue { - res, err := data.Exec(stmt) + str := StringValue{Stmt: stmt, Value: "", JSON: false, Identifiers: []Identifier{}, Error: nil} + res, identifiers, err := data.Exec(stmt) if err != nil { - return "", err + str.Error = err + return str } if res == nil { - return "", nil + return str } - if v, ok := res.(string); ok { - return v, nil + str.Data = res + switch v := res.(type) { + case string: + str.Value = v + break + case []byte: + str.Value = string(v) + break + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, bool: + str.Value = fmt.Sprintf("%v", v) + break + default: + res, err := jsoniter.MarshalToString(res) + if err != nil { + str.Error = err + break + } + str.Value = res + str.JSON = true } - return fmt.Sprintf("%v", res), nil + + str.Identifiers = identifiers + return str } // Replace replace the statement -func (data Data) Replace(value string) (string, bool) { +func (data Data) Replace(value string) (string, []StringValue) { return data.ReplaceUse(stmtRe, value) } // ReplaceUse replace the statement use the regexp -func (data Data) ReplaceUse(re *regexp.Regexp, value string) (string, bool) { - hasStmt := false +func (data Data) ReplaceUse(re *regexp.Regexp, value string) (string, []StringValue) { + values := []StringValue{} res := re.ReplaceAllStringFunc(value, func(stmt string) string { - hasStmt = true - res, err := data.ExecString(stmt) - if err != nil { - log.Warn("Replace %s: %s", stmt, err) - } - return res + v := data.ExecString(stmt) + values = append(values, v) + return v.Value }) - return res, hasStmt + return res, values } // ReplaceSelection replace the statement in the selection -func (data Data) ReplaceSelection(sel *goquery.Selection) bool { +func (data Data) ReplaceSelection(sel *goquery.Selection) []StringValue { return data.ReplaceSelectionUse(stmtRe, sel) } // ReplaceSelectionUse replace the statement in the selection use the regexp -func (data Data) ReplaceSelectionUse(re *regexp.Regexp, sel *goquery.Selection) bool { - hasStmt := false +func (data Data) ReplaceSelectionUse(re *regexp.Regexp, sel *goquery.Selection) []StringValue { + res := []StringValue{} for _, node := range sel.Nodes { - ok := data.replaceNodeUse(re, node) - if ok { - hasStmt = true + values := data.replaceNodeUse(re, node) + if len(values) > 0 { + res = append(res, values...) } } - return hasStmt + return res } -func (data Data) replaceNodeUse(re *regexp.Regexp, node *html.Node) bool { - hasStmt := false +func (data Data) replaceNodeUse(re *regexp.Regexp, node *html.Node) []StringValue { + res := []StringValue{} switch node.Type { case html.TextNode: - v, ok := data.ReplaceUse(re, node.Data) + v, values := data.ReplaceUse(re, node.Data) node.Data = v - if ok { - hasStmt = true + if len(values) > 0 { + res = append(res, values...) } break @@ -144,23 +212,22 @@ func (data Data) replaceNodeUse(re *regexp.Regexp, node *html.Node) bool { continue } - v, ok := data.ReplaceUse(re, node.Attr[i].Val) + v, values := data.ReplaceUse(re, node.Attr[i].Val) node.Attr[i].Val = v - if ok { - hasStmt = true + if len(values) > 0 { + res = append(res, values...) } } for c := node.FirstChild; c != nil; c = c.NextSibling { - ok := data.replaceNodeUse(re, c) - if ok { - hasStmt = true + values := data.replaceNodeUse(re, c) + if len(values) > 0 { + res = append(res, values...) } } break } - - return hasStmt + return res } diff --git a/sui/core/jit.go b/sui/core/jit.go index 32d3a590..a0673f3f 100644 --- a/sui/core/jit.go +++ b/sui/core/jit.go @@ -257,7 +257,7 @@ func (parser *TemplateParser) componentProps(sel *goquery.Selection) (map[string if strings.HasPrefix(attr.Key, "...$props") { data := Data{"$props": parentProps} - values, err := data.Exec(fmt.Sprintf("{{ %s }}", strings.TrimPrefix(attr.Key, "..."))) + values, _, err := data.Exec(fmt.Sprintf("{{ %s }}", strings.TrimPrefix(attr.Key, "..."))) if err != nil { return map[string]interface{}{}, err } @@ -283,7 +283,7 @@ func (parser *TemplateParser) parseComponentProps(props map[string]string) (map[ result := map[string]interface{}{} for key, val := range props { if strings.HasPrefix(key, "...") { - values, err := parser.data.Exec(fmt.Sprintf("{{ %s }}", strings.TrimPrefix(key, "..."))) + values, _, err := parser.data.Exec(fmt.Sprintf("{{ %s }}", strings.TrimPrefix(key, "..."))) if err != nil { return nil, err } @@ -434,13 +434,13 @@ 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 + res := data.ExecString(fmt.Sprintf("{{ %s }}", exp)) + return res.Value }) data = Data{"$props": data} return slotRe.ReplaceAllStringFunc(value, func(exp string) string { - res, _ := data.ExecString(exp) - return res + res := data.ExecString(exp) + return res.Value }) } diff --git a/sui/core/parser.go b/sui/core/parser.go index 4140705d..dd33a2e7 100644 --- a/sui/core/parser.go +++ b/sui/core/parser.go @@ -258,10 +258,14 @@ func (parser *TemplateParser) parseElementComponent(sel *goquery.Selection) { sel.SetAttr("parsed", "true") com := sel.AttrOr("s:cn", "") - props := map[string]string{} + props := map[string]interface{}{} for _, attr := range sel.Nodes[0].Attr { if !strings.HasPrefix(attr.Key, "s:") && attr.Key != "parsed" { - props[attr.Key] = attr.Val + var val any = attr.Val + if _, exist := sel.Attr("json-attr-" + attr.Key); exist { + val = ValueJSON(attr.Val) + } + props[attr.Key] = val } } @@ -480,7 +484,7 @@ func (parser *TemplateParser) setStatementNode(sel *goquery.Selection) { valueExp := sel.AttrOr("value", "") if stmtRe.MatchString(valueExp) { - val, err := parser.data.Exec(valueExp) + val, _, err := parser.data.Exec(valueExp) if err != nil { log.Warn("Set %s: %s", valueExp, err) parser.data[name] = valueExp @@ -507,7 +511,7 @@ func (parser *TemplateParser) parseElementAttrs(sel *goquery.Selection) { if strings.HasPrefix(attr.Key, "s:attr-") { parser.sequence = parser.sequence + 1 - val, _ := parser.data.Exec(attr.Val) + val, _, _ := parser.data.Exec(attr.Val) if v, ok := val.(bool); ok { if v { sel.SetAttr(strings.TrimPrefix(attr.Key, "s:attr-"), "") @@ -522,8 +526,8 @@ func (parser *TemplateParser) parseElementAttrs(sel *goquery.Selection) { } parser.sequence = parser.sequence + 1 - res, hasStmt := parser.data.Replace(attr.Val) - if hasStmt { + res, values := parser.data.Replace(attr.Val) + if values != nil && len(values) > 0 { bindings := strings.TrimSpace(attr.Val) key := fmt.Sprintf("%v", parser.sequence) parser.mapping[attr.Key] = Mapping{ @@ -534,6 +538,9 @@ func (parser *TemplateParser) parseElementAttrs(sel *goquery.Selection) { sel.SetAttr(attr.Key, res) bindname := fmt.Sprintf("s:bind:%s", attr.Key) sel.SetAttr(bindname, bindings) + if HasJSON(values) { + sel.SetAttr(fmt.Sprintf("json-attr-%s", attr.Key), "true") + } } } } @@ -553,9 +560,9 @@ func checkIsRawElement(node *html.Node) bool { func (parser *TemplateParser) parseTextNode(node *html.Node) { parser.transTextNode(node) // Translations parser.sequence = parser.sequence + 1 - res, hasStmt := parser.data.Replace(node.Data) + res, values := parser.data.Replace(node.Data) // Bind the variable to the parent node - if node.Parent != nil && hasStmt { + if node.Parent != nil && values != nil && len(values) > 0 { bindings := strings.TrimSpace(node.Data) key := fmt.Sprintf("%v", parser.sequence) if bindings != "" { @@ -585,7 +592,7 @@ func (parser *TemplateParser) forStatementNode(sel *goquery.Selection) { parser.hide(sel) // Hide loop node forAttr, _ := sel.Attr("s:for") - forItems, err := parser.data.Exec(forAttr) + forItems, _, err := parser.data.Exec(forAttr) if err != nil { parser.errors = append(parser.errors, err) return @@ -619,7 +626,7 @@ func (parser *TemplateParser) forStatementNode(sel *goquery.Selection) { // Copy the if Attr from the parent node if ifAttr, exists := new.Attr("s:if"); exists { - res, err := parser.data.Exec(ifAttr) + res, _, err := parser.data.Exec(ifAttr) if err != nil { parser.errors = append(parser.errors, fmt.Errorf("if statement %v error: %v", parser.sequence, err)) setError(new, err) @@ -684,7 +691,7 @@ func (parser *TemplateParser) ifStatementNode(sel *goquery.Selection) { } // show the node if the condition is true - res, err := parser.data.Exec(ifAttr) + res, _, err := parser.data.Exec(ifAttr) if err != nil { parser.errors = append(parser.errors, fmt.Errorf("if statement %v error: %v", parser.sequence, err)) return @@ -701,7 +708,7 @@ func (parser *TemplateParser) ifStatementNode(sel *goquery.Selection) { // else if for _, elifNode := range elifNodes { elifAttr := elifNode.AttrOr("s:elif", "") - res, err := parser.data.Exec(elifAttr) + res, _, err := parser.data.Exec(elifAttr) if err != nil { parser.errors = append(parser.errors, err) return diff --git a/sui/core/script.go b/sui/core/script.go index e16dc8d4..3f1f015d 100644 --- a/sui/core/script.go +++ b/sui/core/script.go @@ -100,7 +100,7 @@ func (script *Script) Call(r *Request, method string, args ...any) (interface{}, } // BeforeRender the script method -func (script *Script) BeforeRender(r *Request, props map[string]string) (Data, error) { +func (script *Script) BeforeRender(r *Request, props map[string]interface{}) (Data, error) { ctx, err := script.NewContext(r.Sid, nil) if err != nil { diff --git a/sui/core/utils.go b/sui/core/utils.go index e4a505cb..6ec0794a 100644 --- a/sui/core/utils.go +++ b/sui/core/utils.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/PuerkitoBio/goquery" + jsoniter "github.com/json-iterator/go" "golang.org/x/net/html" ) @@ -110,3 +111,23 @@ func ToCamelCase(s string, split ...string) string { return strings.Join(parts, "") } + +// ValueJSON parse the value to a json value +func ValueJSON(value string) interface{} { + var v interface{} + err := jsoniter.UnmarshalFromString(value, &v) + if err != nil { + return fmt.Sprintf("json error: %s", err.Error()) + } + return v +} + +// HasJSON check if the values has json value +func HasJSON(values []StringValue) bool { + for _, value := range values { + if value.JSON { + return true + } + } + return false +}