Optimize the SUI compiler to better support component code

This commit is contained in:
Max 2024-06-28 19:46:08 +08:00
parent 7db3940914
commit 4a1a1b2dbc
6 changed files with 121 additions and 37 deletions

View file

@ -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"+`<style type="text/css">`+"\n%s\n"+`</style>`+"\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" + `<script src="` + script + `"></script>` + "\n")
}
}
doc.Selection.Find("body").AppendHtml(code)
doc.Selection.Find("body").AppendHtml(fmt.Sprintf("\n"+`<script type="text/javascript">`+"\n%s\n"+`</script>`+"\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("<div>" + html + "</div>")
}
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("<style type=\"text/css\">\n%s\n</style>\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("<script type=\"text/javascript\">\n%s\n</script>\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("<script type=\"text/javascript\">\n%s\n</script>\n", code), scripts, nil
}
return fmt.Sprintf("<script type=\"text/javascript\">\nfunction %s(){\n%s\n}\n</script>\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("<script type=\"text/javascript\">%s</script>\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("<script type=\"text/javascript\">\n%s\n</script>\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("<script type=\"text/javascript\">\nfunction %s(){\n%s\n}\n</script>\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("<script type=\"text/javascript\">%s</script>\n", componentCode), scripts, nil
}
func addTabToEachLine(input string, prefix ...string) string {

View file

@ -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
}

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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("<div>\n%s\n</div>", 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)
}