A go template renderer based on Perl's Template Toolkit
at main 802 lines 16 kB view raw
1package gott 2 3import ( 4 "strconv" 5) 6 7// Parser parses a token stream into an AST 8type Parser struct { 9 lexer *Lexer 10 token Token // current token 11 peekToken Token // lookahead token 12 errors []error // accumulated parse errors 13} 14 15// NewParser creates a new parser for the given input 16func NewParser(input string) *Parser { 17 p := &Parser{ 18 lexer: NewLexer(input), 19 } 20 // Load first two tokens 21 p.advance() 22 p.advance() 23 return p 24} 25 26// advance moves to the next token 27func (p *Parser) advance() { 28 p.token = p.peekToken 29 p.peekToken = p.lexer.NextToken() 30} 31 32// expect checks that the current token is of the expected type and advances 33func (p *Parser) expect(t TokenType) bool { 34 if p.token.Type != t { 35 p.errorf("expected %s, got %s", t, p.token.Type) 36 return false 37 } 38 p.advance() 39 return true 40} 41 42// errorf records a parse error 43func (p *Parser) errorf(format string, args ...any) { 44 err := &ParseError{ 45 Pos: p.token.Pos, 46 Message: sprintf(format, args...), 47 } 48 p.errors = append(p.errors, err) 49} 50 51// Parse parses the input and returns the AST 52func (p *Parser) Parse() (*Template, []error) { 53 t := &Template{ 54 Position: Position{Line: 1, Column: 1, Offset: 0}, 55 Nodes: []Node{}, 56 } 57 58 for p.token.Type != TokenEOF && p.token.Type != TokenError { 59 node := p.parseNode() 60 if node != nil { 61 t.Nodes = append(t.Nodes, node) 62 } 63 } 64 65 if p.token.Type == TokenError { 66 p.errors = append(p.errors, &ParseError{ 67 Pos: p.token.Pos, 68 Message: p.token.Value, 69 }) 70 } 71 72 return t, p.errors 73} 74 75// parseNode parses a single node (text or tag) 76func (p *Parser) parseNode() Node { 77 switch p.token.Type { 78 case TokenText: 79 return p.parseText() 80 case TokenTagOpen: 81 return p.parseTag() 82 default: 83 p.errorf("unexpected token: %s", p.token.Type) 84 p.advance() 85 return nil 86 } 87} 88 89// parseText parses a text node 90func (p *Parser) parseText() Node { 91 node := &TextNode{ 92 Position: p.token.Pos, 93 Text: p.token.Value, 94 } 95 p.advance() 96 return node 97} 98 99// parseTag parses a [% ... %] tag 100func (p *Parser) parseTag() Node { 101 p.expect(TokenTagOpen) 102 103 switch p.token.Type { 104 case TokenIF: 105 return p.parseIf() 106 case TokenUNLESS: 107 return p.parseUnless() 108 case TokenFOREACH: 109 return p.parseForeach() 110 case TokenBLOCK: 111 return p.parseBlock() 112 case TokenINCLUDE: 113 return p.parseInclude() 114 case TokenWRAPPER: 115 return p.parseWrapper() 116 case TokenSET: 117 return p.parseSet() 118 case TokenTRY: 119 return p.parseTry() 120 default: 121 // Expression output: [% expr %] 122 return p.parseOutput() 123 } 124} 125 126// parseIf parses an IF statement with optional ELSIF and ELSE 127func (p *Parser) parseIf() *IfStmt { 128 pos := p.token.Pos 129 p.expect(TokenIF) 130 131 cond := p.parseExpr() 132 if !p.expect(TokenTagClose) { 133 return nil 134 } 135 136 stmt := &IfStmt{ 137 Position: pos, 138 Condition: cond, 139 } 140 141 // Parse body until ELSIF, ELSE, or END 142 stmt.Body = p.parseBody(TokenELSIF, TokenELSE, TokenEND) 143 144 // Parse ELSIF chain 145 for p.token.Type == TokenTagOpen && p.peekToken.Type == TokenELSIF { 146 p.expect(TokenTagOpen) 147 elsifPos := p.token.Pos 148 p.expect(TokenELSIF) 149 150 elsifCond := p.parseExpr() 151 if !p.expect(TokenTagClose) { 152 return nil 153 } 154 155 elsifBody := p.parseBody(TokenELSIF, TokenELSE, TokenEND) 156 157 stmt.ElsIf = append(stmt.ElsIf, &ElsIfClause{ 158 Position: elsifPos, 159 Condition: elsifCond, 160 Body: elsifBody, 161 }) 162 } 163 164 // Parse optional ELSE 165 if p.token.Type == TokenTagOpen && p.peekToken.Type == TokenELSE { 166 p.expect(TokenTagOpen) 167 p.expect(TokenELSE) 168 p.expect(TokenTagClose) 169 stmt.Else = p.parseBody(TokenEND) 170 } 171 172 // Expect END 173 p.expectEndTag() 174 175 return stmt 176} 177 178// parseUnless parses an UNLESS statement 179func (p *Parser) parseUnless() *UnlessStmt { 180 pos := p.token.Pos 181 p.expect(TokenUNLESS) 182 183 cond := p.parseExpr() 184 if !p.expect(TokenTagClose) { 185 return nil 186 } 187 188 stmt := &UnlessStmt{ 189 Position: pos, 190 Condition: cond, 191 } 192 193 stmt.Body = p.parseBody(TokenELSE, TokenEND) 194 195 // Parse optional ELSE 196 if p.token.Type == TokenTagOpen && p.peekToken.Type == TokenELSE { 197 p.expect(TokenTagOpen) 198 p.expect(TokenELSE) 199 p.expect(TokenTagClose) 200 stmt.Else = p.parseBody(TokenEND) 201 } 202 203 p.expectEndTag() 204 205 return stmt 206} 207 208// parseForeach parses a FOREACH loop 209func (p *Parser) parseForeach() *ForeachStmt { 210 pos := p.token.Pos 211 p.expect(TokenFOREACH) 212 213 if p.token.Type != TokenIdent { 214 p.errorf("expected identifier, got %s", p.token.Type) 215 return nil 216 } 217 itemVar := p.token.Value 218 p.advance() 219 220 if !p.expect(TokenIN) { 221 return nil 222 } 223 224 listExpr := p.parseExpr() 225 if !p.expect(TokenTagClose) { 226 return nil 227 } 228 229 body := p.parseBody(TokenEND) 230 p.expectEndTag() 231 232 return &ForeachStmt{ 233 Position: pos, 234 ItemVar: itemVar, 235 ListExpr: listExpr, 236 Body: body, 237 } 238} 239 240// parseBlock parses a BLOCK definition 241func (p *Parser) parseBlock() *BlockStmt { 242 pos := p.token.Pos 243 p.expect(TokenBLOCK) 244 245 if p.token.Type != TokenIdent { 246 p.errorf("expected block name, got %s", p.token.Type) 247 return nil 248 } 249 name := p.token.Value 250 p.advance() 251 252 if !p.expect(TokenTagClose) { 253 return nil 254 } 255 256 body := p.parseBody(TokenEND) 257 p.expectEndTag() 258 259 return &BlockStmt{ 260 Position: pos, 261 Name: name, 262 Body: body, 263 } 264} 265 266// parseInclude parses an INCLUDE directive 267// Supports both static paths and dynamic paths with $variable interpolation: 268// 269// [% INCLUDE templates/header.html %] 270// [% INCLUDE templates/$category/page.html %] 271func (p *Parser) parseInclude() *IncludeStmt { 272 pos := p.token.Pos 273 p.expect(TokenINCLUDE) 274 275 name, pathParts := p.parsePath() 276 277 p.expect(TokenTagClose) 278 279 return &IncludeStmt{ 280 Position: pos, 281 Name: name, 282 PathParts: pathParts, 283 } 284} 285 286// parsePath parses a path that may contain $variable interpolations. 287// Returns (staticPath, nil) for static paths, or ("", pathParts) for dynamic paths. 288func (p *Parser) parsePath() (string, []PathPart) { 289 var pathParts []PathPart 290 var staticPath string 291 hasDynamic := false 292 293 // Handle quoted string paths (which can still contain variables in our syntax) 294 if p.token.Type == TokenString { 295 // For now, string literals are static-only (could be extended) 296 staticPath = p.token.Value 297 p.advance() 298 return staticPath, nil 299 } 300 301 // Parse path components: identifiers, /, ., and $variables 302 for { 303 switch p.token.Type { 304 case TokenIdent: 305 // Literal path segment 306 if hasDynamic { 307 pathParts = append(pathParts, PathPart{ 308 IsVariable: false, 309 Value: p.token.Value, 310 }) 311 } else { 312 staticPath += p.token.Value 313 } 314 p.advance() 315 316 case TokenDiv: 317 // Path separator / 318 if hasDynamic { 319 pathParts = append(pathParts, PathPart{ 320 IsVariable: false, 321 Value: "/", 322 }) 323 } else { 324 staticPath += "/" 325 } 326 p.advance() 327 328 case TokenDot: 329 // File extension separator . 330 if hasDynamic { 331 pathParts = append(pathParts, PathPart{ 332 IsVariable: false, 333 Value: ".", 334 }) 335 } else { 336 staticPath += "." 337 } 338 p.advance() 339 340 case TokenDollar: 341 // Variable interpolation: $varname or $var.name 342 hasDynamic = true 343 p.advance() 344 345 // Convert any accumulated static path to pathParts 346 if staticPath != "" { 347 pathParts = append(pathParts, PathPart{ 348 IsVariable: false, 349 Value: staticPath, 350 }) 351 staticPath = "" 352 } 353 354 // Parse variable name with optional dot notation 355 if p.token.Type != TokenIdent { 356 p.errorf("expected variable name after $, got %s", p.token.Type) 357 return "", pathParts 358 } 359 360 varParts := []string{p.token.Value} 361 p.advance() 362 363 // Check for dot notation: $user.name.value 364 for p.token.Type == TokenDot && p.peekToken.Type == TokenIdent { 365 p.advance() // consume dot 366 varParts = append(varParts, p.token.Value) 367 p.advance() // consume ident 368 } 369 370 pathParts = append(pathParts, PathPart{ 371 IsVariable: true, 372 Parts: varParts, 373 }) 374 375 default: 376 // End of path 377 if hasDynamic { 378 return "", pathParts 379 } 380 return staticPath, nil 381 } 382 } 383} 384 385// parseWrapper parses a WRAPPER directive 386// Supports both static paths and dynamic paths with $variable interpolation: 387// 388// [% WRAPPER layouts/main.html %]content[% END %] 389// [% WRAPPER layouts/$theme/main.html %]content[% END %] 390func (p *Parser) parseWrapper() *WrapperStmt { 391 pos := p.token.Pos 392 p.expect(TokenWRAPPER) 393 394 name, pathParts := p.parsePath() 395 396 if !p.expect(TokenTagClose) { 397 return nil 398 } 399 400 content := p.parseBody(TokenEND) 401 p.expectEndTag() 402 403 return &WrapperStmt{ 404 Position: pos, 405 Name: name, 406 PathParts: pathParts, 407 Content: content, 408 } 409} 410 411// parseSet parses a SET directive 412func (p *Parser) parseSet() *SetStmt { 413 pos := p.token.Pos 414 p.expect(TokenSET) 415 416 if p.token.Type != TokenIdent { 417 p.errorf("expected variable name, got %s", p.token.Type) 418 return nil 419 } 420 varName := p.token.Value 421 p.advance() 422 423 if !p.expect(TokenAssign) { 424 return nil 425 } 426 427 value := p.parseExpr() 428 p.expect(TokenTagClose) 429 430 return &SetStmt{ 431 Position: pos, 432 Var: varName, 433 Value: value, 434 } 435} 436 437// parseTry parses a TRY/CATCH block 438func (p *Parser) parseTry() *TryStmt { 439 pos := p.token.Pos 440 p.expect(TokenTRY) 441 p.expect(TokenTagClose) 442 443 stmt := &TryStmt{ 444 Position: pos, 445 } 446 447 // Parse try body until CATCH or END 448 stmt.Try = p.parseBody(TokenCATCH, TokenEND) 449 450 // Parse optional CATCH 451 if p.token.Type == TokenTagOpen && p.peekToken.Type == TokenCATCH { 452 p.expect(TokenTagOpen) 453 p.expect(TokenCATCH) 454 p.expect(TokenTagClose) 455 stmt.Catch = p.parseBody(TokenEND) 456 } 457 458 // Expect END 459 p.expectEndTag() 460 461 return stmt 462} 463 464// parseOutput parses an expression output: [% expr %] 465func (p *Parser) parseOutput() *OutputStmt { 466 pos := p.token.Pos 467 expr := p.parseExpr() 468 p.expect(TokenTagClose) 469 470 return &OutputStmt{ 471 Position: pos, 472 Expr: expr, 473 } 474} 475 476// parseBody parses nodes until one of the stop tokens is seen as the next keyword 477func (p *Parser) parseBody(stopTokens ...TokenType) []Node { 478 var nodes []Node 479 480 for { 481 // Check for EOF 482 if p.token.Type == TokenEOF { 483 break 484 } 485 486 // Check if next tag starts with a stop token 487 if p.token.Type == TokenTagOpen { 488 for _, stop := range stopTokens { 489 if p.peekToken.Type == stop { 490 return nodes 491 } 492 } 493 } 494 495 node := p.parseNode() 496 if node != nil { 497 nodes = append(nodes, node) 498 } 499 } 500 501 return nodes 502} 503 504// expectEndTag expects [% END %] 505func (p *Parser) expectEndTag() { 506 if p.token.Type == TokenTagOpen && p.peekToken.Type == TokenEND { 507 p.expect(TokenTagOpen) 508 p.expect(TokenEND) 509 p.expect(TokenTagClose) 510 } else { 511 p.errorf("expected [%% END %%], got %s", p.token.Type) 512 } 513} 514 515// ---- Expression Parsing (with precedence) ---- 516 517// parseExpr is the entry point for expression parsing 518func (p *Parser) parseExpr() Expr { 519 return p.parseOr() 520} 521 522// parseOr handles || (logical OR) and || (default value) 523// When || is followed by a literal and left is an identifier/filter expr, treat as default 524func (p *Parser) parseOr() Expr { 525 left := p.parseAnd() 526 527 for p.token.Type == TokenOr { 528 pos := p.token.Pos 529 p.advance() 530 right := p.parseAnd() 531 532 // Check if this looks like a default value expression: 533 // left is identifier/filter, right is a literal 534 if isDefaultCandidate(left) && isLiteralExpr(right) { 535 left = &DefaultExpr{ 536 Position: pos, 537 Expr: left, 538 Default: right, 539 } 540 } else { 541 left = &BinaryExpr{ 542 Position: pos, 543 Op: TokenOr, 544 Left: left, 545 Right: right, 546 } 547 } 548 } 549 550 return left 551} 552 553// isDefaultCandidate returns true if the expression can have a default value 554func isDefaultCandidate(e Expr) bool { 555 switch e.(type) { 556 case *IdentExpr, *FilterExpr: 557 return true 558 } 559 return false 560} 561 562// isLiteralExpr returns true if the expression is a literal 563func isLiteralExpr(e Expr) bool { 564 _, ok := e.(*LiteralExpr) 565 return ok 566} 567 568// parseAnd handles && (logical AND) 569func (p *Parser) parseAnd() Expr { 570 left := p.parseComparison() 571 572 for p.token.Type == TokenAnd { 573 op := p.token.Type 574 pos := p.token.Pos 575 p.advance() 576 right := p.parseComparison() 577 left = &BinaryExpr{ 578 Position: pos, 579 Op: op, 580 Left: left, 581 Right: right, 582 } 583 } 584 585 return left 586} 587 588// parseComparison handles ==, !=, <, <=, >, >= 589func (p *Parser) parseComparison() Expr { 590 left := p.parseAdditive() 591 592 if isComparisonOp(p.token.Type) { 593 op := p.token.Type 594 pos := p.token.Pos 595 p.advance() 596 right := p.parseAdditive() 597 return &BinaryExpr{ 598 Position: pos, 599 Op: op, 600 Left: left, 601 Right: right, 602 } 603 } 604 605 return left 606} 607 608// parseAdditive handles + and - 609func (p *Parser) parseAdditive() Expr { 610 left := p.parseMultiplicative() 611 612 for p.token.Type == TokenPlus || p.token.Type == TokenMinus { 613 op := p.token.Type 614 pos := p.token.Pos 615 p.advance() 616 right := p.parseMultiplicative() 617 left = &BinaryExpr{ 618 Position: pos, 619 Op: op, 620 Left: left, 621 Right: right, 622 } 623 } 624 625 return left 626} 627 628// parseMultiplicative handles *, /, % 629func (p *Parser) parseMultiplicative() Expr { 630 left := p.parseUnary() 631 632 for p.token.Type == TokenMul || p.token.Type == TokenDiv || p.token.Type == TokenMod { 633 op := p.token.Type 634 pos := p.token.Pos 635 p.advance() 636 right := p.parseUnary() 637 left = &BinaryExpr{ 638 Position: pos, 639 Op: op, 640 Left: left, 641 Right: right, 642 } 643 } 644 645 return left 646} 647 648// parseUnary handles unary - (negation) 649func (p *Parser) parseUnary() Expr { 650 if p.token.Type == TokenMinus { 651 pos := p.token.Pos 652 p.advance() 653 return &UnaryExpr{ 654 Position: pos, 655 Op: TokenMinus, 656 X: p.parseUnary(), 657 } 658 } 659 return p.parsePrimary() 660} 661 662// parsePrimary handles literals, identifiers, function calls, and parentheses 663func (p *Parser) parsePrimary() Expr { 664 switch p.token.Type { 665 case TokenNumber: 666 val, _ := strconv.ParseFloat(p.token.Value, 64) 667 expr := &LiteralExpr{ 668 Position: p.token.Pos, 669 Value: val, 670 } 671 p.advance() 672 return expr 673 674 case TokenString: 675 expr := &LiteralExpr{ 676 Position: p.token.Pos, 677 Value: p.token.Value, 678 } 679 p.advance() 680 return expr 681 682 case TokenIdent: 683 return p.parseIdentOrCall() 684 685 case TokenLParen: 686 p.advance() 687 expr := p.parseExpr() 688 p.expect(TokenRParen) 689 return expr 690 } 691 692 p.errorf("unexpected token in expression: %s", p.token.Type) 693 return &LiteralExpr{Position: p.token.Pos, Value: ""} 694} 695 696// parseIdentOrCall parses an identifier, possibly with dots, function calls, or filters 697func (p *Parser) parseIdentOrCall() Expr { 698 pos := p.token.Pos 699 700 // Collect dot-separated parts: foo.bar.baz 701 parts := []string{p.token.Value} 702 p.advance() 703 704 for p.token.Type == TokenDot { 705 p.advance() 706 if p.token.Type != TokenIdent { 707 p.errorf("expected identifier after '.', got %s", p.token.Type) 708 break 709 } 710 parts = append(parts, p.token.Value) 711 p.advance() 712 } 713 714 var expr Expr = &IdentExpr{ 715 Position: pos, 716 Parts: parts, 717 } 718 719 // Check for function call: func(args) or obj.method(args) 720 if p.token.Type == TokenLParen { 721 p.advance() 722 args := p.parseArgList() 723 p.expect(TokenRParen) 724 725 // If we have multiple parts, this is a method call: obj.method(args) 726 if len(parts) > 1 { 727 methodName := parts[len(parts)-1] 728 receiver := &IdentExpr{ 729 Position: pos, 730 Parts: parts[:len(parts)-1], 731 } 732 expr = &MethodCallExpr{ 733 Position: pos, 734 Receiver: receiver, 735 Method: methodName, 736 Args: args, 737 } 738 } else { 739 // Single part, this is a function call: func(args) 740 expr = &CallExpr{ 741 Position: pos, 742 Func: parts[0], 743 Args: args, 744 } 745 } 746 } 747 748 // Check for filter chain: expr | filter | filter(args) 749 for p.token.Type == TokenPipe { 750 p.advance() 751 if p.token.Type != TokenIdent { 752 p.errorf("expected filter name after '|', got %s", p.token.Type) 753 break 754 } 755 filterName := p.token.Value 756 filterPos := p.token.Pos 757 p.advance() 758 759 var filterArgs []Expr 760 if p.token.Type == TokenLParen { 761 p.advance() 762 filterArgs = p.parseArgList() 763 p.expect(TokenRParen) 764 } 765 766 expr = &FilterExpr{ 767 Position: filterPos, 768 Input: expr, 769 Filter: filterName, 770 Args: filterArgs, 771 } 772 } 773 774 return expr 775} 776 777// parseArgList parses a comma-separated list of expressions 778func (p *Parser) parseArgList() []Expr { 779 var args []Expr 780 781 if p.token.Type == TokenRParen { 782 return args 783 } 784 785 args = append(args, p.parseExpr()) 786 787 for p.token.Type == TokenComma { 788 p.advance() 789 args = append(args, p.parseExpr()) 790 } 791 792 return args 793} 794 795// isComparisonOp returns true if the token is a comparison operator 796func isComparisonOp(t TokenType) bool { 797 switch t { 798 case TokenEq, TokenNe, TokenLt, TokenLe, TokenGt, TokenGe: 799 return true 800 } 801 return false 802}