mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 13:55:46 +01:00
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:
parent
c50999e3ee
commit
ec37652bc1
14 changed files with 1675 additions and 1460 deletions
194
from_tag_test.go
Normal file
194
from_tag_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
213
macros_test.go
213
macros_test.go
|
|
@ -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) {
|
func TestMacrosFromImport(t *testing.T) {
|
||||||
engine := New()
|
engine := New()
|
||||||
|
|
||||||
|
|
@ -144,20 +203,25 @@ func TestMacrosFromImport(t *testing.T) {
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
`
|
`
|
||||||
|
|
||||||
// Main template that selectively imports macros
|
// Main template that selectively imports macros using from import syntax
|
||||||
// Using import as syntax which has better support
|
mainTemplate := `{% from "macro_lib.twig" import input, button %}
|
||||||
mainTemplate := `
|
|
||||||
{% import "macro_lib.twig" as lib %}
|
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
{{ lib.input('username', 'john') }}
|
{{ input('username', 'john') }}
|
||||||
{{ lib.button('submit', 'Submit Form') }}
|
{{ button('submit', 'Submit Form') }}
|
||||||
</form>
|
</form>
|
||||||
`
|
`
|
||||||
|
|
||||||
// Register both templates
|
// Register both templates
|
||||||
engine.RegisterString("macro_lib.twig", macroLib)
|
err := engine.RegisterString("macro_lib.twig", macroLib)
|
||||||
engine.RegisterString("from_import.twig", mainTemplate)
|
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
|
// Render the main template
|
||||||
result, err := engine.Render("from_import.twig", nil)
|
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
|
// TestMacrosWithContext tests macros with context variables
|
||||||
func TestMacrosWithContext(t *testing.T) {
|
func TestMacrosWithContext(t *testing.T) {
|
||||||
engine := New()
|
engine := New()
|
||||||
|
|
|
||||||
65
parse_block.go
Normal file
65
parse_block.go
Normal 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
70
parse_do.go
Normal 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
28
parse_extends.go
Normal 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
143
parse_for.go
Normal 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
83
parse_from.go
Normal 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
147
parse_if.go
Normal 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
96
parse_import.go
Normal 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
245
parse_macro.go
Normal 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
61
parse_set.go
Normal 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
132
parse_verbatim.go
Normal 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)
|
||||||
|
}
|
||||||
177
parser_include.go
Normal file
177
parser_include.go
Normal 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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue