Merge pull request #711 from trheyi/main
Optimize expression parsing in SUI
This commit is contained in:
commit
e866243cf2
6 changed files with 160 additions and 64 deletions
|
|
@ -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 == "" {
|
||||
|
|
|
|||
149
sui/core/data.go
149
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
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue