From 58ca39a32b5469efc2658a7bf1c592bfddccad35 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 25 Jul 2024 15:37:23 +0800 Subject: [PATCH 1/5] [feat] Sharing the Constants variable between frontend and backend scripts --- sui/api/request.go | 8 ++ sui/core/build.go | 15 ++- sui/core/cache.go | 4 +- sui/core/injections.go | 25 +++++ sui/core/parser.go | 21 ++-- sui/core/script.go | 191 +++++++++++++++++++++++++++++++++ sui/core/types.go | 7 +- sui/storages/local/build.go | 33 +++++- sui/storages/local/template.go | 2 +- 9 files changed, 287 insertions(+), 19 deletions(-) create mode 100644 sui/core/script.go diff --git a/sui/api/request.go b/sui/api/request.go index 90d3f1c7..e9b27743 100644 --- a/sui/api/request.go +++ b/sui/api/request.go @@ -167,6 +167,7 @@ func (r *Request) Render() (string, int, error) { DisableCache: r.Request.DisableCache(), Route: r.Request.URL.Path, Root: c.Root, + Script: c.Script, Request: true, } @@ -254,6 +255,12 @@ func (r *Request) MakeCache() (*core.Cache, int, error) { return nil, 500, fmt.Errorf("parse error, please re-complie the page %s", err.Error()) } + // Backend script + script, err := core.LoadScript(r.File) + if err != nil { + return nil, 500, fmt.Errorf("script error, please re-complie the page %s", err.Error()) + } + // Save to The Cache cache := &core.Cache{ Data: dataText, @@ -266,6 +273,7 @@ func (r *Request) MakeCache() (*core.Cache, int, error) { Root: root, CacheTime: time.Duration(cacheTime) * time.Second, DataCacheTime: time.Duration(dataCacheTime) * time.Second, + Script: script, } go core.SetCache(r.File, cache) diff --git a/sui/core/build.go b/sui/core/build.go index c8136361..55dc7245 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -589,6 +589,16 @@ func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component } injectScript := componentInitScript(arguments) + // Get the Constants and Helpers + var err error = nil + constants := "" + if page.Script != nil { + constants, err = page.Script.ConstantsToString() + if err != nil { + return nil, err + } + } + scripts := []ScriptNode{} if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" { return scripts, nil @@ -599,7 +609,6 @@ func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component ctx.scriptUnique[component] = true - var err error = nil var imports []string = nil var source []byte = nil if page.Codes.TS.Code != "" { @@ -639,6 +648,10 @@ func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component }) code := string(source) + if constants != "" { + code = fmt.Sprintf("this.Constants = %s\n%s", constants, code) + } + parent := "body" if !ispage { parent = "head" diff --git a/sui/core/cache.go b/sui/core/cache.go index 544ee3aa..c3cc176d 100644 --- a/sui/core/cache.go +++ b/sui/core/cache.go @@ -4,7 +4,6 @@ import ( "time" jsoniter "github.com/json-iterator/go" - v8 "github.com/yaoapp/gou/runtime/v8" "github.com/yaoapp/gou/store" "github.com/yaoapp/kun/log" ) @@ -21,7 +20,7 @@ type Cache struct { CacheStore string CacheTime time.Duration DataCacheTime time.Duration - Script *v8.Script // the backend script + Script *Script } const ( @@ -73,6 +72,7 @@ func GetCache(file string) *Cache { // RemoveCache remove the cache func RemoveCache(file string) { ch <- &cacheData{file, nil, removeCache} + chScript <- &scriptData{file, nil, removeScript} } // CleanCache clean the cache diff --git a/sui/core/injections.go b/sui/core/injections.go index afd08b2c..8eadefc4 100644 --- a/sui/core/injections.go +++ b/sui/core/injections.go @@ -186,6 +186,26 @@ const componentInitScriptTmpl = ` this.store = new __sui_store(this.root); ` +// Inject code +const backendScriptTmpl = ` +this.__sui_page = '%s'; +this.__sui_constants = {}; +this.__sui_helpers = []; +this.__sui_hooks = null; + +if (typeof Helpers === 'object') { + this.__sui_helpers = Object.keys(Helpers); +} + +if (typeof Hooks === 'function') { + this.__sui_hooks = new Hooks(); +} + +if (typeof Constants === 'object') { + this.__sui_constants = Constants; +} +` + func bodyInjectionScript(jsonRaw string, debug bool) string { jsPrintData := "" if debug { @@ -209,3 +229,8 @@ func compEventInjectScript(eventID, eventName, component, dataKeys, jsonKeys, ha func componentInitScript(root string) string { return fmt.Sprintf(componentInitScriptTmpl, root) } + +// BackendScript inject the backend script +func BackendScript(route string) string { + return fmt.Sprintf(backendScriptTmpl, route) +} diff --git a/sui/core/parser.go b/sui/core/parser.go index 3b6be196..bfef4b03 100644 --- a/sui/core/parser.go +++ b/sui/core/parser.go @@ -40,16 +40,17 @@ type Mapping struct { // ParserOption parser option type ParserOption struct { - Component bool `json:"component,omitempty"` - Editor bool `json:"editor,omitempty"` - Preview bool `json:"preview,omitempty"` - Debug bool `json:"debug,omitempty"` - DisableCache bool `json:"disableCache,omitempty"` - Request bool `json:"request,omitempty"` - Route string `json:"route,omitempty"` - Theme any `json:"theme,omitempty"` - Locale any `json:"locale,omitempty"` - Root string `json:"root,omitempty"` + Component bool `json:"component,omitempty"` + Editor bool `json:"editor,omitempty"` + Preview bool `json:"preview,omitempty"` + Debug bool `json:"debug,omitempty"` + DisableCache bool `json:"disableCache,omitempty"` + Request bool `json:"request,omitempty"` + Route string `json:"route,omitempty"` + Theme any `json:"theme,omitempty"` + Locale any `json:"locale,omitempty"` + Root string `json:"root,omitempty"` + Script *Script `json:"-"` // backend script } var keepWords = map[string]bool{ diff --git a/sui/core/script.go b/sui/core/script.go new file mode 100644 index 00000000..1e9f60ce --- /dev/null +++ b/sui/core/script.go @@ -0,0 +1,191 @@ +package core + +import ( + "fmt" + "strings" + "time" + + "github.com/google/uuid" + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou/application" + v8 "github.com/yaoapp/gou/runtime/v8" + "github.com/yaoapp/gou/runtime/v8/bridge" +) + +// Scripts loaded scripts +var Scripts = map[string]*Script{} + +const ( + saveScript uint8 = iota + removeScript +) + +// Script the script +type Script struct { + *v8.Script +} + +type scriptData struct { + file string + script *Script + cmd uint8 +} + +var chScript = make(chan *scriptData, 1) + +func init() { + go scriptWriter() +} + +func scriptWriter() { + for { + select { + case data := <-chScript: + switch data.cmd { + case saveScript: + Scripts[data.file] = data.script + case removeScript: + delete(Scripts, data.file) + } + } + } +} + +// LoadScript load the script +func LoadScript(file string) (*Script, error) { + + if script, has := Scripts[file]; has { + return script, nil + } + + base := strings.TrimSuffix(file, ".sui") + file = base + ".ts" + if exist, _ := application.App.Exists(file); !exist { + file = base + ".js" + } + + if exist, _ := application.App.Exists(file); !exist { + return nil, nil + } + + source, err := application.App.Read(file) + if err != nil { + return nil, err + } + + v8script, err := v8.MakeScript(source, file, 5*time.Second) + if err != nil { + return nil, err + } + + script := &Script{Script: v8script} + chScript <- &scriptData{file, script, saveScript} + return script, nil +} + +// Call the script method +// This will be refactored to improve the performance +func (script *Script) Call(r *Request, method string, args ...any) (interface{}, error) { + ctx, err := script.NewContext(r.Sid, nil) + if err != nil { + return nil, err + } + defer ctx.Close() + + res, err := ctx.Call(method, args...) + if err != nil { + return nil, err + } + return res, nil +} + +// ConstantsToString get the constants from the script +func (script *Script) ConstantsToString() (string, error) { + constants, err := script.Constants() + if err != nil { + return "", err + } + raw, err := jsoniter.MarshalToString(constants) + if err != nil { + return "", err + } + return raw, nil +} + +// Constants get the constants from the script +// This will be refactored to improve the performance +func (script *Script) Constants() (map[string]interface{}, error) { + uuid := uuid.New().String() + ctx, err := script.NewContext(uuid, nil) + if err != nil { + return nil, err + } + defer ctx.Close() + + global := ctx.Global() + if global == nil { + return nil, fmt.Errorf("global is nil") + } + + if !global.Has("__sui_constants") { + return nil, nil + } + + res, err := global.Get("__sui_constants") + if err != nil { + return nil, err + } + defer res.Release() + + goValues, err := bridge.GoValue(res, ctx.Context) + if err != nil { + return nil, err + } + + if constants, ok := goValues.(map[string]interface{}); ok { + return constants, nil + } + + return nil, fmt.Errorf("constants is %v should be Record", goValues) +} + +// Helpers get the helpers from the script +// This will be refactored to improve the performance +func (script *Script) Helpers() ([]string, error) { + uuid := uuid.New().String() + ctx, err := script.NewContext(uuid, nil) + if err != nil { + return nil, err + } + defer ctx.Close() + + global := ctx.Global() + if global == nil { + return nil, fmt.Errorf("global is nil") + } + + if !global.Has("__sui_helpers") { + return nil, nil + } + + res, err := global.Get("__sui_helpers") + if err != nil { + return nil, err + } + defer res.Release() + + goValues, err := bridge.GoValue(res, ctx.Context) + if err != nil { + return nil, err + } + + if helpers, ok := goValues.([]interface{}); ok { + methods := []string{} + for _, key := range helpers { + methods = append(methods, fmt.Sprintf("%v", key)) + } + return methods, nil + } + + return nil, fmt.Errorf("helpers is %v should be []string", goValues) +} diff --git a/sui/core/types.go b/sui/core/types.go index fbc61d9c..a5f45a6a 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -5,7 +5,6 @@ import ( "regexp" "github.com/PuerkitoBio/goquery" - v8 "github.com/yaoapp/gou/runtime/v8" "golang.org/x/net/html" ) @@ -39,7 +38,7 @@ type Page struct { Path string `json:"-"` Root string `json:"-"` Codes SourceCodes `json:"-"` - Script *v8.Script `json:"-"` // The backend script name.backend.ts / name.backend.js + Script *Script `json:"-"` // The backend script name.backend.ts / name.backend.js Document []byte `json:"-"` GlobalData []byte `json:"-"` Attrs map[string]string `json:"-"` @@ -179,8 +178,8 @@ type Template struct { GlobalData []byte `json:"-"` Scripts *TemplateScirpts `json:"scripts,omitempty"` Translator string `json:"translator,omitempty"` - BuildScript *v8.Script `json:"-"` // __build.backend.ts / __build.backend.js - GlobalScript *v8.Script `json:"-"` // __global.backend.ts / __global.backend.js + BuildScript *Script `json:"-"` // __build.backend.ts / __build.backend.js + GlobalScript *Script `json:"-"` // __global.backend.ts / __global.backend.js } // TemplateScirpts is the struct for the template scripts diff --git a/sui/storages/local/build.go b/sui/storages/local/build.go index 009280bc..36c02d89 100644 --- a/sui/storages/local/build.go +++ b/sui/storages/local/build.go @@ -5,10 +5,12 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/hashicorp/go-multierror" "github.com/yaoapp/gou/application" "github.com/yaoapp/gou/process" + v8 "github.com/yaoapp/gou/runtime/v8" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/sui/core" "golang.org/x/text/language" @@ -338,7 +340,6 @@ func (tmpl *Template) getLocale(name string, route string, pageOnly ...bool) cor func (page *Page) Build(globalCtx *core.GlobalBuildContext, option *core.BuildOption) ([]string, error) { ctx := core.NewBuildContext(globalCtx) - var err error = nil root := option.PublicRoot if root == "" { @@ -353,6 +354,11 @@ func (page *Page) Build(globalCtx *core.GlobalBuildContext, option *core.BuildOp } page.Root = root + err = page.loadBackendScript() + if err != nil { + return nil, err + } + html, warnings, err := page.Page.Compile(ctx, option) if err != nil { return warnings, fmt.Errorf("Compile the page %s error: %s", page.Route, err.Error()) @@ -425,6 +431,11 @@ func (page *Page) BuildAsComponent(globalCtx *core.GlobalBuildContext, option *c option.AssetRoot = filepath.Join(root, "assets") } + err := page.loadBackendScript() + if err != nil { + return nil, err + } + html, messages, err := page.Page.CompileAsComponent(ctx, option) if err != nil { return warnings, err @@ -656,9 +667,29 @@ func (page *Page) backendScriptSource() (string, []byte, error) { return "", nil, err } + source = []byte(fmt.Sprintf("%s\n%s", source, core.BackendScript(page.Route))) return backendFile, source, nil } +func (page *Page) loadBackendScript() error { + file, source, err := page.backendScriptSource() + if err != nil { + return err + } + + if source == nil { + return nil + } + approot := page.tmpl.local.AppRoot() + file = filepath.Join(approot, file) + script, err := v8.MakeScript(source, file, 5*time.Second) + if err != nil { + return err + } + page.Script = &core.Script{Script: script} + return nil +} + func (page *Page) writeBackendScript(data map[string]interface{}) error { file, source, err := page.backendScriptSource() diff --git a/sui/storages/local/template.go b/sui/storages/local/template.go index 5d2ef00f..52894d72 100644 --- a/sui/storages/local/template.go +++ b/sui/storages/local/template.go @@ -103,7 +103,7 @@ func (tmpl *Template) loadBuildScript() error { if err != nil { return err } - tmpl.BuildScript = script + tmpl.BuildScript = &core.Script{Script: script} return nil } From fe247d1e282c6f28b59ebf4a4868672c3c5f45dd Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 25 Jul 2024 16:10:21 +0800 Subject: [PATCH 2/5] Refactor SUI core to load backend scripts for templates --- sui/core/build.go | 4 ++++ sui/storages/local/build.go | 10 ---------- sui/storages/local/page.go | 7 +++++++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/sui/core/build.go b/sui/core/build.go index 55dc7245..b4575a5d 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -650,12 +650,16 @@ func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component code := string(source) if constants != "" { code = fmt.Sprintf("this.Constants = %s\n%s", constants, code) + } parent := "body" if !ispage { parent = "head" code = fmt.Sprintf("function %s(){\n%s\n}\n", component, addTabToEachLine(code)) + if constants != "" { + fmt.Println("page.Script.ConstantsToString", page.Route) + } } scripts = append(scripts, ScriptNode{ diff --git a/sui/storages/local/build.go b/sui/storages/local/build.go index 36c02d89..f9800dc9 100644 --- a/sui/storages/local/build.go +++ b/sui/storages/local/build.go @@ -354,11 +354,6 @@ func (page *Page) Build(globalCtx *core.GlobalBuildContext, option *core.BuildOp } page.Root = root - err = page.loadBackendScript() - if err != nil { - return nil, err - } - html, warnings, err := page.Page.Compile(ctx, option) if err != nil { return warnings, fmt.Errorf("Compile the page %s error: %s", page.Route, err.Error()) @@ -431,11 +426,6 @@ func (page *Page) BuildAsComponent(globalCtx *core.GlobalBuildContext, option *c option.AssetRoot = filepath.Join(root, "assets") } - err := page.loadBackendScript() - if err != nil { - return nil, err - } - html, messages, err := page.Page.CompileAsComponent(ctx, option) if err != nil { return warnings, err diff --git a/sui/storages/local/page.go b/sui/storages/local/page.go index aa8caf7a..841be543 100644 --- a/sui/storages/local/page.go +++ b/sui/storages/local/page.go @@ -453,6 +453,13 @@ func (page *Page) Load() error { // Set the page global data page.GlobalData = page.tmpl.GlobalData + + // Load the backend script + err := page.loadBackendScript() + if err != nil { + return err + } + return nil } From 8b93c3b2196ac82326f02c2bb8c021f9b8250abf Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 25 Jul 2024 16:14:40 +0800 Subject: [PATCH 3/5] Refactor SUI core to remove unnecessary code in BuildScripts function --- sui/core/build.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sui/core/build.go b/sui/core/build.go index b4575a5d..55dc7245 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -650,16 +650,12 @@ func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component code := string(source) if constants != "" { code = fmt.Sprintf("this.Constants = %s\n%s", constants, code) - } parent := "body" if !ispage { parent = "head" code = fmt.Sprintf("function %s(){\n%s\n}\n", component, addTabToEachLine(code)) - if constants != "" { - fmt.Println("page.Script.ConstantsToString", page.Route) - } } scripts = append(scripts, ScriptNode{ From 276f29d77726b622cc4673da747fcf34989b8a8b Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 25 Jul 2024 17:56:40 +0800 Subject: [PATCH 4/5] [feat] SUI support BeforeRender hook for dataset processing, and deprecating the s:set method. --- sui/api/request.go | 15 +++- sui/api/request_test.go | 2 +- sui/core/build.go | 4 +- sui/core/cache.go | 1 + sui/core/compile.go | 8 +- sui/core/context.go | 2 +- sui/core/injections.go | 5 -- sui/core/jit.go | 9 +-- sui/core/parser.go | 160 +++++++++++++++++++++++++++++++--------- sui/core/script.go | 25 +++++++ sui/core/types.go | 2 +- 11 files changed, 174 insertions(+), 59 deletions(-) diff --git a/sui/api/request.go b/sui/api/request.go index e9b27743..af060ba7 100644 --- a/sui/api/request.go +++ b/sui/api/request.go @@ -168,7 +168,8 @@ func (r *Request) Render() (string, int, error) { Route: r.Request.URL.Path, Root: c.Root, Script: c.Script, - Request: true, + Imports: c.Imports, + Request: r.Request, } // Parse the template @@ -250,6 +251,17 @@ func (r *Request) MakeCache() (*core.Cache, int, error) { 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() if err != nil { 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, DataCacheTime: time.Duration(dataCacheTime) * time.Second, Script: script, + Imports: imports, } go core.SetCache(r.File, cache) diff --git a/sui/api/request_test.go b/sui/api/request_test.go index ea8e0936..d08cffb4 100644 --- a/sui/api/request_test.go +++ b/sui/api/request_test.go @@ -71,7 +71,7 @@ func makeParser(route string, t *testing.T) (*core.TemplateParser, string, core. Debug: r.Request.DebugMode(), DisableCache: r.Request.DisableCache(), Route: r.Request.URL.Path, - Request: true, + Request: r.Request, } // Parse the template diff --git a/sui/core/build.go b/sui/core/build.go index 55dc7245..4e8b392c 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -236,7 +236,7 @@ func (page *Page) BuildAsComponent(sel *goquery.Selection, ctx *BuildContext, op ctx.styles = append(ctx.styles, styles...) sel.ReplaceWithSelection(body.Contents()) - ctx.components[page.Route] = true + ctx.components[component] = page.Route 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) } - public := sui.GetPublic() tmpl, err := sui.GetTemplate(page.TemplateID) if err != nil { return warnings, err @@ -480,7 +479,6 @@ func (page *Page) buildComponents(doc *goquery.Document, ctx *BuildContext, opti if ctx.isJitComponent(name) { sel.SetAttr("s:jit", "true") sel.SetAttr("s:parent", page.namespace) - sel.SetAttr("s:root", public.Root) ctx.addJitComponent(name) return } diff --git a/sui/core/cache.go b/sui/core/cache.go index c3cc176d..e902be51 100644 --- a/sui/core/cache.go +++ b/sui/core/cache.go @@ -21,6 +21,7 @@ type Cache struct { CacheTime time.Duration DataCacheTime time.Duration Script *Script + Imports map[string]string } const ( diff --git a/sui/core/compile.go b/sui/core/compile.go index 83986c33..0c0e053f 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -93,13 +93,7 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, []str } // Page Components - components := []string{} - if ctx != nil && ctx.components != nil && len(ctx.components) > 0 { - for route := range ctx.components { - components = append(components, route) - } - } - rawComponents, _ := jsoniter.MarshalToString(components) + rawComponents, _ := jsoniter.MarshalToString(ctx.components) body.AppendHtml("\n\n" + `\n\n") page.ReplaceDocument(doc) diff --git a/sui/core/context.go b/sui/core/context.go index f8b78b05..c7c1b483 100644 --- a/sui/core/context.go +++ b/sui/core/context.go @@ -3,7 +3,7 @@ package core // NewBuildContext create a new build context func NewBuildContext(global *GlobalBuildContext) *BuildContext { return &BuildContext{ - components: map[string]bool{}, + components: map[string]string{}, sequence: 1, scripts: []ScriptNode{}, scriptUnique: map[string]bool{}, diff --git a/sui/core/injections.go b/sui/core/injections.go index 8eadefc4..99efe181 100644 --- a/sui/core/injections.go +++ b/sui/core/injections.go @@ -191,16 +191,11 @@ const backendScriptTmpl = ` this.__sui_page = '%s'; this.__sui_constants = {}; this.__sui_helpers = []; -this.__sui_hooks = null; if (typeof Helpers === 'object') { this.__sui_helpers = Object.keys(Helpers); } -if (typeof Hooks === 'function') { - this.__sui_hooks = new Hooks(); -} - if (typeof Constants === 'object') { this.__sui_constants = Constants; } diff --git a/sui/core/jit.go b/sui/core/jit.go index 64737c86..32d3a590 100644 --- a/sui/core/jit.go +++ b/sui/core/jit.go @@ -45,7 +45,7 @@ func init() { } // parseComponent parse the component -func (parser *TemplateParser) parseComponent(sel *goquery.Selection) { +func (parser *TemplateParser) parseJitComponent(sel *goquery.Selection) { parser.parsed(sel) comp, props, slots, children, err := parser.getComponent(sel) if err != nil { @@ -218,8 +218,8 @@ func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponen return comp, props, slots, children, nil } -// isComponent check if the selection is a component -func (parser *TemplateParser) isComponent(sel *goquery.Selection) bool { +// isJitComponent check if the selection is a component +func (parser *TemplateParser) isJitComponent(sel *goquery.Selection) bool { _, exist := sel.Attr("s:jit") is := sel.AttrOr("is", "") return exist && is != "" @@ -314,8 +314,7 @@ func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[st data := Data{"$props": props} route, _ = data.ReplaceUse(slotRe, route) route, _ = parser.data.Replace(route) - root := sel.AttrOr("s:root", "/") - file := filepath.Join(string(os.PathSeparator), "public", root, route+".jit") + file := filepath.Join(string(os.PathSeparator), "public", parser.option.Root, route+".jit") return file, route, nil } diff --git a/sui/core/parser.go b/sui/core/parser.go index bfef4b03..286eba1a 100644 --- a/sui/core/parser.go +++ b/sui/core/parser.go @@ -2,6 +2,8 @@ package core import ( "fmt" + "os" + "path/filepath" "strings" "github.com/PuerkitoBio/goquery" @@ -40,17 +42,18 @@ type Mapping struct { // ParserOption parser option type ParserOption struct { - Component bool `json:"component,omitempty"` - Editor bool `json:"editor,omitempty"` - Preview bool `json:"preview,omitempty"` - Debug bool `json:"debug,omitempty"` - DisableCache bool `json:"disableCache,omitempty"` - Request bool `json:"request,omitempty"` - Route string `json:"route,omitempty"` - Theme any `json:"theme,omitempty"` - Locale any `json:"locale,omitempty"` - Root string `json:"root,omitempty"` - Script *Script `json:"-"` // backend script + Component bool `json:"component,omitempty"` + Editor bool `json:"editor,omitempty"` + Preview bool `json:"preview,omitempty"` + Debug bool `json:"debug,omitempty"` + DisableCache bool `json:"disableCache,omitempty"` + Route string `json:"route,omitempty"` + Theme any `json:"theme,omitempty"` + Locale any `json:"locale,omitempty"` + Root string `json:"root,omitempty"` + Imports map[string]string `json:"imports,omitempty"` + Script *Script `json:"-"` // backend script + Request *Request `json:"request,omitempty"` } var keepWords = map[string]bool{ @@ -99,9 +102,6 @@ func NewTemplateParser(data Data, option *ParserOption) *TemplateParser { // Render parses and renders the HTML template func (parser *TemplateParser) Render(html string) (string, error) { - // Set the locale - parser.locale = parser.Locale() - if !strings.Contains(html, "%s`, html) } @@ -111,13 +111,11 @@ func (parser *TemplateParser) Render(html string) (string, error) { return "", err } - root := doc.Selection.Find("html") - parser.parseNode(root.Nodes[0]) - - // Replace the nodes - for sel, nodes := range parser.replace { - sel.ReplaceWithNodes(nodes...) - delete(parser.replace, sel) + // Set the locale + parser.locale = parser.Locale() + err = parser.RenderSelection(doc.Selection) + if err != nil { + return "", err } // Append the head @@ -150,16 +148,13 @@ func (parser *TemplateParser) Render(html string) (string, error) { parser.addScripts(body, parser.filterScripts("body", parser.scripts)) } - // Fmt - parser.Fmt(doc) - // For editor if parser.option != nil && parser.option.Editor { return doc.Find("body").Html() } // 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 doc.Find("[sui-hide]").Remove() parser.tidy(doc.Selection) @@ -170,8 +165,26 @@ func (parser *TemplateParser) Render(html string) (string, error) { 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 -func (parser *TemplateParser) Fmt(doc *goquery.Document) { +func (parser *TemplateParser) Fmt(doc *goquery.Selection) { if parser.locale != nil { sels := doc.Find(`[s\:trans-fmt]`) sels.Each(func(i int, sel *goquery.Selection) { @@ -194,10 +207,8 @@ func (parser *TemplateParser) parseNode(node *html.Node) { } parser.parseElementNode(sel) - // Skip children if the node is a loop node - if _, exist := sel.Attr("s:for"); exist { - skipChildren = true - } + // Skip children if the node is a loop node态element component or JIT component + skipChildren = parser.hasForStatement(sel) || parser.isElementComponent(sel) || parser.isJitComponent(sel) case html.TextNode: parser.parseTextNode(node) @@ -229,13 +240,86 @@ func (parser *TemplateParser) parseElementNode(sel *goquery.Selection) { parser.setStatementNode(sel) } - // JIT Compile the element - if parser.isComponent(sel) { - parser.parseComponent(sel) - } - // Parse the attributes 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) { @@ -486,6 +570,12 @@ func (parser *TemplateParser) parseTextNode(node *html.Node) { } 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) { diff --git a/sui/core/script.go b/sui/core/script.go index 1e9f60ce..e16dc8d4 100644 --- a/sui/core/script.go +++ b/sui/core/script.go @@ -99,6 +99,31 @@ func (script *Script) Call(r *Request, method string, args ...any) (interface{}, 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", res) +} + // ConstantsToString get the constants from the script func (script *Script) ConstantsToString() (string, error) { constants, err := script.Constants() diff --git a/sui/core/types.go b/sui/core/types.go index a5f45a6a..5d0ed685 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -59,7 +59,7 @@ type PageProp struct { // BuildContext is the struct for the build context type BuildContext struct { - components map[string]bool + components map[string]string jitComponents map[string]bool sequence int doc *goquery.Document From 0dbdc0b99bc0d3ee79499fcf5c0ac98e3da4544e Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 25 Jul 2024 18:05:21 +0800 Subject: [PATCH 5/5] Refactor SUI core to conditionally append page components in Compile function --- sui/core/compile.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sui/core/compile.go b/sui/core/compile.go index 0c0e053f..3624c373 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -93,8 +93,10 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, []str } // Page Components - rawComponents, _ := jsoniter.MarshalToString(ctx.components) - body.AppendHtml("\n\n" + `\n\n") + if ctx != nil && ctx.components != nil && len(ctx.components) > 0 { + rawComponents, _ := jsoniter.MarshalToString(ctx.components) + body.AppendHtml("\n\n" + `\n\n") + } page.ReplaceDocument(doc) html, err := doc.Html()