[feat] SUI support BeforeRender hook for dataset processing, and deprecating the s:set method.

This commit is contained in:
Max 2024-07-25 17:56:40 +08:00
parent 8b93c3b219
commit 276f29d777
11 changed files with 174 additions and 59 deletions

View file

@ -168,7 +168,8 @@ func (r *Request) Render() (string, int, error) {
Route: r.Request.URL.Path, Route: r.Request.URL.Path,
Root: c.Root, Root: c.Root,
Script: c.Script, Script: c.Script,
Request: true, Imports: c.Imports,
Request: r.Request,
} }
// Parse the template // Parse the template
@ -250,6 +251,17 @@ func (r *Request) MakeCache() (*core.Cache, int, error) {
globalDataSel.Remove() globalDataSel.Remove()
} }
var imports map[string]string
importsSel := doc.Find("script[name=imports]")
if importsSel != nil && importsSel.Length() > 0 {
importsRaw := importsSel.Text()
importsSel.Remove()
err := jsoniter.UnmarshalFromString(importsRaw, &imports)
if err != nil {
return nil, 500, fmt.Errorf("imports error, please re-complie the page %s", err.Error())
}
}
html, err := doc.Html() html, err := doc.Html()
if err != nil { if err != nil {
return nil, 500, fmt.Errorf("parse error, please re-complie the page %s", err.Error()) return nil, 500, fmt.Errorf("parse error, please re-complie the page %s", err.Error())
@ -274,6 +286,7 @@ func (r *Request) MakeCache() (*core.Cache, int, error) {
CacheTime: time.Duration(cacheTime) * time.Second, CacheTime: time.Duration(cacheTime) * time.Second,
DataCacheTime: time.Duration(dataCacheTime) * time.Second, DataCacheTime: time.Duration(dataCacheTime) * time.Second,
Script: script, Script: script,
Imports: imports,
} }
go core.SetCache(r.File, cache) go core.SetCache(r.File, cache)

View file

@ -71,7 +71,7 @@ func makeParser(route string, t *testing.T) (*core.TemplateParser, string, core.
Debug: r.Request.DebugMode(), Debug: r.Request.DebugMode(),
DisableCache: r.Request.DisableCache(), DisableCache: r.Request.DisableCache(),
Route: r.Request.URL.Path, Route: r.Request.URL.Path,
Request: true, Request: r.Request,
} }
// Parse the template // Parse the template

View file

@ -236,7 +236,7 @@ func (page *Page) BuildAsComponent(sel *goquery.Selection, ctx *BuildContext, op
ctx.styles = append(ctx.styles, styles...) ctx.styles = append(ctx.styles, styles...)
sel.ReplaceWithSelection(body.Contents()) sel.ReplaceWithSelection(body.Contents())
ctx.components[page.Route] = true ctx.components[component] = page.Route
return source, nil return source, nil
} }
@ -456,7 +456,6 @@ func (page *Page) buildComponents(doc *goquery.Document, ctx *BuildContext, opti
return warnings, fmt.Errorf("SUI %s not found", page.SuiID) return warnings, fmt.Errorf("SUI %s not found", page.SuiID)
} }
public := sui.GetPublic()
tmpl, err := sui.GetTemplate(page.TemplateID) tmpl, err := sui.GetTemplate(page.TemplateID)
if err != nil { if err != nil {
return warnings, err return warnings, err
@ -480,7 +479,6 @@ func (page *Page) buildComponents(doc *goquery.Document, ctx *BuildContext, opti
if ctx.isJitComponent(name) { if ctx.isJitComponent(name) {
sel.SetAttr("s:jit", "true") sel.SetAttr("s:jit", "true")
sel.SetAttr("s:parent", page.namespace) sel.SetAttr("s:parent", page.namespace)
sel.SetAttr("s:root", public.Root)
ctx.addJitComponent(name) ctx.addJitComponent(name)
return return
} }

View file

@ -21,6 +21,7 @@ type Cache struct {
CacheTime time.Duration CacheTime time.Duration
DataCacheTime time.Duration DataCacheTime time.Duration
Script *Script Script *Script
Imports map[string]string
} }
const ( const (

View file

@ -93,13 +93,7 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, []str
} }
// Page Components // Page Components
components := []string{} rawComponents, _ := jsoniter.MarshalToString(ctx.components)
if ctx != nil && ctx.components != nil && len(ctx.components) > 0 {
for route := range ctx.components {
components = append(components, route)
}
}
rawComponents, _ := jsoniter.MarshalToString(components)
body.AppendHtml("\n\n" + `<script name="imports" type="json">` + "\n" + rawComponents + "\n</script>\n\n") body.AppendHtml("\n\n" + `<script name="imports" type="json">` + "\n" + rawComponents + "\n</script>\n\n")
page.ReplaceDocument(doc) page.ReplaceDocument(doc)

View file

@ -3,7 +3,7 @@ package core
// NewBuildContext create a new build context // NewBuildContext create a new build context
func NewBuildContext(global *GlobalBuildContext) *BuildContext { func NewBuildContext(global *GlobalBuildContext) *BuildContext {
return &BuildContext{ return &BuildContext{
components: map[string]bool{}, components: map[string]string{},
sequence: 1, sequence: 1,
scripts: []ScriptNode{}, scripts: []ScriptNode{},
scriptUnique: map[string]bool{}, scriptUnique: map[string]bool{},

View file

@ -191,16 +191,11 @@ const backendScriptTmpl = `
this.__sui_page = '%s'; this.__sui_page = '%s';
this.__sui_constants = {}; this.__sui_constants = {};
this.__sui_helpers = []; this.__sui_helpers = [];
this.__sui_hooks = null;
if (typeof Helpers === 'object') { if (typeof Helpers === 'object') {
this.__sui_helpers = Object.keys(Helpers); this.__sui_helpers = Object.keys(Helpers);
} }
if (typeof Hooks === 'function') {
this.__sui_hooks = new Hooks();
}
if (typeof Constants === 'object') { if (typeof Constants === 'object') {
this.__sui_constants = Constants; this.__sui_constants = Constants;
} }

View file

@ -45,7 +45,7 @@ func init() {
} }
// parseComponent parse the component // parseComponent parse the component
func (parser *TemplateParser) parseComponent(sel *goquery.Selection) { func (parser *TemplateParser) parseJitComponent(sel *goquery.Selection) {
parser.parsed(sel) parser.parsed(sel)
comp, props, slots, children, err := parser.getComponent(sel) comp, props, slots, children, err := parser.getComponent(sel)
if err != nil { if err != nil {
@ -218,8 +218,8 @@ func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponen
return comp, props, slots, children, nil return comp, props, slots, children, nil
} }
// isComponent check if the selection is a component // isJitComponent check if the selection is a component
func (parser *TemplateParser) isComponent(sel *goquery.Selection) bool { func (parser *TemplateParser) isJitComponent(sel *goquery.Selection) bool {
_, exist := sel.Attr("s:jit") _, exist := sel.Attr("s:jit")
is := sel.AttrOr("is", "") is := sel.AttrOr("is", "")
return exist && is != "" return exist && is != ""
@ -314,8 +314,7 @@ func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[st
data := Data{"$props": props} data := Data{"$props": props}
route, _ = data.ReplaceUse(slotRe, route) route, _ = data.ReplaceUse(slotRe, route)
route, _ = parser.data.Replace(route) route, _ = parser.data.Replace(route)
root := sel.AttrOr("s:root", "/") file := filepath.Join(string(os.PathSeparator), "public", parser.option.Root, route+".jit")
file := filepath.Join(string(os.PathSeparator), "public", root, route+".jit")
return file, route, nil return file, route, nil
} }

View file

@ -2,6 +2,8 @@ package core
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"strings" "strings"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
@ -40,17 +42,18 @@ type Mapping struct {
// ParserOption parser option // ParserOption parser option
type ParserOption struct { type ParserOption struct {
Component bool `json:"component,omitempty"` Component bool `json:"component,omitempty"`
Editor bool `json:"editor,omitempty"` Editor bool `json:"editor,omitempty"`
Preview bool `json:"preview,omitempty"` Preview bool `json:"preview,omitempty"`
Debug bool `json:"debug,omitempty"` Debug bool `json:"debug,omitempty"`
DisableCache bool `json:"disableCache,omitempty"` DisableCache bool `json:"disableCache,omitempty"`
Request bool `json:"request,omitempty"` Route string `json:"route,omitempty"`
Route string `json:"route,omitempty"` Theme any `json:"theme,omitempty"`
Theme any `json:"theme,omitempty"` Locale any `json:"locale,omitempty"`
Locale any `json:"locale,omitempty"` Root string `json:"root,omitempty"`
Root string `json:"root,omitempty"` Imports map[string]string `json:"imports,omitempty"`
Script *Script `json:"-"` // backend script Script *Script `json:"-"` // backend script
Request *Request `json:"request,omitempty"`
} }
var keepWords = map[string]bool{ var keepWords = map[string]bool{
@ -99,9 +102,6 @@ func NewTemplateParser(data Data, option *ParserOption) *TemplateParser {
// Render parses and renders the HTML template // Render parses and renders the HTML template
func (parser *TemplateParser) Render(html string) (string, error) { func (parser *TemplateParser) Render(html string) (string, error) {
// Set the locale
parser.locale = parser.Locale()
if !strings.Contains(html, "<html") { if !strings.Contains(html, "<html") {
html = fmt.Sprintf(`<!DOCTYPE html><html lang="en-us">%s</html>`, html) html = fmt.Sprintf(`<!DOCTYPE html><html lang="en-us">%s</html>`, html)
} }
@ -111,13 +111,11 @@ func (parser *TemplateParser) Render(html string) (string, error) {
return "", err return "", err
} }
root := doc.Selection.Find("html") // Set the locale
parser.parseNode(root.Nodes[0]) parser.locale = parser.Locale()
err = parser.RenderSelection(doc.Selection)
// Replace the nodes if err != nil {
for sel, nodes := range parser.replace { return "", err
sel.ReplaceWithNodes(nodes...)
delete(parser.replace, sel)
} }
// Append the head // Append the head
@ -150,16 +148,13 @@ func (parser *TemplateParser) Render(html string) (string, error) {
parser.addScripts(body, parser.filterScripts("body", parser.scripts)) parser.addScripts(body, parser.filterScripts("body", parser.scripts))
} }
// Fmt
parser.Fmt(doc)
// For editor // For editor
if parser.option != nil && parser.option.Editor { if parser.option != nil && parser.option.Editor {
return doc.Find("body").Html() return doc.Find("body").Html()
} }
// For Request // For Request
if parser.option != nil && (parser.option.Request || parser.option.Preview) { if parser.option != nil && (parser.option.Request != nil || parser.option.Preview) {
// Remove the sui-hide attribute // Remove the sui-hide attribute
doc.Find("[sui-hide]").Remove() doc.Find("[sui-hide]").Remove()
parser.tidy(doc.Selection) parser.tidy(doc.Selection)
@ -170,8 +165,26 @@ func (parser *TemplateParser) Render(html string) (string, error) {
return doc.Html() return doc.Html()
} }
// RenderSelection parses and renders the HTML template
func (parser *TemplateParser) RenderSelection(section *goquery.Selection) error {
if len(section.Nodes) == 0 {
return fmt.Errorf("No nodes found")
}
parser.parseNode(section.Nodes[0])
// Replace the nodes
for sel, nodes := range parser.replace {
sel.ReplaceWithNodes(nodes...)
delete(parser.replace, sel)
}
parser.Fmt(section)
return nil
}
// Fmt formats the HTML template // Fmt formats the HTML template
func (parser *TemplateParser) Fmt(doc *goquery.Document) { func (parser *TemplateParser) Fmt(doc *goquery.Selection) {
if parser.locale != nil { if parser.locale != nil {
sels := doc.Find(`[s\:trans-fmt]`) sels := doc.Find(`[s\:trans-fmt]`)
sels.Each(func(i int, sel *goquery.Selection) { sels.Each(func(i int, sel *goquery.Selection) {
@ -194,10 +207,8 @@ func (parser *TemplateParser) parseNode(node *html.Node) {
} }
parser.parseElementNode(sel) parser.parseElementNode(sel)
// Skip children if the node is a loop node // Skip children if the node is a loop node、element component or JIT component
if _, exist := sel.Attr("s:for"); exist { skipChildren = parser.hasForStatement(sel) || parser.isElementComponent(sel) || parser.isJitComponent(sel)
skipChildren = true
}
case html.TextNode: case html.TextNode:
parser.parseTextNode(node) parser.parseTextNode(node)
@ -229,13 +240,86 @@ func (parser *TemplateParser) parseElementNode(sel *goquery.Selection) {
parser.setStatementNode(sel) parser.setStatementNode(sel)
} }
// JIT Compile the element
if parser.isComponent(sel) {
parser.parseComponent(sel)
}
// Parse the attributes // Parse the attributes
parser.parseElementAttrs(sel) parser.parseElementAttrs(sel)
// if the element is a component
if parser.isElementComponent(sel) {
parser.parseElementComponent(sel)
}
// JIT Compile the element
if parser.isJitComponent(sel) {
parser.parseJitComponent(sel)
}
}
func (parser *TemplateParser) parseElementComponent(sel *goquery.Selection) {
sel.SetAttr("parsed", "true")
com := sel.AttrOr("s:cn", "")
props := map[string]string{}
for _, attr := range sel.Nodes[0].Attr {
if !strings.HasPrefix(attr.Key, "s:") && attr.Key != "parsed" {
props[attr.Key] = attr.Val
}
}
// load the component based on the route
var script *Script
var err error
if parser.option.Imports != nil {
if route, has := parser.option.Imports[com]; has {
file := filepath.Join(string(os.PathSeparator), "public", parser.option.Root, route)
script, err = LoadScript(file)
if err != nil {
parser.errors = append(parser.errors, err)
setError(sel, err)
return
}
}
}
compParser := parser.clone(script)
// Call the BeforeRender Hook
if script != nil {
data, err := script.BeforeRender(parser.option.Request, props)
if err != nil {
parser.errors = append(parser.errors, err)
setError(sel, err)
return
}
if data != nil {
for k, v := range data {
compParser.data[k] = v
}
}
}
err = compParser.RenderSelection(sel)
if err != nil {
parser.errors = append(parser.errors, err)
setError(sel, err)
}
parser.sequence = parser.sequence + 1
}
func (parser *TemplateParser) clone(script *Script) *TemplateParser {
var new = *parser
new.data = Data{}
for k, v := range parser.data {
new.data[k] = v
}
new.option.Script = script
return &new
}
func (parser *TemplateParser) isElementComponent(sel *goquery.Selection) bool {
if comp, exist := sel.Attr("s:cn"); exist && comp != "" && sel.Nodes[0].Data != "script" {
return true
}
return false
} }
func (parser *TemplateParser) transTextNode(node *html.Node) { func (parser *TemplateParser) transTextNode(node *html.Node) {
@ -486,6 +570,12 @@ func (parser *TemplateParser) parseTextNode(node *html.Node) {
} }
node.Data = res node.Data = res
} }
func (parser *TemplateParser) hasForStatement(sel *goquery.Selection) bool {
if _, exist := sel.Attr("s:for"); exist {
return true
}
return false
}
func (parser *TemplateParser) forStatementNode(sel *goquery.Selection) { func (parser *TemplateParser) forStatementNode(sel *goquery.Selection) {

View file

@ -99,6 +99,31 @@ func (script *Script) Call(r *Request, method string, args ...any) (interface{},
return res, nil return res, nil
} }
// BeforeRender the script method
func (script *Script) BeforeRender(r *Request, props map[string]string) (Data, error) {
ctx, err := script.NewContext(r.Sid, nil)
if err != nil {
return nil, err
}
defer ctx.Close()
if !ctx.Global().Has("BeforeRender") {
return nil, nil
}
res, err := ctx.Call("BeforeRender", r, props)
if err != nil {
return nil, err
}
if data, ok := res.(map[string]interface{}); ok {
return data, nil
}
return nil, fmt.Errorf("BeforeRender return %v should be Record<string, any>", res)
}
// ConstantsToString get the constants from the script // ConstantsToString get the constants from the script
func (script *Script) ConstantsToString() (string, error) { func (script *Script) ConstantsToString() (string, error) {
constants, err := script.Constants() constants, err := script.Constants()

View file

@ -59,7 +59,7 @@ type PageProp struct {
// BuildContext is the struct for the build context // BuildContext is the struct for the build context
type BuildContext struct { type BuildContext struct {
components map[string]bool components map[string]string
jitComponents map[string]bool jitComponents map[string]bool
sequence int sequence int
doc *goquery.Document doc *goquery.Document