From dfe594c53ddfef5e2053277058e1c23d22d21400 Mon Sep 17 00:00:00 2001 From: early Date: Tue, 30 Jul 2024 09:53:59 -0600 Subject: [PATCH] support web components a lil bit --- .../ex07-components/counter.html | 15 + .../ex07-components/example.go | 11 + .../ex07-components/page.html | 17 ++ cmd/run-mast-examples/index.html | 1 + cmd/run-mast-examples/main.go | 2 + component/component.go | 46 +++ internal/compile/compile.go | 170 ++--------- internal/compile/component.go | 69 +++++ internal/compile/replace.go | 163 ---------- internal/compile/template.go | 289 ++++++++++++++++++ page/page.go | 17 +- part/part.go | 12 +- part/parts.go | 11 - 13 files changed, 493 insertions(+), 330 deletions(-) create mode 100644 cmd/run-mast-examples/ex07-components/counter.html create mode 100644 cmd/run-mast-examples/ex07-components/example.go create mode 100644 cmd/run-mast-examples/ex07-components/page.html create mode 100644 component/component.go create mode 100644 internal/compile/component.go delete mode 100644 internal/compile/replace.go create mode 100644 internal/compile/template.go delete mode 100644 part/parts.go diff --git a/cmd/run-mast-examples/ex07-components/counter.html b/cmd/run-mast-examples/ex07-components/counter.html new file mode 100644 index 0000000..5ee7850 --- /dev/null +++ b/cmd/run-mast-examples/ex07-components/counter.html @@ -0,0 +1,15 @@ + + diff --git a/cmd/run-mast-examples/ex07-components/example.go b/cmd/run-mast-examples/ex07-components/example.go new file mode 100644 index 0000000..f9413be --- /dev/null +++ b/cmd/run-mast-examples/ex07-components/example.go @@ -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 index 0000000..7a81538 --- /dev/null +++ b/cmd/run-mast-examples/ex07-components/page.html @@ -0,0 +1,17 @@ + + +
+ Example 1 +
+ +

Web Components

+ + + Example: + + + + Parts + + + \ No newline at end of file diff --git a/cmd/run-mast-examples/index.html b/cmd/run-mast-examples/index.html index 707a0a8..c75f526 100644 --- a/cmd/run-mast-examples/index.html +++ b/cmd/run-mast-examples/index.html @@ -14,6 +14,7 @@
  • Go Templates
  • N/A
  • Passing Data to Parts
  • +
  • Web Components
  • \ No newline at end of file diff --git a/cmd/run-mast-examples/main.go b/cmd/run-mast-examples/main.go index ee62a19..f99e1b4 100644 --- a/cmd/run-mast-examples/main.go +++ b/cmd/run-mast-examples/main.go @@ -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 index 0000000..6c26470 --- /dev/null +++ b/component/component.go @@ -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 +} diff --git a/internal/compile/compile.go b/internal/compile/compile.go index 6b8cf53..4dd8cf1 100644 --- a/internal/compile/compile.go +++ b/internal/compile/compile.go @@ -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 index 0000000..b0d7aa7 --- /dev/null +++ b/internal/compile/component.go @@ -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: + //