yao/sui/core/data.go

186 lines
4.1 KiB
Go

package core
import (
"fmt"
"hash/fnv"
"regexp"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/kun/log"
"golang.org/x/net/html"
)
// If set the map value, should keep the space at the end of the statement
var stmtRe = regexp.MustCompile(`\{\{([\s\S]*?)\}\}`)
var propRe = regexp.MustCompile(`\[\{([\s\S]*?)\}\]`)
// Data data for the template
type Data map[string]interface{}
var options = []expr.Option{
expr.Function("P_", _process),
expr.AllowUndefinedVariables(),
}
// Hash get the hash of the data
func (data Data) Hash() string {
h := fnv.New64a()
h.Write([]byte(fmt.Sprintf("%v", data)))
return fmt.Sprintf("%x", h.Sum64())
}
// New create a new expression
func (data Data) New(stmt string) (*vm.Program, error) {
stmt = stmtRe.ReplaceAllStringFunc(stmt, func(stmt string) string {
matches := stmtRe.FindStringSubmatch(stmt)
if len(matches) > 0 {
stmt = strings.ReplaceAll(stmt, matches[0], matches[1])
}
return stmt
})
stmt = propRe.ReplaceAllStringFunc(stmt, func(stmt string) string {
matches := propRe.FindStringSubmatch(stmt)
if len(matches) > 0 {
stmt = strings.ReplaceAll(stmt, matches[0], matches[1])
}
return stmt
})
stmt = strings.TrimSpace(stmt)
// ' => ' " => "
stmt = strings.ReplaceAll(stmt, "'", "'")
stmt = strings.ReplaceAll(stmt, """, "\"")
return expr.Compile(stmt, append([]expr.Option{expr.Env(data)}, options...)...)
}
// 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
}
if res == nil {
return "", nil
}
if v, ok := res.(string); ok {
return v, nil
}
return fmt.Sprintf("%v", res), nil
}
// Replace replace the statement
func (data Data) Replace(value string) (string, bool) {
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
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
})
return res, hasStmt
}
// ReplaceSelection replace the statement in the selection
func (data Data) ReplaceSelection(sel *goquery.Selection) bool {
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
for _, node := range sel.Nodes {
ok := data.replaceNodeUse(re, node)
if ok {
hasStmt = true
}
}
return hasStmt
}
func (data Data) replaceNodeUse(re *regexp.Regexp, node *html.Node) bool {
hasStmt := false
switch node.Type {
case html.TextNode:
v, ok := data.ReplaceUse(re, node.Data)
node.Data = v
if ok {
hasStmt = true
}
break
case html.ElementNode:
for i := range node.Attr {
// Keepwords
if strings.HasPrefix(node.Attr[i].Key, "s:") || node.Attr[i].Key == "is" {
continue
}
v, ok := data.ReplaceUse(re, node.Attr[i].Val)
node.Attr[i].Val = v
if ok {
hasStmt = true
}
}
for c := node.FirstChild; c != nil; c = c.NextSibling {
ok := data.replaceNodeUse(re, c)
if ok {
hasStmt = true
}
}
break
}
return hasStmt
}
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
}
res, err := process.Exec()
if err != nil {
return nil, err
}
return res, nil
}