diff --git a/sui/core/build.go b/sui/core/build.go index 19ea51a7..438f3273 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -19,8 +19,18 @@ var cssRe = regexp.MustCompile(`([\.a-z0-9A-Z-:# ]+)\{`) var langFuncRe = regexp.MustCompile(`L\s*\(\s*["'](.*?)["']\s*\)`) var langAttrRe = regexp.MustCompile(`'::(.*?)'`) +// NewBuildContext create a new build context +func NewBuildContext() *BuildContext { + return &BuildContext{ + components: map[string]bool{}, + sequence: 1, + scripts: []string{}, + styles: []string{}, + } +} + // Build is the struct for the public -func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error) { +func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Document, []string, error) { warnings := []string{} html, err := page.BuildHTML(option) @@ -34,21 +44,24 @@ func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error warnings = append(warnings, err.Error()) } + // Add components scripts and styles + ctx.doc = doc + // Append the nested html - err = page.parse(doc, option, warnings) + err = page.parse(ctx, doc, option, warnings) if err != nil { warnings = append(warnings, err.Error()) } // Add Style - style, err := page.BuildStyle(option) + _, err = page.BuildStyle(ctx, option) if err != nil { warnings = append(warnings, err.Error()) } - doc.Selection.Find("head").AppendHtml(style) + doc.Selection.Find("head").AppendHtml(fmt.Sprintf("\n"+``+"\n", strings.Join(ctx.styles, "\n"))) // Add Script - code, scripts, err := page.BuildScript(option, option.Namespace) + _, scripts, err := page.BuildScript(ctx, option, option.Namespace) if err != nil { warnings = append(warnings, err.Error()) } @@ -57,13 +70,20 @@ func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error doc.Selection.Find("body").AppendHtml("\n" + `` + "\n") } } - doc.Selection.Find("body").AppendHtml(code) + doc.Selection.Find("body").AppendHtml(fmt.Sprintf("\n"+``+"\n", strings.Join(ctx.scripts, "\n"))) return doc, warnings, nil } // BuildForImport build the page for import -func (page *Page) BuildForImport(option *BuildOption, slots map[string]interface{}, attrs map[string]string) (string, string, string, []string, error) { +func (page *Page) BuildForImport(ctx *BuildContext, option *BuildOption, slots map[string]interface{}, attrs map[string]string) (string, string, string, []string, error) { + + defer func() { + if option.ComponentName != "" { + ctx.components[option.ComponentName] = true + } + }() + warnings := []string{} html, err := page.BuildHTML(option) if err != nil { @@ -87,24 +107,24 @@ func (page *Page) BuildForImport(option *BuildOption, slots map[string]interface } // Add Style & Script & Warning - doc, err := NewDocumentString(html) + doc, err := NewDocumentStringWithWrapper(html) if err != nil { warnings = append(warnings, err.Error()) } // Append the nested html - err = page.parse(doc, option, warnings) + err = page.parse(ctx, doc, option, warnings) if err != nil { warnings = append(warnings, err.Error()) } // Add Style - style, err := page.BuildStyle(option) + style, err := page.BuildStyle(ctx, option) if err != nil { warnings = append(warnings, err.Error()) } - code, _, err := page.BuildScript(option, option.Namespace) + code, _, err := page.BuildScript(ctx, option, option.Namespace) if err != nil { warnings = append(warnings, err.Error()) } @@ -114,6 +134,7 @@ func (page *Page) BuildForImport(option *BuildOption, slots map[string]interface body.SetHtml("
" + html + "
") } + body.Children().First().SetAttr("s:cn", option.ComponentName) body.Children().First().SetAttr("s:ns", option.Namespace) body.Children().First().SetAttr("s:ready", option.Namespace+"()") html, err = body.Html() @@ -126,7 +147,7 @@ func (page *Page) BuildForImport(option *BuildOption, slots map[string]interface return html, style, code, warnings, nil } -func (page *Page) parse(doc *goquery.Document, option *BuildOption, warnings []string) error { +func (page *Page) parse(ctx *BuildContext, doc *goquery.Document, option *BuildOption, warnings []string) error { pages := doc.Find("*").FilterFunction(func(i int, sel *goquery.Selection) bool { @@ -158,7 +179,7 @@ func (page *Page) parse(doc *goquery.Document, option *BuildOption, warnings []s return err } - for idx, node := range pages.Nodes { + for _, node := range pages.Nodes { sel := goquery.NewDocumentFromNode(node) name, has := sel.Attr("is") if !has { @@ -218,14 +239,16 @@ func (page *Page) parse(doc *goquery.Document, option *BuildOption, warnings []s } p := ipage.Get() - namespace := Namespace(name, idx) - html, style, script, warns, err := p.BuildForImport(&BuildOption{ + namespace := Namespace(name, ctx.sequence+1) + componentName := ComponentName(name) + html, _, _, warns, err := p.BuildForImport(ctx, &BuildOption{ SSR: option.SSR, AssetRoot: option.AssetRoot, IgnoreAssetRoot: option.IgnoreAssetRoot, KeepPageTag: option.KeepPageTag, IgnoreDocument: true, Namespace: namespace, + ComponentName: componentName, }, slots, attrs) // append translations @@ -243,9 +266,8 @@ func (page *Page) parse(doc *goquery.Document, option *BuildOption, warnings []s sel.SetAttr("s:ns", namespace) sel.SetAttr("s:ready", namespace+"()") - if option.KeepPageTag { - sel.SetHtml(fmt.Sprintf("\n%s\n%s\n%s\n", style, addTabToEachLine(html), script)) + sel.SetHtml(fmt.Sprintf("\n%s\n", addTabToEachLine(html))) // Set Slot HTML slotsAttr, err := jsoniter.MarshalToString(slots) @@ -262,8 +284,8 @@ func (page *Page) parse(doc *goquery.Document, option *BuildOption, warnings []s } continue } - sel.ReplaceWithHtml(fmt.Sprintf("\n%s\n%s\n%s\n", style, html, script)) - + sel.ReplaceWithHtml(fmt.Sprintf("\n%s\n", addTabToEachLine(html))) + ctx.sequence++ } return nil } @@ -297,7 +319,7 @@ func (page *Page) BuildHTML(option *BuildOption) (string, error) { } // BuildStyle build the style -func (page *Page) BuildStyle(option *BuildOption) (string, error) { +func (page *Page) BuildStyle(ctx *BuildContext, option *BuildOption) (string, error) { if page.Codes.CSS.Code == "" { return "", nil } @@ -311,9 +333,9 @@ func (page *Page) BuildStyle(option *BuildOption) (string, error) { }) } - if option.Namespace != "" { + if option.ComponentName != "" { code = cssRe.ReplaceAllStringFunc(code, func(css string) string { - return fmt.Sprintf("[s\\:ns=%s] %s", option.Namespace, css) + return fmt.Sprintf("[s\\:cn=%s] %s", option.ComponentName, css) }) } @@ -322,22 +344,37 @@ func (page *Page) BuildStyle(option *BuildOption) (string, error) { return "", err } + ctx.styles = append(ctx.styles, string(res)) return fmt.Sprintf("\n", res), nil } // BuildScript build the script -func (page *Page) BuildScript(option *BuildOption, namespace string) (string, []string, error) { +func (page *Page) BuildScript(ctx *BuildContext, option *BuildOption, namespace string) (string, []string, error) { if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" { return "", nil, nil } + instanceCode := fmt.Sprintf("function %s(){ %s(...arguments);}", option.Namespace, option.ComponentName) + + // if the script is a component and not the first import + if option.ComponentName != "" && ctx.components[option.ComponentName] { + ctx.scripts = append(ctx.scripts, instanceCode) + return fmt.Sprintf("\n", instanceCode), []string{}, nil + } + + // TypeScript if page.Codes.TS.Code != "" { code, scripts, err := page.CompileTS([]byte(page.Codes.TS.Code), false) if err != nil { return "", nil, err } + // Get the translation + if translations := getScriptTranslation(string(code), namespace); len(translations) > 0 { + page.Translations = append(page.Translations, translations...) + } + // Replace the assets if !option.IgnoreAssetRoot { code = AssetsRe.ReplaceAllFunc(code, func(match []byte) []byte { @@ -355,20 +392,27 @@ func (page *Page) BuildScript(option *BuildOption, namespace string) (string, [] return fmt.Sprintf("\n", code), scripts, nil } - return fmt.Sprintf("\n", option.Namespace, addTabToEachLine(string(code))), scripts, nil + componentCode := fmt.Sprintf("function %s(){\n%s\n}\n%s\n", option.ComponentName, addTabToEachLine(string(code)), instanceCode) + ctx.scripts = append(ctx.scripts, componentCode) + return fmt.Sprintf("\n", componentCode), scripts, nil } + // JavaScript code, scripts, err := page.CompileJS([]byte(page.Codes.JS.Code), true) if err != nil { return "", nil, err } + // Get the translation + if translations := getScriptTranslation(string(code), namespace); len(translations) > 0 { + page.Translations = append(page.Translations, translations...) + } + // Replace the assets if !option.IgnoreAssetRoot { code = AssetsRe.ReplaceAllFunc(code, func(match []byte) []byte { return []byte(strings.ReplaceAll(string(match), "@assets", option.AssetRoot)) }) - if scripts != nil { for i, script := range scripts { scripts[i] = filepath.Join(option.AssetRoot, script) @@ -380,12 +424,9 @@ func (page *Page) BuildScript(option *BuildOption, namespace string) (string, [] return fmt.Sprintf("\n", code), scripts, nil } - // Get the translation - if translations := getScriptTranslation(string(code), namespace); len(translations) > 0 { - page.Translations = append(page.Translations, translations...) - } - - return fmt.Sprintf("\n", option.Namespace, addTabToEachLine(string(code))), scripts, nil + componentCode := fmt.Sprintf("function %s(){\n%s\n}\n%s\n", option.ComponentName, addTabToEachLine(string(code)), instanceCode) + ctx.scripts = append(ctx.scripts, componentCode) + return fmt.Sprintf("\n", componentCode), scripts, nil } func addTabToEachLine(input string, prefix ...string) string { diff --git a/sui/core/compile.go b/sui/core/compile.go index 61b08e72..977bb570 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -18,7 +18,8 @@ var AssetsRe = regexp.MustCompile(`[` + quoteRe + `]@assets\/([^` + quoteRe + `] // Compile the page func (page *Page) Compile(option *BuildOption) (string, error) { - doc, warnings, err := page.Build(option) + ctx := NewBuildContext() + doc, warnings, err := page.Build(ctx, option) if err != nil { return "", err } diff --git a/sui/core/editor.go b/sui/core/editor.go index 8459940a..a996f9e9 100644 --- a/sui/core/editor.go +++ b/sui/core/editor.go @@ -48,8 +48,8 @@ func (page *Page) EditorRender() (*ResponseEditorRender, error) { // Render tools // res.Scripts = append(res.Scripts, filepath.Join("@assets", "__render.js")) // res.Styles = append(res.Styles, filepath.Join("@assets", "__render.css")) - - doc, warnings, err := page.Build(&BuildOption{ + ctx := NewBuildContext() + doc, warnings, err := page.Build(ctx, &BuildOption{ SSR: true, IgnoreAssetRoot: true, IgnoreDocument: true, diff --git a/sui/core/preview.go b/sui/core/preview.go index d415a7d3..e135aa1a 100644 --- a/sui/core/preview.go +++ b/sui/core/preview.go @@ -17,7 +17,8 @@ func (page *Page) PreviewRender(referer string) (string, error) { } warnings := []string{} - doc, warnings, err := page.Build(&BuildOption{ + ctx := NewBuildContext() + doc, warnings, err := page.Build(ctx, &BuildOption{ SSR: true, AssetRoot: fmt.Sprintf("/api/__yao/sui/v1/%s/asset/%s/@assets", page.SuiID, page.TemplateID), KeepPageTag: false, diff --git a/sui/core/types.go b/sui/core/types.go index 73fd7e00..125e213b 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -3,6 +3,8 @@ package core import ( "net/url" "regexp" + + "github.com/PuerkitoBio/goquery" ) // DSL the struct for the DSL @@ -40,6 +42,15 @@ type Page struct { Translations []Translation `json:"-"` } +// BuildContext is the struct for the build context +type BuildContext struct { + components map[string]bool + sequence int + doc *goquery.Document + scripts []string + styles []string +} + // Translation is the struct for the translation type Translation struct { Key string `json:"key,omitempty"` @@ -196,6 +207,7 @@ type BuildOption struct { KeepPageTag bool `json:"keep_page_tag,omitempty"` Namespace string `json:"namespace,omitempty"` Data map[string]interface{} `json:"data,omitempty"` + ComponentName string `json:"component_name,omitempty"` } // Request is the struct for the request diff --git a/sui/core/utils.go b/sui/core/utils.go index 32fe9a69..95d002fc 100644 --- a/sui/core/utils.go +++ b/sui/core/utils.go @@ -27,11 +27,40 @@ func NewDocumentString(htmlContent string) (*goquery.Document, error) { return goquery.NewDocumentFromNode(docNode), nil } +// NewDocumentStringWithWrapper create a new document with a wrapper +func NewDocumentStringWithWrapper(htmlContent string) (*goquery.Document, error) { + doc, err := NewDocumentString(htmlContent) + if err != nil { + return nil, err + } + + // Check if the doc has root element add a div wrapper + nodes := doc.Find("Body *").Nodes + if len(nodes) == 1 { + sel := goquery.NewDocumentFromNode(nodes[0]) + if _, has := sel.Attr("is"); has { + doc, err := NewDocumentString(fmt.Sprintf("
\n%s\n
", htmlContent)) + if err != nil { + return nil, err + } + return doc, nil + } + } + return doc, nil +} + // Namespace convert the name to namespace func Namespace(name string, idx int) string { name = strings.ReplaceAll(name, "/", "_") name = strings.ReplaceAll(name, "[", "_") name = strings.ReplaceAll(name, "]", "_") - namespace := fmt.Sprintf("__page_%s_%d", name, idx) - return namespace + return fmt.Sprintf("__page_%s_%d", name, idx) +} + +// ComponentName convert the name to component name +func ComponentName(name string) string { + name = strings.ReplaceAll(name, "/", "_") + name = strings.ReplaceAll(name, "[", "_") + name = strings.ReplaceAll(name, "]", "_") + return fmt.Sprintf("__component_%s", name) }