[add] sui TemplateParser
This commit is contained in:
parent
eaf17e2b27
commit
90f05a6ad9
9 changed files with 693 additions and 208 deletions
3
go.mod
3
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
|
||||
|
|
|
|||
2
go.sum
2
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=
|
||||
|
|
|
|||
242
sui/core/data.go
242
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 > 0</div>
|
||||
</div>
|
||||
<div s:if="length > 0" :name="input.data">
|
||||
<div>{{ length > 0 }} len > 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",
|
||||
|
|
@ -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
35
sui/core/render_test.go
Normal 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
185
sui/core/request.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue