Merge pull request #711 from trheyi/main

Optimize expression parsing in SUI
This commit is contained in:
Max 2024-07-26 10:09:00 +08:00 committed by GitHub
commit e866243cf2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 160 additions and 64 deletions

View file

@ -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 {
@ -369,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 == "" {

View file

@ -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
}

View file

@ -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
})
}

View file

@ -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

View file

@ -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 {

View file

@ -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
}