diff --git a/go.mod b/go.mod index aeaf7413..8ee143bf 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( github.com/PuerkitoBio/goquery v1.8.1 + github.com/antonmedv/expr v1.15.4 github.com/blang/semver v3.5.1+incompatible github.com/caarlos0/env/v6 v6.10.1 github.com/dchest/captcha v1.0.0 @@ -26,6 +27,7 @@ require ( github.com/yaoapp/kun v0.9.0 github.com/yaoapp/xun v0.9.0 golang.org/x/crypto v0.15.0 + golang.org/x/net v0.18.0 golang.org/x/text v0.14.0 gopkg.in/yaml.v3 v3.0.1 rogchap.com/v8go v0.9.0 @@ -107,7 +109,6 @@ require ( golang.org/x/arch v0.6.0 // indirect golang.org/x/image v0.14.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.18.0 // indirect golang.org/x/oauth2 v0.14.0 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.14.0 // indirect diff --git a/go.sum b/go.sum index c5945c76..2a90307c 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/TylerBrock/colorjson v0.0.0-20200706003622-8a50f05110d2/go.mod h1:VSw github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/antonmedv/expr v1.15.4 h1:CrNads8WDnDVJNWt/FeUINBO+vDNjurEwT7SoQN132o= +github.com/antonmedv/expr v1.15.4/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= diff --git a/sui/core/data.go b/sui/core/data.go index 862c4174..88876546 100644 --- a/sui/core/data.go +++ b/sui/core/data.go @@ -2,209 +2,81 @@ package core import ( "fmt" + "regexp" "strings" - jsoniter "github.com/json-iterator/go" + "github.com/antonmedv/expr" + "github.com/antonmedv/expr/ast" + "github.com/antonmedv/expr/vm" "github.com/yaoapp/gou/process" - "github.com/yaoapp/kun/any" ) -// Data get the data -func (page *Page) Data(request *Request) (map[string]interface{}, map[string]interface{}, error) { +var stmtRe = regexp.MustCompile(`\{\{([^}]+)\}\}`) - setting := map[string]interface{}{ - "title": strings.ToUpper(page.Name), - } +// Data data for the template +type Data map[string]interface{} - if page.Codes.DATA.Code != "" { - err := jsoniter.UnmarshalFromString(page.Codes.DATA.Code, &setting) - if err != nil { - return nil, nil, err - } - } - return nil, setting, nil +var functions = map[string]*ast.Function{} + +var options = []expr.Option{ + expr.Function("P_", _process), + expr.AllowUndefinedVariables(), } -// Exec get the data -func (page *Page) Exec(request *Request) (map[string]interface{}, error) { +// New create a new expression +func (data Data) New(stmt string) (*vm.Program, error) { + stmt = strings.TrimSpace(strings.TrimRight(strings.TrimLeft(stmt, "{{ "), "}}")) + return expr.Compile(stmt, append([]expr.Option{expr.Env(data)}, options...)...) +} - if page.Codes.DATA.Code == "" { - return map[string]interface{}{}, nil +// Exec exec statement for the template +func (data Data) Exec(stmt string) (interface{}, error) { + program, err := data.New(stmt) + if err != nil { + return nil, err + } + return expr.Run(program, data) +} + +// ExecString exec statement for the template +func (data Data) ExecString(stmt string) (string, error) { + + res, err := data.Exec(stmt) + if err != nil { + return "", err } - data := map[string]interface{}{} - err := jsoniter.UnmarshalFromString(page.Codes.DATA.Code, &data) + if res == nil { + return "", nil + } + + if v, ok := res.(string); ok { + return v, nil + } + return fmt.Sprintf("%v", res), nil +} + +func _process(args ...any) (interface{}, error) { + + if len(args) < 1 { + return nil, fmt.Errorf("process should have at least one parameter") + } + + name, ok := args[0].(string) + if !ok { + return nil, fmt.Errorf("process function only accept string") + } + + args = append([]any{}, args[1:]...) + process, err := process.Of(name, args...) if err != nil { return nil, err } - err = page.execMap(request, data) + res, err := process.Exec() if err != nil { return nil, err } - return data, nil -} - -func (page *Page) execMap(r *Request, m map[string]interface{}) error { - - for key, value := range m { - - if strings.HasPrefix(key, "$") { - res, err := page.call(r, value) - if err != nil { - return err - } - newKey := key[1:] - m[newKey] = res - delete(m, key) - continue - } - - res, err := page.execValue(r, value) - if err != nil { - return err - } - m[key] = res - - } - - return nil -} - -func (page *Page) execValue(r *Request, value interface{}) (interface{}, error) { - switch v := value.(type) { - case string: - if strings.HasPrefix(v, "$") { - return page.call(r, strings.TrimLeft(v, "$")) - } - return v, nil - - case []interface{}: - for i, item := range v { - res, err := page.execValue(r, item) - if err != nil { - return nil, err - } - v[i] = res - } - return v, nil - - case []string: - interfaceSlice := make([]interface{}, len(v)) - for i, item := range v { - interfaceSlice[i] = item - } - return page.execValue(r, interfaceSlice) - - case map[string]interface{}: - - if _, ok := v["process"].(string); ok { - if call, _ := v["__exec"].(bool); call { - res, err := page.call(r, v) - if err != nil { - return nil, err - } - return res, nil - } - } - - err := page.execMap(r, v) - if err != nil { - return nil, err - } - return v, nil - - default: - return v, nil - } -} - -func (page *Page) call(r *Request, p interface{}) (interface{}, error) { - - processName := "" - processArgs := []interface{}{r} - switch v := p.(type) { - case string: - processName = v - break - - case map[string]interface{}: - if name, ok := v["process"].(string); ok { - processName = name - } - - if args, ok := v["args"].([]interface{}); ok { - args, err := page.parseArgs(r, args) - if err != nil { - return nil, err - } - processArgs = append(args, processArgs...) - } - } - - if processName == "" { - return nil, fmt.Errorf("process name is empty") - } - - process, err := process.Of(processName, processArgs...) - if err != nil { - return nil, err - } - - return process.Exec() -} - -func (page *Page) parseArgs(r *Request, args []interface{}) ([]interface{}, error) { - - data := any.MapOf(map[string]interface{}{ - "param": r.Params, - "query": r.Query, - "payload": map[string]interface{}{}, - "header": r.Headers, - "theme": r.Theme, - "locale": r.Locale, - }).Dot() - - for i, arg := range args { - switch v := arg.(type) { - - case string: - if strings.HasPrefix(v, "$") { - key := strings.TrimLeft(v, "$") - args[i] = key - if data.Has(key) { - v := data.Get(key) - if strings.HasPrefix(key, "query.") || strings.HasPrefix(key, "header.") { - switch arg := v.(type) { - case []interface{}: - if len(arg) == 1 { - args[i] = arg[0] - } - case []string: - if len(arg) == 1 { - args[i] = arg[0] - } - } - } - } - } - - case []interface{}: - res, err := page.parseArgs(r, v) - if err != nil { - return nil, err - } - args[i] = res - - case map[string]interface{}: - res, err := page.parseArgs(r, []interface{}{v}) - if err != nil { - return nil, err - } - args[i] = res[0] - } - } - - return args, nil + return res, nil } diff --git a/sui/core/editor.go b/sui/core/editor.go index 012d03c5..c9d41615 100644 --- a/sui/core/editor.go +++ b/sui/core/editor.go @@ -113,6 +113,11 @@ func (page *Page) EditorRender(request *Request) (*ResponseEditorRender, error) return res, nil } +// Render render for the html +func (page *Page) Render(html string, data map[string]interface{}, warnings []string) (string, error) { + return html, nil +} + // EditorPageSource get the editor page source code func (page *Page) EditorPageSource() SourceData { return SourceData{ diff --git a/sui/core/page.go b/sui/core/page.go index c4b4f1a2..04c9b76f 100644 --- a/sui/core/page.go +++ b/sui/core/page.go @@ -1,6 +1,10 @@ package core -import jsoniter "github.com/json-iterator/go" +import ( + "strings" + + jsoniter "github.com/json-iterator/go" +) // Get get the base info func (page *Page) Get() *Page { @@ -18,3 +22,34 @@ func (page *Page) GetConfig() *PageConfig { } return page.Config } + +// Data get the data +func (page *Page) Data(request *Request) (Data, map[string]interface{}, error) { + + setting := map[string]interface{}{ + "title": strings.ToUpper(page.Name), + } + + if page.Codes.DATA.Code != "" { + err := jsoniter.UnmarshalFromString(page.Codes.DATA.Code, &setting) + if err != nil { + return nil, nil, err + } + } + return nil, setting, nil +} + +// Exec get the data +func (page *Page) Exec(request *Request) (Data, error) { + + if page.Codes.DATA.Code == "" { + return map[string]interface{}{}, nil + } + + data, err := request.ExecString(page.Codes.DATA.Code) + if err != nil { + return nil, err + } + + return data, nil +} diff --git a/sui/core/data_test.go b/sui/core/page_test.go similarity index 72% rename from sui/core/data_test.go rename to sui/core/page_test.go index a39d7878..f032334b 100644 --- a/sui/core/data_test.go +++ b/sui/core/page_test.go @@ -50,40 +50,38 @@ func testPage(t *testing.T) *Page { File: "test.html", Code: `
For
-
+
{{ idx }} {{ article.title }}
{{ article.desc }}
{{ article.type == "article" ? "article" : "others"}}
article
image
-
others
+
others
IF
-
-
{{ articles.length > 0 }} articles.length 大于 0
+
+
{{ len(articles) > 0 }} articles.length > 0
+
+
+
{{ length > 0 }} len > 0
+
+
+ hello space {{ P_('scripts.article.Space', 'hello') }}
showImage
noImage
-
others
- +
autoImage
+
otherImage
Bind
{{ input.data }}
- +
- +
`, @@ -96,6 +94,7 @@ func testPage(t *testing.T) *Page { "process": "scripts.article.ShowImage", "args": ["$query.show"] }, + "length": 20, "array": [ "item-1", "$scripts.article.Setting", diff --git a/sui/core/render.go b/sui/core/render.go index 63845ad7..22b94987 100644 --- a/sui/core/render.go +++ b/sui/core/render.go @@ -1,6 +1,357 @@ package core -// Render render for the html -func (page *Page) Render(html string, data map[string]interface{}, warnings []string) (string, error) { - return html, nil +import ( + "bytes" + "fmt" + "strings" + + "github.com/PuerkitoBio/goquery" + "golang.org/x/net/html" +) + +// TemplateParser parser for the template +type TemplateParser struct { + data Data + mapping map[string]Mapping // variable mapping + sequence int // sequence for the rendering + errors []error // errors + replace map[*goquery.Selection][]*html.Node // replace nodes +} + +// Mapping mapping for the template +type Mapping struct { + Key string `json:"key,omitempty"` + Type string `json:"type,omitempty"` + Value interface{} `json:"value,omitempty"` +} + +// NewTemplateParser create a new template parser +func NewTemplateParser(data Data) *TemplateParser { + return &TemplateParser{ + data: data, + mapping: map[string]Mapping{}, + sequence: 0, + errors: []error{}, + replace: map[*goquery.Selection][]*html.Node{}, + } +} + +// Render parses and renders the HTML template +func (parser *TemplateParser) Render(html string) (string, error) { + reader := bytes.NewReader([]byte(fmt.Sprintf("%s", html))) + doc, err := goquery.NewDocumentFromReader(reader) + if err != nil { + return "", err + } + root := doc.Selection.Find("root") + parser.parseNode(root.Nodes[0]) + + // Replace the nodes + for sel, nodes := range parser.replace { + sel.ReplaceWithNodes(nodes...) + delete(parser.replace, sel) + } + + // fmt.Println(root.Html()) + // fmt.Println(parser.errors) + return root.Html() +} + +// Parse parses and renders the HTML template +func (parser *TemplateParser) parseNode(node *html.Node) { + + skipChildren := false + + switch node.Type { + case html.ElementNode: + sel := goquery.NewDocumentFromNode(node).Selection + if parser.hasParsed(sel) { + break + } + parser.parseElementNode(sel) + + // Skip children if the node is a loop node + if _, exist := sel.Attr("s:for"); exist { + skipChildren = true + } + + case html.TextNode: + parser.parseTextNode(node) + } + + // Recursively process child nodes + if !skipChildren { + for child := node.FirstChild; child != nil; child = child.NextSibling { + parser.parseNode(child) + } + } +} + +func (parser *TemplateParser) parseElementNode(sel *goquery.Selection) { + + if _, exist := sel.Attr("s:if"); exist { + parser.ifStatementNode(sel) + } + + if _, exist := sel.Attr("s:for"); exist { + parser.forStatementNode(sel) + } +} + +func (parser *TemplateParser) parseTextNode(node *html.Node) { + parser.sequence = parser.sequence + 1 + hasStmt := false + res := stmtRe.ReplaceAllFunc([]byte(node.Data), func(stmt []byte) []byte { + hasStmt = true + res, err := parser.data.ExecString(string(stmt)) + if err != nil { + parser.errors = append(parser.errors, err) + return []byte(``) + } + return []byte(res) + }) + + // Bind the variable to the parent node + if node.Parent != nil && hasStmt { + bindings := strings.TrimSpace(node.Data) + key := fmt.Sprintf("%v", parser.sequence) + if bindings != "" { + node.Parent.Attr = append(node.Parent.Attr, []html.Attribute{ + {Key: "s:bind", Val: bindings}, + {Key: "s:key-text", Val: key}, + }...) + } + } + + node.Data = string(res) +} + +func (parser *TemplateParser) forStatementNode(sel *goquery.Selection) { + + parser.sequence = parser.sequence + 1 + parser.setKey("for", sel, parser.sequence) + parser.parsed(sel) + parser.hide(sel) // Hide loop node + + forAttr, _ := sel.Attr("s:for") + forItems, err := parser.data.Exec(forAttr) + if err != nil { + parser.errors = append(parser.errors, err) + return + } + + items, err := parser.toArray(forItems) + if err != nil { + parser.errors = append(parser.errors, err) + return + } + + itemVarName := sel.AttrOr("s:for-item", "item") + indexVarName := sel.AttrOr("s:for-index", "index") + itemNodes := []*html.Node{} + + for idx, item := range items { + + // Create a new node + new := sel.Clone() + + // Set the key + parser.sequence = parser.sequence + 1 + parser.setKey("for-item-index", new, idx) + parser.setKey("for-item-key", new, parser.sequence) + + // Show the node + parser.show(new) + parser.data[itemVarName] = item + parser.data[indexVarName] = idx + + // Process the new node + for i := range new.Nodes { + parser.parseNode(new.Nodes[i]) + } + itemNodes = append(itemNodes, new.Nodes...) + } + + // Clean up the variables + delete(parser.data, itemVarName) + delete(parser.data, indexVarName) + + // Replace the node + // sel.ReplaceWithNodes(itemNodes...) + parser.replace[sel] = itemNodes +} + +func (parser *TemplateParser) ifStatementNode(sel *goquery.Selection) { + + parser.sequence = parser.sequence + 1 + parser.setKey("if", sel, parser.sequence) + parser.parsed(sel) + parser.hide(sel) // Hide all elif and else nodes + + ifAttr, _ := sel.Attr("s:if") + elifNodes, elseNode := parser.elseStatementNode(sel) + + for _, elifNode := range elifNodes { + parser.hide(elifNode) + } + + if elseNode != nil { + parser.hide(elseNode) + } + + // show the node if the condition is true + 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 + } + + if res == true { + parser.show(sel) + return + } + + // else if + for _, elifNode := range elifNodes { + elifAttr := elifNode.AttrOr("s:elif", "") + res, err := parser.data.Exec(elifAttr) + if err != nil { + parser.errors = append(parser.errors, err) + return + } + + if res == true { + parser.show(elifNode) + return + } + } + + // else + if elseNode != nil { + parser.show(elseNode) + } +} + +func (parser *TemplateParser) elseStatementNode(sel *goquery.Selection) ([]*goquery.Selection, *goquery.Selection) { + var elseNode *goquery.Selection = nil + elifNodes := []*goquery.Selection{} + key := parser.key("if", sel) + for next := sel.Next(); next != nil; next = next.Next() { + if _, exist := next.Attr("s:elif"); exist { + parser.parsed(next) + parser.setKey("if", next, key) + elifNodes = append(elifNodes, next) + continue + } + + if _, exist := next.Attr("s:else"); exist { + parser.parsed(next) + parser.setKey("if", next, key) + elseNode = next + continue + } + break + } + + return elifNodes, elseNode +} + +func (parser *TemplateParser) hide(sel *goquery.Selection) { + style := sel.AttrOr("style", "") + if strings.Contains(style, "display: none") { + return + } + + if style != "" { + style = fmt.Sprintf("%s; display: none", style) + } else { + style = "display: none" + } + sel.SetAttr("style", style) +} + +func (parser *TemplateParser) show(sel *goquery.Selection) { + style := sel.AttrOr("style", "") + if !strings.Contains(style, "display: none") { + return + } + + style = strings.ReplaceAll(style, "display: none", "") + if style == "" { + sel.RemoveAttr("style") + return + } + + sel.SetAttr("style", style) +} + +func (parser *TemplateParser) key(prefix string, sel *goquery.Selection) string { + name := fmt.Sprintf("s:key-%s", prefix) + return sel.AttrOr(name, "") +} + +func (parser *TemplateParser) setKey(prefix string, sel *goquery.Selection, key interface{}) { + name := fmt.Sprintf("s:key-%s", prefix) + value := fmt.Sprintf("%v", key) + sel.SetAttr(name, value) +} + +func (parser *TemplateParser) parsed(sel *goquery.Selection) { + sel.SetAttr("parsed", "true") +} + +func (parser *TemplateParser) hasParsed(sel *goquery.Selection) bool { + if parseed, exist := sel.Attr("parsed"); exist && parseed == "true" { + return true + } + return false +} + +func (parser *TemplateParser) toArray(value interface{}) ([]interface{}, error) { + switch values := value.(type) { + + case []interface{}: + return values, nil + + case []map[string]interface{}: + res := []interface{}{} + for _, v := range values { + res = append(res, v) + } + return res, nil + + case nil: + return []interface{}{}, nil + + case []map[string]string: + res := []interface{}{} + for _, v := range values { + res = append(res, v) + } + return res, nil + + case []string: + res := []interface{}{} + for _, v := range values { + res = append(res, v) + } + return res, nil + + case []float64: + res := []interface{}{} + for _, v := range values { + res = append(res, v) + } + return res, nil + + case []int: + res := []interface{}{} + for _, v := range values { + res = append(res, v) + } + return res, nil + + } + + return nil, fmt.Errorf("Cannot convert %v to array", value) } diff --git a/sui/core/render_test.go b/sui/core/render_test.go new file mode 100644 index 00000000..caf0a498 --- /dev/null +++ b/sui/core/render_test.go @@ -0,0 +1,35 @@ +package core + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRender(t *testing.T) { + prepare(t) + defer clean() + + page := testPage(t) + request := &Request{ + Query: map[string][]string{"show": {"no"}}, + Locale: "zh-CN", + Theme: "dark", + } + + data, err := page.Exec(request) + if err != nil { + t.Fatalf("Exec error: %v", err) + } + + assert.NotEmpty(t, data) + parser := NewTemplateParser(data) + html, err := parser.Render(page.Codes.HTML.Code) + if err != nil { + t.Fatalf("Render error: %v", err) + } + + assert.NotEmpty(t, html) + assert.Contains(t, html, "hello space") + assert.Equal(t, 0, len(parser.errors)) +} diff --git a/sui/core/request.go b/sui/core/request.go new file mode 100644 index 00000000..468a14e2 --- /dev/null +++ b/sui/core/request.go @@ -0,0 +1,185 @@ +package core + +import ( + "fmt" + "strings" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou/process" + "github.com/yaoapp/kun/any" +) + +// ExecString get the data +func (r *Request) ExecString(data string) (Data, error) { + var res Data + err := jsoniter.UnmarshalFromString(data, &res) + if err != nil { + return nil, err + } + r.Exec(res) + return res, nil +} + +// Exec get the data +func (r *Request) Exec(m Data) error { + + for key, value := range m { + + if strings.HasPrefix(key, "$") { + res, err := r.call(value) + if err != nil { + return err + } + newKey := key[1:] + m[newKey] = res + delete(m, key) + continue + } + + res, err := r.execValue(value) + if err != nil { + return err + } + m[key] = res + + } + + return nil +} + +func (r *Request) execValue(value interface{}) (interface{}, error) { + switch v := value.(type) { + case string: + if strings.HasPrefix(v, "$") { + return r.call(strings.TrimLeft(v, "$")) + } + return v, nil + + case []interface{}: + for i, item := range v { + res, err := r.execValue(item) + if err != nil { + return nil, err + } + v[i] = res + } + return v, nil + + case []string: + interfaceSlice := make([]interface{}, len(v)) + for i, item := range v { + interfaceSlice[i] = item + } + return r.execValue(interfaceSlice) + + case map[string]interface{}: + + if _, ok := v["process"].(string); ok { + if call, _ := v["__exec"].(bool); call { + res, err := r.call(v) + if err != nil { + return nil, err + } + return res, nil + } + } + + err := r.Exec(v) + if err != nil { + return nil, err + } + return v, nil + + default: + return v, nil + } +} + +func (r *Request) call(p interface{}) (interface{}, error) { + + processName := "" + processArgs := []interface{}{r} + switch v := p.(type) { + case string: + processName = v + break + + case map[string]interface{}: + if name, ok := v["process"].(string); ok { + processName = name + } + + if args, ok := v["args"].([]interface{}); ok { + args, err := r.parseArgs(args) + if err != nil { + return nil, err + } + processArgs = append(args, processArgs...) + } + } + + if processName == "" { + return nil, fmt.Errorf("process name is empty") + } + + process, err := process.Of(processName, processArgs...) + if err != nil { + return nil, err + } + + return process.Exec() +} + +func (r *Request) parseArgs(args []interface{}) ([]interface{}, error) { + + data := any.MapOf(map[string]interface{}{ + "param": r.Params, + "query": r.Query, + "payload": map[string]interface{}{}, + "header": r.Headers, + "theme": r.Theme, + "locale": r.Locale, + }).Dot() + + for i, arg := range args { + switch v := arg.(type) { + + case string: + if strings.HasPrefix(v, "$") { + key := strings.TrimLeft(v, "$") + args[i] = key + if data.Has(key) { + v := data.Get(key) + if strings.HasPrefix(key, "query.") || strings.HasPrefix(key, "header.") { + switch arg := v.(type) { + case []interface{}: + if len(arg) == 1 { + args[i] = arg[0] + } + case []string: + if len(arg) == 1 { + args[i] = arg[0] + } + } + } + } + } + + case []interface{}: + res, err := r.parseArgs(v) + if err != nil { + return nil, err + } + args[i] = res + + case map[string]interface{}: + res, err := r.parseArgs([]interface{}{v}) + if err != nil { + return nil, err + } + args[i] = res[0] + } + } + + return args, nil +}