A go template renderer based on Perl's Template Toolkit
at main 19 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// [% INCLUDE templates/header.html %] 269// [% INCLUDE templates/$category/page.html %] 270func (p *Parser) parseInclude() *IncludeStmt { 271 pos := p.token.Pos 272 p.expect(TokenINCLUDE) 273 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. 287func (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) 293 if p.token.Type == TokenString { 294 // For now, string literals are static-only (could be extended) 295 staticPath = p.token.Value 296 p.advance() 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} 360 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 367 } 368 369 pathParts = append(pathParts, PathPart{ 370 IsVariable: true, 371 Parts: varParts, 372 }) 373 374 default: 375 // End of path 376 if hasDynamic { 377 return "", pathParts 378 } 379 return staticPath, nil 380 } 381 } 382} 383 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 %] 388func (p *Parser) parseWrapper() *WrapperStmt { 389 pos := p.token.Pos 390 p.expect(TokenWRAPPER) 391 392 name, pathParts := p.parsePath() 393 394 if !p.expect(TokenTagClose) { 395 return nil 396 } 397 398 content := p.parseBody(TokenEND) 399 p.expectEndTag() 400 401 return &WrapperStmt{ 402 Position: pos, 403 Name: name, 404 PathParts: pathParts, 405 Content: content, 406 } 407} 408 409// parseSet parses a SET directive 410func (p *Parser) parseSet() *SetStmt { 411 pos := p.token.Pos 412 p.expect(TokenSET) 413 414 if p.token.Type != TokenIdent { 415 p.errorf("expected variable name, got %s", p.token.Type) 416 return nil 417 } 418 varName := p.token.Value 419 p.advance() 420 421 if !p.expect(TokenAssign) { 422 return nil 423 } 424 425 value := p.parseExpr() 426 p.expect(TokenTagClose) 427 428 return &SetStmt{ 429 Position: pos, 430 Var: varName, 431 Value: value, 432 } 433} 434 435// parseTry parses a TRY/CATCH block 436func (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 460} 461 462// parseOutput parses an expression output: [% expr %] 463func (p *Parser) parseOutput() *OutputStmt { 464 pos := p.token.Pos 465 expr := p.parseExpr() 466 p.expect(TokenTagClose) 467 468 return &OutputStmt{ 469 Position: pos, 470 Expr: expr, 471 } 472} 473 474// parseBody parses nodes until one of the stop tokens is seen as the next keyword 475func (p *Parser) parseBody(stopTokens ...TokenType) []Node { 476 var nodes []Node 477 478 for { 479 // Check for EOF 480 if p.token.Type == TokenEOF { 481 break 482 } 483 484 // Check if next tag starts with a stop token 485 if p.token.Type == TokenTagOpen { 486 for _, stop := range stopTokens { 487 if p.peekToken.Type == stop { 488 return nodes 489 } 490 } 491 } 492 493 node := p.parseNode() 494 if node != nil { 495 nodes = append(nodes, node) 496 } 497 } 498 499 return nodes 500} 501 502// expectEndTag expects [% END %] 503func (p *Parser) expectEndTag() { 504 if p.token.Type == TokenTagOpen && p.peekToken.Type == TokenEND { 505 p.expect(TokenTagOpen) 506 p.expect(TokenEND) 507 p.expect(TokenTagClose) 508 } else { 509 p.errorf("expected [%% END %%], got %s", p.token.Type) 510 } 511} 512 513// ---- Expression Parsing (with precedence) ---- 514 515// parseExpr is the entry point for expression parsing 516func (p *Parser) parseExpr() Expr { 517 return p.parseOr() 518} 519 520// parseOr handles || (logical OR) and || (default value) 521// When || is followed by a literal and left is an identifier/filter expr, treat as default 522func (p *Parser) parseOr() Expr { 523 left := p.parseAnd() 524 525 for p.token.Type == TokenOr { 526 pos := p.token.Pos 527 p.advance() 528 right := p.parseAnd() 529 530 // Check if this looks like a default value expression: 531 // left is identifier/filter, right is a literal 532 if isDefaultCandidate(left) && isLiteralExpr(right) { 533 left = &DefaultExpr{ 534 Position: pos, 535 Expr: left, 536 Default: right, 537 } 538 } else { 539 left = &BinaryExpr{ 540 Position: pos, 541 Op: TokenOr, 542 Left: left, 543 Right: right, 544 } 545 } 546 } 547 548 return left 549} 550 551// isDefaultCandidate returns true if the expression can have a default value 552func isDefaultCandidate(e Expr) bool { 553 switch e.(type) { 554 case *IdentExpr, *FilterExpr: 555 return true 556 } 557 return false 558} 559 560// isLiteralExpr returns true if the expression is a literal 561func isLiteralExpr(e Expr) bool { 562 _, ok := e.(*LiteralExpr) 563 return ok 564} 565 566// parseAnd handles && (logical AND) 567func (p *Parser) parseAnd() Expr { 568 left := p.parseComparison() 569 570 for p.token.Type == TokenAnd { 571 op := p.token.Type 572 pos := p.token.Pos 573 p.advance() 574 right := p.parseComparison() 575 left = &BinaryExpr{ 576 Position: pos, 577 Op: op, 578 Left: left, 579 Right: right, 580 } 581 } 582 583 return left 584} 585 586// parseComparison handles ==, !=, <, <=, >, >= 587func (p *Parser) parseComparison() Expr { 588 left := p.parseAdditive() 589 590 if isComparisonOp(p.token.Type) { 591 op := p.token.Type 592 pos := p.token.Pos 593 p.advance() 594 right := p.parseAdditive() 595 return &BinaryExpr{ 596 Position: pos, 597 Op: op, 598 Left: left, 599 Right: right, 600 } 601 } 602 603 return left 604} 605 606// parseAdditive handles + and - 607func (p *Parser) parseAdditive() Expr { 608 left := p.parseMultiplicative() 609 610 for p.token.Type == TokenPlus || p.token.Type == TokenMinus { 611 op := p.token.Type 612 pos := p.token.Pos 613 p.advance() 614 right := p.parseMultiplicative() 615 left = &BinaryExpr{ 616 Position: pos, 617 Op: op, 618 Left: left, 619 Right: right, 620 } 621 } 622 623 return left 624} 625 626// parseMultiplicative handles *, /, % 627func (p *Parser) parseMultiplicative() Expr { 628 left := p.parseUnary() 629 630 for p.token.Type == TokenMul || p.token.Type == TokenDiv || p.token.Type == TokenMod { 631 op := p.token.Type 632 pos := p.token.Pos 633 p.advance() 634 right := p.parseUnary() 635 left = &BinaryExpr{ 636 Position: pos, 637 Op: op, 638 Left: left, 639 Right: right, 640 } 641 } 642 643 return left 644} 645 646// parseUnary handles unary - (negation) 647func (p *Parser) parseUnary() Expr { 648 if p.token.Type == TokenMinus { 649 pos := p.token.Pos 650 p.advance() 651 return &UnaryExpr{ 652 Position: pos, 653 Op: TokenMinus, 654 X: p.parseUnary(), 655 } 656 } 657 return p.parsePrimary() 658} 659 660// parsePrimary handles literals, identifiers, function calls, and parentheses 661func (p *Parser) parsePrimary() Expr { 662 switch p.token.Type { 663 case TokenNumber: 664 val, _ := strconv.ParseFloat(p.token.Value, 64) 665 expr := &LiteralExpr{ 666 Position: p.token.Pos, 667 Value: val, 668 } 669 p.advance() 670 return expr 671 672 case TokenString: 673 expr := &LiteralExpr{ 674 Position: p.token.Pos, 675 Value: p.token.Value, 676 } 677 p.advance() 678 return expr 679 680 case TokenIdent: 681 return p.parseIdentOrCall() 682 683 case TokenLParen: 684 p.advance() 685 expr := p.parseExpr() 686 p.expect(TokenRParen) 687 return expr 688 } 689 690 p.errorf("unexpected token in expression: %s", p.token.Type) 691 return &LiteralExpr{Position: p.token.Pos, Value: ""} 692} 693 694// parseIdentOrCall parses an identifier, possibly with dots, function calls, or filters 695func (p *Parser) parseIdentOrCall() Expr { 696 pos := p.token.Pos 697 698 // Collect dot-separated parts: foo.bar.baz 699 parts := []string{p.token.Value} 700 p.advance() 701 702 for p.token.Type == TokenDot { 703 p.advance() 704 if p.token.Type != TokenIdent { 705 p.errorf("expected identifier after '.', got %s", p.token.Type) 706 break 707 } 708 parts = append(parts, p.token.Value) 709 p.advance() 710 } 711 712 var expr Expr = &IdentExpr{ 713 Position: pos, 714 Parts: parts, 715 } 716 717 // Check for function call: func(args) 718 if p.token.Type == TokenLParen && len(parts) == 1 { 719 p.advance() 720 args := p.parseArgList() 721 p.expect(TokenRParen) 722 expr = &CallExpr{ 723 Position: pos, 724 Func: parts[0], 725 Args: args, 726 } 727 } 728 729 // Check for filter chain: expr | filter | filter(args) 730 for p.token.Type == TokenPipe { 731 p.advance() 732 if p.token.Type != TokenIdent { 733 p.errorf("expected filter name after '|', got %s", p.token.Type) 734 break 735 } 736 filterName := p.token.Value 737 filterPos := p.token.Pos 738 p.advance() 739 740 var filterArgs []Expr 741 if p.token.Type == TokenLParen { 742 p.advance() 743 filterArgs = p.parseArgList() 744 p.expect(TokenRParen) 745 } 746 747 expr = &FilterExpr{ 748 Position: filterPos, 749 Input: expr, 750 Filter: filterName, 751 Args: filterArgs, 752 } 753 } 754 755 return expr 756} 757 758// parseArgList parses a comma-separated list of expressions 759func (p *Parser) parseArgList() []Expr { 760 var args []Expr 761 762 if p.token.Type == TokenRParen { 763 return args 764 } 765 766 args = append(args, p.parseExpr()) 767 768 for p.token.Type == TokenComma { 769 p.advance() 770 args = append(args, p.parseExpr()) 771 } 772 773 return args 774} 775 776// isComparisonOp returns true if the token is a comparison operator 777func isComparisonOp(t TokenType) bool { 778 switch t { 779 case TokenEq, TokenNe, TokenLt, TokenLe, TokenGt, TokenGe: 780 return true 781 } 782 return false 783}