[add] sui TemplateParser

This commit is contained in:
Max 2023-11-21 20:37:24 +08:00
parent eaf17e2b27
commit 90f05a6ad9
9 changed files with 693 additions and 208 deletions

3
go.mod
View file

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

2
go.sum
View file

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

View file

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

View file

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

View file

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

View file

@ -50,40 +50,38 @@ func testPage(t *testing.T) *Page {
File: "test.html",
Code: `<div class="p-10">
<div>For</div>
<div s:for="articles" s:for-item="article" s:for-index="idx">
<div s:for="articles.data" s:for-item="article" s:for-index="idx">
<div>{{ idx }} {{ article.title }}</div>
<div>{{ article.desc }}</div>
<div>{{ article.type == "article" ? "article" : "others"}}</div>
<div s:if="article.type == 'article'">article</div>
<div s:elif="article.type == 'image'">image</div>
<div s:else="">others</div>
<div s:else>others</div>
</div>
<div class="mt-10">IF</div>
<div s:if="articles.length > 0">
<div>{{ articles.length > 0 }} articles.length 大于 0</div>
<div s:if="len(articles) > 0" :name="input.data">
<div>{{ len(articles) > 0 }} articles.length &gt; 0</div>
</div>
<div s:if="length > 0" :name="input.data">
<div>{{ length > 0 }} len &gt; 0</div>
</div>
<div s:if="P_('scripts.article.Space', 'hello') == 'hello space'" >
hello space {{ P_('scripts.article.Space', 'hello') }}
</div>
<div s:if="showImage == 'yes'">showImage</div>
<div s:elif="showImage == 'no'">noImage</div>
<div s:else="">others</div>
<div s:elif="showImage == 'auto'">autoImage</div>
<div s:else>otherImage</div>
<div class="mt-10">Bind</div>
<div>
<div class="w-200">{{ input.data }}</div>
<div class="mt-5">
<input
class="w-200 p-2 bg-purple-900 text-white"
type="text"
s:bind="input.data"
placeholder="数据双向绑定"
/>
<input type="text" s:bind="input.data" placeholder="Bind Input Data"
class="w-200 p-2 bg-purple-900 text-white" />
</div>
<div class="mt-5">
<input
type="button"
value="修改"
class="text-blue-600 p-2"
s:click="changeInput"
/>
<input type="button" value="Change" s:click="changeInput"
class="text-blue-600 p-2" />
</div>
</div>
</div>`,
@ -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",

View file

@ -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("<root>%s</root>", 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)
}

35
sui/core/render_test.go Normal file
View file

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

185
sui/core/request.go Normal file
View file

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