From: early Date: Sat, 2 Nov 2024 18:22:45 +0000 (-0600) Subject: Add 'engine/' from commit 'af763240f03d2f9f91c3bb3cce15d320acfd6223' X-Git-Url: https://git.earlybird.gay/?a=commitdiff_plain;h=c8f6334677fca905b5756aa2e719757f64df036a;p=today Add 'engine/' from commit 'af763240f03d2f9f91c3bb3cce15d320acfd6223' git-subtree-dir: engine git-subtree-mainline: a4b4db1b57a547474699d6c3213aa9e6663709cc git-subtree-split: af763240f03d2f9f91c3bb3cce15d320acfd6223 --- c8f6334677fca905b5756aa2e719757f64df036a diff --cc engine/.gitignore index 0000000,0000000..3845427 new file mode 100644 --- /dev/null +++ b/engine/.gitignore @@@ -1,0 -1,0 +1,5 @@@ ++# Ignore anything ending in local ++*local ++ ++# Disregard ignore rules for example files ++!*.example diff --cc engine/LICENSE.txt index 0000000,0000000..a8876ed new file mode 100644 --- /dev/null +++ b/engine/LICENSE.txt @@@ -1,0 -1,0 +1,18 @@@ ++Copyright © 2024 Early ++ ++Permission is hereby granted, free of charge, to any person obtaining a copy of ++this software and associated documentation files (the “Software”), to deal in ++the Software without restriction, including without limitation the rights to ++use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of ++the Software, and to permit persons to whom the Software is furnished to do so, ++subject to the following conditions: ++ ++The above copyright notice and this permission notice shall be included in all ++copies or substantial portions of the Software. ++ ++THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ++IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS ++FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR ++COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER ++IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN ++CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --cc engine/README.md index 0000000,0000000..df9e1df new file mode 100644 --- /dev/null +++ b/engine/README.md @@@ -1,0 -1,0 +1,31 @@@ ++# Today Engine ++ ++The Today engine builds on Go's templating with reusable parts in the style of ++the Web Components standard. It aims to be usable for anything from static HTML ++websites to complex applications with a mix of server and client-side behavior. ++ ++Author: Early N. ++License: MIT, BSD ++ ++## Installation ++ ++go get git.earlybird.gay/today-engine@latest ++ ++## Why? ++ ++In short, to help guide new developers in making a website (today). The ++technologies that dominate the modern web are complex in unique ways that do ++not promote foundational knowledge of the web platform. Today focuses on: ++ ++- Building websites from a basis of standard HTML/CSS ++- Promoting use of the Web Components standard of JS ++- Expanding on Go's robust template system with reusable chunks of HTML ++ ++## How? ++ ++## License ++ ++The portions of `today-engine` owned by Early N. are under the MIT license. Some ++code sections have a different license: ++ ++- `package html` is under a BSD-3-Clause style license from Google. diff --cc engine/cmd/standard-test/main.go index 0000000,0000000..88fad7c new file mode 100644 --- /dev/null +++ b/engine/cmd/standard-test/main.go @@@ -1,0 -1,0 +1,54 @@@ ++package main ++ ++import ( ++ "context" ++ "net/http" ++ "syscall" ++ "text/template" ++ ++ tapp "git.earlybird.gay/today-app/app" ++ "git.earlybird.gay/today-engine/page" ++ "git.earlybird.gay/today-engine/part" ++ "git.earlybird.gay/today-engine/render" ++ stdpart "git.earlybird.gay/today-engine/standard/part" ++) ++ ++var Thing = part.New("test-thing", "test_thing.html", ++ part.OnLoad(func(ctx context.Context, data render.Data) error { ++ // fmt.Println(data.Get("value")) ++ return nil ++ }), ++) ++ ++var Page = page.New("index", "pages/index.html", ++ page.Includes( ++ stdpart.ContactForm([]string{"Feedback"}), Thing, ++ ), ++ page.Funcs(template.FuncMap{ ++ "SliceOfLen": func(i int) []int { ++ out := make([]int, i) ++ for i := range i { ++ out[i] = i ++ } ++ return out ++ }, ++ }), ++) ++ ++var Static = page.Static("pages/static.html") ++ ++func main() { ++ app := tapp.New() ++ tapp.GetEnv().Apply(app) ++ app.ShutdownOnSignal(syscall.SIGINT, syscall.SIGTERM) ++ ++ app.Handle("GET /{$}", Page) ++ app.Handle("GET /static", Static) ++ app.Handle("GET /", http.FileServer(http.Dir("public"))) ++ app.Handle("GET /contact", stdpart.HandleContactForm(nil, stdpart.PrintContact)) ++ app.Handle("POST /contact", stdpart.HandleContactForm(nil, stdpart.PrintContact)) ++ ++ // fmt.Println(Page.Raw()) ++ ++ app.ListenAndServe() ++} diff --cc engine/cmd/standard-test/pages/index.html index 0000000,0000000..b7a2f80 new file mode 100644 --- /dev/null +++ b/engine/cmd/standard-test/pages/index.html @@@ -1,0 -1,0 +1,22 @@@ ++ ++ ++ ++ ++ Today Engine Examples ++ ++ ++ ++ ++
++ ++ ++ ++ {{- range $i, $v := SliceOfLen 5 }} ++ ++ {{ end -}} ++ ++ Link to static page ++
++ ++ ++ diff --cc engine/cmd/standard-test/pages/static.html index 0000000,0000000..e03b202 new file mode 100644 --- /dev/null +++ b/engine/cmd/standard-test/pages/static.html @@@ -1,0 -1,0 +1,16 @@@ ++ ++ ++ ++ ++ Today Engine Examples ++ ++ ++ ++ ++
++

This is a normal static page.

++

It doesn't do anything except exist. Neat.

++
++ ++ ++ diff --cc engine/cmd/standard-test/public/style.css index 0000000,0000000..c0a3157 new file mode 100644 --- /dev/null +++ b/engine/cmd/standard-test/public/style.css @@@ -1,0 -1,0 +1,51 @@@ ++/* The Great Reset */ ++* { ++ padding: 0; ++ margin: 0; ++} ++ ++/* Containers */ ++ ++html { ++ font-family: sans-serif; ++} ++ ++main, section, nav { ++ display: flex; ++ flex-direction: column; ++ gap: 1rem; ++} ++ ++main { ++ margin: 2rem 25%; ++} ++ ++span { ++ display: inline; ++} ++ ++span + span { ++ margin-left: .5rem; ++} ++ ++/* Forms */ ++ ++form { ++ display: grid; ++ grid-template-columns: 1fr 3fr; ++ gap: .5rem; ++} ++ ++form > input[type="submit"] { ++ grid-column: span 2; ++} ++ ++/* Typography */ ++ ++:not(pre) > code { ++ font-size: 1rem; ++} ++ ++li { ++ margin-left: 20px; ++} diff --cc engine/cmd/standard-test/test_thing.html index 0000000,0000000..5988d75 new file mode 100644 --- /dev/null +++ b/engine/cmd/standard-test/test_thing.html @@@ -1,0 -1,0 +1,3 @@@ ++ diff --cc engine/component/component.go index 0000000,0000000..22370e2 new file mode 100644 --- /dev/null +++ b/engine/component/component.go @@@ -1,0 -1,0 +1,52 @@@ ++// Copyright (C) 2024 early (LGPL) ++package component ++ ++import ( ++ "git.earlybird.gay/today-engine/include" ++ "git.earlybird.gay/today-engine/internal/compile" ++) ++ ++type Component struct { ++ name string ++ fileName 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.fileName = source ++ p.source = include.File(source, "git.earlybird.gay/today-engine/component") ++ // Run optional arguments ++ for _, of := range optional { ++ of(p) ++ } ++ return p ++} ++ ++func (p *Component) Name() string { ++ return p.name ++} ++ ++func (p *Component) FileName() string { ++ return p.fileName ++} ++ ++func (p *Component) File() include.Opener { ++ return p.source ++} ++ ++func (p *Component) Includes() []compile.Source { ++ return p.includes ++} diff --cc engine/go.mod index 0000000,0000000..fb5c1b4 new file mode 100644 --- /dev/null +++ b/engine/go.mod @@@ -1,0 -1,0 +1,8 @@@ ++module git.earlybird.gay/today-engine ++ ++go 1.22.4 ++ ++require ( ++ git.earlybird.gay/today-app v0.0.0-20240813010007-675f08ae35b1 ++ golang.org/x/text v0.17.0 ++) diff --cc engine/go.sum index 0000000,0000000..ca1ee0b new file mode 100644 --- /dev/null +++ b/engine/go.sum @@@ -1,0 -1,0 +1,4 @@@ ++git.earlybird.gay/today-app v0.0.0-20240813010007-675f08ae35b1 h1:Fbz2uRIWK6CR+jcNN0KQrna4u/kYZccn2obcla7dPfQ= ++git.earlybird.gay/today-app v0.0.0-20240813010007-675f08ae35b1/go.mod h1:AxsoC2ERffYriW60C1vdV34ew6JPKxllG9mHOTs128I= ++golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= ++golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= diff --cc engine/htmltree/attrs.go index 0000000,0000000..4d9ab2e new file mode 100644 --- /dev/null +++ b/engine/htmltree/attrs.go @@@ -1,0 -1,0 +1,22 @@@ ++// Copyright (C) 2024 early (LGPL) ++package htmltree ++ ++import "git.earlybird.gay/today-engine/internal/html" ++ ++func GetAttr(n *html.Node, name string) string { ++ for _, attr := range n.Attr { ++ if attr.Key == name { ++ return attr.Val ++ } ++ } ++ return "" ++} ++ ++func SetAttr(n *html.Node, name string, value string) { ++ for i, attr := range n.Attr { ++ if attr.Key == name { ++ n.Attr = append(n.Attr[:i], n.Attr[i+1:]...) ++ } ++ } ++ n.Attr = append(n.Attr, html.Attribute{Key: name, Val: value}) ++} diff --cc engine/htmltree/prettify.go index 0000000,0000000..33fbdd0 new file mode 100644 --- /dev/null +++ b/engine/htmltree/prettify.go @@@ -1,0 -1,0 +1,142 @@@ ++// Copyright (C) 2024 early (LGPL) ++package htmltree ++ ++import ( ++ "fmt" ++ "regexp" ++ "strings" ++ ++ "git.earlybird.gay/today-engine/internal/html" ++) ++ ++func Walk(root *html.Node, f func(*html.Node) (bool, error)) error { ++ var node, child *html.Node ++ var siblings []*html.Node ++ var stop bool ++ var err error ++ ++ node = root ++ ++ for node != nil && err == nil { ++ child = node.FirstChild ++ if node.NextSibling != nil { ++ siblings = append(siblings, node.NextSibling) ++ } ++ stop, err = f(node) ++ ++ if !stop && child != nil { ++ node = child ++ child = nil ++ } else if nextSibling := len(siblings) - 1; nextSibling != -1 { ++ node = siblings[nextSibling] ++ siblings = siblings[:nextSibling] ++ } else { ++ node = nil ++ } ++ } ++ return err ++} ++ ++func Print(root *html.Node) { ++ Walk(root, func(n *html.Node) (bool, error) { ++ fmt.Printf("%p: %+v\n", n, n) ++ return false, nil ++ }) ++} ++ ++func doctype(n *html.Node) bool { ++ return n.Type == html.DoctypeNode ++} ++ ++func comment(n *html.Node) bool { ++ return n.Type == html.CommentNode ++} ++ ++func template(n *html.Node) bool { ++ return n.Type == html.TextNode && strings.HasPrefix(n.Data, "{{") ++} ++ ++func elem(n *html.Node) bool { ++ return n.Type == html.ElementNode || comment(n) || template(n) ++} ++ ++var unindentRegexp = regexp.MustCompile(`^\n\s*`) ++var despaceRegexp = regexp.MustCompile(`\n\s*`) ++ ++func Minify() func(root *html.Node) { ++ return func(root *html.Node) { ++ Walk(root, func(n *html.Node) (bool, error) { ++ if n.Type == html.ElementNode && n.Data == "pre" { ++ return true, nil ++ } ++ if n.Type == html.ElementNode && n.Data == "script" { ++ return true, nil ++ } ++ if n.Type == html.TextNode { ++ n.Data = unindentRegexp.ReplaceAllString(n.Data, "") ++ n.Data = despaceRegexp.ReplaceAllString(n.Data, " ") ++ if n.PrevSibling == nil { ++ n.Data = strings.TrimLeft(n.Data, " \t") ++ } ++ if n.NextSibling == nil { ++ n.Data = strings.TrimRight(n.Data, " \t") ++ } ++ // Remove empty nodes ++ if n.Data == "" { ++ n.Parent.RemoveChild(n) ++ } ++ } ++ return false, nil ++ }) ++ } ++} ++ ++func spacer(indent string, depth int) *html.Node { ++ return &html.Node{ ++ Type: html.TextNode, ++ Data: "\n" + strings.Repeat(indent, depth), ++ } ++} ++ ++func pad(n *html.Node, indent string, depth int) { ++ if depth == 0 { ++ return ++ } ++ n.InsertBefore(spacer(indent, depth), n.FirstChild) ++ n.InsertBefore(spacer(indent, depth-1), nil) ++} ++ ++func newline(n *html.Node, indent string, depth int) { ++ if n.Parent == nil { ++ return ++ } ++ n.Parent.InsertBefore(spacer(indent, depth), n.NextSibling) ++} ++ ++func Prettify(indent string) func(root *html.Node) { ++ return func(root *html.Node) { ++ Minify()(root) ++ ++ var prettify func(n *html.Node, depth int) ++ ++ prettify = func(n *html.Node, depth int) { ++ if n == nil { ++ return ++ } ++ if elem(n) && n.Data == "pre" { ++ return ++ } ++ if n.NextSibling != nil && (elem(n) || doctype(n)) && elem(n.NextSibling) { ++ newline(n, indent, depth-1) ++ } ++ if n.FirstChild != nil && elem(n.FirstChild) { ++ pad(n, indent, depth) ++ } ++ ++ prettify(n.FirstChild, depth+1) ++ prettify(n.NextSibling, depth) ++ } ++ ++ prettify(root, 0) ++ } ++} diff --cc engine/include/callstack.go index 0000000,0000000..15e4910 new file mode 100644 --- /dev/null +++ b/engine/include/callstack.go @@@ -1,0 -1,0 +1,64 @@@ ++// Copyright (C) 2024 early (LGPL) ++package include ++ ++import ( ++ "errors" ++ "runtime" ++ "slices" ++ "strings" ++) ++ ++var ErrNoEligibleCaller = errors.New("no eligible caller on the stack") ++var ErrNoRuntimeAccess = errors.New("couldn't access the runtime") ++ ++var callStackNotEligible = []string{ ++ "runtime", ++ "git.earlybird.gay/today-engine/include", ++} ++ ++// SetNotEligible marks a package as ineligible for including files. ++// You should use the arguments of include to to this if you can, but if you are ++// using a package that passes through to include, you can call this in an init ++// function. ++func SetNotEligible(pkg string) { ++ callStackNotEligible = append(callStackNotEligible, pkg) ++} ++ ++func isNotEligible(caller string, ignorePackages []string) bool { ++ return slices.ContainsFunc(callStackNotEligible, func(notEligible string) bool { ++ return strings.HasPrefix(caller, notEligible+".") ++ }) || slices.ContainsFunc(ignorePackages, func(notEligible string) bool { ++ return strings.HasPrefix(caller, notEligible+".") ++ }) ++} ++ ++// getCallStackButt gets the calling file, ignoring any file in an ignored ++// package. ++func getCallStackButt(ignorePackages []string) (string, error) { ++ const incr int = 2 ++ const max int = incr * 10 ++ for skip := 0; skip < max; skip += incr { ++ callers := make([]uintptr, incr) ++ count := runtime.Callers(skip, callers) ++ frames := runtime.CallersFrames(callers) ++ ++ frame, more := frames.Next() ++ for { ++ // If getCallStackButt gets called from main, use the runtime to ++ // determine what module main is in. ++ if isNotEligible(frame.Function, ignorePackages) { ++ if !more { ++ break ++ } ++ frame, more = frames.Next() ++ } else { ++ return frame.File, nil ++ } ++ } ++ if count < incr { ++ break ++ } ++ } ++ ++ return "", ErrNoEligibleCaller ++} diff --cc engine/include/include.go index 0000000,0000000..c2ee4fd new file mode 100644 --- /dev/null +++ b/engine/include/include.go @@@ -1,0 -1,0 +1,58 @@@ ++// Copyright (C) 2024 early (LGPL) ++package include ++ ++import ( ++ "io" ++ "os" ++ "path" ++) ++ ++type Opener interface { ++ Open() (io.ReadCloser, error) ++} ++ ++type FileOpener interface { ++ Opener ++ FileName() string ++} ++ ++type OpenerFunc func() (io.ReadCloser, error) ++ ++func (opener OpenerFunc) Open() (io.ReadCloser, error) { ++ return opener() ++} ++ ++type fileOpener struct { ++ absPath string ++ alwaysErr error ++} ++ ++func (opener *fileOpener) Open() (io.ReadCloser, error) { ++ if opener.alwaysErr != nil { ++ return nil, opener.alwaysErr ++ } ++ return os.Open(opener.absPath) ++} ++ ++func (fopener *fileOpener) FileName() string { ++ return fopener.absPath ++} ++ ++// File returns an Opener that Opens() a file. ++// If filename is a relative path, it is considered relative to the *calling ++// file*, not the working directory. ++// If ignorePackages is nonempty, callers in any package specified are ignored. ++func File(filename string, ignorePackages ...string) FileOpener { ++ opener := new(fileOpener) ++ if path.IsAbs(filename) { ++ opener.absPath = filename ++ } else { ++ caller, err := getCallStackButt(ignorePackages) ++ if err != nil { ++ opener.alwaysErr = err ++ } else { ++ opener.absPath = path.Join(path.Dir(caller), filename) ++ } ++ } ++ return opener ++} diff --cc engine/internal/compile/compile.go index 0000000,0000000..9b88807 new file mode 100644 --- /dev/null +++ b/engine/internal/compile/compile.go @@@ -1,0 -1,0 +1,188 @@@ ++// Copyright (C) 2024 early (LGPL) ++package compile ++ ++import ( ++ "errors" ++ "html/template" ++ "regexp" ++ "strings" ++ ++ "git.earlybird.gay/today-engine/htmltree" ++ "git.earlybird.gay/today-engine/include" ++ "git.earlybird.gay/today-engine/internal/html" ++ "git.earlybird.gay/today-engine/render" ++) ++ ++type Source interface { ++ Name() string ++ Source() include.Opener ++ Includes() []Source ++} ++ ++type TemplateSource interface { ++ Source ++ IncludeTagName() bool ++ TemplateFuncs() template.FuncMap ++ OnLoad() render.OnLoadFunc ++} ++ ++type Sources map[string]Source ++ ++func mapSources(dst Sources, slice []Source) { ++ for _, source := range slice { ++ dst[source.Name()] = source ++ mapSources(dst, source.Includes()) ++ } ++} ++ ++type Result struct { ++ TemplateRaw string ++ TemplateFuncs template.FuncMap ++ TemplateDataLoader render.Loader ++} ++ ++func Compile(root TemplateSource, transform ...func(root *html.Node)) (Result, error) { ++ var result Result ++ reader, err := root.Source().Open() ++ if err != nil { ++ return result, err ++ } ++ document, err := html.Parse(reader) ++ if err != nil { ++ return result, err ++ } ++ ++ fullDependencies := Sources{root.Name(): root} ++ mapSources(fullDependencies, root.Includes()) ++ computeRoot := &computeNode{ ++ name: root.Name(), ++ compute: root.OnLoad(), ++ } ++ // Insert an assignment to $compute so we can always raise scope, even ++ // in pipelines ++ document.InsertBefore(&html.Node{ ++ Type: html.TextNode, ++ Data: "{{ $compute := .compute }}", ++ }, document.FirstChild) ++ ++ // Replace template functions in the root before we add any subsources ++ // fmt.Println(root.TemplateFuncs()) ++ htmltree.Walk(document, func(n *html.Node) (bool, error) { ++ replaceTemplateFuncs(root, n) ++ return false, nil ++ }) ++ ++ // 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 ++ templateFuncs := make(template.FuncMap) ++ for fname, f := range root.TemplateFuncs() { ++ templateFuncs[snakeCase(root.Name())+"_"+fname] = f ++ } ++ ++ var process func(n *html.Node, c *computeNode) error ++ process = func(n *html.Node, c *computeNode) error { ++ childComputeNode := c ++ siblingComputeNode := c ++ if n.Type == html.ElementNode { ++ if subSource, ok := fullDependencies[n.Data]; ok { ++ 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 ++ } ++ // Add compute node for subsource ++ c.children = append(c.children, computeSubSource) ++ childComputeNode = computeSubSource ++ } ++ } ++ } ++ var procErr error ++ if n.FirstChild != nil { ++ procErr = errors.Join(procErr, process(n.FirstChild, childComputeNode)) ++ } ++ if n.NextSibling != nil { ++ procErr = errors.Join(procErr, process(n.NextSibling, siblingComputeNode)) ++ } ++ return procErr ++ } ++ err = process(document, computeRoot) ++ if err != nil { ++ return result, err ++ } ++ ++ // POSTPROCESSING ++ ++ // Assign .SetDot to $setDot ++ document.InsertBefore(&html.Node{ ++ Type: html.TextNode, ++ Data: `{{ $setDot := .SetDot }}{{ with .Data }}`, ++ }, document.FirstChild) ++ // Add end to end ++ document.InsertBefore(&html.Node{ ++ Type: html.TextNode, ++ Data: "{{ end -}}", ++ }, nil) ++ ++ // Split up templates in text nodes ++ splitTemplateNodes(document) ++ ++ // Traverse the document and add $setDot on entry and exit from pipelines ++ addSetDots(document) ++ ++ for _, tf := range transform { ++ tf(document) ++ } ++ ++ raw, err := renderDocument(document) ++ if err != nil { ++ return result, err ++ } ++ result = Result{ ++ TemplateRaw: raw, ++ TemplateFuncs: templateFuncs, ++ TemplateDataLoader: computeRoot, ++ } ++ return result, err ++} ++ ++func renderDocument(document *html.Node) (string, error) { ++ // Basic render ++ buf := new(strings.Builder) ++ err := html.Render(buf, document) ++ if err != nil { ++ return "", err ++ } ++ raw := html.UnescapeString(buf.String()) ++ ++ // Clean boolean attributes ++ raw = removeEmptyAttrValues(raw) ++ ++ return raw, nil ++} ++ ++var emptyAttrRegex = regexp.MustCompile(`%%(?P[^<>]+?)%%=""`) ++ ++func removeEmptyAttrValues(raw string) string { ++ return emptyAttrRegex.ReplaceAllString(raw, "$templateattr") ++} diff --cc engine/internal/compile/component.go index 0000000,0000000..7f77949 new file mode 100644 --- /dev/null +++ b/engine/internal/compile/component.go @@@ -1,0 -1,0 +1,67 @@@ ++package compile ++ ++import ( ++ "errors" ++ ++ "git.earlybird.gay/today-engine/htmltree" ++ "git.earlybird.gay/today-engine/internal/html" ++ "git.earlybird.gay/today-engine/internal/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: ++ //