-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwalker.go
More file actions
282 lines (257 loc) · 9.89 KB
/
walker.go
File metadata and controls
282 lines (257 loc) · 9.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
package templar
import (
"bytes"
"fmt"
"log/slog"
"path/filepath"
ttmpl "text/template"
)
// Walker provides a mechanism for walking through templates and their dependencies
// in a customizable way, applying visitor patterns as templates are processed.
// Unlike the WalkTemplate method which uses post-order traversal, Walker implements
// in-order traversal, processing includes immediately when encountered.
type Walker struct {
// Buffer stores the processed template content
Buffer *bytes.Buffer
// Loader is used to resolve and load template dependencies
Loader TemplateLoader
// FoundInclude is called when an include directive is encountered.
// If it returns true, the include is skipped and not processed.
FoundInclude func(included string) bool
// Called before a template is preprocessed. This is an opportunity
// for the handler to control entering/preprocessing etc. For example
// This could be a place for the handler to skip processing a template
EnteringTemplate func(template *Template) (skip bool, err error)
// ProcessedTemplate is called after a template and all its children
// have been processed. This allows for custom post-processing.
ProcessedTemplate func(template *Template) error
// inProgress tracks templates currently being processed to detect cycles (infinite recursion)
inProgress map[string]bool
}
// Walk processes a template and its dependencies using in-order traversal.
// This means includes are processed as soon as they are encountered in the template.
// After processing, the template's ParsedSource will contain the processed content.
// If ProcessedTemplate is defined, it will be called on each processed template.
func (w *Walker) Walk(root *Template) (err error) {
if w.Buffer == nil {
w.Buffer = bytes.NewBufferString("")
}
if w.inProgress == nil {
w.inProgress = make(map[string]bool)
}
// Check if this template is currently being processed (cycle detection)
if root.Path != "" {
if w.inProgress[root.Path] {
slog.Warn("cycle detected, skipping template already in progress", "path", root.Path)
return nil
}
w.inProgress[root.Path] = true
defer func() { w.inProgress[root.Path] = false }()
}
// An Inorder walk of of a template. Unlike WalkTemplate which applies a PostOrder traversal (first collects all
// includes, processes them and then the root template), here we will process an included template as soon as it is
// encountered.
cwd := root.Path
if cwd != "" {
cwd = filepath.Dir(cwd)
}
if w.EnteringTemplate != nil {
skip, err := w.EnteringTemplate(root)
if skip || err != nil {
return err
}
}
// parse the template and render it
fm := ttmpl.FuncMap{
"include": func(args ...string) (string, error) {
// Syntax: include "file.html" ["template1" "template2" ...]
// If no templates specified, includes all templates from the file.
// If templates specified, includes only those (and their dependencies).
if len(args) < 1 {
return "", fmt.Errorf("include requires at least a file path")
}
glob := args[0]
var entryPoints []string
if len(args) > 1 {
entryPoints = args[1:]
}
skipped, err := w.processInclude(root, glob, entryPoints, cwd)
if skipped {
return fmt.Sprintf("{{/* Skipping: '%s' */}}", glob), err
} else {
return fmt.Sprintf("{{/* Finished Including: '%s' */}}", glob), err
}
},
"namespace": func(args ...string) (string, error) {
// Syntax: namespace "NS" "file.html" ["template1" "template2" ...]
// Loads templates into namespace NS with tree-shaking.
if len(args) < 2 {
return "", fmt.Errorf("namespace requires: namespace file [templates...]")
}
namespace, glob := args[0], args[1]
if namespace == "" {
return "", fmt.Errorf("namespace requires a non-empty namespace name")
}
var entryPoints []string
if len(args) > 2 {
entryPoints = args[2:]
}
skipped, err := w.processNamespace(root, namespace, glob, entryPoints, cwd)
if skipped {
return fmt.Sprintf("{{/* Skipping namespace '%s' from '%s' */}}", namespace, glob), err
} else {
return fmt.Sprintf("{{/* Loaded namespace '%s' from '%s' */}}", namespace, glob), err
}
},
"extend": func(args ...string) (string, error) {
// Syntax: extend "SourceTemplate" "DestTemplate" "block1" "override1" ...
// Creates DestTemplate as a copy of SourceTemplate with references rewired.
// SourceTemplate must already exist (from a prior include/namespace).
if len(args) < 2 {
return "", fmt.Errorf("extend requires at least: sourceTemplate destTemplate")
}
if len(args)%2 != 0 {
return "", fmt.Errorf("extend requires pairs of block/override after destTemplate")
}
source, dest := args[0], args[1]
if dest == "" {
return "", fmt.Errorf("extend requires a non-empty destination template name")
}
// Parse block/override pairs
rewrites := make(map[string]string)
for i := 2; i < len(args); i += 2 {
block, override := args[i], args[i+1]
rewrites[block] = override
}
w.processExtend(root, source, dest, rewrites)
return fmt.Sprintf("{{/* Extended '%s' as '%s' */}}", source, dest), nil
},
}
templ, err := ttmpl.New("").Funcs(fm).Delims("{{#", "#}}").Parse(string(root.RawSource))
if err != nil {
slog.Error("error preprocessing template: ", "path", root.Path, "error", err)
return panicOrError(err)
}
if err := templ.Execute(w.Buffer, nil); err != nil {
slog.Error("error preprocessing template: ", "path", root.Path, "error", err)
root.Error = err
return panicOrError(err)
} else {
root.ParsedSource = w.Buffer.String()
}
// No handle this template
if w.ProcessedTemplate != nil {
return w.ProcessedTemplate(root)
}
return nil
}
// processInclude handles the inclusion of another template within the current template.
// If FoundInclude returns true, the include is skipped. Otherwise, the included template
// and its dependencies are loaded and processed.
//
// If entryPoints is non-empty, only those templates (and their dependencies) are included.
// Returns a boolean indicating if the include was skipped, and any error encountered.
func (w *Walker) processInclude(root *Template, included string, entryPoints []string, cwd string) (skipped bool, err error) {
skipped = w.FoundInclude != nil && w.FoundInclude(included)
if skipped {
return
}
children, err := w.Loader.Load(included, cwd)
if err != nil {
slog.Error("error loading include: ", "included", included, "error", err)
return false, panicOrError(err)
}
for _, child := range children {
// Inherit namespace from parent template
if root.Namespace != "" {
child.Namespace = root.Namespace
}
// Set entry points for selective inclusion (tree-shaking)
if len(entryPoints) > 0 {
child.NamespaceEntryPoints = entryPoints
}
if child.Path != "" {
if !root.AddDependency(child) {
slog.Error(fmt.Sprintf("found cyclical dependency: %s -> %s", child.Path, root.Path), "from", child.Path, "to", root.Path)
continue
}
}
// If the child has a namespace (inherited or otherwise), use a fresh walker
// with its own buffer. This ensures the child's ParsedSource contains only
// its own content, not contaminated with the parent's partial buffer content.
if child.Namespace != "" {
childWalker := &Walker{
Loader: w.Loader,
FoundInclude: w.FoundInclude,
EnteringTemplate: w.EnteringTemplate,
ProcessedTemplate: w.ProcessedTemplate,
inProgress: w.inProgress, // Share inProgress map for cycle detection
}
err = childWalker.Walk(child)
} else {
err = w.Walk(child)
}
if err != nil {
slog.Error("error walking", "included", included, "error", err)
root.Error = err
return false, panicOrError(err)
}
}
return
}
// processNamespace handles the inclusion of templates into a namespace.
// Templates are loaded from the file and will be registered with the given namespace prefix.
// If entryPoints is non-empty, only those templates (and their dependencies) are included.
func (w *Walker) processNamespace(root *Template, namespace string, included string, entryPoints []string, cwd string) (skipped bool, err error) {
skipped = w.FoundInclude != nil && w.FoundInclude(included)
if skipped {
return
}
children, err := w.Loader.Load(included, cwd)
if err != nil {
slog.Error("error loading namespace: ", "included", included, "error", err)
return false, panicOrError(err)
}
for _, child := range children {
// Set the namespace and entry points on the child template
child.Namespace = namespace
if len(entryPoints) > 0 {
child.NamespaceEntryPoints = entryPoints
}
if child.Path != "" {
if !root.AddDependency(child) {
slog.Error(fmt.Sprintf("found cyclical dependency: %s -> %s", child.Path, root.Path), "from", child.Path, "to", root.Path)
continue
}
}
// Use a fresh walker with its own buffer for namespaced includes.
// This ensures the child's ParsedSource contains only its own content,
// avoiding conflicts when the same template is included multiple times
// with different namespaces.
// IMPORTANT: Share the inProgress map to detect cycles (infinite recursion).
childWalker := &Walker{
Loader: w.Loader,
FoundInclude: w.FoundInclude,
EnteringTemplate: w.EnteringTemplate,
ProcessedTemplate: w.ProcessedTemplate,
inProgress: w.inProgress, // Share inProgress map for cycle detection
}
err = childWalker.Walk(child)
if err != nil {
slog.Error("error walking namespace", "included", included, "error", err)
root.Error = err
return false, panicOrError(err)
}
}
return
}
// processExtend records an extend directive on the root template.
// The actual extension (copying and rewiring) is performed later in group.go
// after all templates have been parsed.
func (w *Walker) processExtend(root *Template, source string, dest string, rewrites map[string]string) {
root.Extensions = append(root.Extensions, Extension{
SourceTemplate: source,
DestTemplate: dest,
Rewrites: rewrites,
})
}