-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtag_include.go
More file actions
304 lines (274 loc) · 8.55 KB
/
tag_include.go
File metadata and controls
304 lines (274 loc) · 8.55 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
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
package template
import (
"errors"
"fmt"
"io"
)
// IncludeNode represents an {% include %} statement.
//
// Shapes:
// - Static path (string literal), resolved at parse time: prepared != nil.
// - Parse-time circular static path: prepared == nil, staticName holds the
// literal for runtime lookup.
// - Dynamic path (expression): pathExpr != nil.
//
// Options:
// - withPairs: {% include "x" with k1=expr1 k2=expr2 %}
// - only: {% include "x" only %} — fully isolates the child context,
// excluding parent variables AND defaults.
// - ifExists: {% include "x" if_exists %} — missing template is a no-op
// instead of an error.
type IncludeNode struct {
Line int
Col int
// prepared is the pre-parsed template for static includes. nil when lazy.
prepared *Template
// staticName is the literal name for string-literal includes, used for
// runtime lookup when lazy downgrade is active.
staticName string
// pathExpr is set for dynamic includes (non-string-literal path).
pathExpr Expression
// withPairs holds "with k=expr" bindings; may be nil.
withPairs []withPair
// only indicates the child template must not see the parent's context.
only bool
// ifExists indicates a missing template should silently render nothing.
ifExists bool
}
// withPair is a single "key=expression" binding on an include tag.
type withPair struct {
name string
expr Expression
}
// Position returns the source position of the include tag.
func (n *IncludeNode) Position() (int, int) { return n.Line, n.Col }
// String returns a debug representation.
func (n *IncludeNode) String() string {
if n.prepared != nil {
return fmt.Sprintf("Include(%q)", n.prepared.name)
}
return fmt.Sprintf("Include(%q lazy)", n.staticName)
}
// maxIncludeDepth is the hard cap on {% include %} nesting depth. It
// defends against runaway recursion (self-include, mutual include, data-
// driven deep trees). 32 is well beyond any reasonable real-world need.
const maxIncludeDepth = 32
// Execute renders the included template. The parser only produces
// IncludeNode inside an Engine with layout enabled, so ctx.engine is guaranteed non-nil by the
// time we reach here.
func (n *IncludeNode) Execute(ctx *RenderContext, w io.Writer) error {
if ctx.includeDepth >= maxIncludeDepth {
return fmt.Errorf("%w at line %d (max %d)",
ErrIncludeDepthExceeded, n.Line, maxIncludeDepth)
}
child, err := n.resolveChild(ctx)
if err != nil {
return err
}
if child == nil {
// if_exists miss: render nothing, return cleanly.
return nil
}
childCtx, err := n.buildChildContext(ctx)
if err != nil {
return err
}
return child.Execute(childCtx, w)
}
// resolveChild loads the sub-template this include points at. It
// returns (nil, nil) when the target is missing and if_exists is set,
// signalling "silently render nothing". Any other failure is returned
// as an error.
func (n *IncludeNode) resolveChild(ctx *RenderContext) (*Template, error) {
if n.prepared != nil {
return n.prepared, nil
}
name, err := n.resolveName(ctx)
if err != nil {
if n.ifExists && errors.Is(err, ErrTemplateNotFound) {
return nil, nil
}
return nil, err
}
tpl, err := ctx.engine.Load(name)
if err != nil {
if n.ifExists && errors.Is(err, ErrTemplateNotFound) {
return nil, nil
}
return nil, err
}
return tpl, nil
}
// buildChildContext constructs the render context the sub-template
// will run in. It honors the "only" keyword (full isolation) and
// evaluates "with" bindings in the PARENT context.
func (n *IncludeNode) buildChildContext(ctx *RenderContext) (*RenderContext, error) {
var childCtx *RenderContext
if n.only {
// Fully isolated: only the with-pairs are visible.
childCtx = NewIsolatedChildContext(ctx)
} else {
// Inherit parent's render data and runtime state with isolated
// locals for the child render.
childCtx = NewChildContext(ctx)
}
childCtx.includeDepth = ctx.includeDepth + 1
// Evaluate "with" bindings in the PARENT context, then insert them
// into the CHILD context's Locals (which is always a fresh per-
// child map, so this does not leak into the parent).
for _, wp := range n.withPairs {
val, err := wp.expr.Evaluate(ctx)
if err != nil {
return nil, err
}
childCtx.Set(wp.name, val.Interface())
}
return childCtx, nil
}
// resolveName returns the target template name for a lazy include,
// re-validating dynamic names against fs.ValidPath (defense in depth).
func (n *IncludeNode) resolveName(ctx *RenderContext) (string, error) {
if n.pathExpr == nil {
return n.staticName, nil
}
val, err := n.pathExpr.Evaluate(ctx)
if err != nil {
return "", err
}
name, ok := val.Interface().(string)
if !ok {
return "", fmt.Errorf("%w: got %T", ErrIncludePathNotString, val.Interface())
}
if err := ValidateName(name); err != nil {
return "", err
}
return name, nil
}
// parseIncludeTag parses {% include %}.
//
// Syntax:
//
// {% include "path/to/template.html" %}
// {% include name_expr %}
// {% include "card.html" with title="Hi" count=3 %}
// {% include "card.html" with title="Hi" only %}
// {% include "card.html" only %}
// {% include "card.html" if_exists %}
// {% include "card.html" with k=v only if_exists %}
//
// String literals are loaded and compiled at parse time (fast fail on
// missing templates). Expressions are resolved at runtime (lazy mode).
// Parse-time circular references are automatically downgraded to lazy
// to support recursive template patterns.
func parseIncludeTag(_ *Parser, start *Token, args *Parser) (Statement, error) {
node := &IncludeNode{Line: start.Line, Col: start.Col}
tok := args.Current()
if tok == nil {
return nil, newParseError("include: missing path", start.Line, start.Col)
}
if tok.Type == TokenString {
node.staticName = tok.Value
args.Advance()
} else {
// Dynamic path: parse as expression for runtime resolution.
expr, err := args.ParseExpression()
if err != nil {
return nil, err
}
node.pathExpr = expr
}
if err := parseIncludeOptions(node, args); err != nil {
return nil, err
}
// parseIncludeTag is only reachable when a layout-enabled engine is present
// (the tag is not in the global registry), so args.Engine() is non-nil here.
engine := args.Engine()
// Dynamic paths always resolve at runtime.
if node.pathExpr != nil {
return node, nil
}
// Detect parse-time circular reference and downgrade to lazy.
// Without this, mutual includes (A→B→A) cause infinite parse recursion.
if engine.isParsing(node.staticName) {
return node, nil
}
prepared, err := engine.Load(node.staticName)
if err != nil {
if errors.Is(err, ErrTemplateNotFound) {
if node.ifExists {
// Silently accept missing template — execute is a no-op.
return node, nil
}
return nil, fmt.Errorf("include: %w", err)
}
return nil, err
}
node.prepared = prepared
return node, nil
}
// parseIncludeOptions consumes the "with k=v …", "only", and "if_exists"
// trailing options from an include tag's argument parser.
func parseIncludeOptions(node *IncludeNode, args *Parser) error {
for args.Remaining() > 0 {
tok := args.Current()
if tok == nil {
break
}
if tok.Type != TokenIdentifier {
return newParseError(
fmt.Sprintf("include: unexpected token %q", tok.Value),
tok.Line, tok.Col)
}
switch tok.Value {
case "with":
args.Advance()
if err := parseIncludeWithPairs(node, args); err != nil {
return err
}
case "only":
args.Advance()
node.only = true
case "if_exists":
args.Advance()
node.ifExists = true
default:
return newParseError(
fmt.Sprintf("include: unknown option %q", tok.Value),
tok.Line, tok.Col)
}
}
return nil
}
// parseIncludeWithPairs consumes one or more "key=expression" bindings.
// It stops at "only", "if_exists", or end-of-args.
func parseIncludeWithPairs(node *IncludeNode, args *Parser) error {
for args.Remaining() > 0 {
tok := args.Current()
if tok == nil {
break
}
// Stop when we hit a trailing option keyword.
if tok.Type == TokenIdentifier && (tok.Value == "only" || tok.Value == "if_exists") {
return nil
}
if tok.Type != TokenIdentifier {
return newParseError(
fmt.Sprintf("include: expected identifier, got %q", tok.Value),
tok.Line, tok.Col)
}
name := tok.Value
args.Advance()
if eq := args.Current(); eq == nil || eq.Type != TokenSymbol || eq.Value != "=" {
return newParseError(
fmt.Sprintf("include: expected '=' after %q", name),
tok.Line, tok.Col)
}
args.Advance() // consume '='
expr, err := args.ParseExpression()
if err != nil {
return err
}
node.withPairs = append(node.withPairs, withPair{name: name, expr: expr})
}
return nil
}