+27
-5
ast.go
+27
-5
ast.go
···
187
187
func (s *BlockStmt) node() {}
188
188
func (s *BlockStmt) stmt() {}
189
189
190
+
// PathPart represents a segment of a dynamic path for INCLUDE/WRAPPER.
191
+
// A path like "templates/$category/page.html" becomes:
192
+
// [{IsVariable: false, Value: "templates/"}, {IsVariable: true, Parts: ["category"]}, {IsVariable: false, Value: "/page.html"}]
193
+
type PathPart struct {
194
+
IsVariable bool // true if this is a $variable reference
195
+
Value string // literal text (when IsVariable is false)
196
+
Parts []string // variable parts for dot notation: $user.name -> ["user", "name"]
197
+
}
198
+
190
199
// IncludeStmt represents an INCLUDE directive
191
200
type IncludeStmt struct {
192
-
Position Position
193
-
Name string // block name or file path
201
+
Position Position
202
+
Name string // static path (used when PathParts is empty)
203
+
PathParts []PathPart // dynamic path parts (used when path contains $variables)
194
204
}
195
205
196
206
func (s *IncludeStmt) Pos() Position { return s.Position }
···
199
209
200
210
// WrapperStmt represents a WRAPPER directive
201
211
type WrapperStmt struct {
202
-
Position Position
203
-
Name string // wrapper template name
204
-
Content []Node // content to be wrapped
212
+
Position Position
213
+
Name string // static path (used when PathParts is empty)
214
+
PathParts []PathPart // dynamic path parts (used when path contains $variables)
215
+
Content []Node // content to be wrapped
205
216
}
206
217
207
218
func (s *WrapperStmt) Pos() Position { return s.Position }
···
218
229
func (s *SetStmt) Pos() Position { return s.Position }
219
230
func (s *SetStmt) node() {}
220
231
func (s *SetStmt) stmt() {}
232
+
233
+
// TryStmt represents a TRY/CATCH error handling block
234
+
type TryStmt struct {
235
+
Position Position
236
+
Try []Node // content to try
237
+
Catch []Node // fallback content if error occurs
238
+
}
239
+
240
+
func (s *TryStmt) Pos() Position { return s.Position }
241
+
func (s *TryStmt) node() {}
242
+
func (s *TryStmt) stmt() {}
+2
cache_disk.go
+2
cache_disk.go
···
34
34
gob.Register(&IncludeStmt{})
35
35
gob.Register(&WrapperStmt{})
36
36
gob.Register(&SetStmt{})
37
+
gob.Register(&TryStmt{})
37
38
38
39
// Supporting types
39
40
gob.Register(&ElsIfClause{})
41
+
gob.Register(PathPart{})
40
42
}
41
43
42
44
// diskCache persists compiled templates to disk using gob encoding.
+102
-11
eval.go
+102
-11
eval.go
···
88
88
}
89
89
e.vars[n.Var] = val
90
90
91
+
case *TryStmt:
92
+
return e.evalTry(n)
93
+
91
94
case *BlockStmt:
92
95
// Block definitions are handled in first pass, skip here
93
96
}
···
210
213
211
214
// evalInclude evaluates an INCLUDE directive
212
215
func (e *Evaluator) evalInclude(n *IncludeStmt) error {
216
+
// Resolve the path (may be static or dynamic)
217
+
includeName, err := e.resolvePath(n.Name, n.PathParts)
218
+
if err != nil {
219
+
return err
220
+
}
221
+
213
222
// First check if it's a defined block
214
-
if block, ok := e.blocks[n.Name]; ok {
223
+
if block, ok := e.blocks[includeName]; ok {
215
224
return e.evalNodes(block.Body)
216
225
}
217
226
218
227
// Otherwise, try to load from filesystem
219
-
content, err := e.renderer.loadFile(n.Name)
228
+
content, err := e.renderer.loadFile(includeName)
220
229
if err != nil {
221
-
e.output.WriteString(fmt.Sprintf("[Include '%s' not found]", n.Name))
222
-
return nil
230
+
return &EvalError{
231
+
Pos: n.Position,
232
+
Message: fmt.Sprintf("include '%s' not found", includeName),
233
+
}
223
234
}
224
235
225
236
// Parse with caching
226
-
tmpl, err := e.renderer.parseTemplate(n.Name, content)
237
+
tmpl, err := e.renderer.parseTemplate(includeName, content)
227
238
if err != nil {
228
239
return err
229
240
}
···
243
254
return nil
244
255
}
245
256
257
+
// resolvePath resolves a static or dynamic path to its final string value.
258
+
// If pathParts is non-empty, variables are interpolated; otherwise staticPath is used.
259
+
func (e *Evaluator) resolvePath(staticPath string, pathParts []PathPart) (string, error) {
260
+
// Static path - no interpolation needed
261
+
if len(pathParts) == 0 {
262
+
return staticPath, nil
263
+
}
264
+
265
+
// Dynamic path - interpolate variables
266
+
var result strings.Builder
267
+
for _, part := range pathParts {
268
+
if part.IsVariable {
269
+
// Resolve the variable
270
+
val, err := e.resolveIdent(part.Parts)
271
+
if err != nil {
272
+
return "", err
273
+
}
274
+
if val == nil {
275
+
return "", &EvalError{
276
+
Message: fmt.Sprintf("undefined variable in path: $%s", strings.Join(part.Parts, ".")),
277
+
}
278
+
}
279
+
result.WriteString(e.toString(val))
280
+
} else {
281
+
result.WriteString(part.Value)
282
+
}
283
+
}
284
+
return result.String(), nil
285
+
}
286
+
246
287
// evalWrapper evaluates a WRAPPER directive
247
288
func (e *Evaluator) evalWrapper(n *WrapperStmt) error {
289
+
// Resolve the path (may be static or dynamic)
290
+
wrapperPath, err := e.resolvePath(n.Name, n.PathParts)
291
+
if err != nil {
292
+
return err
293
+
}
294
+
248
295
// First, evaluate the wrapped content
249
296
contentEval := NewEvaluator(e.renderer, e.copyVars())
250
297
for name, block := range e.blocks {
···
260
307
// Load the wrapper template
261
308
var wrapperSource string
262
309
var wrapperName string
263
-
if block, ok := e.blocks[n.Name]; ok {
310
+
if block, ok := e.blocks[wrapperPath]; ok {
264
311
// Wrapper is a defined block - evaluate it
265
312
blockEval := NewEvaluator(e.renderer, e.copyVars())
266
313
for name, b := range e.blocks {
···
272
319
}
273
320
}
274
321
wrapperSource = blockEval.output.String()
275
-
wrapperName = "block:" + n.Name
322
+
wrapperName = "block:" + wrapperPath
276
323
} else {
277
-
content, err := e.renderer.loadFile(n.Name)
324
+
content, err := e.renderer.loadFile(wrapperPath)
278
325
if err != nil {
279
-
e.output.WriteString(fmt.Sprintf("[Wrapper '%s' not found]", n.Name))
280
-
return nil
326
+
return &EvalError{
327
+
Pos: n.Position,
328
+
Message: fmt.Sprintf("wrapper '%s' not found", wrapperPath),
329
+
}
281
330
}
282
331
wrapperSource = content
283
-
wrapperName = n.Name
332
+
wrapperName = wrapperPath
284
333
}
285
334
286
335
// Parse the wrapper template with caching
···
302
351
return err
303
352
}
304
353
e.output.WriteString(result)
354
+
355
+
return nil
356
+
}
357
+
358
+
// evalTry evaluates a TRY/CATCH block
359
+
func (e *Evaluator) evalTry(n *TryStmt) error {
360
+
// Create a new evaluator for the TRY block to isolate output
361
+
tryEval := NewEvaluator(e.renderer, e.copyVars())
362
+
for name, block := range e.blocks {
363
+
tryEval.blocks[name] = block
364
+
}
365
+
366
+
// Attempt to evaluate the TRY block
367
+
var tryErr error
368
+
for _, node := range n.Try {
369
+
if err := tryEval.evalNode(node); err != nil {
370
+
tryErr = err
371
+
break
372
+
}
373
+
}
374
+
375
+
// If no error, use the TRY output
376
+
if tryErr == nil {
377
+
e.output.WriteString(tryEval.output.String())
378
+
return nil
379
+
}
380
+
381
+
// Error occurred - evaluate CATCH block if present
382
+
if len(n.Catch) > 0 {
383
+
catchEval := NewEvaluator(e.renderer, e.copyVars())
384
+
for name, block := range e.blocks {
385
+
catchEval.blocks[name] = block
386
+
}
387
+
388
+
for _, node := range n.Catch {
389
+
if err := catchEval.evalNode(node); err != nil {
390
+
// Error in CATCH block - propagate it
391
+
return err
392
+
}
393
+
}
394
+
e.output.WriteString(catchEval.output.String())
395
+
}
305
396
306
397
return nil
307
398
}
+221
gott_test.go
+221
gott_test.go
···
720
720
}
721
721
return false
722
722
}
723
+
724
+
// TestTryCatch tests the TRY/CATCH error handling blocks
725
+
func TestTryCatch(t *testing.T) {
726
+
memFS := fstest.MapFS{
727
+
"exists.html": &fstest.MapFile{Data: []byte("EXISTS")},
728
+
"templates/a.html": &fstest.MapFile{Data: []byte("Template A")},
729
+
}
730
+
731
+
r, err := New(&Config{
732
+
IncludePaths: []fs.FS{memFS},
733
+
})
734
+
if err != nil {
735
+
t.Fatalf("New() error = %v", err)
736
+
}
737
+
738
+
tests := []struct {
739
+
name string
740
+
template string
741
+
vars map[string]any
742
+
want string
743
+
}{
744
+
{
745
+
name: "TRY succeeds - no CATCH executed",
746
+
template: "[% TRY %]success[% CATCH %]fallback[% END %]",
747
+
want: "success",
748
+
},
749
+
{
750
+
name: "TRY with include that exists",
751
+
template: "[% TRY %][% INCLUDE exists.html %][% CATCH %]not found[% END %]",
752
+
want: "EXISTS",
753
+
},
754
+
{
755
+
name: "TRY with include that does not exist - CATCH executed",
756
+
template: "[% TRY %][% INCLUDE notfound.html %][% CATCH %]fallback content[% END %]",
757
+
want: "fallback content",
758
+
},
759
+
{
760
+
name: "TRY without CATCH - error suppressed",
761
+
template: "[% TRY %][% INCLUDE notfound.html %][% END %]",
762
+
want: "",
763
+
},
764
+
{
765
+
name: "nested TRY blocks",
766
+
template: "[% TRY %][% TRY %][% INCLUDE notfound.html %][% CATCH %]inner[% END %][% CATCH %]outer[% END %]",
767
+
want: "inner",
768
+
},
769
+
{
770
+
name: "TRY with variable in CATCH",
771
+
template: "[% TRY %][% INCLUDE notfound.html %][% CATCH %]Error for [% name %][% END %]",
772
+
vars: map[string]any{"name": "test"},
773
+
want: "Error for test",
774
+
},
775
+
}
776
+
777
+
for _, tt := range tests {
778
+
t.Run(tt.name, func(t *testing.T) {
779
+
got, err := r.Process(tt.template, tt.vars)
780
+
if err != nil {
781
+
t.Fatalf("Process() error = %v", err)
782
+
}
783
+
if got != tt.want {
784
+
t.Errorf("Process() = %q, want %q", got, tt.want)
785
+
}
786
+
})
787
+
}
788
+
}
789
+
790
+
// TestDynamicIncludePaths tests variable interpolation in INCLUDE paths
791
+
func TestDynamicIncludePaths(t *testing.T) {
792
+
memFS := fstest.MapFS{
793
+
"templates/en/header.html": &fstest.MapFile{Data: []byte("English Header")},
794
+
"templates/de/header.html": &fstest.MapFile{Data: []byte("German Header")},
795
+
"templates/default/page.html": &fstest.MapFile{Data: []byte("Default Page")},
796
+
"templates/users/alice/profile.html": &fstest.MapFile{Data: []byte("Alice's Profile")},
797
+
}
798
+
799
+
r, err := New(&Config{
800
+
IncludePaths: []fs.FS{memFS},
801
+
})
802
+
if err != nil {
803
+
t.Fatalf("New() error = %v", err)
804
+
}
805
+
806
+
tests := []struct {
807
+
name string
808
+
template string
809
+
vars map[string]any
810
+
want string
811
+
}{
812
+
{
813
+
name: "simple variable in path",
814
+
template: "[% INCLUDE templates/$lang/header.html %]",
815
+
vars: map[string]any{"lang": "en"},
816
+
want: "English Header",
817
+
},
818
+
{
819
+
name: "different variable value",
820
+
template: "[% INCLUDE templates/$lang/header.html %]",
821
+
vars: map[string]any{"lang": "de"},
822
+
want: "German Header",
823
+
},
824
+
{
825
+
name: "nested variable in path",
826
+
template: "[% INCLUDE templates/users/$user.name/profile.html %]",
827
+
vars: map[string]any{"user": map[string]any{"name": "alice"}},
828
+
want: "Alice's Profile",
829
+
},
830
+
{
831
+
name: "static path still works",
832
+
template: "[% INCLUDE templates/default/page.html %]",
833
+
vars: nil,
834
+
want: "Default Page",
835
+
},
836
+
}
837
+
838
+
for _, tt := range tests {
839
+
t.Run(tt.name, func(t *testing.T) {
840
+
got, err := r.Process(tt.template, tt.vars)
841
+
if err != nil {
842
+
t.Fatalf("Process() error = %v", err)
843
+
}
844
+
if got != tt.want {
845
+
t.Errorf("Process() = %q, want %q", got, tt.want)
846
+
}
847
+
})
848
+
}
849
+
}
850
+
851
+
// TestDynamicWrapperPaths tests variable interpolation in WRAPPER paths
852
+
func TestDynamicWrapperPaths(t *testing.T) {
853
+
memFS := fstest.MapFS{
854
+
"layouts/light/main.html": &fstest.MapFile{Data: []byte("<light>[% content %]</light>")},
855
+
"layouts/dark/main.html": &fstest.MapFile{Data: []byte("<dark>[% content %]</dark>")},
856
+
}
857
+
858
+
r, err := New(&Config{
859
+
IncludePaths: []fs.FS{memFS},
860
+
})
861
+
if err != nil {
862
+
t.Fatalf("New() error = %v", err)
863
+
}
864
+
865
+
tests := []struct {
866
+
name string
867
+
template string
868
+
vars map[string]any
869
+
want string
870
+
}{
871
+
{
872
+
name: "dynamic wrapper with light theme",
873
+
template: "[% WRAPPER layouts/$theme/main.html %]content here[% END %]",
874
+
vars: map[string]any{"theme": "light"},
875
+
want: "<light>content here</light>",
876
+
},
877
+
{
878
+
name: "dynamic wrapper with dark theme",
879
+
template: "[% WRAPPER layouts/$theme/main.html %]content here[% END %]",
880
+
vars: map[string]any{"theme": "dark"},
881
+
want: "<dark>content here</dark>",
882
+
},
883
+
}
884
+
885
+
for _, tt := range tests {
886
+
t.Run(tt.name, func(t *testing.T) {
887
+
got, err := r.Process(tt.template, tt.vars)
888
+
if err != nil {
889
+
t.Fatalf("Process() error = %v", err)
890
+
}
891
+
if got != tt.want {
892
+
t.Errorf("Process() = %q, want %q", got, tt.want)
893
+
}
894
+
})
895
+
}
896
+
}
897
+
898
+
// TestTryCatchWithDynamicPaths tests the combination of TRY/CATCH with dynamic paths
899
+
func TestTryCatchWithDynamicPaths(t *testing.T) {
900
+
memFS := fstest.MapFS{
901
+
"templates/custom/page.html": &fstest.MapFile{Data: []byte("Custom Page")},
902
+
"templates/default/page.html": &fstest.MapFile{Data: []byte("Default Page")},
903
+
}
904
+
905
+
r, err := New(&Config{
906
+
IncludePaths: []fs.FS{memFS},
907
+
})
908
+
if err != nil {
909
+
t.Fatalf("New() error = %v", err)
910
+
}
911
+
912
+
tests := []struct {
913
+
name string
914
+
template string
915
+
vars map[string]any
916
+
want string
917
+
}{
918
+
{
919
+
name: "fallback to default when custom not found",
920
+
template: `[% TRY %][% INCLUDE templates/$category/page.html %][% CATCH %][% INCLUDE templates/default/page.html %][% END %]`,
921
+
vars: map[string]any{"category": "nonexistent"},
922
+
want: "Default Page",
923
+
},
924
+
{
925
+
name: "use custom when it exists",
926
+
template: `[% TRY %][% INCLUDE templates/$category/page.html %][% CATCH %][% INCLUDE templates/default/page.html %][% END %]`,
927
+
vars: map[string]any{"category": "custom"},
928
+
want: "Custom Page",
929
+
},
930
+
}
931
+
932
+
for _, tt := range tests {
933
+
t.Run(tt.name, func(t *testing.T) {
934
+
got, err := r.Process(tt.template, tt.vars)
935
+
if err != nil {
936
+
t.Fatalf("Process() error = %v", err)
937
+
}
938
+
if got != tt.want {
939
+
t.Errorf("Process() = %q, want %q", got, tt.want)
940
+
}
941
+
})
942
+
}
943
+
}
+3
lexer.go
+3
lexer.go
+141
-44
parser.go
+141
-44
parser.go
···
115
115
return p.parseWrapper()
116
116
case TokenSET:
117
117
return p.parseSet()
118
+
case TokenTRY:
119
+
return p.parseTry()
118
120
default:
119
121
// Expression output: [% expr %]
120
122
return p.parseOutput()
···
262
264
}
263
265
264
266
// parseInclude parses an INCLUDE directive
267
+
// Supports both static paths and dynamic paths with $variable interpolation:
268
+
// [% INCLUDE templates/header.html %]
269
+
// [% INCLUDE templates/$category/page.html %]
265
270
func (p *Parser) parseInclude() *IncludeStmt {
266
271
pos := p.token.Pos
267
272
p.expect(TokenINCLUDE)
268
273
269
-
var name string
274
+
name, pathParts := p.parsePath()
275
+
276
+
p.expect(TokenTagClose)
277
+
278
+
return &IncludeStmt{
279
+
Position: pos,
280
+
Name: name,
281
+
PathParts: pathParts,
282
+
}
283
+
}
284
+
285
+
// parsePath parses a path that may contain $variable interpolations.
286
+
// Returns (staticPath, nil) for static paths, or ("", pathParts) for dynamic paths.
287
+
func (p *Parser) parsePath() (string, []PathPart) {
288
+
var pathParts []PathPart
289
+
var staticPath string
290
+
hasDynamic := false
291
+
292
+
// Handle quoted string paths (which can still contain variables in our syntax)
270
293
if p.token.Type == TokenString {
271
-
name = p.token.Value
294
+
// For now, string literals are static-only (could be extended)
295
+
staticPath = p.token.Value
272
296
p.advance()
273
-
} else if p.token.Type == TokenIdent {
274
-
// Could be a simple name or a path like "partials/header"
275
-
name = p.token.Value
276
-
p.advance()
277
-
// Handle path-like includes: partials/header.html
278
-
for p.token.Type == TokenDiv || p.token.Type == TokenDot {
279
-
name += p.token.Value
297
+
return staticPath, nil
298
+
}
299
+
300
+
// Parse path components: identifiers, /, ., and $variables
301
+
for {
302
+
switch p.token.Type {
303
+
case TokenIdent:
304
+
// Literal path segment
305
+
if hasDynamic {
306
+
pathParts = append(pathParts, PathPart{
307
+
IsVariable: false,
308
+
Value: p.token.Value,
309
+
})
310
+
} else {
311
+
staticPath += p.token.Value
312
+
}
313
+
p.advance()
314
+
315
+
case TokenDiv:
316
+
// Path separator /
317
+
if hasDynamic {
318
+
pathParts = append(pathParts, PathPart{
319
+
IsVariable: false,
320
+
Value: "/",
321
+
})
322
+
} else {
323
+
staticPath += "/"
324
+
}
325
+
p.advance()
326
+
327
+
case TokenDot:
328
+
// File extension separator .
329
+
if hasDynamic {
330
+
pathParts = append(pathParts, PathPart{
331
+
IsVariable: false,
332
+
Value: ".",
333
+
})
334
+
} else {
335
+
staticPath += "."
336
+
}
337
+
p.advance()
338
+
339
+
case TokenDollar:
340
+
// Variable interpolation: $varname or $var.name
341
+
hasDynamic = true
342
+
p.advance()
343
+
344
+
// Convert any accumulated static path to pathParts
345
+
if staticPath != "" {
346
+
pathParts = append(pathParts, PathPart{
347
+
IsVariable: false,
348
+
Value: staticPath,
349
+
})
350
+
staticPath = ""
351
+
}
352
+
353
+
// Parse variable name with optional dot notation
354
+
if p.token.Type != TokenIdent {
355
+
p.errorf("expected variable name after $, got %s", p.token.Type)
356
+
return "", pathParts
357
+
}
358
+
359
+
varParts := []string{p.token.Value}
280
360
p.advance()
281
-
if p.token.Type == TokenIdent {
282
-
name += p.token.Value
283
-
p.advance()
361
+
362
+
// Check for dot notation: $user.name.value
363
+
for p.token.Type == TokenDot && p.peekToken.Type == TokenIdent {
364
+
p.advance() // consume dot
365
+
varParts = append(varParts, p.token.Value)
366
+
p.advance() // consume ident
284
367
}
285
-
}
286
-
} else {
287
-
p.errorf("expected include name, got %s", p.token.Type)
288
-
return nil
289
-
}
290
368
291
-
p.expect(TokenTagClose)
369
+
pathParts = append(pathParts, PathPart{
370
+
IsVariable: true,
371
+
Parts: varParts,
372
+
})
292
373
293
-
return &IncludeStmt{
294
-
Position: pos,
295
-
Name: name,
374
+
default:
375
+
// End of path
376
+
if hasDynamic {
377
+
return "", pathParts
378
+
}
379
+
return staticPath, nil
380
+
}
296
381
}
297
382
}
298
383
299
384
// parseWrapper parses a WRAPPER directive
385
+
// Supports both static paths and dynamic paths with $variable interpolation:
386
+
// [% WRAPPER layouts/main.html %]content[% END %]
387
+
// [% WRAPPER layouts/$theme/main.html %]content[% END %]
300
388
func (p *Parser) parseWrapper() *WrapperStmt {
301
389
pos := p.token.Pos
302
390
p.expect(TokenWRAPPER)
303
391
304
-
var name string
305
-
if p.token.Type == TokenString {
306
-
name = p.token.Value
307
-
p.advance()
308
-
} else if p.token.Type == TokenIdent {
309
-
name = p.token.Value
310
-
p.advance()
311
-
// Handle path-like wrappers
312
-
for p.token.Type == TokenDiv || p.token.Type == TokenDot {
313
-
name += p.token.Value
314
-
p.advance()
315
-
if p.token.Type == TokenIdent {
316
-
name += p.token.Value
317
-
p.advance()
318
-
}
319
-
}
320
-
} else {
321
-
p.errorf("expected wrapper name, got %s", p.token.Type)
322
-
return nil
323
-
}
392
+
name, pathParts := p.parsePath()
324
393
325
394
if !p.expect(TokenTagClose) {
326
395
return nil
···
330
399
p.expectEndTag()
331
400
332
401
return &WrapperStmt{
333
-
Position: pos,
334
-
Name: name,
335
-
Content: content,
402
+
Position: pos,
403
+
Name: name,
404
+
PathParts: pathParts,
405
+
Content: content,
336
406
}
337
407
}
338
408
···
360
430
Var: varName,
361
431
Value: value,
362
432
}
433
+
}
434
+
435
+
// parseTry parses a TRY/CATCH block
436
+
func (p *Parser) parseTry() *TryStmt {
437
+
pos := p.token.Pos
438
+
p.expect(TokenTRY)
439
+
p.expect(TokenTagClose)
440
+
441
+
stmt := &TryStmt{
442
+
Position: pos,
443
+
}
444
+
445
+
// Parse try body until CATCH or END
446
+
stmt.Try = p.parseBody(TokenCATCH, TokenEND)
447
+
448
+
// Parse optional CATCH
449
+
if p.token.Type == TokenTagOpen && p.peekToken.Type == TokenCATCH {
450
+
p.expect(TokenTagOpen)
451
+
p.expect(TokenCATCH)
452
+
p.expect(TokenTagClose)
453
+
stmt.Catch = p.parseBody(TokenEND)
454
+
}
455
+
456
+
// Expect END
457
+
p.expectEndTag()
458
+
459
+
return stmt
363
460
}
364
461
365
462
// parseOutput parses an expression output: [% expr %]
+11
token.go
+11
token.go
···
22
22
TokenPipe // |
23
23
TokenComma // ,
24
24
TokenAssign // =
25
+
TokenDollar // $ (variable interpolation in paths)
25
26
26
27
// Operators
27
28
TokenOr // ||
···
50
51
TokenINCLUDE
51
52
TokenWRAPPER
52
53
TokenSET
54
+
TokenTRY
55
+
TokenCATCH
53
56
)
54
57
55
58
// String returns a human-readable name for the token type
···
83
86
return ","
84
87
case TokenAssign:
85
88
return "="
89
+
case TokenDollar:
90
+
return "$"
86
91
case TokenOr:
87
92
return "||"
88
93
case TokenAnd:
···
131
136
return "WRAPPER"
132
137
case TokenSET:
133
138
return "SET"
139
+
case TokenTRY:
140
+
return "TRY"
141
+
case TokenCATCH:
142
+
return "CATCH"
134
143
default:
135
144
return "Unknown"
136
145
}
···
163
172
"INCLUDE": TokenINCLUDE,
164
173
"WRAPPER": TokenWRAPPER,
165
174
"SET": TokenSET,
175
+
"TRY": TokenTRY,
176
+
"CATCH": TokenCATCH,
166
177
}
167
178
168
179
// LookupKeyword returns the token type for an identifier,