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// [% 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}