A go template renderer based on Perl's Template Toolkit

feat: add dynamic template paths and TRY/CATCH blocks

- Add variable interpolation in INCLUDE/WRAPPER paths using $variable
syntax (e.g., [% INCLUDE templates/$category/page.html %])
- Support dot notation in path variables (e.g., $user.id)
- Add TRY/CATCH blocks for error handling with fallback content
- Register new AST types (TryStmt, PathPart) with gob for disk caching

+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
··· 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
··· 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
··· 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
··· 281 281 case '=': 282 282 l.emit(TokenAssign) 283 283 return lexInsideTag 284 + case '$': 285 + l.emit(TokenDollar) 286 + return lexInsideTag 284 287 case '"', '\'': 285 288 l.backup() 286 289 return lexString
+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
··· 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,