yao/sui/core/build.go

384 lines
9.2 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
}
if attrs != nil {
data["$prop"] = 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}
val, _ = parentProps.ReplaceUse(slotRe, val)
}
attrs[attr.Key] = val
}
}
p := ipage.Get()
namespace := fmt.Sprintf("__page_%s_%d", strings.ReplaceAll(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")
}