go-twig/parser.go
semihalev f9b283c393 Add template compilation capabilities
- Implement a compiled template format using gob encoding
- Add methods to compile templates and load from compiled templates
- Create dedicated CompiledLoader for managing compiled templates
- Enable auto-reload support for compiled templates
- Add comprehensive tests including benchmarks
- Create example application for template compilation workflow
- Update documentation with compilation features and examples

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-10 09:21:20 +03:00

1814 lines
53 KiB
Go

package twig
import (
"fmt"
"strconv"
"strings"
)
// Token types
const (
TOKEN_TEXT = iota
TOKEN_VAR_START // {{
TOKEN_VAR_END // }}
TOKEN_BLOCK_START // {%
TOKEN_BLOCK_END // %}
TOKEN_COMMENT_START // {#
TOKEN_COMMENT_END // #}
TOKEN_NAME
TOKEN_NUMBER
TOKEN_STRING
TOKEN_OPERATOR
TOKEN_PUNCTUATION
TOKEN_EOF
// Whitespace control token types
TOKEN_VAR_START_TRIM // {{-
TOKEN_VAR_END_TRIM // -}}
TOKEN_BLOCK_START_TRIM // {%-
TOKEN_BLOCK_END_TRIM // -%}
)
// Parser handles parsing Twig templates into node trees
type Parser struct {
source string
tokens []Token
tokenIndex int
filename string
cursor int
line int
blockHandlers map[string]blockHandlerFunc
}
type blockHandlerFunc func(*Parser) (Node, error)
// Token represents a lexical token
type Token struct {
Type int
Value string
Line int
}
// Parse parses a template source into a node tree
func (p *Parser) Parse(source string) (Node, error) {
p.source = source
p.cursor = 0
p.line = 1
p.tokenIndex = 0
// Initialize default block handlers
p.initBlockHandlers()
// Tokenize source
var err error
p.tokens, err = p.tokenize()
if err != nil {
return nil, err
}
// Debug tokenization output
/*
fmt.Println("Tokenized template:")
for i, t := range p.tokens {
fmt.Printf("Token %d: Type=%d, Value=%q, Line=%d\n", i, t.Type, t.Value, t.Line)
}
*/
// Apply whitespace control processing to the tokens to handle
// the whitespace trimming between template elements
p.tokens = processWhitespaceControl(p.tokens)
// Parse tokens into nodes
nodes, err := p.parseOuterTemplate()
if err != nil {
return nil, err
}
return NewRootNode(nodes, 1), nil
}
// Initialize block handlers for different tag types
func (p *Parser) initBlockHandlers() {
p.blockHandlers = map[string]blockHandlerFunc{
"if": p.parseIf,
"for": p.parseFor,
"block": p.parseBlock,
"extends": p.parseExtends,
"include": p.parseInclude,
"set": p.parseSet,
"do": p.parseDo,
"macro": p.parseMacro,
"import": p.parseImport,
"from": p.parseFrom,
"spaceless": p.parseSpaceless,
// Special closing tags - they will be handled in their corresponding open tag parsers
"endif": p.parseEndTag,
"endfor": p.parseEndTag,
"endmacro": p.parseEndTag,
"endblock": p.parseEndTag,
"endspaceless": p.parseEndTag,
"else": p.parseEndTag,
"elseif": p.parseEndTag,
}
}
// Tokenize the source into a list of tokens
func (p *Parser) tokenize() ([]Token, error) {
var tokens []Token
for p.cursor < len(p.source) {
// Check for variable syntax with whitespace control {{ }} or {{- -}}
if p.matchString("{{-") {
tokens = append(tokens, Token{Type: TOKEN_VAR_START_TRIM, Line: p.line})
p.cursor += 3
// Skip whitespace after opening braces
for p.cursor < len(p.source) && isWhitespace(p.current()) {
if p.current() == '\n' {
p.line++
}
p.cursor++
}
continue
} else if p.matchString("{{") {
tokens = append(tokens, Token{Type: TOKEN_VAR_START, Line: p.line})
p.cursor += 2
// Skip whitespace after opening braces
for p.cursor < len(p.source) && isWhitespace(p.current()) {
if p.current() == '\n' {
p.line++
}
p.cursor++
}
continue
}
if p.matchString("-}}") {
tokens = append(tokens, Token{Type: TOKEN_VAR_END_TRIM, Line: p.line})
p.cursor += 3
continue
} else if p.matchString("}}") {
tokens = append(tokens, Token{Type: TOKEN_VAR_END, Line: p.line})
p.cursor += 2
continue
}
// Check for block syntax with whitespace control {% %} or {%- -%}
if p.matchString("{%-") {
tokens = append(tokens, Token{Type: TOKEN_BLOCK_START_TRIM, Line: p.line})
p.cursor += 3
// Skip whitespace after opening braces
for p.cursor < len(p.source) && isWhitespace(p.current()) {
if p.current() == '\n' {
p.line++
}
p.cursor++
}
continue
} else if p.matchString("{%") {
tokens = append(tokens, Token{Type: TOKEN_BLOCK_START, Line: p.line})
p.cursor += 2
// Skip whitespace after opening braces
for p.cursor < len(p.source) && isWhitespace(p.current()) {
if p.current() == '\n' {
p.line++
}
p.cursor++
}
continue
}
if p.matchString("-%}") {
tokens = append(tokens, Token{Type: TOKEN_BLOCK_END_TRIM, Line: p.line})
p.cursor += 3
continue
} else if p.matchString("%}") {
tokens = append(tokens, Token{Type: TOKEN_BLOCK_END, Line: p.line})
p.cursor += 2
continue
}
// Check for comment syntax {# #}
if p.matchString("{#") {
tokens = append(tokens, Token{Type: TOKEN_COMMENT_START, Line: p.line})
p.cursor += 2
// Skip whitespace after opening braces
for p.cursor < len(p.source) && isWhitespace(p.current()) {
if p.current() == '\n' {
p.line++
}
p.cursor++
}
continue
}
if p.matchString("#}") {
tokens = append(tokens, Token{Type: TOKEN_COMMENT_END, Line: p.line})
p.cursor += 2
continue
}
// Check for string literals
if p.current() == '"' || p.current() == '\'' {
quote := p.current()
p.cursor++
start := p.cursor
var inEmbeddedVar bool = false
for p.cursor < len(p.source) && (p.current() != quote || inEmbeddedVar) {
// Handle embedded Twig syntax like {{ }}
if p.cursor+1 < len(p.source) && p.current() == '{' && (p.source[p.cursor+1] == '{' || p.source[p.cursor+1] == '%') {
inEmbeddedVar = true
}
// Check for end of embedded variable
if inEmbeddedVar && p.cursor+1 < len(p.source) && p.current() == '}' && (p.source[p.cursor+1] == '}' || p.source[p.cursor+1] == '%') {
p.cursor += 2 // Skip the closing brackets
inEmbeddedVar = false
continue
}
// Skip escaped quote characters
if p.current() == '\\' && p.cursor+1 < len(p.source) {
// Skip the backslash and the next character (which might be a quote)
p.cursor += 2
continue
}
if p.current() == '\n' {
p.line++
}
p.cursor++
}
if p.cursor >= len(p.source) {
return nil, fmt.Errorf("unterminated string at line %d", p.line)
}
value := p.source[start:p.cursor]
tokens = append(tokens, Token{Type: TOKEN_STRING, Value: value, Line: p.line})
p.cursor++
continue
}
// Check for numbers
if isDigit(p.current()) {
start := p.cursor
for p.cursor < len(p.source) && (isDigit(p.current()) || p.current() == '.') {
p.cursor++
}
value := p.source[start:p.cursor]
tokens = append(tokens, Token{Type: TOKEN_NUMBER, Value: value, Line: p.line})
continue
}
// Check for identifiers/names
if isAlpha(p.current()) {
start := p.cursor
for p.cursor < len(p.source) && isAlphaNumeric(p.current()) {
p.cursor++
}
value := p.source[start:p.cursor]
tokens = append(tokens, Token{Type: TOKEN_NAME, Value: value, Line: p.line})
continue
}
// Check for operators
if isOperator(p.current()) {
start := p.cursor
for p.cursor < len(p.source) && isOperator(p.current()) {
p.cursor++
}
value := p.source[start:p.cursor]
tokens = append(tokens, Token{Type: TOKEN_OPERATOR, Value: value, Line: p.line})
continue
}
// Check for punctuation
if isPunctuation(p.current()) {
tokens = append(tokens, Token{
Type: TOKEN_PUNCTUATION,
Value: string(p.current()),
Line: p.line,
})
p.cursor++
continue
}
// Check for whitespace and newlines
if isWhitespace(p.current()) {
if p.current() == '\n' {
p.line++
}
p.cursor++
continue
}
// Handle plain text
start := p.cursor
for p.cursor < len(p.source) &&
!p.matchString("{{-") && !p.matchString("{{") &&
!p.matchString("-}}") && !p.matchString("}}") &&
!p.matchString("{%-") && !p.matchString("{%") &&
!p.matchString("-%}") && !p.matchString("%}") &&
!p.matchString("{#") && !p.matchString("#}") {
if p.current() == '\n' {
p.line++
}
p.cursor++
}
if start != p.cursor {
value := p.source[start:p.cursor]
tokens = append(tokens, Token{Type: TOKEN_TEXT, Value: value, Line: p.line})
}
}
tokens = append(tokens, Token{Type: TOKEN_EOF, Line: p.line})
return tokens, nil
}
// Helper methods for tokenization
func (p *Parser) current() byte {
if p.cursor >= len(p.source) {
return 0
}
return p.source[p.cursor]
}
// Helper function to check if a token is any kind of block end token (regular or trim variant)
func isBlockEndToken(tokenType int) bool {
return tokenType == TOKEN_BLOCK_END || tokenType == TOKEN_BLOCK_END_TRIM
}
// Helper function to check if a token is any kind of variable end token (regular or trim variant)
func isVarEndToken(tokenType int) bool {
return tokenType == TOKEN_VAR_END || tokenType == TOKEN_VAR_END_TRIM
}
func (p *Parser) matchString(s string) bool {
if p.cursor+len(s) > len(p.source) {
return false
}
return p.source[p.cursor:p.cursor+len(s)] == s
}
func isDigit(c byte) bool {
return c >= '0' && c <= '9'
}
func isAlpha(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
}
func isAlphaNumeric(c byte) bool {
return isAlpha(c) || isDigit(c)
}
func isOperator(c byte) bool {
return strings.ContainsRune("+-*/=<>!&~^%?:", rune(c))
}
func isPunctuation(c byte) bool {
return strings.ContainsRune("()[]{},.:|", rune(c))
}
func isWhitespace(c byte) bool {
return c == ' ' || c == '\t' || c == '\n' || c == '\r'
}
// processEscapeSequences handles escape sequences in string literals
func processEscapeSequences(s string) string {
var result strings.Builder
result.Grow(len(s))
for i := 0; i < len(s); i++ {
if s[i] == '\\' && i+1 < len(s) {
i++
switch s[i] {
case 'n':
result.WriteByte('\n')
case 'r':
result.WriteByte('\r')
case 't':
result.WriteByte('\t')
case '\\':
result.WriteByte('\\')
case '"':
result.WriteByte('"')
case '\'':
result.WriteByte('\'')
default:
result.WriteByte(s[i])
}
} else {
result.WriteByte(s[i])
}
}
return result.String()
}
// Replace HTML attributes like type="{{ type }}" with actual Twig variables in HTML
func fixHTMLAttributes(input string) string {
// Search for patterns like: type="{{ type }}" or name="{{ name }}"
for i := 0; i < len(input); i++ {
// Find potential attribute patterns
attrStart := strings.Index(input[i:], "=\"{{")
if attrStart == -1 {
break // No more attributes with embedded variables
}
attrStart += i // Adjust to full string position
// Find the end of the attribute value
attrEnd := strings.Index(input[attrStart+3:], "}}\"")
if attrEnd == -1 {
break // No closing variable
}
attrEnd += attrStart + 3 // Adjust to full string position
// Extract the variable name (between {{ and }})
varName := strings.TrimSpace(input[attrStart+3 : attrEnd])
// Replace the attribute string with an empty string for now
// We'll need to handle this specially in the parsing logic
input = input[:attrStart] + "=" + varName + input[attrEnd+2:]
}
return input
}
// Parse the outer level of a template (text, print tags, blocks)
func (p *Parser) parseOuterTemplate() ([]Node, error) {
var nodes []Node
for p.tokenIndex < len(p.tokens) && p.tokens[p.tokenIndex].Type != TOKEN_EOF {
token := p.tokens[p.tokenIndex]
switch token.Type {
case TOKEN_TEXT:
nodes = append(nodes, NewTextNode(token.Value, token.Line))
p.tokenIndex++
case TOKEN_VAR_START, TOKEN_VAR_START_TRIM:
// Handle both normal and whitespace trimming var start tokens
p.tokenIndex++
expr, err := p.parseExpression()
if err != nil {
return nil, err
}
nodes = append(nodes, NewPrintNode(expr, token.Line))
// Check for either normal or whitespace trimming var end tokens
if p.tokenIndex >= len(p.tokens) || !isVarEndToken(p.tokens[p.tokenIndex].Type) {
return nil, fmt.Errorf("expected }} or -}} at line %d", token.Line)
}
p.tokenIndex++
case TOKEN_BLOCK_START, TOKEN_BLOCK_START_TRIM:
// Handle both normal and whitespace trimming block start tokens
p.tokenIndex++
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected block name at line %d", token.Line)
}
blockName := p.tokens[p.tokenIndex].Value
p.tokenIndex++
// Check if this is a control ending tag (endif, endfor, endblock, etc.)
if blockName == "endif" || blockName == "endfor" || blockName == "endblock" ||
blockName == "endmacro" || blockName == "else" || blockName == "elseif" ||
blockName == "endspaceless" {
// We should return to the parent parser that's handling the parent block
// First move back two steps to the start of the block tag
p.tokenIndex -= 2
return nodes, nil
}
// Check if we have a handler for this block type
handler, ok := p.blockHandlers[blockName]
if !ok {
return nil, fmt.Errorf("unknown block type '%s' at line %d", blockName, token.Line)
}
node, err := handler(p)
if err != nil {
return nil, err
}
nodes = append(nodes, node)
case TOKEN_COMMENT_START:
// Skip comments
p.tokenIndex++
startLine := token.Line
// Find the end of the comment
for p.tokenIndex < len(p.tokens) && p.tokens[p.tokenIndex].Type != TOKEN_COMMENT_END {
p.tokenIndex++
}
if p.tokenIndex >= len(p.tokens) {
return nil, fmt.Errorf("unclosed comment starting at line %d", startLine)
}
p.tokenIndex++
// Add special handling for trim token types
case TOKEN_VAR_END_TRIM, TOKEN_BLOCK_END_TRIM:
// These should have been handled with their corresponding start tokens
return nil, fmt.Errorf("unexpected token %v at line %d", token.Type, token.Line)
// Add special handling for TOKEN_NAME outside of a tag
case TOKEN_NAME, TOKEN_PUNCTUATION, TOKEN_OPERATOR, TOKEN_STRING, TOKEN_NUMBER:
// For raw names, punctuation, operators, and literals not inside tags, convert to text
// In many languages, the text "true" is a literal boolean, but in our parser it's just a name token
// outside of an expression context
// Special handling for text content words - add spaces between consecutive text tokens
// This fixes issues with the spaceless tag's handling of text content
if token.Type == TOKEN_NAME && p.tokenIndex+1 < len(p.tokens) &&
p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
p.tokens[p.tokenIndex+1].Line == token.Line {
// Look ahead for consecutive name tokens and join them with spaces
var textContent strings.Builder
textContent.WriteString(token.Value)
currentLine := token.Line
p.tokenIndex++ // Skip the first token as we've already added it
// Collect consecutive name tokens on the same line
for p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_NAME &&
p.tokens[p.tokenIndex].Line == currentLine {
textContent.WriteString(" ") // Add space between words
textContent.WriteString(p.tokens[p.tokenIndex].Value)
p.tokenIndex++
}
nodes = append(nodes, NewTextNode(textContent.String(), token.Line))
} else {
// Regular handling for single text tokens
nodes = append(nodes, NewTextNode(token.Value, token.Line))
p.tokenIndex++
}
default:
return nil, fmt.Errorf("unexpected token %v at line %d", token.Type, token.Line)
}
}
return nodes, nil
}
// Parse an expression
func (p *Parser) parseExpression() (Node, error) {
// Parse the primary expression first
expr, err := p.parseSimpleExpression()
if err != nil {
return nil, err
}
// Now check for filter operator (|)
if p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "|" {
expr, err = p.parseFilters(expr)
if err != nil {
return nil, err
}
}
// Check for binary operators (and, or, ==, !=, <, >, etc.)
if p.tokenIndex < len(p.tokens) &&
(p.tokens[p.tokenIndex].Type == TOKEN_OPERATOR ||
(p.tokens[p.tokenIndex].Type == TOKEN_NAME &&
(p.tokens[p.tokenIndex].Value == "and" ||
p.tokens[p.tokenIndex].Value == "or" ||
p.tokens[p.tokenIndex].Value == "in" ||
p.tokens[p.tokenIndex].Value == "not" ||
p.tokens[p.tokenIndex].Value == "is" ||
p.tokens[p.tokenIndex].Value == "matches" ||
p.tokens[p.tokenIndex].Value == "starts" ||
p.tokens[p.tokenIndex].Value == "ends"))) {
expr, err = p.parseBinaryExpression(expr)
if err != nil {
return nil, err
}
}
// Check for ternary operator (? :)
if p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "?" {
return p.parseConditionalExpression(expr)
}
return expr, nil
}
// Parse ternary conditional expression (condition ? true_expr : false_expr)
func (p *Parser) parseConditionalExpression(condition Node) (Node, error) {
line := p.tokens[p.tokenIndex].Line
// Skip the "?" token
p.tokenIndex++
// Parse the "true" expression
trueExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
// Expect ":" token
if p.tokenIndex >= len(p.tokens) ||
p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != ":" {
return nil, fmt.Errorf("expected ':' after true expression in conditional at line %d", line)
}
p.tokenIndex++ // Skip ":"
// Parse the "false" expression
falseExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
// Create a conditional node
return &ConditionalNode{
ExpressionNode: ExpressionNode{
exprType: ExprConditional,
line: line,
},
condition: condition,
trueExpr: trueExpr,
falseExpr: falseExpr,
}, nil
}
// Parse a simple expression (literal, variable, function call, array)
func (p *Parser) parseSimpleExpression() (Node, error) {
if p.tokenIndex >= len(p.tokens) {
return nil, fmt.Errorf("unexpected end of template")
}
token := p.tokens[p.tokenIndex]
switch token.Type {
case TOKEN_STRING:
p.tokenIndex++
// For string literals, process escape sequences
processedValue := processEscapeSequences(token.Value)
return NewLiteralNode(processedValue, token.Line), nil
case TOKEN_NUMBER:
p.tokenIndex++
// Attempt to convert to int or float
if strings.Contains(token.Value, ".") {
// It's a float
val, _ := strconv.ParseFloat(token.Value, 64)
return NewLiteralNode(val, token.Line), nil
} else {
// It's an int
val, _ := strconv.Atoi(token.Value)
return NewLiteralNode(val, token.Line), nil
}
case TOKEN_NAME:
p.tokenIndex++
// Store the variable name for function calls
varName := token.Value
varLine := token.Line
// Special handling for boolean literals and null
if varName == "true" {
return NewLiteralNode(true, varLine), nil
} else if varName == "false" {
return NewLiteralNode(false, varLine), nil
} else if varName == "null" || varName == "nil" {
return NewLiteralNode(nil, varLine), nil
}
// Check if this is a function call (name followed by opening parenthesis)
if p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "(" {
// This is a function call
p.tokenIndex++ // Skip the opening parenthesis
// Parse arguments list
var args []Node
// If there are arguments (not empty parentheses)
if p.tokenIndex < len(p.tokens) &&
!(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == ")") {
for {
// Parse each argument expression
argExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
args = append(args, argExpr)
// Check for comma separator between arguments
if p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "," {
p.tokenIndex++ // Skip comma
continue
}
// No comma, so must be end of argument list
break
}
}
// Expect closing parenthesis
if p.tokenIndex >= len(p.tokens) ||
p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != ")" {
return nil, fmt.Errorf("expected closing parenthesis after function arguments at line %d", varLine)
}
p.tokenIndex++ // Skip closing parenthesis
// Create and return function node
return NewFunctionNode(varName, args, varLine), nil
}
// If not a function call, it's a regular variable
var result Node = NewVariableNode(varName, varLine)
// Check for attribute access (obj.attr)
for p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "." {
p.tokenIndex++
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected attribute name at line %d", varLine)
}
attrName := p.tokens[p.tokenIndex].Value
attrNode := NewLiteralNode(attrName, p.tokens[p.tokenIndex].Line)
result = NewGetAttrNode(result, attrNode, varLine)
p.tokenIndex++
}
return result, nil
case TOKEN_PUNCTUATION:
// Handle array literals [1, 2, 3]
if token.Value == "[" {
return p.parseArrayExpression()
}
// Handle parenthesized expressions
if token.Value == "(" {
p.tokenIndex++ // Skip "("
expr, err := p.parseExpression()
if err != nil {
return nil, err
}
// Expect closing parenthesis
if p.tokenIndex >= len(p.tokens) ||
p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != ")" {
return nil, fmt.Errorf("expected closing parenthesis at line %d", token.Line)
}
p.tokenIndex++ // Skip ")"
return expr, nil
}
default:
return nil, fmt.Errorf("unexpected token in expression at line %d", token.Line)
}
return nil, fmt.Errorf("unexpected token in expression at line %d", token.Line)
}
// Parse array expression [item1, item2, ...]
func (p *Parser) parseArrayExpression() (Node, error) {
// Save the line number for error reporting
line := p.tokens[p.tokenIndex].Line
// Skip the opening bracket
p.tokenIndex++
// Parse the array items
var items []Node
// Check if there are any items
if p.tokenIndex < len(p.tokens) &&
!(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "]") {
for {
// Parse each item expression
itemExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
items = append(items, itemExpr)
// Check for comma separator between items
if p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "," {
p.tokenIndex++ // Skip comma
continue
}
// No comma, so must be end of array
break
}
}
// Expect closing bracket
if p.tokenIndex >= len(p.tokens) ||
p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != "]" {
return nil, fmt.Errorf("expected closing bracket after array items at line %d", line)
}
p.tokenIndex++ // Skip closing bracket
// Create array node
return &ArrayNode{
ExpressionNode: ExpressionNode{
exprType: ExprArray,
line: line,
},
items: items,
}, nil
}
// Parse filter expressions: variable|filter(args)
func (p *Parser) parseFilters(node Node) (Node, error) {
line := p.tokens[p.tokenIndex].Line
// Loop to handle multiple filters (e.g. var|filter1|filter2)
for p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "|" {
p.tokenIndex++ // Skip the | token
// Expect filter name
if p.tokenIndex >= len(p.tokens) || p.tokens[p.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected filter name at line %d", line)
}
filterName := p.tokens[p.tokenIndex].Value
p.tokenIndex++
// Check for filter arguments
var args []Node
// If there are arguments in parentheses
if p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "(" {
p.tokenIndex++ // Skip opening parenthesis
// Parse arguments
if p.tokenIndex < len(p.tokens) &&
!(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == ")") {
for {
// Parse each argument expression
argExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
args = append(args, argExpr)
// Check for comma separator
if p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "," {
p.tokenIndex++ // Skip comma
continue
}
// No comma, so end of argument list
break
}
}
// Expect closing parenthesis
if p.tokenIndex >= len(p.tokens) ||
p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != ")" {
return nil, fmt.Errorf("expected closing parenthesis after filter arguments at line %d", line)
}
p.tokenIndex++ // Skip closing parenthesis
}
// Create a new FilterNode
node = &FilterNode{
ExpressionNode: ExpressionNode{
exprType: ExprFilter,
line: line,
},
node: node,
filter: filterName,
args: args,
}
}
return node, nil
}
// Parse binary expressions (a + b, a and b, a in b, etc.)
func (p *Parser) parseBinaryExpression(left Node) (Node, error) {
token := p.tokens[p.tokenIndex]
operator := token.Value
line := token.Line
// Process multi-word operators
if token.Type == TOKEN_NAME {
// Handle 'not in' operator
if token.Value == "not" && p.tokenIndex+1 < len(p.tokens) &&
p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
p.tokens[p.tokenIndex+1].Value == "in" {
operator = "not in"
p.tokenIndex += 2 // Skip both 'not' and 'in'
} else if token.Value == "is" && p.tokenIndex+1 < len(p.tokens) &&
p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
p.tokens[p.tokenIndex+1].Value == "not" {
// Handle 'is not' operator
operator = "is not"
p.tokenIndex += 2 // Skip both 'is' and 'not'
} else if token.Value == "starts" && p.tokenIndex+1 < len(p.tokens) &&
p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
p.tokens[p.tokenIndex+1].Value == "with" {
// Handle 'starts with' operator
operator = "starts with"
p.tokenIndex += 2 // Skip both 'starts' and 'with'
} else if token.Value == "ends" && p.tokenIndex+1 < len(p.tokens) &&
p.tokens[p.tokenIndex+1].Type == TOKEN_NAME &&
p.tokens[p.tokenIndex+1].Value == "with" {
// Handle 'ends with' operator
operator = "ends with"
p.tokenIndex += 2 // Skip both 'ends' and 'with'
} else {
// Single word operators like 'is', 'and', 'or', 'in', 'matches'
p.tokenIndex++ // Skip the operator token
}
} else {
// Regular operators like +, -, *, /, etc.
p.tokenIndex++ // Skip the operator token
}
// Handle 'is' followed by a test
if operator == "is" || operator == "is not" {
// Check if this is a test
if p.tokenIndex < len(p.tokens) && p.tokens[p.tokenIndex].Type == TOKEN_NAME {
testName := p.tokens[p.tokenIndex].Value
p.tokenIndex++ // Skip the test name
// Parse test arguments if any
var args []Node
// If there's an opening parenthesis, parse arguments
if p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "(" {
p.tokenIndex++ // Skip opening parenthesis
// Parse arguments
if p.tokenIndex < len(p.tokens) &&
!(p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == ")") {
for {
// Parse each argument expression
argExpr, err := p.parseExpression()
if err != nil {
return nil, err
}
args = append(args, argExpr)
// Check for comma separator
if p.tokenIndex < len(p.tokens) &&
p.tokens[p.tokenIndex].Type == TOKEN_PUNCTUATION &&
p.tokens[p.tokenIndex].Value == "," {
p.tokenIndex++ // Skip comma
continue
}
// No comma, so end of argument list
break
}
}
// Expect closing parenthesis
if p.tokenIndex >= len(p.tokens) ||
p.tokens[p.tokenIndex].Type != TOKEN_PUNCTUATION ||
p.tokens[p.tokenIndex].Value != ")" {
return nil, fmt.Errorf("expected closing parenthesis after test arguments at line %d", line)
}
p.tokenIndex++ // Skip closing parenthesis
}
// Create the test node
test := &TestNode{
ExpressionNode: ExpressionNode{
exprType: ExprTest,
line: line,
},
node: left,
test: testName,
args: args,
}
// If it's a negated test (is not), create a unary 'not' node
if operator == "is not" {
return &UnaryNode{
ExpressionNode: ExpressionNode{
exprType: ExprUnary,
line: line,
},
operator: "not",
node: test,
}, nil
}
return test, nil
}
}
// If we get here, we have a regular binary operator
// For regular binary operators, parse the right operand
right, err := p.parseExpression()
if err != nil {
return nil, err
}
return NewBinaryNode(operator, left, right, line), nil
}
// Parse if statement
func (p *Parser) parseIf(parser *Parser) (Node, error) {
// Get the line number of the if token
ifLine := parser.tokens[parser.tokenIndex-2].Line
// Parse the condition expression
condition, err := parser.parseExpression()
if err != nil {
return nil, err
}
// Expect the block end token (either regular or whitespace-trimming variant)
if parser.tokenIndex >= len(parser.tokens) ||
(parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END &&
parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
return nil, fmt.Errorf("expected block end after if condition at line %d", ifLine)
}
parser.tokenIndex++
// Parse the if body (statements between if and endif/else)
ifBody, err := parser.parseOuterTemplate()
if err != nil {
return nil, err
}
var elseBody []Node
// Check for else or endif
if parser.tokenIndex < len(parser.tokens) && parser.tokens[parser.tokenIndex].Type == TOKEN_BLOCK_START {
parser.tokenIndex++
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected block name at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
// Check if this is an else block
if parser.tokens[parser.tokenIndex].Value == "else" {
parser.tokenIndex++
// Expect the block end token
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end after else at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
parser.tokenIndex++
// Parse the else body
elseBody, err = parser.parseOuterTemplate()
if err != nil {
return nil, err
}
// Now expect the endif
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_START {
return nil, fmt.Errorf("expected endif block at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
parser.tokenIndex++
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected endif at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
if parser.tokens[parser.tokenIndex].Value != "endif" {
return nil, fmt.Errorf("expected endif, got %s at line %d", parser.tokens[parser.tokenIndex].Value, parser.tokens[parser.tokenIndex].Line)
}
parser.tokenIndex++
} else if parser.tokens[parser.tokenIndex].Value == "endif" {
parser.tokenIndex++
} else {
return nil, fmt.Errorf("expected else or endif, got %s at line %d", parser.tokens[parser.tokenIndex].Value, parser.tokens[parser.tokenIndex].Line)
}
// Expect the final block end token (either regular or trim variant)
if parser.tokenIndex >= len(parser.tokens) || !isBlockEndToken(parser.tokens[parser.tokenIndex].Type) {
return nil, fmt.Errorf("expected block end after endif at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
parser.tokenIndex++
} else {
return nil, fmt.Errorf("unexpected end of template, expected endif at line %d", ifLine)
}
// Create the if node
ifNode := &IfNode{
conditions: []Node{condition},
bodies: [][]Node{ifBody},
elseBranch: elseBody,
line: ifLine,
}
return ifNode, nil
}
func (p *Parser) parseFor(parser *Parser) (Node, error) {
// Get the line number of the for token
forLine := parser.tokens[parser.tokenIndex-2].Line
// Parse the loop variable name(s)
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected variable name after for at line %d", forLine)
}
// Get value variable name
valueVar := parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
var keyVar string
// Check for key, value syntax
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_PUNCTUATION &&
parser.tokens[parser.tokenIndex].Value == "," {
// Move past the comma
parser.tokenIndex++
// Now valueVar is actually the key, and we need to get the value
keyVar = valueVar
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected value variable name after comma at line %d", forLine)
}
valueVar = parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
}
// Expect 'in' keyword
if parser.tokenIndex >= len(parser.tokens) ||
parser.tokens[parser.tokenIndex].Type != TOKEN_NAME ||
parser.tokens[parser.tokenIndex].Value != "in" {
return nil, fmt.Errorf("expected 'in' keyword after variable name at line %d", forLine)
}
parser.tokenIndex++
// Parse the sequence expression
sequence, err := parser.parseExpression()
if err != nil {
return nil, err
}
// Expect the block end token (either regular or trim variant)
if parser.tokenIndex >= len(parser.tokens) || !isBlockEndToken(parser.tokens[parser.tokenIndex].Type) {
return nil, fmt.Errorf("expected block end after for statement at line %d", forLine)
}
parser.tokenIndex++
// Parse the for loop body
loopBody, err := parser.parseOuterTemplate()
if err != nil {
return nil, err
}
var elseBody []Node
// Check for else or endfor
if parser.tokenIndex < len(parser.tokens) && parser.tokens[parser.tokenIndex].Type == TOKEN_BLOCK_START {
parser.tokenIndex++
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected block name at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
// Check if this is an else block
if parser.tokens[parser.tokenIndex].Value == "else" {
parser.tokenIndex++
// Expect the block end token
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end after else at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
parser.tokenIndex++
// Parse the else body
elseBody, err = parser.parseOuterTemplate()
if err != nil {
return nil, err
}
// Now expect the endfor
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_START {
return nil, fmt.Errorf("expected endfor block at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
parser.tokenIndex++
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected endfor at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
if parser.tokens[parser.tokenIndex].Value != "endfor" {
return nil, fmt.Errorf("expected endfor, got %s at line %d", parser.tokens[parser.tokenIndex].Value, parser.tokens[parser.tokenIndex].Line)
}
parser.tokenIndex++
} else if parser.tokens[parser.tokenIndex].Value == "endfor" {
parser.tokenIndex++
} else {
return nil, fmt.Errorf("expected else or endfor, got %s at line %d", parser.tokens[parser.tokenIndex].Value, parser.tokens[parser.tokenIndex].Line)
}
// Expect the final block end token
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end after endfor at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
parser.tokenIndex++
} else {
return nil, fmt.Errorf("unexpected end of template, expected endfor at line %d", forLine)
}
// Create the for node
forNode := &ForNode{
keyVar: keyVar,
valueVar: valueVar,
sequence: sequence,
body: loopBody,
elseBranch: elseBody,
line: forLine,
}
return forNode, nil
}
func (p *Parser) parseBlock(parser *Parser) (Node, error) {
// Get the line number of the block token
blockLine := parser.tokens[parser.tokenIndex-2].Line
// Get the block name
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected block name at line %d", blockLine)
}
blockName := parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
// Expect the block end token
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end token after block name at line %d", blockLine)
}
parser.tokenIndex++
// Parse the block body
blockBody, err := parser.parseOuterTemplate()
if err != nil {
return nil, err
}
// Expect endblock tag
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_START {
return nil, fmt.Errorf("expected endblock tag at line %d", blockLine)
}
parser.tokenIndex++
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME ||
parser.tokens[parser.tokenIndex].Value != "endblock" {
return nil, fmt.Errorf("expected endblock at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
parser.tokenIndex++
// Check for optional block name in endblock
if parser.tokenIndex < len(parser.tokens) && parser.tokens[parser.tokenIndex].Type == TOKEN_NAME {
endBlockName := parser.tokens[parser.tokenIndex].Value
if endBlockName != blockName {
return nil, fmt.Errorf("mismatched block name, expected %s but got %s at line %d",
blockName, endBlockName, parser.tokens[parser.tokenIndex].Line)
}
parser.tokenIndex++
}
// Expect the final block end token
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end token after endblock at line %d", parser.tokens[parser.tokenIndex-1].Line)
}
parser.tokenIndex++
// Create the block node
blockNode := &BlockNode{
name: blockName,
body: blockBody,
line: blockLine,
}
return blockNode, nil
}
func (p *Parser) parseExtends(parser *Parser) (Node, error) {
// Get the line number of the extends token
extendsLine := parser.tokens[parser.tokenIndex-2].Line
// Get the parent template expression
parentExpr, err := parser.parseExpression()
if err != nil {
return nil, err
}
// Expect the block end token
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end token after extends at line %d", extendsLine)
}
parser.tokenIndex++
// Create the extends node
extendsNode := &ExtendsNode{
parent: parentExpr,
line: extendsLine,
}
return extendsNode, nil
}
func (p *Parser) parseInclude(parser *Parser) (Node, error) {
// Get the line number of the include token
includeLine := parser.tokens[parser.tokenIndex-2].Line
// Get the template expression
templateExpr, err := parser.parseExpression()
if err != nil {
return nil, err
}
// Check for optional parameters
var variables map[string]Node
var ignoreMissing bool
var onlyContext bool
// Look for 'with', 'ignore missing', or 'only'
for parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_NAME {
keyword := parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
switch keyword {
case "with":
// Parse variables as a hash
if variables == nil {
variables = make(map[string]Node)
}
// Parse the variable assignments
for parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_NAME {
// Get the variable name
varName := parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
// Expect '='
if parser.tokenIndex >= len(parser.tokens) ||
parser.tokens[parser.tokenIndex].Type != TOKEN_OPERATOR ||
parser.tokens[parser.tokenIndex].Value != "=" {
return nil, fmt.Errorf("expected '=' after variable name at line %d", includeLine)
}
parser.tokenIndex++
// Parse the value expression
varExpr, err := parser.parseExpression()
if err != nil {
return nil, err
}
// Add to variables map
variables[varName] = varExpr
// If there's a comma, skip it
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_PUNCTUATION &&
parser.tokens[parser.tokenIndex].Value == "," {
parser.tokenIndex++
} else {
break
}
}
case "ignore":
// Check for 'missing' keyword
if parser.tokenIndex >= len(parser.tokens) ||
parser.tokens[parser.tokenIndex].Type != TOKEN_NAME ||
parser.tokens[parser.tokenIndex].Value != "missing" {
return nil, fmt.Errorf("expected 'missing' after 'ignore' at line %d", includeLine)
}
parser.tokenIndex++
ignoreMissing = true
case "only":
onlyContext = true
default:
return nil, fmt.Errorf("unexpected keyword '%s' in include at line %d", keyword, includeLine)
}
}
// Expect the block end token
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end token after include at line %d", includeLine)
}
parser.tokenIndex++
// Create the include node
includeNode := &IncludeNode{
template: templateExpr,
variables: variables,
ignoreMissing: ignoreMissing,
only: onlyContext,
line: includeLine,
}
return includeNode, nil
}
func (p *Parser) parseSet(parser *Parser) (Node, error) {
// Get the line number of the set token
setLine := parser.tokens[parser.tokenIndex-2].Line
// Get the variable name
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected variable name after set at line %d", setLine)
}
varName := parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
// Expect '='
if parser.tokenIndex >= len(parser.tokens) ||
parser.tokens[parser.tokenIndex].Type != TOKEN_OPERATOR ||
parser.tokens[parser.tokenIndex].Value != "=" {
return nil, fmt.Errorf("expected '=' after variable name at line %d", setLine)
}
parser.tokenIndex++
// Parse the value expression
valueExpr, err := parser.parseExpression()
if err != nil {
return nil, err
}
// For expressions like 5 + 10, we need to parse both sides and make a binary node
// Check if there's an operator after the first token
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_OPERATOR &&
parser.tokens[parser.tokenIndex].Value != "=" {
// Get the operator
operator := parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
// Parse the right side
rightExpr, err := parser.parseExpression()
if err != nil {
return nil, err
}
// Create a binary node
valueExpr = NewBinaryNode(operator, valueExpr, rightExpr, setLine)
}
// Expect the block end token
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end token after set expression at line %d", setLine)
}
parser.tokenIndex++
// Create the set node
setNode := NewSetNode(varName, valueExpr, setLine)
return setNode, nil
}
func (p *Parser) parseDo(parser *Parser) (Node, error) {
// Placeholder for do parsing
return nil, fmt.Errorf("do not implemented yet")
}
func (p *Parser) parseMacro(parser *Parser) (Node, error) {
// Get the line number of the macro token
macroLine := parser.tokens[parser.tokenIndex-2].Line
// Get the macro name
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected macro name after macro keyword at line %d", macroLine)
}
macroName := parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
// Expect opening parenthesis for parameters
if parser.tokenIndex >= len(parser.tokens) ||
parser.tokens[parser.tokenIndex].Type != TOKEN_PUNCTUATION ||
parser.tokens[parser.tokenIndex].Value != "(" {
return nil, fmt.Errorf("expected '(' after macro name at line %d", macroLine)
}
parser.tokenIndex++
// Parse parameters
var params []string
defaults := make(map[string]Node)
// If we don't have a closing parenthesis immediately, we have parameters
if parser.tokenIndex < len(parser.tokens) &&
(parser.tokens[parser.tokenIndex].Type != TOKEN_PUNCTUATION ||
parser.tokens[parser.tokenIndex].Value != ")") {
for {
// Get parameter name
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected parameter name at line %d", macroLine)
}
paramName := parser.tokens[parser.tokenIndex].Value
params = append(params, paramName)
parser.tokenIndex++
// Check for default value
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_OPERATOR &&
parser.tokens[parser.tokenIndex].Value == "=" {
parser.tokenIndex++ // Skip =
// Parse default value expression
defaultExpr, err := parser.parseExpression()
if err != nil {
return nil, err
}
defaults[paramName] = defaultExpr
}
// Check if we have more parameters
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_PUNCTUATION &&
parser.tokens[parser.tokenIndex].Value == "," {
parser.tokenIndex++ // Skip comma
continue
}
break
}
}
// Expect closing parenthesis
if parser.tokenIndex >= len(parser.tokens) ||
parser.tokens[parser.tokenIndex].Type != TOKEN_PUNCTUATION ||
parser.tokens[parser.tokenIndex].Value != ")" {
return nil, fmt.Errorf("expected ')' after macro parameters at line %d", macroLine)
}
parser.tokenIndex++
// Expect block end
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end token after macro declaration at line %d", macroLine)
}
parser.tokenIndex++
// Parse the macro body
bodyNodes, err := parser.parseOuterTemplate()
if err != nil {
return nil, err
}
// Expect endmacro tag
if parser.tokenIndex+1 >= len(parser.tokens) ||
parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_START ||
parser.tokens[parser.tokenIndex+1].Type != TOKEN_NAME ||
parser.tokens[parser.tokenIndex+1].Value != "endmacro" {
return nil, fmt.Errorf("missing endmacro tag for macro '%s' at line %d",
macroName, macroLine)
}
// Skip {% endmacro %}
parser.tokenIndex += 2 // Skip {% endmacro
// Expect block end
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end token after endmacro at line %d", parser.tokens[parser.tokenIndex].Line)
}
parser.tokenIndex++
// Create the macro node
return NewMacroNode(macroName, params, defaults, bodyNodes, macroLine), nil
}
func (p *Parser) parseImport(parser *Parser) (Node, error) {
// Get the line number of the import token
importLine := parser.tokens[parser.tokenIndex-2].Line
// Get the template to import
templateExpr, err := parser.parseExpression()
if err != nil {
return nil, err
}
// Expect 'as' keyword
if parser.tokenIndex >= len(parser.tokens) ||
parser.tokens[parser.tokenIndex].Type != TOKEN_NAME ||
parser.tokens[parser.tokenIndex].Value != "as" {
return nil, fmt.Errorf("expected 'as' after template path at line %d", importLine)
}
parser.tokenIndex++
// Get the alias name
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected identifier after 'as' at line %d", importLine)
}
alias := parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
// Expect block end
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end token after import statement at line %d", importLine)
}
parser.tokenIndex++
// Create import node
return NewImportNode(templateExpr, alias, importLine), nil
}
func (p *Parser) parseFrom(parser *Parser) (Node, error) {
// Get the line number of the from token
fromLine := parser.tokens[parser.tokenIndex-2].Line
// Get the template to import from
templateExpr, err := parser.parseExpression()
if err != nil {
return nil, err
}
// Expect 'import' keyword
if parser.tokenIndex >= len(parser.tokens) ||
parser.tokens[parser.tokenIndex].Type != TOKEN_NAME ||
parser.tokens[parser.tokenIndex].Value != "import" {
return nil, fmt.Errorf("expected 'import' after template path at line %d", fromLine)
}
parser.tokenIndex++
// Parse the imported items
var macros []string
aliases := make(map[string]string)
// We need at least one macro to import
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected at least one identifier after 'import' at line %d", fromLine)
}
for parser.tokenIndex < len(parser.tokens) && parser.tokens[parser.tokenIndex].Type == TOKEN_NAME {
// Get macro name
macroName := parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
// Check for 'as' keyword for aliasing
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_NAME &&
parser.tokens[parser.tokenIndex].Value == "as" {
parser.tokenIndex++ // Skip 'as'
// Get alias name
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected identifier after 'as' at line %d", fromLine)
}
aliasName := parser.tokens[parser.tokenIndex].Value
aliases[macroName] = aliasName
parser.tokenIndex++
} else {
// No alias, just add to macros list
macros = append(macros, macroName)
}
// Check for comma to separate items
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_PUNCTUATION &&
parser.tokens[parser.tokenIndex].Value == "," {
parser.tokenIndex++ // Skip comma
// Expect another identifier after comma
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME {
return nil, fmt.Errorf("expected identifier after ',' at line %d", fromLine)
}
} else {
// End of imports
break
}
}
// Expect block end
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expected block end token after from import statement at line %d", fromLine)
}
parser.tokenIndex++
// Create from import node
return NewFromImportNode(templateExpr, macros, aliases, fromLine), nil
}
// parseEndTag handles closing tags like endif, endfor, endblock, etc.
// These tags should only be encountered inside their respective block parsing methods,
// so if we reach here directly, it's an error.
func (p *Parser) parseEndTag(parser *Parser) (Node, error) {
// Get the line number and tag name
tagLine := parser.tokens[parser.tokenIndex-2].Line
tagName := parser.tokens[parser.tokenIndex-1].Value
return nil, fmt.Errorf("unexpected '%s' tag at line %d", tagName, tagLine)
}
// parseSpaceless parses a spaceless block
func (p *Parser) parseSpaceless(parser *Parser) (Node, error) {
// Get the line number of the spaceless token
spacelessLine := parser.tokens[parser.tokenIndex-2].Line
// Expect the block end token
if parser.tokenIndex >= len(parser.tokens) ||
(parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END &&
parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
return nil, fmt.Errorf("expected block end token after spaceless at line %d", spacelessLine)
}
parser.tokenIndex++
// Parse the spaceless body
spacelessBody, err := parser.parseOuterTemplate()
if err != nil {
return nil, err
}
// Expect endspaceless tag
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_START {
return nil, fmt.Errorf("expected endspaceless tag at line %d", spacelessLine)
}
parser.tokenIndex++
// Expect the endspaceless token
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_NAME ||
parser.tokens[parser.tokenIndex].Value != "endspaceless" {
return nil, fmt.Errorf("expected endspaceless token at line %d", parser.tokens[parser.tokenIndex].Line)
}
parser.tokenIndex++
// Expect the block end token
if parser.tokenIndex >= len(parser.tokens) ||
(parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END &&
parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
return nil, fmt.Errorf("expected block end token after endspaceless at line %d", parser.tokens[parser.tokenIndex].Line)
}
parser.tokenIndex++
// Create and return the spaceless node
return NewSpacelessNode(spacelessBody, spacelessLine), nil
}