refactor: Update parsing logic for JIT components

This commit is contained in:
Max 2024-07-08 10:31:35 +08:00
parent d188f967d0
commit ffb858b76f
7 changed files with 251 additions and 60 deletions

View file

@ -40,7 +40,7 @@ func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Docume
ctx.sequence++
namespace := Namespace(page.Name, ctx.sequence, option.ScriptMinify)
namespace := Namespace(page.Route, ctx.sequence, option.ScriptMinify)
page.namespace = namespace
html, err := page.BuildHTML(option)
@ -367,6 +367,11 @@ func (page *Page) BuildStyles(ctx *BuildContext, option *BuildOption, component
// BuildScripts build the scripts for the page
func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component string, namespace string) ([]ScriptNode, error) {
ispage := component == "__page"
if ispage {
component = ComponentName(page.Route, option.ScriptMinify)
}
scripts := []ScriptNode{}
if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" {
return scripts, nil
@ -416,7 +421,7 @@ func (page *Page) BuildScripts(ctx *BuildContext, option *BuildOption, component
code := string(source)
parent := "body"
if component != "__page" {
if !ispage {
parent = "head"
code = fmt.Sprintf("function %s(){\n%s\n}\n", component, addTabToEachLine(code))
}

View file

@ -1,10 +1,12 @@
package core
import (
"fmt"
"regexp"
"strings"
"github.com/evanw/esbuild/pkg/api"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/runtime/transform"
"github.com/yaoapp/kun/log"
)
@ -93,44 +95,6 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, error
return html, nil
}
// HTML return the html of the script
func (script ScriptNode) HTML() string {
attrs := []string{
"s:ns=\"" + script.Namespace + "\"",
"s:cn=\"" + script.Component + "\"",
}
if script.Attrs != nil {
for _, attr := range script.Attrs {
attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"")
}
}
// Inline Script
if script.Source == "" {
return "<script " + strings.Join(attrs, " ") + "></script>"
}
return "<script " + strings.Join(attrs, " ") + ">\n" + script.Source + "\n</script>"
}
// HTML return the html of the style node
func (style StyleNode) HTML() string {
attrs := []string{
"s:ns=\"" + style.Namespace + "\"",
"s:cn=\"" + style.Component + "\"",
}
if style.Attrs != nil {
for _, attr := range style.Attrs {
attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"")
}
}
// Inline Style
if style.Source == "" {
return "<link " + strings.Join(attrs, " ") + "></link>"
}
return "<style " + strings.Join(attrs, " ") + ">\n" + style.Source + "\n</style>"
}
// CompileAsComponent compile the page as component
func (page *Page) CompileAsComponent(ctx *BuildContext, option *BuildOption) (string, error) {
@ -148,8 +112,34 @@ func (page *Page) CompileAsComponent(ctx *BuildContext, option *BuildOption) (st
}
}
page.ReplaceDocument(doc)
return doc.Find("body").Html()
body := doc.Find("body")
rawScripts, err := jsoniter.MarshalToString(ctx.scripts)
if err != nil {
return "", err
}
rawStyles, err := jsoniter.MarshalToString(ctx.styles)
if err != nil {
return "", err
}
rawOption, err := jsoniter.MarshalToString(option)
if err != nil {
return "", err
}
if body.Children().Length() == 0 {
return "", fmt.Errorf("page %s as component should have one root element", page.Route)
}
if body.Children().Length() > 1 {
return "", fmt.Errorf("page %s as component should have only one root element", page.Route)
}
body.Children().First().AppendHtml(fmt.Sprintf(`<script name="scripts" type="json">%s</script>`+"\n", rawScripts))
body.Children().First().AppendHtml(fmt.Sprintf(`<script name="styles" type="json">%s</script>`+"\n", rawStyles))
body.Children().First().AppendHtml(fmt.Sprintf(`<script name="option" type="json">%s</script>`+"\n", rawOption))
return body.Html()
}
// CompileJS compile the javascript
@ -212,3 +202,62 @@ func (page *Page) CompileCSS(source []byte, minify bool) ([]byte, error) {
func (page *Page) CompileHTML(source []byte, minify bool) ([]byte, error) {
return source, nil
}
// HTML return the html of the script
func (script ScriptNode) HTML() string {
attrs := []string{
"s:ns=\"" + script.Namespace + "\"",
"s:cn=\"" + script.Component + "\"",
}
if script.Attrs != nil {
for _, attr := range script.Attrs {
attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"")
}
}
// Inline Script
if script.Source == "" {
return "<script " + strings.Join(attrs, " ") + "></script>"
}
return "<script " + strings.Join(attrs, " ") + ">\n" + script.Source + "\n</script>"
}
// ComponentHTML return the html of the script
func (script ScriptNode) ComponentHTML(ns string) string {
attrs := []string{
"s:ns=\"" + ns + "\"",
"s:cn=\"" + script.Component + "\"",
}
if script.Attrs != nil {
for _, attr := range script.Attrs {
attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"")
}
}
// Inline Script
if script.Source == "" {
return "<script " + strings.Join(attrs, " ") + "></script>"
}
source := fmt.Sprintf(`function %s(){%s};`, script.Component, script.Source)
return "<script " + strings.Join(attrs, " ") + ">\n" + source + "\n</script>"
}
// HTML return the html of the style node
func (style StyleNode) HTML() string {
attrs := []string{
"s:ns=\"" + style.Namespace + "\"",
"s:cn=\"" + style.Component + "\"",
}
if style.Attrs != nil {
for _, attr := range style.Attrs {
attrs = append(attrs, attr.Key+"=\""+attr.Val+"\"")
}
}
// Inline Style
if style.Source == "" {
return "<link " + strings.Join(attrs, " ") + "></link>"
}
return "<style " + strings.Join(attrs, " ") + ">\n" + style.Source + "\n</style>"
}

View file

@ -4,18 +4,23 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/PuerkitoBio/goquery"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/application"
)
// JitComponent component
type JitComponent struct {
html string
scripts []ScriptNode
styles []StyleNode
file string
route string
html string
scripts []ScriptNode
styles []StyleNode
buildOption *BuildOption
}
const (
@ -32,6 +37,9 @@ type componentData struct {
// Components loaded JIT components
var Components = map[string]*JitComponent{}
var chComp = make(chan *componentData, 1)
var reScripts = regexp.MustCompile(`<script[^>]*name="scripts"[^>]*>(.*?)</script>`)
var reStyles = regexp.MustCompile(`<script[^>]*name="styles"[^>]*>(.*?)</script>`)
var reOption = regexp.MustCompile(`<script[^>]*name="option"[^>]*>(.*?)</script>`)
func init() {
go componentWriter()
@ -47,17 +55,18 @@ func (parser *TemplateParser) parseComponent(sel *goquery.Selection) {
return
}
html, err := parser.RenderComponent(comp, props, slots)
html, _, err := parser.RenderComponent(comp, props, slots)
if err != nil {
parser.errors = append(parser.errors, err)
setError(sel, err)
return
}
sel.SetHtml(html)
}
// RenderComponent render the component
func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[string]interface{}, slots *goquery.Selection) (string, error) {
func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[string]interface{}, slots *goquery.Selection) (string, string, error) {
html := comp.html
randvar := fmt.Sprintf("__%s_$props", time.Now().Format("20060102150405"))
html = slotRe.ReplaceAllStringFunc(html, func(exp string) string {
@ -77,7 +86,7 @@ func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[stri
sel, err := NewDocumentString(`<body>` + html + `</body>`)
if err != nil {
return "", err
return "", "", err
}
root := sel.Find("body")
@ -103,15 +112,65 @@ func (parser *TemplateParser) RenderComponent(comp *JitComponent, props map[stri
compParser := NewTemplateParser(data, &option)
// Parse the node
ns := Namespace(comp.route, compParser.sequence, comp.buildOption.ScriptMinify)
cn := ComponentName(comp.route, comp.buildOption.ScriptMinify)
compParser.parseNode(root.Get(0))
for sel, nodes := range compParser.replace {
sel.ReplaceWithNodes(nodes...)
delete(parser.replace, sel)
}
if comp.scripts != nil {
for _, script := range comp.scripts {
script.Namespace = ns
parser.scripts = append(parser.scripts, script)
}
}
if comp.styles != nil {
for _, style := range comp.styles {
style.Namespace = ns
parser.styles = append(parser.styles, style)
}
}
// Update sequence
parser.sequence = compParser.sequence + parser.sequence
return root.Html()
first := root.Children().First()
first.SetAttr("s:ns", ns)
first.SetAttr("s:cn", cn)
first.SetAttr("s:ready", cn+"()")
html, err = root.Html()
if err != nil {
return "", "", err
}
return html, ns, nil
}
func (parser *TemplateParser) addScripts(sel *goquery.Selection, scripts []ScriptNode) {
if scripts == nil {
return
}
for _, script := range scripts {
query := fmt.Sprintf(`script[s\:cn="%s"]`, script.Component)
if sel.Find(query).Length() > 0 {
continue
}
sel.AppendHtml(script.ComponentHTML(script.Namespace))
}
}
func (parser *TemplateParser) addStyles(sel *goquery.Selection, styles []StyleNode) {
if styles == nil {
return
}
for _, style := range styles {
query := fmt.Sprintf(`style[s\:cn="%s"]`, style.Component)
if sel.Find(query).Length() > 0 {
continue
}
sel.Append(style.HTML())
}
}
func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponent, map[string]interface{}, *goquery.Selection, error) {
@ -123,12 +182,12 @@ func (parser *TemplateParser) getComponent(sel *goquery.Selection) (*JitComponen
return nil, nil, slots, err
}
file, err := parser.componentFile(sel, props)
file, route, err := parser.componentFile(sel, props)
if err != nil {
return nil, props, slots, err
}
comp, err := getComponent(file, parser.disableCache())
comp, err := getComponent(route, file, parser.disableCache())
if err != nil {
return nil, props, slots, err
}
@ -225,10 +284,10 @@ func (parser *TemplateParser) parseComponentProps(props map[string]string) (map[
return result, nil
}
func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[string]interface{}) (string, error) {
func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[string]interface{}) (string, string, error) {
route := sel.AttrOr("is", "")
if route == "" {
return "", fmt.Errorf("Component route is required")
return "", "", fmt.Errorf("Component route is required")
}
data := Data{"$props": props}
@ -236,7 +295,7 @@ func (parser *TemplateParser) componentFile(sel *goquery.Selection, props map[st
route, _ = parser.data.Replace(route)
root := sel.AttrOr("s:root", "/")
file := filepath.Join(string(os.PathSeparator), "public", root, route+".jit")
return file, nil
return file, route, nil
}
func componentWriter() {
@ -253,7 +312,7 @@ func componentWriter() {
}
}
func getComponent(file string, disableCache ...bool) (*JitComponent, error) {
func getComponent(route string, file string, disableCache ...bool) (*JitComponent, error) {
noCache := false
if len(disableCache) > 0 && disableCache[0] {
@ -264,7 +323,7 @@ func getComponent(file string, disableCache ...bool) (*JitComponent, error) {
return comp, nil
}
comp, err := readComponent(file)
comp, err := readComponent(route, file)
if err != nil {
return nil, err
}
@ -275,10 +334,63 @@ func getComponent(file string, disableCache ...bool) (*JitComponent, error) {
}
// readComponent read the JIT component
func readComponent(file string) (*JitComponent, error) {
func readComponent(route string, file string) (*JitComponent, error) {
content, err := application.App.Read(file)
if err != nil {
return nil, err
}
return &JitComponent{html: string(content)}, nil
// Get the scripts
html := string(content)
rawScripts := ""
rawStyles := ""
rawOption := ""
html = reScripts.ReplaceAllStringFunc(html, func(exp string) string {
rawScripts = reScripts.ReplaceAllString(exp, "$1")
return ""
})
html = reStyles.ReplaceAllStringFunc(html, func(exp string) string {
rawStyles = reStyles.ReplaceAllString(exp, "$1")
return ""
})
html = reOption.ReplaceAllStringFunc(html, func(exp string) string {
rawOption = reOption.ReplaceAllString(exp, "$1")
return ""
})
scripts := []ScriptNode{}
styles := []StyleNode{}
buildOption := BuildOption{}
if rawScripts != "" {
err := jsoniter.UnmarshalFromString(rawScripts, &scripts)
if err != nil {
return nil, err
}
}
if rawStyles != "" {
err := jsoniter.UnmarshalFromString(rawStyles, &styles)
if err != nil {
return nil, err
}
}
if rawOption != "" {
err := jsoniter.UnmarshalFromString(rawOption, &buildOption)
if err != nil {
return nil, err
}
}
return &JitComponent{
file: file,
route: route,
html: html,
scripts: scripts,
styles: styles,
buildOption: &buildOption,
}, nil
}

View file

@ -26,6 +26,8 @@ type TemplateParser struct {
option *ParserOption // parser option
locale *Locale // locale
context *ParserContext // parser context
scripts []ScriptNode // scripts
styles []StyleNode // styles
}
// ParserContext parser context for the template
@ -171,6 +173,8 @@ func NewTemplateParser(data Data, option *ParserOption) *TemplateParser {
errors: []error{},
replace: map[*goquery.Selection][]*html.Node{},
option: option,
scripts: []ScriptNode{},
styles: []StyleNode{},
}
}
@ -202,6 +206,8 @@ func (parser *TemplateParser) Render(html string) (string, error) {
head := doc.Find("head")
if head.Length() > 0 {
head.AppendHtml(headInjectionScript())
parser.addScripts(head, parser.scripts)
parser.addStyles(head, parser.styles)
}
// Append the data to the body

View file

@ -191,8 +191,9 @@ type Template struct {
// TemplateScirpts is the struct for the template scripts
type TemplateScirpts struct {
BeforeBuild []*TemplateScript `json:"before:build,omitempty"` // Run before build
AfterBuild []*TemplateScript `json:"after:build,omitempty"` // Run after build
BeforeBuild []*TemplateScript `json:"before:build,omitempty"` // Run before build
AfterBuild []*TemplateScript `json:"after:build,omitempty"` // Run after build
BuildComplete []*TemplateScript `json:"build:complete,omitempty"` // Run build complete
}
// TemplateScript is the struct for the template script

View file

@ -16,6 +16,11 @@ import (
// Build the template
func (tmpl *Template) Build(option *core.BuildOption) error {
var err error
defer func() {
if option.ExecScripts {
tmpl.ExecBuildCompleteScripts()
}
}()
// Execute the build before hook
if option.ExecScripts {

View file

@ -79,6 +79,14 @@ func (tmpl *Template) Reload() error {
return nil
}
// ExecBuildCompleteScripts execute the build complete scripts
func (tmpl *Template) ExecBuildCompleteScripts() []core.TemplateScirptResult {
if tmpl.Scripts == nil || len(tmpl.Scripts.BuildComplete) == 0 {
return nil
}
return tmpl.ExecScripts(tmpl.Scripts.BuildComplete)
}
// ExecBeforeBuildScripts execute the before build scripts
func (tmpl *Template) ExecBeforeBuildScripts() []core.TemplateScirptResult {
if tmpl.Scripts == nil || len(tmpl.Scripts.BeforeBuild) == 0 {
@ -97,7 +105,12 @@ func (tmpl *Template) ExecAfterBuildScripts() []core.TemplateScirptResult {
// ExecScripts execute the scripts
func (tmpl *Template) ExecScripts(scripts []*core.TemplateScript) []core.TemplateScirptResult {
results := []core.TemplateScirptResult{}
if scripts == nil {
return results
}
for _, script := range scripts {
switch script.Type {
case "command":