From e76fc241d3a81a559777c5d1a7745f2140a47a27 Mon Sep 17 00:00:00 2001 From: early Date: Tue, 27 Aug 2024 22:47:34 -0600 Subject: [PATCH] revise compute pattern but again lol --- cmd/standard-test/index.html | 9 ++- cmd/standard-test/main.go | 15 +++- cmd/standard-test/test_thing.html | 3 + internal/compile/compile.go | 19 +++++ internal/compile/compute.go | 65 +++++++++------- internal/compile/template.go | 123 ++++++++++++++++++++++++++---- page/serve.go | 17 ++++- render/data.go | 4 +- 8 files changed, 206 insertions(+), 49 deletions(-) create mode 100644 cmd/standard-test/test_thing.html diff --git a/cmd/standard-test/index.html b/cmd/standard-test/index.html index a5e890c..bb6af6c 100644 --- a/cmd/standard-test/index.html +++ b/cmd/standard-test/index.html @@ -1,14 +1,19 @@ -
+ + Today Engine Examples -
+
+ + {{- range $i, $v := SliceOfLen 5 }} + + {{ end -}}
diff --git a/cmd/standard-test/main.go b/cmd/standard-test/main.go index f90d82e..43a1495 100644 --- a/cmd/standard-test/main.go +++ b/cmd/standard-test/main.go @@ -1,19 +1,28 @@ 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 { diff --git a/cmd/standard-test/test_thing.html b/cmd/standard-test/test_thing.html new file mode 100644 index 0000000..5988d75 --- /dev/null +++ b/cmd/standard-test/test_thing.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/internal/compile/compile.go b/internal/compile/compile.go index 6b84682..8d3279b 100644 --- a/internal/compile/compile.go +++ b/internal/compile/compile.go @@ -121,6 +121,25 @@ func Compile(root TemplateSource, transform ...func(root *html.Node)) (Result, e 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) } diff --git a/internal/compile/compute.go b/internal/compile/compute.go index 79e6bcb..5c5f69a 100644 --- a/internal/compile/compute.go +++ b/internal/compile/compute.go @@ -19,13 +19,19 @@ type computeNode struct { 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" @@ -42,31 +48,30 @@ func (root *computeNode) Compute(r *http.Request) (render.Data, error) { 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) @@ -74,18 +79,24 @@ func (root *computeNode) Compute(r *http.Request) (render.Data, error) { } 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() diff --git a/internal/compile/template.go b/internal/compile/template.go index 4f67c2d..b9ecacf 100644 --- a/internal/compile/template.go +++ b/internal/compile/template.go @@ -19,8 +19,8 @@ const ( 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) @@ -34,19 +34,20 @@ func containsTemplate(token string) bool { 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 @@ -57,6 +58,65 @@ func getAttr(n *html.Node, key string) string { 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) { @@ -84,10 +144,10 @@ 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 @@ -353,7 +413,7 @@ 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) { + if htmlId == "" || containsTemplate(htmlId) { htmlId = "" scopeName = computedName } @@ -380,7 +440,7 @@ func insertTemplateSource(subSource TemplateSource, context *html.Node) (*comput } 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, @@ -392,3 +452,40 @@ func scopeNodes(scopeName string) (up, down *html.Node) { } 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 + }) +} diff --git a/page/serve.go b/page/serve.go index 2f91ad5..ad05884 100644 --- a/page/serve.go +++ b/page/serve.go @@ -3,14 +3,27 @@ package page 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) } diff --git a/render/data.go b/render/data.go index 4e38e9e..3f9fe9f 100644 --- a/render/data.go +++ b/render/data.go @@ -47,7 +47,7 @@ func (d Data) Set(key string, value any) { 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) } -- 2.39.5