The Namespace function in utils.go has been updated to handle special characters in the name parameter. This ensures that the generated namespace is valid and avoids any potential issues with naming conflicts. The function now replaces forward slashes, square brackets, and hyphens with underscores before generating the namespace. This change improves the reliability and robustness of the code.
389 lines
9.4 KiB
Go
389 lines
9.4 KiB
Go
package core
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/yaoapp/kun/log"
|
|
)
|
|
|
|
var slotRe = regexp.MustCompile(`\[\{([^\}]+)\}\]`)
|
|
var cssRe = regexp.MustCompile(`([\.a-z0-9A-Z-:# ]+)\{`)
|
|
|
|
// Build is the struct for the public
|
|
func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error) {
|
|
|
|
warnings := []string{}
|
|
html, err := page.BuildHTML(option)
|
|
if err != nil {
|
|
warnings = append(warnings, err.Error())
|
|
}
|
|
|
|
// Add Style & Script & Warning
|
|
doc, err := NewDocumentString(html)
|
|
if err != nil {
|
|
warnings = append(warnings, err.Error())
|
|
}
|
|
|
|
// Append the nested html
|
|
err = page.parse(doc, option, warnings)
|
|
if err != nil {
|
|
warnings = append(warnings, err.Error())
|
|
}
|
|
|
|
// Add Style
|
|
style, err := page.BuildStyle(option)
|
|
if err != nil {
|
|
warnings = append(warnings, err.Error())
|
|
}
|
|
doc.Selection.Find("head").AppendHtml(style)
|
|
|
|
// Add Script
|
|
code, scripts, err := page.BuildScript(option)
|
|
if err != nil {
|
|
warnings = append(warnings, err.Error())
|
|
}
|
|
if scripts != nil {
|
|
for _, script := range scripts {
|
|
doc.Selection.Find("body").AppendHtml("\n" + `<script src="` + script + `"></script>` + "\n")
|
|
}
|
|
}
|
|
doc.Selection.Find("body").AppendHtml(code)
|
|
|
|
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) {
|
|
warnings := []string{}
|
|
html, err := page.BuildHTML(option)
|
|
if err != nil {
|
|
warnings = append(warnings, err.Error())
|
|
}
|
|
|
|
data := map[string]interface{}{}
|
|
if slots != nil {
|
|
slotvars := map[string]interface{}{}
|
|
for k, v := range slots {
|
|
slotvars[k] = v
|
|
}
|
|
data["$slot"] = slotvars // Will be deprecated use $slots instead
|
|
data["$slots"] = slotvars
|
|
}
|
|
|
|
if attrs != nil {
|
|
data["$prop"] = attrs // Will be deprecated use $props instead
|
|
data["$props"] = attrs
|
|
page.Attrs = attrs
|
|
}
|
|
|
|
// Add Style & Script & Warning
|
|
doc, err := NewDocumentString(html)
|
|
if err != nil {
|
|
warnings = append(warnings, err.Error())
|
|
}
|
|
|
|
// Append the nested html
|
|
err = page.parse(doc, option, warnings)
|
|
if err != nil {
|
|
warnings = append(warnings, err.Error())
|
|
}
|
|
|
|
// Add Style
|
|
style, err := page.BuildStyle(option)
|
|
if err != nil {
|
|
warnings = append(warnings, err.Error())
|
|
}
|
|
|
|
code, _, err := page.BuildScript(option)
|
|
if err != nil {
|
|
warnings = append(warnings, err.Error())
|
|
}
|
|
|
|
body := doc.Selection.Find("body")
|
|
if body.Length() > 1 {
|
|
body.SetHtml("<div>" + html + "</div>")
|
|
}
|
|
|
|
body.Children().First().SetAttr("s:ns", option.Namespace)
|
|
body.Children().First().SetAttr("s:ready", option.Namespace+"()")
|
|
html, err = body.Html()
|
|
if err != nil {
|
|
return "", "", "", warnings, err
|
|
}
|
|
|
|
// Replace the slots
|
|
html, _ = Data(data).ReplaceUse(slotRe, html)
|
|
return html, style, code, warnings, nil
|
|
}
|
|
|
|
func (page *Page) parse(doc *goquery.Document, option *BuildOption, warnings []string) error {
|
|
|
|
pages := doc.Find("*").FilterFunction(func(i int, sel *goquery.Selection) bool {
|
|
tagName := sel.Get(0).Data
|
|
if tagName == "page" {
|
|
return true
|
|
}
|
|
|
|
if tagName == "slot" {
|
|
return false
|
|
}
|
|
|
|
_, has := sel.Attr("is")
|
|
return has
|
|
})
|
|
|
|
sui := SUIs[page.SuiID]
|
|
if sui == nil {
|
|
return fmt.Errorf("SUI %s not found", page.SuiID)
|
|
}
|
|
|
|
tmpl, err := sui.GetTemplate(page.TemplateID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for idx, node := range pages.Nodes {
|
|
sel := goquery.NewDocumentFromNode(node)
|
|
name, has := sel.Attr("is")
|
|
if !has {
|
|
msg := fmt.Sprintf("Page %s/%s/%s: page tag must have an is attribute", page.SuiID, page.TemplateID, page.Route)
|
|
sel.ReplaceWith(fmt.Sprintf("<!-- %s -->", msg))
|
|
log.Warn(msg)
|
|
continue
|
|
}
|
|
|
|
sel.SetAttr("parsed", "true")
|
|
ipage, err := tmpl.Page(name)
|
|
if err != nil {
|
|
sel.ReplaceWith(fmt.Sprintf("<!-- %s -->", err.Error()))
|
|
log.Warn("Page %s/%s/%s: %s", page.SuiID, page.TemplateID, page.Route, err.Error())
|
|
continue
|
|
}
|
|
|
|
err = ipage.Load()
|
|
if err != nil {
|
|
sel.ReplaceWith(fmt.Sprintf("<!-- %s -->", err.Error()))
|
|
log.Warn("Page %s/%s/%s: %s", page.SuiID, page.TemplateID, page.Route, err.Error())
|
|
continue
|
|
}
|
|
|
|
// Set the parent
|
|
slots := map[string]interface{}{}
|
|
for _, slot := range sel.Find("slot").Nodes {
|
|
slotSel := goquery.NewDocumentFromNode(slot)
|
|
slotName, has := slotSel.Attr("is")
|
|
if !has {
|
|
continue
|
|
}
|
|
slotHTML, err := slotSel.Html()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
slots[slotName] = strings.TrimSpace(slotHTML)
|
|
}
|
|
|
|
// Set Attrs
|
|
attrs := map[string]string{}
|
|
if sel.Length() > 0 {
|
|
for _, attr := range sel.Nodes[0].Attr {
|
|
if attr.Key == "is" || attr.Key == "parsed" {
|
|
continue
|
|
}
|
|
val := attr.Val
|
|
if page.Attrs != nil {
|
|
parentProps := Data{
|
|
"$prop": page.Attrs, // Will be deprecated use $props instead
|
|
"$props": page.Attrs,
|
|
}
|
|
val, _ = parentProps.ReplaceUse(slotRe, val)
|
|
}
|
|
attrs[attr.Key] = val
|
|
}
|
|
}
|
|
|
|
p := ipage.Get()
|
|
namespace := Namespace(name, idx)
|
|
html, style, script, warns, err := p.BuildForImport(&BuildOption{
|
|
SSR: option.SSR,
|
|
AssetRoot: option.AssetRoot,
|
|
IgnoreAssetRoot: option.IgnoreAssetRoot,
|
|
KeepPageTag: option.KeepPageTag,
|
|
IgnoreDocument: true,
|
|
Namespace: namespace,
|
|
}, slots, attrs)
|
|
|
|
if err != nil {
|
|
sel.ReplaceWith(fmt.Sprintf("<!-- %s -->", err.Error()))
|
|
log.Warn("Page %s/%s/%s: %s", page.SuiID, page.TemplateID, page.Route, err.Error())
|
|
continue
|
|
}
|
|
|
|
if warns != nil {
|
|
warnings = append(warnings, warns...)
|
|
}
|
|
|
|
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))
|
|
|
|
// Set Slot HTML
|
|
slotsAttr, err := jsoniter.MarshalToString(slots)
|
|
if err != nil {
|
|
warns = append(warns, err.Error())
|
|
continue
|
|
}
|
|
|
|
sel.SetAttr("s:slots", slotsAttr)
|
|
|
|
// Set Attrs
|
|
for k, v := range attrs {
|
|
sel.SetAttr(k, v)
|
|
}
|
|
continue
|
|
}
|
|
sel.ReplaceWithHtml(fmt.Sprintf("\n%s\n%s\n%s\n", style, html, script))
|
|
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BuildHTML build the html
|
|
func (page *Page) BuildHTML(option *BuildOption) (string, error) {
|
|
|
|
html := string(page.Codes.HTML.Code)
|
|
|
|
if option.WithWrapper {
|
|
html = fmt.Sprintf("<body>%s</body>", html)
|
|
}
|
|
|
|
if !option.IgnoreDocument {
|
|
html = string(page.Document)
|
|
if page.Codes.HTML.Code != "" {
|
|
html = strings.Replace(html, "{{ __page }}", page.Codes.HTML.Code, 1)
|
|
}
|
|
}
|
|
|
|
if !option.IgnoreAssetRoot {
|
|
html = strings.ReplaceAll(html, "@assets", option.AssetRoot)
|
|
}
|
|
|
|
res, err := page.CompileHTML([]byte(html), false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return string(res), nil
|
|
}
|
|
|
|
// BuildStyle build the style
|
|
func (page *Page) BuildStyle(option *BuildOption) (string, error) {
|
|
if page.Codes.CSS.Code == "" {
|
|
return "", nil
|
|
}
|
|
|
|
code := page.Codes.CSS.Code
|
|
|
|
// Replace the assets
|
|
if !option.IgnoreAssetRoot {
|
|
code = AssetsRe.ReplaceAllStringFunc(code, func(match string) string {
|
|
return strings.ReplaceAll(match, "@assets", option.AssetRoot)
|
|
})
|
|
}
|
|
|
|
if option.Namespace != "" {
|
|
code = cssRe.ReplaceAllStringFunc(code, func(css string) string {
|
|
return fmt.Sprintf("[s\\:ns=%s] %s", option.Namespace, css)
|
|
})
|
|
}
|
|
|
|
res, err := page.CompileCSS([]byte(code), false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return fmt.Sprintf("<style type=\"text/css\">\n%s\n</style>\n", res), nil
|
|
}
|
|
|
|
// BuildScript build the script
|
|
func (page *Page) BuildScript(option *BuildOption) (string, []string, error) {
|
|
|
|
if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" {
|
|
return "", nil, nil
|
|
}
|
|
|
|
if page.Codes.TS.Code != "" {
|
|
code, scripts, err := page.CompileTS([]byte(page.Codes.TS.Code), false)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
if option.Namespace == "" {
|
|
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
|
|
}
|
|
|
|
code, scripts, err := page.CompileJS([]byte(page.Codes.JS.Code), false)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
if option.Namespace == "" {
|
|
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
|
|
}
|
|
|
|
func addTabToEachLine(input string, prefix ...string) string {
|
|
var lines []string
|
|
|
|
space := " "
|
|
if len(prefix) > 0 {
|
|
space = prefix[0]
|
|
}
|
|
|
|
scanner := bufio.NewScanner(strings.NewReader(input))
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
lineWithTab := space + line
|
|
lines = append(lines, lineWithTab)
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|