]> git.earlybird.gay Git - today/commitdiff
support web components a lil bit
authorearly <me@earlybird.gay>
Tue, 30 Jul 2024 15:53:59 +0000 (09:53 -0600)
committerearly <me@earlybird.gay>
Tue, 30 Jul 2024 15:53:59 +0000 (09:53 -0600)
13 files changed:
cmd/run-mast-examples/ex07-components/counter.html [new file with mode: 0644]
cmd/run-mast-examples/ex07-components/example.go [new file with mode: 0644]
cmd/run-mast-examples/ex07-components/page.html [new file with mode: 0644]
cmd/run-mast-examples/index.html
cmd/run-mast-examples/main.go
component/component.go [new file with mode: 0644]
internal/compile/compile.go
internal/compile/component.go [new file with mode: 0644]
internal/compile/replace.go [deleted file]
internal/compile/template.go [new file with mode: 0644]
page/page.go
part/part.go
part/parts.go [deleted file]

diff --git a/cmd/run-mast-examples/ex07-components/counter.html b/cmd/run-mast-examples/ex07-components/counter.html
new file mode 100644 (file)
index 0000000..5ee7850
--- /dev/null
@@ -0,0 +1,15 @@
+<template id="example-counter-template">
+    <slot name="text">Count:</slot>
+</template>
+<script>
+class ExampleCounter extends HTMLElement {
+    constructor() {
+        super();
+        const shadowRoot = this.attachShadow({ mode: "open" });
+        const template = document.getElementById("example-counter-template");
+        
+        shadowRoot.appendChild(template.content.cloneNode(true));
+    }
+}
+customElements.define('example-counter', ExampleCounter);
+</script>
diff --git a/cmd/run-mast-examples/ex07-components/example.go b/cmd/run-mast-examples/ex07-components/example.go
new file mode 100644 (file)
index 0000000..f9413be
--- /dev/null
@@ -0,0 +1,11 @@
+// Copyright (C) 2024 early (LGPL)
+package ex07
+
+import (
+       "git.earlybird.gay/mast-engine/cmd/run-mast-examples/parts"
+       "git.earlybird.gay/mast-engine/component"
+       "git.earlybird.gay/mast-engine/page"
+)
+
+var Page = page.New("ex01", "page.html", page.Includes(parts.ExampleNav, counter))
+var counter = component.New("example-counter", "counter.html")
diff --git a/cmd/run-mast-examples/ex07-components/page.html b/cmd/run-mast-examples/ex07-components/page.html
new file mode 100644 (file)
index 0000000..7a81538
--- /dev/null
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+    <header>
+        <title>Example 1</title>
+    </header>
+    <body>
+        <h1>Web Components</h1>
+
+        <example-counter>
+            <span slot="text">Example:</span>
+        </example-counter>
+        <!-- Ignore me!! I'll be explained in a second! -->
+        <example-nav>
+            <a href="ex02" slot="next">Parts</a>
+        </example-nav>
+    </body>
+</html>
\ No newline at end of file
index 707a0a846dcc168e50d3910f0f12b63a3ed4fc8a..c75f526449a7a7c0e9e8416e269b1fc3532c65ac 100644 (file)
@@ -14,6 +14,7 @@
             <li><a href="/ex04">Go Templates</a></li>
             <li>N/A</li>
             <li><a href="/ex06">Passing Data to Parts</a></li>
+            <li><a href="/ex07">Web Components</a></li>
         </ol>
     </body>
 </html>
\ No newline at end of file
index ee62a19017e1d43e77e8261ceb9fc1f5ab7c59d3..f99e1b40efab6a51b6570cc4f852fc9da967ee49 100644 (file)
@@ -9,6 +9,7 @@ import (
        "git.earlybird.gay/mast-engine/cmd/run-mast-examples/ex03-slots"
        "git.earlybird.gay/mast-engine/cmd/run-mast-examples/ex04-templates"
        "git.earlybird.gay/mast-engine/cmd/run-mast-examples/ex06-data"
+       "git.earlybird.gay/mast-engine/cmd/run-mast-examples/ex07-components"
        "git.earlybird.gay/mast-engine/page"
 )
 
@@ -22,6 +23,7 @@ func main() {
        mux.Handle("GET /ex03", ex03.Page)
        mux.Handle("GET /ex04", ex04.Page)
        mux.Handle("GET /ex06", ex06.Page)
+       mux.Handle("GET /ex07", ex07.Page)
 
        http.ListenAndServe("0.0.0.0:3000", mux)
 }
diff --git a/component/component.go b/component/component.go
new file mode 100644 (file)
index 0000000..6c26470
--- /dev/null
@@ -0,0 +1,46 @@
+// Copyright (C) 2024 early (LGPL)
+package component
+
+import (
+       "git.earlybird.gay/mast-engine/include"
+       "git.earlybird.gay/mast-engine/internal/compile"
+)
+
+type Component struct {
+       name   string
+       source include.Opener
+
+       includes []compile.Source
+}
+
+type Config func(*Component)
+
+func Includes(includes ...compile.Source) Config {
+       return func(p *Component) {
+               p.includes = includes
+       }
+}
+
+func New(name string, source string, optional ...func(*Component)) *Component {
+       p := new(Component)
+       // Assign basic parameters
+       p.name = name
+       p.source = include.File(source, "git.earlybird.gay/mast-engine/component")
+       // Run optional arguments
+       for _, of := range optional {
+               of(p)
+       }
+       return p
+}
+
+func (p *Component) Name() string {
+       return p.name
+}
+
+func (p *Component) File() include.Opener {
+       return p.source
+}
+
+func (p *Component) Includes() []compile.Source {
+       return p.includes
+}
index 6b8cf53fc2228b9bffc159f7839e604de2150699..4dd8cf1d8baf380a0acc7fca66889419180d6269 100644 (file)
@@ -3,12 +3,10 @@ package compile
 
 import (
        "errors"
-       "fmt"
        "html/template"
        "maps"
        "strings"
 
-       "git.earlybird.gay/mast-engine/htmltree"
        "git.earlybird.gay/mast-engine/include"
        "git.earlybird.gay/mast-engine/render"
        "golang.org/x/net/html"
@@ -17,9 +15,13 @@ import (
 type Source interface {
        Name() string
        File() include.Opener
+       Includes() []Source
+}
+
+type TemplateSource interface {
+       Source
        TemplateFuncs() template.FuncMap
        OnLoad() render.OnLoadFunc
-       Includes() []Source
 }
 
 type Sources map[string]Source
@@ -37,130 +39,7 @@ type Result struct {
        TemplateDataLoader render.Loader
 }
 
-func insertSubSource(subSource Source, context *html.Node) (*computeNode, error) {
-       insertedId := render.ID()
-       computedName := fmt.Sprintf("%s_%s", snakeCase(subSource.Name()), insertedId)
-       // ===== CONTEXT PREPROCESSING =====
-       // Raise any fields pre-existing in the context to .parent.field.
-       raiseFields := make([]string, 0)
-       htmltree.Walk(context.FirstChild, func(n *html.Node) (bool, error) {
-               raiseFields = append(raiseFields, replaceTemplateFields(n)...)
-               return false, nil
-       })
-       // Remove :data attributes from the root of context, and prepare them
-       // to be used as data for the computeNode.
-       asData := removeDataAttrs(context)
-       // ===== SUBSOURCE PROCESSING =====
-       // Parse the subSource in the context of context.
-       // Require that subSource be contained in a template.
-       reader, err := subSource.File().Open()
-       if err != nil {
-               return nil, err
-       }
-       innerHTML, err := html.ParseFragment(reader, context)
-       if err != nil {
-               return nil, err
-       }
-       root := innerHTML[0]
-       if root.Type != html.ElementNode || root.Data != "template" {
-               return nil, errors.New("fragments must be contained in a template")
-       }
-
-       // Walk the tree to do a couple of things:
-       //   - Replace template functions with namespaced functions.
-       //   - Find slots in the subSource.
-       slots := make(map[string]*html.Node)
-       usedSlot := make(map[string]bool)
-       htmltree.Walk(root, func(n *html.Node) (bool, error) {
-               // Replace template functions with namespaced functions.
-               replaceTemplateFuncs(subSource, n)
-               // Find slots in the subSource.
-               if n.Type == html.ElementNode && n.Data == "slot" {
-                       slotName := htmltree.GetAttr(n, "name")
-                       if _, ok := slots[slotName]; ok {
-                               return true, fmt.Errorf("found multiple slots named '%s'", slotName)
-                       } else {
-                               slots[slotName] = n
-                               usedSlot[slotName] = false
-                       }
-               }
-               return false, nil
-       })
-
-       // Mix stuff from the context into the template using slots...
-       n := context.FirstChild
-       for n != nil {
-               next := n.NextSibling
-               context.RemoveChild(n)
-               // Do not slot any non-slottable nodes, or comments, because I do not
-               // like them
-               if n.Type == html.CommentNode {
-                       n = next
-                       continue
-               }
-               slotName := htmltree.GetAttr(n, "slot")
-               slot := slots[slotName]
-               if slot != nil {
-                       slot.Parent.InsertBefore(n, slot)
-                       usedSlot[slotName] = true
-               } else {
-                       root.AppendChild(n)
-               }
-               n = next
-       }
-
-       // ...then move content from the template to the context...
-       n = root.FirstChild
-       for n != nil {
-               next := n.NextSibling
-               root.RemoveChild(n)
-               context.AppendChild(n)
-               n = next
-       }
-
-       // ...then remove any slots that were used.
-       // For slots that aren't used, move up their data first.
-       htmltree.Walk(context, func(n *html.Node) (bool, error) {
-               if n.Type == html.ElementNode && n.Data == "slot" {
-                       slotName := htmltree.GetAttr(n, "name")
-                       slot := slots[slotName]
-                       used := usedSlot[slotName]
-                       if !used {
-                               n = slot.FirstChild
-                               for n != nil {
-                                       next := n.NextSibling
-                                       slot.RemoveChild(n)
-                                       slot.Parent.InsertBefore(n, slot)
-                                       n = next
-                               }
-                       }
-                       slot.Parent.RemoveChild(slot)
-               }
-               return false, nil
-       })
-
-       // Generate a computeNode for this part.
-       compute := &computeNode{
-               name:             computedName,
-               compute:          subSource.OnLoad(),
-               asDataFromParent: asData,
-               raiseFromParent:  raiseFields,
-       }
-
-       // Set ID and computed context, then generate a function that fulfills it
-       htmltree.SetAttr(context, "id", insertedId)
-       context.InsertBefore(&html.Node{
-               Type: html.TextNode,
-               Data: fmt.Sprintf("{{- with .computed.%s }}", computedName),
-       }, context.FirstChild)
-       context.InsertBefore(&html.Node{
-               Type: html.TextNode,
-               Data: "{{ end -}}",
-       }, nil)
-       return compute, nil
-}
-
-func Compile(root Source, transform ...func(root *html.Node)) (Result, error) {
+func Compile(root TemplateSource, transform ...func(root *html.Node)) (Result, error) {
        var result Result
        reader, err := root.File().Open()
        if err != nil {
@@ -178,6 +57,20 @@ func Compile(root Source, transform ...func(root *html.Node)) (Result, error) {
                compute: root.OnLoad(),
        }
 
+       // Insert component sources into document
+       for name, subSource := range fullDependencies {
+               // Easiest way to tell right now is what isn't a template source,
+               // but this bears re-evaluating later
+               if _, ok := subSource.(TemplateSource); ok {
+                       continue
+               }
+               err := insertComponentSource(subSource, document)
+               if err != nil {
+                       return result, err
+               }
+               delete(fullDependencies, name)
+       }
+
        // gather global template funcs
        // Start with the root's template, then add template funcs for all subsource
        // with a namespace
@@ -189,17 +82,20 @@ func Compile(root Source, transform ...func(root *html.Node)) (Result, error) {
                siblingComputeNode := c
                if n.Type == html.ElementNode {
                        if subSource, ok := fullDependencies[n.Data]; ok {
-                               // Add template funcs
-                               for fname, f := range subSource.TemplateFuncs() {
-                                       templateFuncs[snakeCase(n.Data)+"_"+fname] = f
-                               }
-                               // Parse HTML fragment and replace the content of the node with it
-                               computeSubSource, err := insertSubSource(subSource, n)
-                               if err != nil {
-                                       return err
+                               if templateSource, ok := subSource.(TemplateSource); ok {
+                                       // Template sources (parts) are inserted inline
+                                       // Add template funcs
+                                       for fname, f := range templateSource.TemplateFuncs() {
+                                               templateFuncs[snakeCase(n.Data)+"_"+fname] = f
+                                       }
+                                       // Parse HTML fragment and replace the content of the node with it
+                                       computeSubSource, err := insertTemplateSource(templateSource, n)
+                                       if err != nil {
+                                               return err
+                                       }
+                                       c.children = append(c.children, computeSubSource)
+                                       childComputeNode = computeSubSource
                                }
-                               c.children = append(c.children, computeSubSource)
-                               childComputeNode = computeSubSource
                        }
                }
                var procErr error
diff --git a/internal/compile/component.go b/internal/compile/component.go
new file mode 100644 (file)
index 0000000..b0d7aa7
--- /dev/null
@@ -0,0 +1,69 @@
+package compile
+
+import (
+       "errors"
+       "fmt"
+
+       "git.earlybird.gay/mast-engine/htmltree"
+       "golang.org/x/net/html"
+       "golang.org/x/net/html/atom"
+)
+
+var ErrBadComponentFormat = errors.New("web components must either be a script or a template and script")
+
+// insertComponentSource inserts a web component subSource into a document.
+// Unlike insertTemplateSource, this expects the root document and not the
+// context where the web component is being included.
+func insertComponentSource(subSource Source, document *html.Node) error {
+       // ===== SUBSOURCE PROCESSING =====
+       // Parse the subSource in the context of a node named subSource.Name().
+       // subSource should be:
+       // <template> and <script>
+       // <script>
+       reader, err := subSource.File().Open()
+       if err != nil {
+               return err
+       }
+       innerHTML, err := html.ParseFragment(reader, &html.Node{
+               Type:     html.ElementNode,
+               Data:     "div",
+               DataAtom: atom.Div,
+       })
+       if err != nil {
+               return err
+       }
+       htmltree.Minify()(innerHTML[0])
+
+       var template, script, body *html.Node
+
+       for _, node := range innerHTML {
+               fmt.Printf("%+v\n", node)
+               if node.Type == html.ElementNode && node.DataAtom == atom.Template {
+                       template = node
+               }
+               if node.Type == html.ElementNode && node.DataAtom == atom.Script {
+                       script = node
+               }
+       }
+
+       if script == nil {
+               return ErrBadComponentFormat
+       }
+
+       // Get body of document
+       htmltree.Walk(document, func(n *html.Node) (bool, error) {
+               if n.Type == html.ElementNode && n.Data == "body" {
+                       body = n
+                       return true, nil
+               }
+               return false, nil
+       })
+
+       // Insert template and script at start of body
+       body.InsertBefore(script, body.FirstChild)
+       if template != nil {
+               body.InsertBefore(template, body.FirstChild)
+       }
+
+       return nil
+}
diff --git a/internal/compile/replace.go b/internal/compile/replace.go
deleted file mode 100644 (file)
index 99005e6..0000000
+++ /dev/null
@@ -1,163 +0,0 @@
-// Copyright (C) 2024 early (LGPL)
-package compile
-
-import (
-       "regexp"
-       "strings"
-
-       "git.earlybird.gay/mast-engine/htmltree"
-       "golang.org/x/net/html"
-)
-
-const (
-       tokenOpenTemplate  = "{{"
-       tokenCloseTemplate = "}}"
-)
-
-var funcRegexp = regexp.MustCompile(`^[a-zA-Z]\w*$`)
-var fieldRegexp = regexp.MustCompile(`^(?:.[a-zA-Z]\w*)+$`)
-
-func isFunc(token string) bool {
-       return funcRegexp.MatchString(token)
-}
-
-func isField(token string) bool {
-       return fieldRegexp.MatchString(token)
-}
-
-func replaceTemplateFuncs(source Source, n *html.Node) {
-       var done, inTemplate bool
-       var datas []string
-       var sets []func(string)
-
-       switch n.Type {
-       case html.TextNode:
-               datas = append(datas, n.Data)
-               sets = append(sets, func(s string) {
-                       n.Data = s
-               })
-       case html.ElementNode:
-               for _, attr := range n.Attr {
-                       datas = append(datas, attr.Val)
-                       sets = append(sets, func(s string) {
-                               htmltree.SetAttr(n, attr.Key, s)
-                       })
-               }
-       }
-       for i, data := range datas {
-               var output []string
-               for {
-                       endToken := strings.Index(data, " ")
-                       if endToken == -1 {
-                               endToken = len(data)
-                               done = true
-                       }
-                       token := data[:endToken]
-
-                       // Track when we're in a template using open/close brackets
-                       if token == tokenOpenTemplate {
-                               inTemplate = true
-                       }
-                       if token == tokenCloseTemplate {
-                               inTemplate = false
-                       }
-                       // If we're in a template and found a function identifier, replace it
-                       // with a namespaced version
-                       if inTemplate && isFunc(token) {
-                               // Skip anything that the target source doesn't have as a templatefunc
-                               if _, ok := source.TemplateFuncs()[token]; ok {
-                                       token = snakeCase(source.Name()) + "_" + token
-                               }
-                       }
-
-                       output = append(output, token)
-                       if done {
-                               break
-                       }
-                       data = data[endToken+1:]
-               }
-               sets[i](strings.Join(output, " "))
-       }
-}
-
-// Sets template fields (.field) in any template strings in this node to
-// .parent.field.
-// Returns a list of raised fields.
-func replaceTemplateFields(n *html.Node) []string {
-       var done, inTemplate, inAttrs bool
-       var datas []string
-       var isDataAttr []bool
-       var sets []func(string)
-       var raised []string
-
-       switch n.Type {
-       case html.TextNode:
-               datas = append(datas, n.Data)
-               sets = append(sets, func(s string) {
-                       n.Data = s
-               })
-       case html.ElementNode:
-               inAttrs = true
-               for _, attr := range n.Attr {
-                       datas = append(datas, attr.Val)
-                       isDataAttr = append(isDataAttr, attr.Key[0] == ':')
-                       sets = append(sets, func(s string) {
-                               htmltree.SetAttr(n, attr.Key, s)
-                       })
-               }
-       }
-       for i, data := range datas {
-               var output []string
-               for {
-                       endToken := strings.Index(data, " ")
-                       if endToken == -1 {
-                               endToken = len(data)
-                               done = true
-                       }
-                       token := data[:endToken]
-
-                       // Track when we're in a template using open/close brackets
-                       if token == tokenOpenTemplate {
-                               inTemplate = true
-                       }
-                       if token == tokenCloseTemplate {
-                               inTemplate = false
-                       }
-                       // If we're in a template and found a function identifier, replace it
-                       // with a namespaced version
-                       if inTemplate && isField(token) ||
-                               inAttrs && isDataAttr[i] {
-                               raised = append(raised, strings.Split(token, ".")[1])
-                               token = ".parent" + token
-                       }
-
-                       output = append(output, token)
-                       if done {
-                               break
-                       }
-                       data = data[endToken+1:]
-               }
-               sets[i](strings.Join(output, " "))
-       }
-
-       return raised
-}
-
-// Removes data attributes :name.
-// Returns a map attr->val of the removed data attributes (: removed).
-func removeDataAttrs(n *html.Node) map[string]string {
-       if n.Type != html.ElementNode {
-               return nil
-       }
-       dataAttrs := make(map[string]string)
-       for i := 0; i < len(n.Attr); {
-               attr := n.Attr[i]
-               if attr.Key[0] == ':' {
-                       dataAttrs[attr.Key[1:]] = attr.Val
-                       n.Attr = append(n.Attr[:i], n.Attr[i+1:]...)
-               } else {
-                       i++
-               }
-       }
-       return dataAttrs
-}
diff --git a/internal/compile/template.go b/internal/compile/template.go
new file mode 100644 (file)
index 0000000..0077ce9
--- /dev/null
@@ -0,0 +1,289 @@
+// Copyright (C) 2024 early (LGPL)
+package compile
+
+import (
+       "errors"
+       "fmt"
+       "regexp"
+       "strings"
+
+       "git.earlybird.gay/mast-engine/htmltree"
+       "git.earlybird.gay/mast-engine/render"
+       "golang.org/x/net/html"
+)
+
+const (
+       tokenOpenTemplate  = "{{"
+       tokenCloseTemplate = "}}"
+)
+
+var funcRegexp = regexp.MustCompile(`^[a-zA-Z]\w*$`)
+var fieldRegexp = regexp.MustCompile(`^(?:.[a-zA-Z]\w*)+$`)
+
+func isFunc(token string) bool {
+       return funcRegexp.MatchString(token)
+}
+
+func isField(token string) bool {
+       return fieldRegexp.MatchString(token)
+}
+
+func replaceTemplateFuncs(source TemplateSource, n *html.Node) {
+       var done, inTemplate bool
+       var datas []string
+       var sets []func(string)
+
+       switch n.Type {
+       case html.TextNode:
+               datas = append(datas, n.Data)
+               sets = append(sets, func(s string) {
+                       n.Data = s
+               })
+       case html.ElementNode:
+               for _, attr := range n.Attr {
+                       datas = append(datas, attr.Val)
+                       sets = append(sets, func(s string) {
+                               htmltree.SetAttr(n, attr.Key, s)
+                       })
+               }
+       }
+       for i, data := range datas {
+               var output []string
+               for {
+                       endToken := strings.Index(data, " ")
+                       if endToken == -1 {
+                               endToken = len(data)
+                               done = true
+                       }
+                       token := data[:endToken]
+
+                       // Track when we're in a template using open/close brackets
+                       if token == tokenOpenTemplate {
+                               inTemplate = true
+                       }
+                       if token == tokenCloseTemplate {
+                               inTemplate = false
+                       }
+                       // If we're in a template and found a function identifier, replace it
+                       // with a namespaced version
+                       if inTemplate && isFunc(token) {
+                               // Skip anything that the target source doesn't have as a templatefunc
+                               if _, ok := source.TemplateFuncs()[token]; ok {
+                                       token = snakeCase(source.Name()) + "_" + token
+                               }
+                       }
+
+                       output = append(output, token)
+                       if done {
+                               break
+                       }
+                       data = data[endToken+1:]
+               }
+               sets[i](strings.Join(output, " "))
+       }
+}
+
+// Sets template fields (.field) in any template strings in this node to
+// .parent.field.
+// Returns a list of raised fields.
+func replaceTemplateFields(n *html.Node) []string {
+       var done, inTemplate, inAttrs bool
+       var datas []string
+       var isDataAttr []bool
+       var sets []func(string)
+       var raised []string
+
+       switch n.Type {
+       case html.TextNode:
+               datas = append(datas, n.Data)
+               sets = append(sets, func(s string) {
+                       n.Data = s
+               })
+       case html.ElementNode:
+               inAttrs = true
+               for _, attr := range n.Attr {
+                       datas = append(datas, attr.Val)
+                       isDataAttr = append(isDataAttr, attr.Key[0] == ':')
+                       sets = append(sets, func(s string) {
+                               htmltree.SetAttr(n, attr.Key, s)
+                       })
+               }
+       }
+       for i, data := range datas {
+               var output []string
+               for {
+                       endToken := strings.Index(data, " ")
+                       if endToken == -1 {
+                               endToken = len(data)
+                               done = true
+                       }
+                       token := data[:endToken]
+
+                       // Track when we're in a template using open/close brackets
+                       if token == tokenOpenTemplate {
+                               inTemplate = true
+                       }
+                       if token == tokenCloseTemplate {
+                               inTemplate = false
+                       }
+                       // If we're in a template and found a function identifier, replace it
+                       // with a namespaced version
+                       if inTemplate && isField(token) ||
+                               inAttrs && isDataAttr[i] {
+                               raised = append(raised, strings.Split(token, ".")[1])
+                               token = ".parent" + token
+                       }
+
+                       output = append(output, token)
+                       if done {
+                               break
+                       }
+                       data = data[endToken+1:]
+               }
+               sets[i](strings.Join(output, " "))
+       }
+
+       return raised
+}
+
+// Removes data attributes :name.
+// Returns a map attr->val of the removed data attributes (: removed).
+func removeDataAttrs(n *html.Node) map[string]string {
+       if n.Type != html.ElementNode {
+               return nil
+       }
+       dataAttrs := make(map[string]string)
+       for i := 0; i < len(n.Attr); {
+               attr := n.Attr[i]
+               if attr.Key[0] == ':' {
+                       dataAttrs[attr.Key[1:]] = attr.Val
+                       n.Attr = append(n.Attr[:i], n.Attr[i+1:]...)
+               } else {
+                       i++
+               }
+       }
+       return dataAttrs
+}
+
+func insertTemplateSource(subSource TemplateSource, context *html.Node) (*computeNode, error) {
+       insertedId := render.ID()
+       computedName := fmt.Sprintf("%s_%s", snakeCase(subSource.Name()), insertedId)
+       // ===== CONTEXT PREPROCESSING =====
+       // Raise any fields pre-existing in the context to .parent.field.
+       raiseFields := make([]string, 0)
+       htmltree.Walk(context.FirstChild, func(n *html.Node) (bool, error) {
+               raiseFields = append(raiseFields, replaceTemplateFields(n)...)
+               return false, nil
+       })
+       // Remove :data attributes from the root of context, and prepare them
+       // to be used as data for the computeNode.
+       asData := removeDataAttrs(context)
+       // ===== SUBSOURCE PROCESSING =====
+       // Parse the subSource in the context of context.
+       // Require that subSource be contained in a template.
+       reader, err := subSource.File().Open()
+       if err != nil {
+               return nil, err
+       }
+       innerHTML, err := html.ParseFragment(reader, context)
+       if err != nil {
+               return nil, err
+       }
+       root := innerHTML[0]
+       if root.Type != html.ElementNode || root.Data != "template" {
+               return nil, errors.New("fragments must be contained in a template")
+       }
+
+       // Walk the tree to do a couple of things:
+       //   - Replace template functions with namespaced functions.
+       //   - Find slots in the subSource.
+       slots := make(map[string]*html.Node)
+       usedSlot := make(map[string]bool)
+       htmltree.Walk(root, func(n *html.Node) (bool, error) {
+               // Replace template functions with namespaced functions.
+               replaceTemplateFuncs(subSource, n)
+               // Find slots in the subSource.
+               if n.Type == html.ElementNode && n.Data == "slot" {
+                       slotName := htmltree.GetAttr(n, "name")
+                       if _, ok := slots[slotName]; ok {
+                               return true, fmt.Errorf("found multiple slots named '%s'", slotName)
+                       } else {
+                               slots[slotName] = n
+                               usedSlot[slotName] = false
+                       }
+               }
+               return false, nil
+       })
+
+       // Mix stuff from the context into the template using slots...
+       n := context.FirstChild
+       for n != nil {
+               next := n.NextSibling
+               context.RemoveChild(n)
+               // Do not slot any non-slottable nodes, or comments, because I do not
+               // like them
+               if n.Type == html.CommentNode {
+                       n = next
+                       continue
+               }
+               slotName := htmltree.GetAttr(n, "slot")
+               slot := slots[slotName]
+               if slot != nil {
+                       slot.Parent.InsertBefore(n, slot)
+                       usedSlot[slotName] = true
+               } else {
+                       root.AppendChild(n)
+               }
+               n = next
+       }
+
+       // ...then move content from the template to the context...
+       n = root.FirstChild
+       for n != nil {
+               next := n.NextSibling
+               root.RemoveChild(n)
+               context.AppendChild(n)
+               n = next
+       }
+
+       // ...then remove any slots that were used.
+       // For slots that aren't used, move up their data first.
+       htmltree.Walk(context, func(n *html.Node) (bool, error) {
+               if n.Type == html.ElementNode && n.Data == "slot" {
+                       slotName := htmltree.GetAttr(n, "name")
+                       slot := slots[slotName]
+                       used := usedSlot[slotName]
+                       if !used {
+                               n = slot.FirstChild
+                               for n != nil {
+                                       next := n.NextSibling
+                                       slot.RemoveChild(n)
+                                       slot.Parent.InsertBefore(n, slot)
+                                       n = next
+                               }
+                       }
+                       slot.Parent.RemoveChild(slot)
+               }
+               return false, nil
+       })
+
+       // Generate a computeNode for this part.
+       compute := &computeNode{
+               name:             computedName,
+               compute:          subSource.OnLoad(),
+               asDataFromParent: asData,
+               raiseFromParent:  raiseFields,
+       }
+
+       // Set ID and computed context, then generate a function that fulfills it
+       htmltree.SetAttr(context, "id", insertedId)
+       context.InsertBefore(&html.Node{
+               Type: html.TextNode,
+               Data: fmt.Sprintf("{{- with .computed.%s }}", computedName),
+       }, context.FirstChild)
+       context.InsertBefore(&html.Node{
+               Type: html.TextNode,
+               Data: "{{ end -}}",
+       }, nil)
+       return compute, nil
+}
index dc154d4f9239dccca73b122527cec5dff5a682aa..539e4e35f690c61610bab09de75afa4fa42ff500 100644 (file)
@@ -7,7 +7,6 @@ import (
        "git.earlybird.gay/mast-engine/htmltree"
        "git.earlybird.gay/mast-engine/include"
        "git.earlybird.gay/mast-engine/internal/compile"
-       "git.earlybird.gay/mast-engine/part"
        "git.earlybird.gay/mast-engine/render"
 )
 
@@ -16,9 +15,9 @@ type Page struct {
        source include.Opener
        raw    string
 
-       parts  []*part.Part
-       onLoad render.OnLoadFunc
-       indent string
+       includes []compile.Source
+       onLoad   render.OnLoadFunc
+       indent   string
 
        template      *template.Template
        templateFuncs template.FuncMap
@@ -35,9 +34,9 @@ func Funcs(funcs template.FuncMap) Config {
        }
 }
 
-func Includes(parts ...*part.Part) Config {
+func Includes(includes ...compile.Source) Config {
        return func(p *Page) {
-               p.parts = parts
+               p.includes = includes
        }
 }
 
@@ -110,11 +109,7 @@ func (p *Page) OnLoad() render.OnLoadFunc {
 }
 
 func (p *Page) Includes() []compile.Source {
-       cast := make([]compile.Source, len(p.parts))
-       for i, child := range p.parts {
-               cast[i] = child
-       }
-       return cast
+       return p.includes
 }
 
 func (p *Page) Raw() string {
index 92c60b05eee2b1ce6c0d2c54fa6f352948c404e8..00d0a6c988a1d05f7bb86da4c7edec6302451d50 100644 (file)
@@ -13,7 +13,7 @@ type Part struct {
        name   string
        source include.Opener
 
-       parts         []*Part
+       includes      []compile.Source
        onLoad        render.OnLoadFunc
        templateFuncs template.FuncMap
 }
@@ -28,9 +28,9 @@ func Funcs(funcs template.FuncMap) Config {
        }
 }
 
-func Includes(parts ...*Part) Config {
+func Includes(includes ...compile.Source) Config {
        return func(p *Part) {
-               p.parts = parts
+               p.includes = includes
        }
 }
 
@@ -73,9 +73,5 @@ func (p *Part) OnLoad() render.OnLoadFunc {
 }
 
 func (p *Part) Includes() []compile.Source {
-       cast := make([]compile.Source, len(p.parts))
-       for i, child := range p.parts {
-               cast[i] = child
-       }
-       return cast
+       return p.includes
 }
diff --git a/part/parts.go b/part/parts.go
deleted file mode 100644 (file)
index 50a4c4d..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright (C) 2024 early (LGPL)
-package part
-
-type Parts []*Part
-
-func (p Parts) GetChild(i int) *Part {
-       if i >= 0 && i < len(p) {
-               return p[i]
-       }
-       return nil
-}