diff --git a/sui/api/request.go b/sui/api/request.go index 90d3f1c7..af060ba7 100644 --- a/sui/api/request.go +++ b/sui/api/request.go @@ -167,7 +167,9 @@ func (r *Request) Render() (string, int, error) { DisableCache: r.Request.DisableCache(), Route: r.Request.URL.Path, Root: c.Root, - Request: true, + Script: c.Script, + Imports: c.Imports, + Request: r.Request, } // Parse the template @@ -249,11 +251,28 @@ 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()) } + // 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 +285,8 @@ func (r *Request) MakeCache() (*core.Cache, int, error) { Root: root, 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 c8136361..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 } @@ -589,6 +587,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 +607,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 +646,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..e902be51 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,8 @@ type Cache struct { CacheStore string CacheTime time.Duration DataCacheTime time.Duration - Script *v8.Script // the backend script + Script *Script + Imports map[string]string } const ( @@ -73,6 +73,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/compile.go b/sui/core/compile.go index 83986c33..3624c373 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -93,14 +93,10 @@ 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(ctx.components) + body.AppendHtml("\n\n" + `\n\n") } - rawComponents, _ := jsoniter.MarshalToString(components) - body.AppendHtml("\n\n" + `\n\n") page.ReplaceDocument(doc) html, err := doc.Html() 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 afd08b2c..99efe181 100644 --- a/sui/core/injections.go +++ b/sui/core/injections.go @@ -186,6 +186,21 @@ const componentInitScriptTmpl = ` this.store = new __sui_store(this.root); ` +// Inject code +const backendScriptTmpl = ` +this.__sui_page = '%s'; +this.__sui_constants = {}; +this.__sui_helpers = []; + +if (typeof Helpers === 'object') { + this.__sui_helpers = Object.keys(Helpers); +} + +if (typeof Constants === 'object') { + this.__sui_constants = Constants; +} +` + func bodyInjectionScript(jsonRaw string, debug bool) string { jsPrintData := "" if debug { @@ -209,3 +224,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/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 3b6be196..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,16 +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"` + 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{ @@ -98,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) } @@ -110,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 @@ -149,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) @@ -169,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) { @@ -193,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) @@ -228,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) { @@ -485,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 new file mode 100644 index 00000000..e16dc8d4 --- /dev/null +++ b/sui/core/script.go @@ -0,0 +1,216 @@ +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 +} + +// 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() + 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..5d0ed685 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:"-"` @@ -60,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 @@ -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..f9800dc9 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 == "" { @@ -656,9 +657,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/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 } 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 }