]> git.earlybird.gay Git - today/commitdiff
revise compute pattern, add context to OnLoad
authorearly <me@earlybird.gay>
Tue, 27 Aug 2024 23:21:30 +0000 (17:21 -0600)
committerearly <me@earlybird.gay>
Tue, 27 Aug 2024 23:21:30 +0000 (17:21 -0600)
14 files changed:
README.md [new file with mode: 0644]
README.txt [deleted file]
cmd/standard-test/index.html
cmd/standard-test/main.go
component/component.go
include/include.go
internal/compile/compile.go
internal/compile/compute.go
internal/compile/template.go
page/page.go
part/part.go
render/data.go
standard/part/contact_form.go
standard/part/contact_form.html

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..7cd3473
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+# 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
+
+## 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?
diff --git a/README.txt b/README.txt
deleted file mode 100644 (file)
index 9a43967..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-===== 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
-
-===== Install ==================================================================
-
-go get git.earlybird.gay/today-engine@latest
-
-===== Usage ====================================================================
-
-I'm working on a by-example application to help with this.
index ca3b29a031264514e2844a0da1eac23cec35e8c3..a5e890c73b191b2f98eeb944ab3f5ed34aa1b930 100644 (file)
@@ -7,7 +7,8 @@
 
 <body>
     <main>
-        <stdp-contact-form :action="/contact" :method="POST"></stdp-contact-form>
+        <stdp-contact-form :action="/contact" id="cf-get"></stdp-contact-form>
+        <stdp-contact-form :action="/contact" :method="POST" id="cf-post"></stdp-contact-form>
     </main>
 </body>
 
index d2ed56c0876e2e2496038760c7800d02d7fd7144..f90d82eab18505f7145a84a55bf7552e04c3f657 100644 (file)
@@ -1,6 +1,7 @@
 package main
 
 import (
+       "html/template"
        "net/http"
        "syscall"
 
@@ -14,6 +15,16 @@ var Page = page.New("index", "index.html",
        page.Includes(
                stdpart.ContactForm([]string{"Feedback"}),
        ),
+       page.Funcs(template.FuncMap{
+               "SliceOfLen": func(i int) []int {
+                       out := make([]int, i)
+                       for i := range i {
+                               out[i] = i
+                       }
+                       return out
+               },
+       }),
+       page.Pretty("  "),
 )
 
 func main() {
@@ -23,7 +34,10 @@ func main() {
 
        app.Handle("GET /{$}", Page)
        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()
 }
index b918c50d2de06f4946b2332298b27b7b5bb142a0..22370e2bee4f9f5c6851bff308e4b71c5c11b890 100644 (file)
@@ -7,8 +7,9 @@ import (
 )
 
 type Component struct {
-       name   string
-       source include.Opener
+       name     string
+       fileName string
+       source   include.Opener
 
        includes []compile.Source
 }
@@ -25,6 +26,7 @@ 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 {
@@ -37,6 +39,10 @@ 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
 }
index 0cb284eb13ea8592fdcc42fee251223094aa3384..874e62a6c024236bad889780baf04e55b7fc5ded 100644 (file)
@@ -2,19 +2,9 @@
 package include
 
 import (
-       "errors"
-       "fmt"
        "io"
        "os"
        "path"
-       "runtime/debug"
-       "strings"
-)
-
-var (
-       ErrNoRuntime            = errors.New("could not access runtime for caller info")
-       ErrNoGoPath             = errors.New("GOPATH is unset")
-       ErrFileNotAllowedInMain = errors.New("include.File can't be called in package main")
 )
 
 type Opener interface {
@@ -27,63 +17,6 @@ func (opener OpenerFunc) Open() (io.ReadCloser, error) {
        return opener()
 }
 
-// dependencies maps module names to module paths (name@version).
-var modPaths map[string]string
-
-func initSource() (err error) {
-       err = readModPaths()
-       if err != nil {
-               return
-       }
-       return
-}
-
-// readDependencies reads the name and version of all module dependencies and
-// stores them as a map of module name -> module path.
-func readModPaths() error {
-       buildInfo, ok := debug.ReadBuildInfo()
-       if !ok {
-               return ErrNoRuntime
-       }
-       goPath := os.Getenv("GOPATH")
-       if goPath == "" {
-               return ErrNoGoPath
-       }
-       workingPath, err := os.Getwd()
-       if err != nil {
-               return err
-       }
-       modPaths = make(map[string]string)
-       for _, dep := range buildInfo.Deps {
-               modPaths[dep.Path] = path.Join(goPath, fmt.Sprintf("%s@%s", dep.Path, dep.Version))
-       }
-       thisModPath := buildInfo.Main.Path
-       modPaths[thisModPath] = workingPath
-       return nil
-}
-
-// getPackagePath gets a path to a package on the filesystem based on its name.
-func getPackagePath(packageName string) (filepath string) {
-       for modName, modPath := range modPaths {
-               if packPath, ok := strings.CutPrefix(packageName, modName); ok {
-                       return path.Join(modPath, packPath)
-               }
-       }
-       return
-}
-
-// openSource opens a file in package packageName on the filesystem.
-func openSource(packageName string, filepath string) (r io.ReadCloser, err error) {
-       ppath := getPackagePath(packageName)
-       if ppath == "" {
-               err = errors.New("tried to get source for unknown package " + packageName)
-               return
-       }
-       absPath := path.Join(ppath, filepath)
-       r, err = os.Open(absPath)
-       return
-}
-
 // File returns an Opener that Opens() a file relative to the calling package.
 func File(relativePath string, ignorePackages ...string) Opener {
        var root string
index 16554a0919fc48347b59aca28ccd79bf9b7062fa..6b846821782025682493fb954b5e982aa9c2e90e 100644 (file)
@@ -15,6 +15,7 @@ import (
 
 type Source interface {
        Name() string
+       FileName() string
        File() include.Opener
        Includes() []Source
 }
@@ -57,6 +58,12 @@ func Compile(root TemplateSource, transform ...func(root *html.Node)) (Result, e
                name:    "root",
                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)
 
        // Insert component sources into document
        for name, subSource := range fullDependencies {
@@ -94,6 +101,7 @@ func Compile(root TemplateSource, transform ...func(root *html.Node)) (Result, e
                                        if err != nil {
                                                return err
                                        }
+                                       // Add compute node for subsource
                                        c.children = append(c.children, computeSubSource)
                                        childComputeNode = computeSubSource
                                }
index d14e84bdafb236179d2061cb2d8bcd8340809e29..79e6bcb940c740e3ff340b558a4274a3f7d3125d 100644 (file)
@@ -12,6 +12,7 @@ import (
 
 type computeNode struct {
        name             string
+       htmlId           string
        compute          render.OnLoadFunc
        asDataFromParent map[string]string
        raiseFromParent  []string
@@ -24,11 +25,23 @@ func (root *computeNode) Compute(r *http.Request) (render.Data, error) {
                        cData.Set(key, pData.Get(key))
                }
        }
+       ctx := r.Context()
        var f func(n *computeNode, data render.Data) error
        f = func(n *computeNode, data render.Data) error {
-               n.compute(data)
-               computedData := make(render.Data)
+               // Set the htmlId of the component if it exists as "id"
+               if n.htmlId != "" {
+                       data.Set("id", n.htmlId)
+               }
+               n.compute(ctx, data)
+               computeFuncs := make(render.Data)
                for _, child := range n.children {
+                       computeFuncName := child.name
+                       if child.htmlId != "" {
+                               computeFuncName += "_" + snakeCase(child.htmlId)
+                       }
+                       if computeFuncs.IsSet(computeFuncName) {
+                               continue
+                       }
                        childData := render.NewData(r)
                        // Stuff here is available to the OnLoad AND renderer:
                        // Assign all data from parent
@@ -48,13 +61,15 @@ func (root *computeNode) Compute(r *http.Request) (render.Data, error) {
                        childParentData := make(render.Data)
                        impose(childParentData, data, child.raiseFromParent)
                        childData.Set("parent", childParentData)
-                       if child.name != "" {
-                               computedData.Set(child.name, childData)
+                       if computeFuncName != "" {
+                               computeFuncs.Set(computeFuncName, func() (render.Data, error) {
+                                       return childData, child.compute(ctx, childData)
+                               })
                        }
                        f(child, childData)
                        // Stuff here is ONLY available to the renderer:
                }
-               data.Set("computed", computedData)
+               data.Set("compute", computeFuncs)
                return nil
        }
 
index bbac33702873cc96dc9e70f7d786e1650c2e17c7..4f67c2d49aa96e9a9502900389a4c7c81afa491d 100644 (file)
@@ -5,10 +5,10 @@ import (
        "errors"
        "fmt"
        "regexp"
+       "slices"
        "strings"
 
        "git.earlybird.gay/today-engine/htmltree"
-       "git.earlybird.gay/today-engine/render"
        "golang.org/x/net/html"
 )
 
@@ -18,7 +18,8 @@ const (
 )
 
 var funcRegexp = regexp.MustCompile(`^[a-zA-Z]\w*$`)
-var fieldRegexp = regexp.MustCompile(`^(?:.[a-zA-Z]\w*)+$`)
+var fieldRegexp = regexp.MustCompile(`^(?:\.[a-zA-Z]\w*)+$`)
+var templateRegexp = regexp.MustCompile(`\{\{.*\}\}`)
 var pipelineTokens = []string{"with", "if"}
 
 func isFunc(token string) bool {
@@ -29,6 +30,10 @@ func isField(token string) bool {
        return fieldRegexp.MatchString(token)
 }
 
+func containsTemplate(token string) bool {
+       return templateRegexp.MatchString(token)
+}
+
 func isPipeline(token string) bool {
        for _, ptok := range pipelineTokens {
                if strings.Contains(token, ptok) {
@@ -42,6 +47,16 @@ func isEndPipeline(token string) bool {
        return strings.Contains(token, "end")
 }
 
+func getAttr(n *html.Node, key string) string {
+       idx := slices.IndexFunc(n.Attr, func(attr html.Attribute) bool {
+               return attr.Key == key
+       })
+       if idx == -1 {
+               return ""
+       }
+       return n.Attr[idx].Val
+}
+
 // replaceTemplateFuncs replaces template functions in a node with their
 // namespaced versions.
 func replaceTemplateFuncs(source TemplateSource, n *html.Node) {
@@ -235,8 +250,7 @@ func removeDataAttrs(n *html.Node) map[string]string {
 }
 
 func insertTemplateSource(subSource TemplateSource, context *html.Node) (*computeNode, error) {
-       insertedId := render.ID()
-       computedName := fmt.Sprintf("%s_%s", snakeCase(subSource.Name()), insertedId)
+       computedName := snakeCase(subSource.Name())
        // ===== CONTEXT PREPROCESSING =====
        // Raise any fields pre-existing in the context to .parent.field.
        raiseFields := make([]string, 0)
@@ -247,7 +261,6 @@ func insertTemplateSource(subSource TemplateSource, context *html.Node) (*comput
        // Remove :data attributes from the root of context, and prepare them
        // to be used as data for the computeNode.
        asData := removeDataAttrs(context)
-       asData["id"] = insertedId
        // ===== SUBSOURCE PROCESSING =====
        // Parse the subSource in the context of context.
        // Require that subSource be contained in a template.
@@ -338,22 +351,44 @@ func insertTemplateSource(subSource TemplateSource, context *html.Node) (*comput
        })
 
        // Generate a computeNode for this part.
+       htmlId := getAttr(context, "id")
+       scopeName := computedName + "_" + snakeCase(htmlId)
+       if containsTemplate(htmlId) {
+               htmlId = ""
+               scopeName = computedName
+       }
        compute := &computeNode{
                name:             computedName,
+               htmlId:           htmlId,
                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{
+       // Insert scope up/down template pipeline
+       head := context.FirstChild
+       up, down := scopeNodes(scopeName)
+       context.InsertBefore(up, head)
+       // Insert an assignment to $compute so we can always raise
+       // scope, even in pipelines
+       n.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)
+               Data: "{{ $compute := .compute }}",
+       }, head)
+       context.InsertBefore(down, nil)
        return compute, nil
 }
+
+func scopeNodes(scopeName string) (up, down *html.Node) {
+       upVal := fmt.Sprintf(`{{- $data := call $compute.%s }}{{ with $data }}`, scopeName)
+       up = &html.Node{
+               Type: html.TextNode,
+               Data: upVal,
+       }
+       downVal := `{{ end -}}`
+       down = &html.Node{
+               Type: html.TextNode,
+               Data: downVal,
+       }
+       return up, down
+}
index 416b10b0fe59969ebd6340594e6804aadb658f6e..16b8a1fabea81c3afa9d596fa2a9785f9f8d9c1c 100644 (file)
@@ -2,6 +2,7 @@
 package page
 
 import (
+       "context"
        "html/template"
 
        "git.earlybird.gay/today-engine/htmltree"
@@ -11,9 +12,10 @@ import (
 )
 
 type Page struct {
-       name   string
-       source include.Opener
-       raw    string
+       name     string
+       fileName string
+       source   include.Opener
+       raw      string
 
        includes []compile.Source
        onLoad   render.OnLoadFunc
@@ -40,7 +42,7 @@ func Includes(includes ...compile.Source) Config {
        }
 }
 
-func OnLoad(f func(data render.Data) error) Config {
+func OnLoad(f render.OnLoadFunc) Config {
        return func(p *Page) {
                p.onLoad = f
        }
@@ -56,8 +58,9 @@ func New(name string, source string, optional ...func(*Page)) *Page {
        p := new(Page)
        // Assign basic parameters
        p.name = name
+       p.fileName = source
        p.source = include.File(source, "git.earlybird.gay/today-engine/page")
-       p.onLoad = func(d render.Data) error {
+       p.onLoad = func(ctx context.Context, d render.Data) error {
                return nil
        }
        p.templateFuncs = make(template.FuncMap)
@@ -96,6 +99,10 @@ func (p *Page) Name() string {
        return p.name
 }
 
+func (p *Page) FileName() string {
+       return p.fileName
+}
+
 func (p *Page) File() include.Opener {
        return p.source
 }
index cefe05c73b5c81a9cb2c77b261804393efd8f5f4..ceb82936ec3ba599fef60c3744d6d2dc3fb523bf 100644 (file)
@@ -2,6 +2,7 @@
 package part
 
 import (
+       "context"
        "text/template"
 
        "git.earlybird.gay/today-engine/include"
@@ -10,8 +11,9 @@ import (
 )
 
 type Part struct {
-       name   string
-       source include.Opener
+       name     string
+       fileName string
+       source   include.Opener
 
        includes      []compile.Source
        onLoad        render.OnLoadFunc
@@ -34,7 +36,7 @@ func Includes(includes ...compile.Source) Config {
        }
 }
 
-func OnLoad(f func(data render.Data) error) Config {
+func OnLoad(f render.OnLoadFunc) Config {
        return func(p *Part) {
                p.onLoad = f
        }
@@ -44,8 +46,9 @@ func New(name string, source string, optional ...func(*Part)) *Part {
        p := new(Part)
        // Assign basic parameters
        p.name = name
+       p.fileName = source
        p.source = include.File(source, "git.earlybird.gay/today-engine/part")
-       p.onLoad = func(d render.Data) error {
+       p.onLoad = func(ctx context.Context, data render.Data) error {
                return nil
        }
        p.templateFuncs = make(template.FuncMap)
@@ -60,6 +63,10 @@ func (p *Part) Name() string {
        return p.name
 }
 
+func (p *Part) FileName() string {
+       return p.fileName
+}
+
 func (p *Part) File() include.Opener {
        return p.source
 }
index d76dc2ff62d3578c91093f6efca18c8467144252..4e38e9e8fb1b9be4fd325bae8f911d310e408a70 100644 (file)
@@ -2,6 +2,7 @@
 package render
 
 import (
+       "context"
        "net/http"
        "regexp"
 )
@@ -20,12 +21,22 @@ func (d Data) Get(key string) any {
        return d[key]
 }
 
+func (d Data) IsSet(key string) bool {
+       _, ok := d[key]
+       return ok
+}
+
 func (d Data) Request() *http.Request {
        return d.Get("request").(*http.Request)
 }
 
+// ID gets the *static* HTML id attribute of the component that owns this data.
+// If the ID cannot be determined at compile time, this returns an empty string.
 func (d Data) ID() string {
-       return d.Get("id").(string)
+       if id, ok := d.Get("id").(string); ok {
+               return id
+       }
+       return ""
 }
 
 // Set sets key to value.
@@ -36,7 +47,7 @@ func (d Data) Set(key string, value any) {
        d[key] = value
 }
 
-type OnLoadFunc func(Data) error
+type OnLoadFunc func(context.Context, Data) error
 type Loader interface {
        Compute(r *http.Request) (Data, error)
 }
index 45515fc0e5ac2c35242fa6a10d9ff4bd60b93a75..f8aae3674447ca2029def131c4cf1ccb166e9b96 100644 (file)
@@ -1,6 +1,7 @@
 package stdpart
 
 import (
+       "context"
        "errors"
        "fmt"
        "net/http"
@@ -45,7 +46,7 @@ func parseContactFormResponse(r *http.Request) (ContactFormResponse, error) {
 
 func ContactForm(categories []string) *part.Part {
        return part.New("stdp-contact-form", "contact_form.html",
-               part.OnLoad(func(data render.Data) error {
+               part.OnLoad(func(ctx context.Context, data render.Data) error {
                        data.Set("categories", categories)
 
                        // Set message if one is given for this part
@@ -74,7 +75,11 @@ func HandleContactForm(validate, handle func(ContactFormResponse) error) http.Ha
                callback := r.FormValue("callback")
                respond := func(ok bool, msg string) {
                        msg = url.QueryEscape(msg)
-                       w.Header().Set("Location", fmt.Sprintf("%s?resp_to=%s&success=%t&message=%s", callback, resp.ID, ok, msg))
+                       redirect := fmt.Sprintf("%s?success=%t&message=%s", callback, ok, msg)
+                       if resp.ID != "" {
+                               redirect += fmt.Sprintf("&resp_to=%s", resp.ID)
+                       }
+                       w.Header().Set("Location", redirect)
                        w.WriteHeader(http.StatusSeeOther)
                }
                if err != nil {
index 273ed3850f89fb0b6b6fafa1843f1c55a4657c7b..9cbc6aeabd921d51051ee318e1a303c65d0dc729 100644 (file)
@@ -1,5 +1,6 @@
 <template>
-  {{ $id := .id }}
+  {{- $id := "contact-form" -}}
+  {{- if .id }}{{ $id = .id }}{{ end -}}
   <form action="{{.action}}"
     {{ with .method }}method="{{.}}"{{ end }}>
     <input type="hidden" id="{{$id}}-callback" name="callback" value="{{.request.URL.Path}}">