Skip to content

Commit

Permalink
Merge pull request #711 from trheyi/main
Browse files Browse the repository at this point in the history
Optimize expression parsing in SUI
  • Loading branch information
trheyi authored Jul 26, 2024
2 parents e7290f7 + 43e37fb commit e866243
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 64 deletions.
9 changes: 5 additions & 4 deletions sui/core/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 == "" {
Expand Down
149 changes: 108 additions & 41 deletions sui/core/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -23,13 +24,52 @@ 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),
expr.Function("False", _false),
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()
Expand Down Expand Up @@ -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

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

}

Expand Down
12 changes: 6 additions & 6 deletions sui/core/jit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
})
}
31 changes: 19 additions & 12 deletions sui/core/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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
Expand All @@ -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-"), "")
Expand All @@ -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{
Expand All @@ -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")
}
}
}
}
Expand All @@ -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 != "" {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit e866243

Please sign in to comment.