A go template renderer based on Perl's Template Toolkit
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}