Split parser functions into separate files and add from tag tests

- Move parsing functions for tags into dedicated files
- Add comprehensive tests for the from tag
- Fix the implementation of parseFrom to correctly handle imports
- Improve test coverage for macros and imports

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
semihalev 2025-03-11 16:52:42 +03:00
commit ec37652bc1
14 changed files with 1675 additions and 1460 deletions

194
from_tag_test.go Normal file
View file

@ -0,0 +1,194 @@
package twig
import (
"strings"
"testing"
)
// TestFromTagBasic tests the most basic from tag use case
func TestFromTagBasic(t *testing.T) {
engine := New()
// Macro library template with a simple macro
macroLib := `{% macro hello(name) %}Hello, {{ name }}!{% endmacro %}`
// Simple template using the from tag
mainTemplate := `{% from "macros.twig" import hello %}
{{ hello('World') }}`
// Register templates
err := engine.RegisterString("macros.twig", macroLib)
if err != nil {
t.Fatalf("Error registering macros.twig: %v", err)
}
err = engine.RegisterString("main.twig", mainTemplate)
if err != nil {
t.Fatalf("Error registering main.twig: %v", err)
}
// Render the template
result, err := engine.Render("main.twig", nil)
if err != nil {
t.Fatalf("Error rendering template: %v", err)
}
// Check the output
expected := "Hello, World!"
if !strings.Contains(result, expected) {
t.Errorf("Expected %q in result, but got: %s", expected, result)
}
}
// TestFromTagWithAlias tests the from tag with an alias
func TestFromTagWithAlias(t *testing.T) {
engine := New()
// Macro library template
macroLib := `{% macro greet(name) %}Hello, {{ name }}!{% endmacro %}
{% macro farewell(name) %}Goodbye, {{ name }}!{% endmacro %}`
// Template using from import with aliases
template := `{% from "macros.twig" import greet as hello, farewell as bye %}
{{ hello('John') }}
{{ bye('Jane') }}`
// Register templates
err := engine.RegisterString("macros.twig", macroLib)
if err != nil {
t.Fatalf("Error registering macros.twig: %v", err)
}
err = engine.RegisterString("template.twig", template)
if err != nil {
t.Fatalf("Error registering template.twig: %v", err)
}
// Render the template
result, err := engine.Render("template.twig", nil)
if err != nil {
t.Fatalf("Error rendering template: %v", err)
}
// Check the output
expectedHello := "Hello, John!"
expectedBye := "Goodbye, Jane!"
if !strings.Contains(result, expectedHello) {
t.Errorf("Expected %q in result, but got: %s", expectedHello, result)
}
if !strings.Contains(result, expectedBye) {
t.Errorf("Expected %q in result, but got: %s", expectedBye, result)
}
}
// TestFromTagMultipleImports tests importing multiple macros from a template
func TestFromTagMultipleImports(t *testing.T) {
engine := New()
// Macro library template with multiple macros
macroLib := `{% macro input(name, value) %}
<input name="{{ name }}" value="{{ value }}">
{% endmacro %}
{% macro label(text) %}
<label>{{ text }}</label>
{% endmacro %}
{% macro button(text) %}
<button>{{ text }}</button>
{% endmacro %}`
// Template importing multiple macros
template := `{% from "form_macros.twig" import input, label, button %}
<form>
{{ label('Username') }}
{{ input('username', 'john') }}
{{ button('Submit') }}
</form>`
// Register templates
err := engine.RegisterString("form_macros.twig", macroLib)
if err != nil {
t.Fatalf("Error registering form_macros.twig: %v", err)
}
err = engine.RegisterString("form.twig", template)
if err != nil {
t.Fatalf("Error registering form.twig: %v", err)
}
// Render the template
result, err := engine.Render("form.twig", nil)
if err != nil {
t.Fatalf("Error rendering template: %v", err)
}
// Check the output
expectedElements := []string{
`<label>Username</label>`,
`<input name="username" value="john">`,
`<button>Submit</button>`,
}
for _, expected := range expectedElements {
if !strings.Contains(result, expected) {
t.Errorf("Expected %q in result, but got: %s", expected, result)
}
}
}
// TestFromTagMixedAliases tests importing some macros with aliases and some without
func TestFromTagMixedAliases(t *testing.T) {
engine := New()
// Macro library template
macroLib := `{% macro header(text) %}
<h1>{{ text }}</h1>
{% endmacro %}
{% macro paragraph(text) %}
<p>{{ text }}</p>
{% endmacro %}
{% macro link(href, text) %}
<a href="{{ href }}">{{ text }}</a>
{% endmacro %}`
// Template with mixed alias usage
template := `{% from "content_macros.twig" import header, paragraph as p, link as a %}
{{ header('Title') }}
{{ p('This is a paragraph.') }}
{{ a('#', 'Click here') }}`
// Register templates
err := engine.RegisterString("content_macros.twig", macroLib)
if err != nil {
t.Fatalf("Error registering content_macros.twig: %v", err)
}
err = engine.RegisterString("content.twig", template)
if err != nil {
t.Fatalf("Error registering content.twig: %v", err)
}
// Render the template
result, err := engine.Render("content.twig", nil)
if err != nil {
t.Fatalf("Error rendering template: %v", err)
}
// Check the output
expectedElements := []string{
`<h1>Title</h1>`,
`<p>This is a paragraph.</p>`,
`<a href="#">Click here</a>`,
}
for _, expected := range expectedElements {
if !strings.Contains(result, expected) {
t.Errorf("Expected %q in result, but got: %s", expected, result)
}
}
}

View file

@ -125,7 +125,66 @@ func TestMacrosImport(t *testing.T) {
}
}
// TestMacrosFromImport tests selective importing macros
// TestMacrosImportAs tests importing macros using the import as syntax
func TestMacrosImportAs(t *testing.T) {
engine := New()
// Macro library template
macroLib := `
{% macro input(name, value = '', type = 'text') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{% macro textarea(name, value = '') %}
<textarea name="{{ name }}">{{ value }}</textarea>
{% endmacro %}
{% macro button(name, value) %}
<button name="{{ name }}">{{ value }}</button>
{% endmacro %}
`
// Main template that imports macros using import as syntax
mainTemplate := `
{% import "macro_lib.twig" as lib %}
<form>
{{ lib.input('username', 'john') }}
{{ lib.button('submit', 'Submit Form') }}
</form>
`
// Register both templates
err := engine.RegisterString("macro_lib.twig", macroLib)
if err != nil {
t.Fatalf("Error registering macro_lib.twig: %v", err)
}
err = engine.RegisterString("import_as.twig", mainTemplate)
if err != nil {
t.Fatalf("Error registering import_as.twig: %v", err)
}
// Render the main template
result, err := engine.Render("import_as.twig", nil)
if err != nil {
t.Fatalf("Error parsing/rendering template: %v", err)
}
// Check the output
expectedHtml := []string{
`<input type="text" name="username" value="john">`,
`<button name="submit">Submit Form</button>`,
}
for _, expected := range expectedHtml {
if !strings.Contains(result, expected) {
t.Errorf("Expected %q in result, but got: %s", expected, result)
}
}
}
// TestMacrosFromImport tests selective importing macros using the from import syntax
func TestMacrosFromImport(t *testing.T) {
engine := New()
@ -144,20 +203,25 @@ func TestMacrosFromImport(t *testing.T) {
{% endmacro %}
`
// Main template that selectively imports macros
// Using import as syntax which has better support
mainTemplate := `
{% import "macro_lib.twig" as lib %}
// Main template that selectively imports macros using from import syntax
mainTemplate := `{% from "macro_lib.twig" import input, button %}
<form>
{{ lib.input('username', 'john') }}
{{ lib.button('submit', 'Submit Form') }}
{{ input('username', 'john') }}
{{ button('submit', 'Submit Form') }}
</form>
`
// Register both templates
engine.RegisterString("macro_lib.twig", macroLib)
engine.RegisterString("from_import.twig", mainTemplate)
err := engine.RegisterString("macro_lib.twig", macroLib)
if err != nil {
t.Fatalf("Error registering macro_lib.twig: %v", err)
}
err = engine.RegisterString("from_import.twig", mainTemplate)
if err != nil {
t.Fatalf("Error registering from_import.twig: %v", err)
}
// Render the main template
result, err := engine.Render("from_import.twig", nil)
@ -178,6 +242,137 @@ func TestMacrosFromImport(t *testing.T) {
}
}
// TestMacrosFromImportWithAliases tests importing macros with aliases using the from import syntax
func TestMacrosFromImportWithAliases(t *testing.T) {
engine := New()
// Macro library template
macroLib := `
{% macro input(name, value = '', type = 'text') %}
<input type="{{ type }}" name="{{ name }}" value="{{ value }}">
{% endmacro %}
{% macro textarea(name, value = '') %}
<textarea name="{{ name }}">{{ value }}</textarea>
{% endmacro %}
{% macro button(name, value) %}
<button name="{{ name }}">{{ value }}</button>
{% endmacro %}
`
// Main template that imports macros with aliases using from import syntax
mainTemplate := `{% from "macro_lib.twig" import input as field, button as btn %}
<form>
{{ field('username', 'john') }}
{{ btn('submit', 'Submit Form') }}
</form>
`
// Register both templates
err := engine.RegisterString("macro_lib.twig", macroLib)
if err != nil {
t.Fatalf("Error registering macro_lib.twig: %v", err)
}
err = engine.RegisterString("from_import_aliases.twig", mainTemplate)
if err != nil {
t.Fatalf("Error registering from_import_aliases.twig: %v", err)
}
// Render the main template
result, err := engine.Render("from_import_aliases.twig", nil)
if err != nil {
t.Fatalf("Error parsing/rendering template: %v", err)
}
// Check the output
expectedHtml := []string{
`<input type="text" name="username" value="john">`,
`<button name="submit">Submit Form</button>`,
}
for _, expected := range expectedHtml {
if !strings.Contains(result, expected) {
t.Errorf("Expected %q in result, but got: %s", expected, result)
}
}
}
// TestMixedImportApproaches tests using both import and from import syntax in the same template
func TestMixedImportApproaches(t *testing.T) {
engine := New()
// First macro library template
formsMacroLib := `
{% macro input(name, value = '') %}
<input name="{{ name }}" value="{{ value }}">
{% endmacro %}
{% macro bold(text) %}
<b>{{ text }}</b>
{% endmacro %}
`
// Second macro library template
layoutMacroLib := `
{% macro header(text) %}
<h1>{{ text }}</h1>
{% endmacro %}
{% macro box(content) %}
<div class="box">{{ content }}</div>
{% endmacro %}
`
// Main template that uses both import approaches
mainTemplate := `{% import "forms_macros.twig" as forms %}
{% from "layout_macros.twig" import header %}
<div>
{{ header('Hello') }}
{{ forms.input('username', 'john') }}
{{ forms.bold('Welcome') }}
</div>
`
// Register templates
err := engine.RegisterString("forms_macros.twig", formsMacroLib)
if err != nil {
t.Fatalf("Error registering forms_macros.twig: %v", err)
}
err = engine.RegisterString("layout_macros.twig", layoutMacroLib)
if err != nil {
t.Fatalf("Error registering layout_macros.twig: %v", err)
}
err = engine.RegisterString("mixed_imports.twig", mainTemplate)
if err != nil {
t.Fatalf("Error registering mixed_imports.twig: %v", err)
}
// Render the main template
result, err := engine.Render("mixed_imports.twig", nil)
if err != nil {
t.Fatalf("Error parsing/rendering template: %v", err)
}
// Check the output
expectedElements := []string{
`<h1>Hello</h1>`,
`<input name="username" value="john">`,
`<b>Welcome</b>`,
}
for _, expected := range expectedElements {
if !strings.Contains(result, expected) {
t.Errorf("Expected %q in result, but got: %s", expected, result)
}
}
}
// TestMacrosWithContext tests macros with context variables
func TestMacrosWithContext(t *testing.T) {
engine := New()

65
parse_block.go Normal file
View file

@ -0,0 +1,65 @@
package twig
import "fmt"
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
}

70
parse_do.go Normal file
View file

@ -0,0 +1,70 @@
package twig
import (
"fmt"
"strconv"
)
func (p *Parser) parseDo(parser *Parser) (Node, error) {
// Get the line number for error reporting
doLine := parser.tokens[parser.tokenIndex-2].Line
// Check for special case: assignment expressions
// These need to be handled specially since they're not normal expressions
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_NAME {
varName := parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_OPERATOR &&
parser.tokens[parser.tokenIndex].Value == "=" {
// Skip the equals sign
parser.tokenIndex++
// Parse the right side expression
expr, err := parser.parseExpression()
if err != nil {
return nil, fmt.Errorf("error parsing expression in do assignment at line %d: %w", doLine, err)
}
// Make sure we have the closing tag
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expecting end of do tag at line %d", doLine)
}
parser.tokenIndex++
// Validate the variable name - it should not be a numeric literal
if _, err := strconv.Atoi(varName); err == nil {
return nil, fmt.Errorf("invalid variable name %q in do tag assignment at line %d", varName, doLine)
}
// Create a SetNode instead of DoNode for assignments
return &SetNode{
name: varName,
value: expr,
line: doLine,
}, nil
}
// If it wasn't an assignment, backtrack to parse it as a normal expression
parser.tokenIndex -= 1
}
// Parse the expression to be executed
expr, err := parser.parseExpression()
if err != nil {
return nil, fmt.Errorf("error parsing expression in do tag at line %d: %w", doLine, err)
}
// Make sure we have the closing tag
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END {
return nil, fmt.Errorf("expecting end of do tag at line %d", doLine)
}
parser.tokenIndex++
// Create and return the DoNode
return NewDoNode(expr, doLine), nil
}

28
parse_extends.go Normal file
View file

@ -0,0 +1,28 @@
package twig
import "fmt"
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
}

143
parse_for.go Normal file
View file

@ -0,0 +1,143 @@
package twig
import (
"fmt"
)
// parseFor parses a for loop construct in Twig templates
// Examples:
// {% for item in items %}...{% endfor %}
// {% for key, value in items %}...{% endfor %}
// {% for item in items %}...{% else %}...{% endfor %}
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
}
// Check for filter operator (|) - needed for cases where filter detection might be missed
if IsDebugEnabled() {
LogDebug("For loop sequence expression type: %T", sequence)
}
// 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
}

83
parse_from.go Normal file
View file

@ -0,0 +1,83 @@
package twig
import (
"fmt"
"strings"
)
// parseFrom handles the from tag which imports macros from another template
// Example: {% from "macros.twig" import input, button %}
// Example: {% from "macros.twig" import input as field, button as btn %}
func (p *Parser) parseFrom(parser *Parser) (Node, error) {
// Get the line number of the from token
fromLine := parser.tokens[parser.tokenIndex-1].Line
// We need to manually extract the template path, import keyword, and macro(s) from
// the current token. The tokenizer seems to be combining them.
if parser.tokenIndex < len(parser.tokens) && parser.tokens[parser.tokenIndex].Type == TOKEN_NAME {
// Extract parts from the combined token value
tokenValue := parser.tokens[parser.tokenIndex].Value
// Try to extract template path and remaining parts
matches := strings.Split(tokenValue, " import ")
if len(matches) == 2 {
// We found the import keyword in the token value
templatePath := strings.TrimSpace(matches[0])
// Remove quotes if present
templatePath = strings.Trim(templatePath, "\"'")
macrosList := strings.TrimSpace(matches[1])
// Create template expression
templateExpr := &LiteralNode{
ExpressionNode: ExpressionNode{
exprType: ExprLiteral,
line: fromLine,
},
value: templatePath,
}
// Parse macros list
macros := []string{}
aliases := map[string]string{}
// Split macros by comma if multiple
macroItems := strings.Split(macrosList, ",")
for _, item := range macroItems {
item = strings.TrimSpace(item)
// Check for "as" alias
asParts := strings.Split(item, " as ")
if len(asParts) == 2 {
// We have an alias
macroName := strings.TrimSpace(asParts[0])
aliasName := strings.TrimSpace(asParts[1])
aliases[macroName] = aliasName
// Still add the macro name to macros list, even with alias
macros = append(macros, macroName)
} else {
// No alias
macros = append(macros, item)
}
}
// Skip the current token
parser.tokenIndex++
// Skip to the block end token
for parser.tokenIndex < len(parser.tokens) {
if parser.tokens[parser.tokenIndex].Type == TOKEN_BLOCK_END ||
parser.tokens[parser.tokenIndex].Type == TOKEN_BLOCK_END_TRIM {
parser.tokenIndex++
break
}
parser.tokenIndex++
}
// Create and return the FromImportNode
return NewFromImportNode(templateExpr, macros, aliases, fromLine), nil
}
}
// If we're here, the standard parsing approach failed, so return an error
return nil, fmt.Errorf("expected 'import' after template path at line %d", fromLine)
}

147
parse_if.go Normal file
View file

@ -0,0 +1,147 @@
package twig
import (
"fmt"
)
// parseIf parses if/elseif/else/endif block structure
// Examples:
// {% if condition %}...{% endif %}
// {% if condition %}...{% else %}...{% endif %}
// {% if condition %}...{% elseif condition2 %}...{% else %}...{% endif %}
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/elseif)
ifBody, err := parser.parseOuterTemplate()
if err != nil {
return nil, err
}
// Initialize conditions and bodies arrays with the initial if condition and body
conditions := []Node{condition}
bodies := [][]Node{ifBody}
var elseBody []Node
// Keep track of whether we've seen an else block
var hasElseBlock bool
// Process subsequent tags (elseif, else, endif)
for {
// We expect a block start token for elseif, else, or endif
if parser.tokenIndex >= len(parser.tokens) || parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_START {
return nil, fmt.Errorf("unexpected end of template, expected endif at line %d", ifLine)
}
parser.tokenIndex++
// We expect a name token (elseif, else, or endif)
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)
}
// Get the tag name
blockName := parser.tokens[parser.tokenIndex].Value
blockLine := parser.tokens[parser.tokenIndex].Line
parser.tokenIndex++
// Process based on the tag type
if blockName == "elseif" {
// Check if we've already seen an else block - elseif can't come after else
if hasElseBlock {
return nil, fmt.Errorf("unexpected elseif after else at line %d", blockLine)
}
// Handle elseif condition
elseifCondition, err := parser.parseExpression()
if err != nil {
return nil, err
}
// Expect block end token
if parser.tokenIndex >= len(parser.tokens) || !isBlockEndToken(parser.tokens[parser.tokenIndex].Type) {
return nil, fmt.Errorf("expected block end after elseif condition at line %d", blockLine)
}
parser.tokenIndex++
// Parse the elseif body
elseifBody, err := parser.parseOuterTemplate()
if err != nil {
return nil, err
}
// Add condition and body to our arrays
conditions = append(conditions, elseifCondition)
bodies = append(bodies, elseifBody)
// Continue checking for more elseif/else/endif tags
} else if blockName == "else" {
// Check if we've already seen an else block - can't have multiple else blocks
if hasElseBlock {
return nil, fmt.Errorf("multiple else blocks found at line %d", blockLine)
}
// Mark that we've seen an else block
hasElseBlock = true
// Expect block end token
if parser.tokenIndex >= len(parser.tokens) || !isBlockEndToken(parser.tokens[parser.tokenIndex].Type) {
return nil, fmt.Errorf("expected block end after else tag at line %d", blockLine)
}
parser.tokenIndex++
// Parse the else body
elseBody, err = parser.parseOuterTemplate()
if err != nil {
return nil, err
}
// After else, we need to find endif next (handled in next iteration)
} else if blockName == "endif" {
// Expect block end token
if parser.tokenIndex >= len(parser.tokens) || !isBlockEndToken(parser.tokens[parser.tokenIndex].Type) {
return nil, fmt.Errorf("expected block end after endif at line %d", blockLine)
}
parser.tokenIndex++
// We found the endif, we're done
break
} else {
return nil, fmt.Errorf("expected elseif, else, or endif, got %s at line %d", blockName, blockLine)
}
}
// Create and return the if node
ifNode := &IfNode{
conditions: conditions,
bodies: bodies,
elseBranch: elseBody,
line: ifLine,
}
return ifNode, nil
}
// Helper function to check if a token type is a block end token
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
}

96
parse_import.go Normal file
View file

@ -0,0 +1,96 @@
package twig
import (
"fmt"
"strings"
)
func (p *Parser) parseImport(parser *Parser) (Node, error) {
// Use debug logging if enabled
if IsDebugEnabled() && debugger.level >= DebugVerbose {
tokenIndex := parser.tokenIndex - 2
LogVerbose("Parsing import, tokens available:")
for i := 0; i < 10 && tokenIndex+i < len(parser.tokens); i++ {
token := parser.tokens[tokenIndex+i]
LogVerbose(" Token %d: Type=%d, Value=%q, Line=%d", i, token.Type, token.Value, token.Line)
}
}
// Get the line number of the import token
importLine := parser.tokens[parser.tokenIndex-2].Line
// Check for incorrectly tokenized import syntax
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_NAME &&
strings.Contains(parser.tokens[parser.tokenIndex].Value, " as ") {
// Special handling for combined syntax like "path.twig as alias"
parts := strings.SplitN(parser.tokens[parser.tokenIndex].Value, " as ", 2)
if len(parts) == 2 {
templatePath := strings.TrimSpace(parts[0])
alias := strings.TrimSpace(parts[1])
if IsDebugEnabled() && debugger.level >= DebugVerbose {
LogVerbose("Found combined import syntax: template=%q, alias=%q", templatePath, alias)
}
// Create an expression node for the template path
var templateExpr Node
if strings.HasPrefix(templatePath, "\"") && strings.HasSuffix(templatePath, "\"") {
// It's already a quoted string
templateExpr = NewLiteralNode(templatePath[1:len(templatePath)-1], importLine)
} else {
// Create a string literal node
templateExpr = NewLiteralNode(templatePath, importLine)
}
// Skip to end of token
parser.tokenIndex++
// Expect block end
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 import statement at line %d", importLine)
}
parser.tokenIndex++
// Create import node
return NewImportNode(templateExpr, alias, importLine), nil
}
}
// Standard parsing path
// 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 &&
parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
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
}

245
parse_macro.go Normal file
View file

@ -0,0 +1,245 @@
package twig
import (
"fmt"
"strconv"
"strings"
)
func (p *Parser) parseMacro(parser *Parser) (Node, error) {
// Use debug logging if enabled
if IsDebugEnabled() && debugger.level >= DebugVerbose {
tokenIndex := parser.tokenIndex - 2
LogVerbose("Parsing macro, tokens available:")
for i := 0; i < 10 && tokenIndex+i < len(parser.tokens); i++ {
token := parser.tokens[tokenIndex+i]
LogVerbose(" Token %d: Type=%d, Value=%q, Line=%d", i, token.Type, token.Value, token.Line)
}
}
// 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)
}
// Special handling for incorrectly tokenized macro declarations
macroNameRaw := parser.tokens[parser.tokenIndex].Value
if IsDebugEnabled() && debugger.level >= DebugVerbose {
LogVerbose("Raw macro name: %s", macroNameRaw)
}
// Check if the name contains parentheses (incorrectly tokenized)
if strings.Contains(macroNameRaw, "(") {
// Extract the actual name before the parenthesis
parts := strings.SplitN(macroNameRaw, "(", 2)
if len(parts) == 2 {
macroName := parts[0]
paramStr := "(" + parts[1]
if IsDebugEnabled() && debugger.level >= DebugVerbose {
LogVerbose("Fixed macro name: %s", macroName)
LogVerbose("Parameter string: %s", paramStr)
}
// Parse parameters
var params []string
defaults := make(map[string]Node)
// Simple parameter parsing - split by comma
paramList := strings.TrimRight(paramStr[1:], ")")
if paramList != "" {
paramItems := strings.Split(paramList, ",")
for _, param := range paramItems {
param = strings.TrimSpace(param)
// Check for default value
if strings.Contains(param, "=") {
parts := strings.SplitN(param, "=", 2)
paramName := strings.TrimSpace(parts[0])
defaultValue := strings.TrimSpace(parts[1])
params = append(params, paramName)
// Handle quoted strings in default values
if (strings.HasPrefix(defaultValue, "'") && strings.HasSuffix(defaultValue, "'")) ||
(strings.HasPrefix(defaultValue, "\"") && strings.HasSuffix(defaultValue, "\"")) {
// Remove quotes
strValue := defaultValue[1 : len(defaultValue)-1]
defaults[paramName] = NewLiteralNode(strValue, macroLine)
} else if defaultValue == "true" {
defaults[paramName] = NewLiteralNode(true, macroLine)
} else if defaultValue == "false" {
defaults[paramName] = NewLiteralNode(false, macroLine)
} else if i, err := strconv.Atoi(defaultValue); err == nil {
defaults[paramName] = NewLiteralNode(i, macroLine)
} else {
// Fallback to string
defaults[paramName] = NewLiteralNode(defaultValue, macroLine)
}
} else {
params = append(params, param)
}
}
}
// Skip to the end of the token
parser.tokenIndex++
// Expect block end
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 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].Type != TOKEN_BLOCK_START_TRIM) ||
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 &&
parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
return nil, fmt.Errorf("expected block end token after endmacro at line %d", parser.tokens[parser.tokenIndex].Line)
}
parser.tokenIndex++
// Create the macro node
if IsDebugEnabled() && debugger.level >= DebugVerbose {
LogVerbose("Creating MacroNode with %d parameters and %d defaults", len(params), len(defaults))
}
return NewMacroNode(macroName, params, defaults, bodyNodes, macroLine), nil
}
}
// Regular parsing path
macroName := parser.tokens[parser.tokenIndex].Value
if IsDebugEnabled() && debugger.level >= DebugVerbose {
LogVerbose("Macro name: %s", macroName)
}
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
fmt.Println("DEBUG: Parameter name:", paramName)
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 {
fmt.Println("DEBUG: Error parsing default value:", err)
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 &&
parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
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].Type != TOKEN_BLOCK_START_TRIM) ||
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 &&
parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
return nil, fmt.Errorf("expected block end token after endmacro at line %d", parser.tokens[parser.tokenIndex].Line)
}
parser.tokenIndex++
// Create the macro node
if IsDebugEnabled() && debugger.level >= DebugVerbose {
LogVerbose("Creating MacroNode with %d parameters and %d defaults", len(params), len(defaults))
}
return NewMacroNode(macroName, params, defaults, bodyNodes, macroLine), nil
}

61
parse_set.go Normal file
View file

@ -0,0 +1,61 @@
package twig
import "fmt"
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
}

132
parse_verbatim.go Normal file
View file

@ -0,0 +1,132 @@
package twig
import (
"fmt"
"strings"
)
// parseVerbatim parses a verbatim tag and its content
func (p *Parser) parseVerbatim(parser *Parser) (Node, error) {
// Get the line number of the verbatim token
verbatimLine := parser.tokens[parser.tokenIndex-2].Line
// Expect the block end token
if parser.tokenIndex >= len(parser.tokens) || !isBlockEndToken(parser.tokens[parser.tokenIndex].Type) {
return nil, fmt.Errorf("expected block end after verbatim tag at line %d", verbatimLine)
}
parser.tokenIndex++
// Collect all content until we find the endverbatim tag
var contentBuilder strings.Builder
for parser.tokenIndex < len(parser.tokens) {
token := parser.tokens[parser.tokenIndex]
// Look for the endverbatim tag
if token.Type == TOKEN_BLOCK_START || token.Type == TOKEN_BLOCK_START_TRIM {
// Check if this is the endverbatim tag
if parser.tokenIndex+1 < len(parser.tokens) &&
parser.tokens[parser.tokenIndex+1].Type == TOKEN_NAME &&
parser.tokens[parser.tokenIndex+1].Value == "endverbatim" {
// Skip the block start and endverbatim name
parser.tokenIndex += 2 // Now at the endverbatim token
// Expect the block end token
if parser.tokenIndex >= len(parser.tokens) || !isBlockEndToken(parser.tokens[parser.tokenIndex].Type) {
return nil, fmt.Errorf("expected block end after endverbatim at line %d", token.Line)
}
parser.tokenIndex++ // Skip the block end token
// Create a verbatim node with the collected content
return NewVerbatimNode(contentBuilder.String(), verbatimLine), nil
}
}
// Add this token's content to our verbatim content
if token.Type == TOKEN_TEXT {
contentBuilder.WriteString(token.Value)
} else if token.Type == TOKEN_VAR_START || token.Type == TOKEN_VAR_START_TRIM {
// For variable tags, preserve them as literal text
contentBuilder.WriteString("{{")
// Skip variable start token and process until variable end
parser.tokenIndex++
// Process tokens until variable end
for parser.tokenIndex < len(parser.tokens) {
innerToken := parser.tokens[parser.tokenIndex]
if innerToken.Type == TOKEN_VAR_END || innerToken.Type == TOKEN_VAR_END_TRIM {
contentBuilder.WriteString("}}")
break
} else if innerToken.Type == TOKEN_NAME || innerToken.Type == TOKEN_STRING ||
innerToken.Type == TOKEN_NUMBER || innerToken.Type == TOKEN_OPERATOR ||
innerToken.Type == TOKEN_PUNCTUATION {
contentBuilder.WriteString(innerToken.Value)
}
parser.tokenIndex++
}
} else if token.Type == TOKEN_BLOCK_START || token.Type == TOKEN_BLOCK_START_TRIM {
// For block tags, preserve them as literal text
contentBuilder.WriteString("{%")
// Skip block start token and process until block end
parser.tokenIndex++
// Process tokens until block end
for parser.tokenIndex < len(parser.tokens) {
innerToken := parser.tokens[parser.tokenIndex]
if innerToken.Type == TOKEN_BLOCK_END || innerToken.Type == TOKEN_BLOCK_END_TRIM {
contentBuilder.WriteString("%}")
break
} else if innerToken.Type == TOKEN_NAME || innerToken.Type == TOKEN_STRING ||
innerToken.Type == TOKEN_NUMBER || innerToken.Type == TOKEN_OPERATOR ||
innerToken.Type == TOKEN_PUNCTUATION {
// If this is the first TOKEN_NAME in a block, add a space after it
if innerToken.Type == TOKEN_NAME && parser.tokenIndex > 0 &&
(parser.tokens[parser.tokenIndex-1].Type == TOKEN_BLOCK_START ||
parser.tokens[parser.tokenIndex-1].Type == TOKEN_BLOCK_START_TRIM) {
contentBuilder.WriteString(innerToken.Value + " ")
} else {
contentBuilder.WriteString(innerToken.Value)
}
}
parser.tokenIndex++
}
} else if token.Type == TOKEN_COMMENT_START {
// For comment tags, preserve them as literal text
contentBuilder.WriteString("{#")
// Skip comment start token and process until comment end
parser.tokenIndex++
// Process tokens until comment end
for parser.tokenIndex < len(parser.tokens) {
innerToken := parser.tokens[parser.tokenIndex]
if innerToken.Type == TOKEN_COMMENT_END {
contentBuilder.WriteString("#}")
break
} else if innerToken.Type == TOKEN_TEXT {
contentBuilder.WriteString(innerToken.Value)
}
parser.tokenIndex++
}
}
parser.tokenIndex++
// Check for end of tokens
if parser.tokenIndex >= len(parser.tokens) {
return nil, fmt.Errorf("unexpected end of template, unclosed verbatim tag at line %d", verbatimLine)
}
}
// If we get here, we never found the endverbatim tag
return nil, fmt.Errorf("unclosed verbatim tag at line %d", verbatimLine)
}

1421
parser.go

File diff suppressed because it is too large Load diff

177
parser_include.go Normal file
View file

@ -0,0 +1,177 @@
package twig
import (
"fmt"
"strings"
)
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)
}
// Check for opening brace
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_PUNCTUATION &&
parser.tokens[parser.tokenIndex].Value == "{" {
parser.tokenIndex++ // Skip opening brace
// Parse key-value pairs
for {
// If we see a closing brace, we're done
if parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_PUNCTUATION &&
parser.tokens[parser.tokenIndex].Value == "}" {
parser.tokenIndex++ // Skip closing brace
break
}
// Get the variable name - can be string literal or name token
var varName string
if parser.tokenIndex < len(parser.tokens) && parser.tokens[parser.tokenIndex].Type == TOKEN_STRING {
// It's a quoted string key
varName = parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
} else if parser.tokenIndex < len(parser.tokens) && parser.tokens[parser.tokenIndex].Type == TOKEN_NAME {
// It's an unquoted key
varName = parser.tokens[parser.tokenIndex].Value
parser.tokenIndex++
} else {
return nil, fmt.Errorf("expected variable name or string at line %d", includeLine)
}
// Expect colon or equals
if parser.tokenIndex >= len(parser.tokens) ||
((parser.tokens[parser.tokenIndex].Type != TOKEN_PUNCTUATION &&
parser.tokens[parser.tokenIndex].Value != ":") &&
(parser.tokens[parser.tokenIndex].Type != TOKEN_OPERATOR &&
parser.tokens[parser.tokenIndex].Value != "=")) {
return nil, fmt.Errorf("expected ':' or '=' after variable name at line %d", includeLine)
}
parser.tokenIndex++ // Skip : or =
// 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++
}
// If we see whitespace, skip it
for parser.tokenIndex < len(parser.tokens) &&
parser.tokens[parser.tokenIndex].Type == TOKEN_TEXT &&
strings.TrimSpace(parser.tokens[parser.tokenIndex].Value) == "" {
parser.tokenIndex++
}
}
} else {
// If there's no opening brace, expect name-value pairs in the old format
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 &&
parser.tokens[parser.tokenIndex].Type != TOKEN_BLOCK_END_TRIM) {
return nil, fmt.Errorf("expected block end token after include at line %d, found token type %d with value '%s'",
includeLine,
parser.tokens[parser.tokenIndex].Type,
parser.tokens[parser.tokenIndex].Value)
}
parser.tokenIndex++
// Create the include node
includeNode := &IncludeNode{
template: templateExpr,
variables: variables,
ignoreMissing: ignoreMissing,
only: onlyContext,
line: includeLine,
}
return includeNode, nil
}