<!DOCTYPE html>
<html>
-<header>
+
+<head>
<title>Today Engine Examples</title>
<link rel="stylesheet" href="/style.css">
-</header>
+</head>
<body>
<main>
<stdp-contact-form :action="/contact" id="cf-get"></stdp-contact-form>
<stdp-contact-form :action="/contact" :method="POST" id="cf-post"></stdp-contact-form>
+
+ {{- range $i, $v := SliceOfLen 5 }}
+ <test-thing :value="."></test-thing>
+ {{ end -}}
</main>
</body>
package main
import (
- "html/template"
+ "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"
+)
- tapp "git.earlybird.gay/today-app/app"
+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", "index.html",
page.Includes(
- stdpart.ContactForm([]string{"Feedback"}),
+ stdpart.ContactForm([]string{"Feedback"}), Thing,
),
page.Funcs(template.FuncMap{
"SliceOfLen": func(i int) []int {
--- /dev/null
+<template>
+ <p>{{ .value }}</p>
+</template>
\ No newline at end of file
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)
}
children []*computeNode
}
-func (root *computeNode) Compute(r *http.Request) (render.Data, error) {
+func (root *computeNode) Compute(r *http.Request) (render.Data, func(any) error, error) {
impose := func(cData, pData render.Data, raise []string) {
for _, key := range raise {
cData.Set(key, pData.Get(key))
}
}
+ var dot any
+ setDot := func(val any) error {
+ dot = val
+ return nil
+ }
ctx := r.Context()
+
var f func(n *computeNode, data render.Data) error
f = func(n *computeNode, data render.Data) error {
// Set the htmlId of the component if it exists as "id"
if computeFuncs.IsSet(computeFuncName) {
continue
}
- childData := render.NewData(r)
- // Stuff here is available to the OnLoad AND renderer:
- // Assign all data from parent
- for dataKey, rawOrIdentifier := range child.asDataFromParent {
- // Assume raw...
- var val any = rawOrIdentifier
- // ...but treat as an identifier if starts with .
- if rawOrIdentifier[0] == '.' {
- val = getVarFromData(data, rawOrIdentifier)
- if val == nil {
- return fmt.Errorf("key %s not found in passed data", rawOrIdentifier)
+ // Defer rendering of the child to when it's needed
+ computeFuncs.Set(computeFuncName, func() (render.Data, error) {
+ childData := render.NewData(r)
+ // Stuff here is available to the OnLoad AND renderer:
+ // Assign all data from parent
+ for dataKey, rawOrIdentifier := range child.asDataFromParent {
+ // Assume raw...
+ var val any = rawOrIdentifier
+ // ...but treat as an identifier if starts with .
+ if rawOrIdentifier[0] == '.' {
+ val = getVarFromData(dot, rawOrIdentifier)
+ if val == nil {
+ return nil, fmt.Errorf("key %s not found in passed data", rawOrIdentifier)
+ }
}
+ childData.Set(dataKey, val)
}
- childData.Set(dataKey, val)
- }
- // Raise parent variables to the "parent" var
- childParentData := make(render.Data)
- impose(childParentData, data, child.raiseFromParent)
- childData.Set("parent", childParentData)
- if computeFuncName != "" {
- computeFuncs.Set(computeFuncName, func() (render.Data, error) {
- return childData, child.compute(ctx, childData)
- })
- }
- f(child, childData)
+ // Raise parent variables to the "parent" var
+ childParentData := make(render.Data)
+ impose(childParentData, data, child.raiseFromParent)
+ childData.Set("parent", childParentData)
+ f(child, childData)
+ return childData, child.compute(ctx, childData)
+ })
// Stuff here is ONLY available to the renderer:
}
data.Set("compute", computeFuncs)
}
data := render.NewData(r)
- return data, f(root, data)
+ return data, setDot, f(root, data)
}
// Get a variable from data.
// key must be of the form .x.y.z, and will try to get that value from data.
-func getVarFromData(data render.Data, key string) any {
+func getVarFromData(data any, key string) any {
+ // special case; if the key is ".", just return data.
+ if key == "." {
+ return data
+ }
var val any = data
keyStack := strings.Split(key, ".")[1:]
-
findKey:
for val != nil && len(keyStack) > 0 {
key = keyStack[0]
+ if key == "" {
+ return data
+ }
keyStack = keyStack[1:]
refV := reflect.ValueOf(val)
refT := refV.Type()
var funcRegexp = regexp.MustCompile(`^[a-zA-Z]\w*$`)
var fieldRegexp = regexp.MustCompile(`^(?:\.[a-zA-Z]\w*)+$`)
+var setsDotRegexp = regexp.MustCompile(`^\{\{-?\s*(?:with|range).*?\}\}$`)
var templateRegexp = regexp.MustCompile(`\{\{.*\}\}`)
-var pipelineTokens = []string{"with", "if"}
func isFunc(token string) bool {
return funcRegexp.MatchString(token)
return templateRegexp.MatchString(token)
}
-func isPipeline(token string) bool {
- for _, ptok := range pipelineTokens {
- if strings.Contains(token, ptok) {
- return true
- }
- }
- return false
+func scopesUp(token string) bool {
+ return strings.Contains(token, "with") ||
+ strings.Contains(token, "if") ||
+ strings.Contains(token, "range")
}
-func isEndPipeline(token string) bool {
+func scopesDown(token string) bool {
return strings.Contains(token, "end")
}
+func setsDot(token string) bool {
+ return setsDotRegexp.MatchString(token)
+}
+
func getAttr(n *html.Node, key string) string {
idx := slices.IndexFunc(n.Attr, func(attr html.Attribute) bool {
return attr.Key == key
return n.Attr[idx].Val
}
+func splitTemplateNodes(n *html.Node) {
+ htmltree.Walk(n, func(n *html.Node) (bool, error) {
+ if n.Parent == nil {
+ return false, nil
+ } else if n.Type != html.TextNode {
+ return false, nil
+ } else if !containsTemplate(n.Data) {
+ return false, nil
+ }
+
+ // n is a text node with at least one template
+ // split it into text nodes where every {{ template }} is its own node
+ splitNodes := make([]string, 0)
+
+ var t, i, open, close, depth int
+ limit := 0
+ for limit < 10 {
+ limit++
+ // Find next open and close token
+ // Increment to the closest one, if at least one exists
+ open = strings.Index(n.Data[i:], tokenOpenTemplate)
+ close = strings.Index(n.Data[i:], tokenCloseTemplate)
+
+ if open != -1 && open < close {
+ if depth == 0 {
+ // If we're just starting a pipeline, set the "template"
+ // cursor to the template open position
+ t += open
+ }
+ depth++
+ i += open + 2
+ } else if close != -1 {
+ depth--
+ if depth == 0 {
+ // If we're closing a pipeline, append from the "template"
+ // cursor to the closing position, + 2 for the length
+ // of the closeToken
+ splitNodes = append(splitNodes, n.Data[t:i+close+2])
+ t = i + close + 2
+ }
+ i += close + 2
+ } else {
+ break
+ }
+ }
+
+ // Append all splitNodes as nodes where n was
+ head := n.NextSibling
+ for _, data := range splitNodes {
+ n.Parent.InsertBefore(&html.Node{
+ Type: html.TextNode,
+ Data: data,
+ }, head)
+ }
+ n.Parent.RemoveChild(n)
+ return false, nil
+ })
+}
+
// replaceTemplateFuncs replaces template functions in a node with their
// namespaced versions.
func replaceTemplateFuncs(source TemplateSource, n *html.Node) {
}
// If in a template, and the current key indicates entering a
// template pipeline, increase depth
- if inTemplate && isPipeline(attr.Key) {
+ if inTemplate && scopesUp(attr.Key) {
depth++
}
- if inTemplate && isEndPipeline(attr.Key) {
+ if inTemplate && scopesDown(attr.Key) {
depth--
}
// Track exiting a template
// Generate a computeNode for this part.
htmlId := getAttr(context, "id")
scopeName := computedName + "_" + snakeCase(htmlId)
- if containsTemplate(htmlId) {
+ if htmlId == "" || containsTemplate(htmlId) {
htmlId = ""
scopeName = computedName
}
}
func scopeNodes(scopeName string) (up, down *html.Node) {
- upVal := fmt.Sprintf(`{{- $data := call $compute.%s }}{{ with $data }}`, scopeName)
+ upVal := fmt.Sprintf(`{{ with call $compute.%s }}`, scopeName)
up = &html.Node{
Type: html.TextNode,
Data: upVal,
}
return up, down
}
+
+func addSetDots(document *html.Node) {
+ endAcc := make([]bool, 0)
+ htmltree.Walk(document, func(n *html.Node) (bool, error) {
+ if n.Type != html.TextNode {
+ return false, nil
+ } else if !containsTemplate(n.Data) {
+ return false, nil
+ }
+
+ if scopesUp(n.Data) {
+ if setsDot(n.Data) {
+ n.Parent.InsertBefore(&html.Node{
+ Type: html.TextNode,
+ Data: `{{ call $setDot . }}`,
+ }, n.NextSibling)
+ endAcc = append(endAcc, true)
+ } else {
+ endAcc = append(endAcc, false)
+ }
+ } else if scopesDown(n.Data) || strings.Contains(n.Data, "else") {
+ shouldSet := endAcc[len(endAcc)-1]
+ endAcc = endAcc[:len(endAcc)-1]
+ if shouldSet {
+ n.Parent.InsertBefore(&html.Node{
+ Type: html.TextNode,
+ Data: "{{ call $setDot . }}",
+ }, n.NextSibling)
+ }
+ if strings.Contains(n.Data, "else") {
+ endAcc = append(endAcc, false)
+ }
+ }
+
+ return false, nil
+ })
+}
import (
"net/http"
+
+ "git.earlybird.gay/today-engine/render"
)
+type RootData struct {
+ SetDot func(value any) error
+
+ Data render.Data
+}
+
func (p *Page) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- data, err := p.templateLoad.Compute(r)
+ data, setDot, err := p.templateLoad.Compute(r)
if err != nil {
panic(err)
}
- err = p.template.Execute(w, data)
+ root := RootData{
+ SetDot: setDot,
+
+ Data: data,
+ }
+ err = p.template.Execute(w, root)
if err != nil {
panic(err)
}
d[key] = value
}
-type OnLoadFunc func(context.Context, Data) error
+type OnLoadFunc func(ctx context.Context, data Data) error
type Loader interface {
- Compute(r *http.Request) (Data, error)
+ Compute(r *http.Request) (Data, func(any) error, error)
}