+230
README.md
+230
README.md
···
1
+
# GOTT - Go Template Toolkit
2
+
3
+
A templating system very much inspired by Perl's Template module (The Template Toolkit). There are other templating packages available, of course, but I really wanted the `[% WRAPPER %]`, `[% BLOCK %]` and `[% INCLUDE %]` directives.
4
+
5
+
This is just a pet project I built for myself, to see if a) I could actually understand/write Go, and b) to see what value a tool like Claude brings to the table. So, YMMV!
6
+
7
+
## Usage
8
+
9
+
Import the package, and instantiate a new renderer:
10
+
11
+
```go
12
+
renderer := gott.New(&gott.Config{
13
+
...
14
+
})
15
+
```
16
+
17
+
The `gott.Config` contains the following fields:
18
+
19
+
- IncludePaths: `[]fs.FS`
20
+
- Filters: `map[string]func(string, ...string) string`
21
+
22
+
The include paths are tried left to right when a template needs rendering; this can be used for instance to do this:
23
+
24
+
```go
25
+
renderer := gott.New(&gott.Config{
26
+
IncludePaths: []fs.FS{
27
+
os.DirFS("..."),
28
+
embeddedFS
29
+
})
30
+
```
31
+
32
+
To have files on disk tried first, and if the requested template wasn't found, fall back to an embedded FS (via `embed`).
33
+
34
+
Filters are functions that filter variables in templates, for instance one could write a filter that turns everything to upper, or to truncate a string to a given length.
35
+
36
+
```go
37
+
import "strings"
38
+
39
+
func filterUpper(content string, args ...string) string {
40
+
return strings.ToUpper(content)
41
+
}
42
+
43
+
func filterTruncate(s string, args ...string) string {
44
+
if len(args) == 0 {
45
+
return s
46
+
}
47
+
maxLen := 0
48
+
fmt.Sscanf(args[0], "%d", &maxLen)
49
+
if maxLen <= 0 || len(s) <= maxLen {
50
+
return s
51
+
}
52
+
return s[:maxLen-3] + "..."
53
+
}
54
+
renderer := gott.New(&gott.Config{
55
+
Filters: map[string]func(string, ...string) string{
56
+
upper: filterUpper,
57
+
truncate: filterTruncate,
58
+
})
59
+
```
60
+
61
+
For convenience. the renderer has an `AddFilter` method:
62
+
63
+
```go
64
+
renderer.AddFilter("truncate", filterTruncate)
65
+
```
66
+
67
+
To render content, there are a few methods available:
68
+
69
+
```go
70
+
// Render takes a http.ResponseWriter, string, and a map[string]any and will attempt to render the given template
71
+
// and writes it to the client as text/html; a 404 error is rendered for templates that aren't found, 500 error is
72
+
// rendered if something goes kaka
73
+
renderer.Render(w, "mytemplate", map[string]any{myvariable:"foo", userCount: 10})
74
+
75
+
// Identical to Render except it lets you override the status code; I have one project that actually renders html
76
+
// with a 420 (made up) status code because reasons. Something tech debt-ish.
77
+
renderer.RenderWithStatus(w, int, string, map[string]any)
78
+
79
+
// Render JSON data, will render a 500 error if marshalling failed; takes a http.ResponseWriter and 'any' as parameter
80
+
renderer.RenderJSON(w, map[string]any{status:"error",errorCode: 200, errorMessage: "it done blew up"})
81
+
82
+
// Same as above but takes the ResponseWriter, status code, and the data that needs to be marshalled
83
+
renderer.RenderJSONWithStatus(w, int, any)
84
+
85
+
// Silly little function to render a blank HTTP 204 response
86
+
renderer.Render204()
87
+
```
88
+
89
+
## Concepts
90
+
91
+
### Filters
92
+
93
+
Filters are simple functions that transform variable content. These should be short and to the point, and should not be used to perform any sort of external lookup or other network requests. Filters can take parameters (e.g. `truncate(30)`) but all parameters are passed as strings, so you'll have to do the proper processing on them in the filter func.
94
+
95
+
The following filters are built-in:
96
+
97
+
- *upper*: turns the variable to all uppercase
98
+
- *lower*: turns the variable to all lowercase
99
+
- *html*: makes the variable safe to render in HTML (e.g. "<foo>" is turned into ">foo<"
100
+
- *uri*: makes the variable safe to use as an URI fragment;
101
+
102
+
### Virtual Methods
103
+
104
+
Virtual methods are, well, methods that one can call on a variable. For instance, given a variable of signature `[]string{"foo","bar","baz"}`, you can use this construct to ge the last element: [% variable.last %].
105
+
106
+
The following virtual methods are built-in:
107
+
108
+
- *exists*: Will return true if the given variable actually exists in the data passed to the renderer, does not check for defined-ness, purely existence.
109
+
- *defined*: Will return true if the given variable exists in the data passed to the renderer, and is not empty (insofar a variable can be empty in go)
110
+
- *length/size*: Will return the length of the variable; handles strings, slices, maps
111
+
- *first/last*: Will return the first or last element of a slice
112
+
113
+
You can add your own virtual methods with the `AddVirtualMethod` method on the renderer, for instance:
114
+
115
+
```go
116
+
renderer.addVirtualMethod("methodname", func(data any) (any, bool) {
117
+
118
+
})
119
+
```
120
+
121
+
A virtual method handler should return whatever data it wants, plus a boolean indicating whether the operation was successful.
122
+
123
+
## Variables and interpolation
124
+
125
+
Variables are passed to the renderer as a `map[string]any` and can be interpolated into a template via the following syntax:
126
+
127
+
```go
128
+
// simple variable
129
+
[% myvariable %]
130
+
131
+
// nested variable, e.g. variable is a map
132
+
[% myvariable.somekey %]
133
+
134
+
// but it could also be a vmethod
135
+
[% myvariable.length %]
136
+
137
+
// if variable is not defined, add a default
138
+
[% user.name || "User doesn't have a name, strange!" %]
139
+
140
+
// variable with filtering
141
+
[% userEnteredHTML |html %]
142
+
143
+
// filter chaining
144
+
[% verylongname |truncate(20)|upper %]
145
+
146
+
// vmethods and defaults and filters, oh my (this would work as you think it would because a value of 0
147
+
// is considered "not truthy"
148
+
[% records.length || "no records found" |somefilter|anotherfilter %]
149
+
150
+
```
151
+
152
+
The following comparison operators are available: `== != < <= => >`
153
+
The following logic operators are available: `&& ||`
154
+
The following arithmetic operators are available: `+ - * / %`
155
+
156
+
Strings can be concatenated using `+` if either the lvalue or rvalue is a string. Given the following data:
157
+
158
+
```
159
+
data := map[string]any{version: "1.2.3", versionNumeric: 123}
160
+
161
+
// renders "1.2.3-dev"
162
+
[% version + "-dev" %]
163
+
164
+
// renders "123-dev"
165
+
[% versionNumeric + "-dev" %]
166
+
167
+
// renders 124
168
+
[% versionNumeric + 1 %]
169
+
170
+
// renders 1-1.2.3
171
+
[% "1-" + version %]
172
+
173
+
```
174
+
175
+
## Directives
176
+
177
+
The following directives are supported:
178
+
179
+
```go
180
+
// logic
181
+
[% IF myvariable == "yes" %]
182
+
Yes!
183
+
[% ELSIF myvariable == "maybe" %]
184
+
Maybe?
185
+
[% ELSE %]
186
+
No...
187
+
[% END %]
188
+
189
+
// this construct is a Perlism
190
+
[% UNLESS user.isAdmin %]
191
+
Not the admin
192
+
[% END %]
193
+
194
+
// include another template
195
+
[% INCLUDE other/template.html %]
196
+
197
+
// include a previously defined block
198
+
[% INCLUDE myshinyblock %]
199
+
200
+
// define a block
201
+
[% BLOCK myshinyblock %]
202
+
[% entry.name %]
203
+
[% END %]
204
+
205
+
// iterate a slice; a foreach can be nested, and a foreach can use all other directives in it's repeated block
206
+
[% FOREACH entry IN myslice %]
207
+
[% INCLUDE myshinyblock %]
208
+
[% END %]
209
+
210
+
// iterate a map
211
+
[% FOREACH entry IN mymap %]
212
+
[% entry.key %] = [% entry.value %]
213
+
[% END %]
214
+
215
+
// wrap content in another template
216
+
// wrapper template content should contain a single `[% content %]` variable where the content it is wrapping will appear
217
+
[% WRAPPER wrap/me/in/this.html %]
218
+
content that can use any other directive
219
+
[% END %]
220
+
221
+
## Author/Disclosure
222
+
223
+
Author: Ben van Staveren/AngryDutchman <ben@blockstackers.net>
224
+
Co-Author: Claude (see below)
225
+
226
+
Heavily inspired by Perl's [Template Toolkit](https://metacpan.org/pod/Template), by Andy Wardley
227
+
228
+
### Claude usage
229
+
230
+
Claude is used to add comments in places that should have them but don't (because I'm awful at doing it), and to take my not-so-efficient implementations and make them more efficient. Claude is also used to solve fun directive parsing problems, and to do refactoring work. All code generated by Claude is vetted and looked at.
+220
ast.go
+220
ast.go
···
1
+
package gott
2
+
3
+
// Node is the interface implemented by all AST nodes
4
+
type Node interface {
5
+
Pos() Position // position of the first token of this node
6
+
node() // marker method to ensure only AST types implement Node
7
+
}
8
+
9
+
// Expr is the interface implemented by all expression nodes
10
+
type Expr interface {
11
+
Node
12
+
expr() // marker method
13
+
}
14
+
15
+
// Stmt is the interface implemented by all statement nodes
16
+
type Stmt interface {
17
+
Node
18
+
stmt() // marker method
19
+
}
20
+
21
+
// ---- Template (root node) ----
22
+
23
+
// Template is the root node containing all parsed nodes
24
+
type Template struct {
25
+
Position Position
26
+
Nodes []Node // mix of TextNode, statements, and output expressions
27
+
}
28
+
29
+
func (t *Template) Pos() Position { return t.Position }
30
+
func (t *Template) node() {}
31
+
32
+
// ---- Literal/Text Nodes ----
33
+
34
+
// TextNode holds literal text outside of tags
35
+
type TextNode struct {
36
+
Position Position
37
+
Text string
38
+
}
39
+
40
+
func (n *TextNode) Pos() Position { return n.Position }
41
+
func (n *TextNode) node() {}
42
+
43
+
// ---- Expression Nodes ----
44
+
45
+
// IdentExpr represents a variable reference with optional dot notation
46
+
// e.g., "foo", "foo.bar", "foo.bar.baz"
47
+
type IdentExpr struct {
48
+
Position Position
49
+
Parts []string // ["foo", "bar", "baz"] for foo.bar.baz
50
+
}
51
+
52
+
func (e *IdentExpr) Pos() Position { return e.Position }
53
+
func (e *IdentExpr) node() {}
54
+
func (e *IdentExpr) expr() {}
55
+
56
+
// LiteralExpr holds a string or number literal
57
+
type LiteralExpr struct {
58
+
Position Position
59
+
Value any // string or float64
60
+
}
61
+
62
+
func (e *LiteralExpr) Pos() Position { return e.Position }
63
+
func (e *LiteralExpr) node() {}
64
+
func (e *LiteralExpr) expr() {}
65
+
66
+
// BinaryExpr represents a binary operation: left op right
67
+
type BinaryExpr struct {
68
+
Position Position
69
+
Op TokenType
70
+
Left Expr
71
+
Right Expr
72
+
}
73
+
74
+
func (e *BinaryExpr) Pos() Position { return e.Position }
75
+
func (e *BinaryExpr) node() {}
76
+
func (e *BinaryExpr) expr() {}
77
+
78
+
// UnaryExpr represents a unary operation: op expr
79
+
type UnaryExpr struct {
80
+
Position Position
81
+
Op TokenType
82
+
X Expr
83
+
}
84
+
85
+
func (e *UnaryExpr) Pos() Position { return e.Position }
86
+
func (e *UnaryExpr) node() {}
87
+
func (e *UnaryExpr) expr() {}
88
+
89
+
// CallExpr represents a function call: func(args...)
90
+
type CallExpr struct {
91
+
Position Position
92
+
Func string
93
+
Args []Expr
94
+
}
95
+
96
+
func (e *CallExpr) Pos() Position { return e.Position }
97
+
func (e *CallExpr) node() {}
98
+
func (e *CallExpr) expr() {}
99
+
100
+
// FilterExpr represents a filter application: expr | filter or expr | filter(args)
101
+
type FilterExpr struct {
102
+
Position Position
103
+
Input Expr
104
+
Filter string
105
+
Args []Expr
106
+
}
107
+
108
+
func (e *FilterExpr) Pos() Position { return e.Position }
109
+
func (e *FilterExpr) node() {}
110
+
func (e *FilterExpr) expr() {}
111
+
112
+
// DefaultExpr represents a default value expression: expr || default
113
+
type DefaultExpr struct {
114
+
Position Position
115
+
Expr Expr
116
+
Default Expr
117
+
}
118
+
119
+
func (e *DefaultExpr) Pos() Position { return e.Position }
120
+
func (e *DefaultExpr) node() {}
121
+
func (e *DefaultExpr) expr() {}
122
+
123
+
// ---- Statement Nodes ----
124
+
125
+
// OutputStmt represents an expression to be output: [% expr %]
126
+
type OutputStmt struct {
127
+
Position Position
128
+
Expr Expr
129
+
}
130
+
131
+
func (s *OutputStmt) Pos() Position { return s.Position }
132
+
func (s *OutputStmt) node() {}
133
+
func (s *OutputStmt) stmt() {}
134
+
135
+
// IfStmt represents an IF/ELSIF/ELSE chain
136
+
type IfStmt struct {
137
+
Position Position
138
+
Condition Expr
139
+
Body []Node
140
+
ElsIf []*ElsIfClause // zero or more ELSIF clauses
141
+
Else []Node // optional ELSE body
142
+
}
143
+
144
+
func (s *IfStmt) Pos() Position { return s.Position }
145
+
func (s *IfStmt) node() {}
146
+
func (s *IfStmt) stmt() {}
147
+
148
+
// ElsIfClause represents an ELSIF branch
149
+
type ElsIfClause struct {
150
+
Position Position
151
+
Condition Expr
152
+
Body []Node
153
+
}
154
+
155
+
// UnlessStmt represents an UNLESS conditional
156
+
type UnlessStmt struct {
157
+
Position Position
158
+
Condition Expr
159
+
Body []Node
160
+
Else []Node // optional ELSE body
161
+
}
162
+
163
+
func (s *UnlessStmt) Pos() Position { return s.Position }
164
+
func (s *UnlessStmt) node() {}
165
+
func (s *UnlessStmt) stmt() {}
166
+
167
+
// ForeachStmt represents a FOREACH loop
168
+
type ForeachStmt struct {
169
+
Position Position
170
+
ItemVar string // loop variable name
171
+
ListExpr Expr // expression for the list/map to iterate
172
+
Body []Node
173
+
}
174
+
175
+
func (s *ForeachStmt) Pos() Position { return s.Position }
176
+
func (s *ForeachStmt) node() {}
177
+
func (s *ForeachStmt) stmt() {}
178
+
179
+
// BlockStmt represents a named block definition
180
+
type BlockStmt struct {
181
+
Position Position
182
+
Name string
183
+
Body []Node
184
+
}
185
+
186
+
func (s *BlockStmt) Pos() Position { return s.Position }
187
+
func (s *BlockStmt) node() {}
188
+
func (s *BlockStmt) stmt() {}
189
+
190
+
// IncludeStmt represents an INCLUDE directive
191
+
type IncludeStmt struct {
192
+
Position Position
193
+
Name string // block name or file path
194
+
}
195
+
196
+
func (s *IncludeStmt) Pos() Position { return s.Position }
197
+
func (s *IncludeStmt) node() {}
198
+
func (s *IncludeStmt) stmt() {}
199
+
200
+
// WrapperStmt represents a WRAPPER directive
201
+
type WrapperStmt struct {
202
+
Position Position
203
+
Name string // wrapper template name
204
+
Content []Node // content to be wrapped
205
+
}
206
+
207
+
func (s *WrapperStmt) Pos() Position { return s.Position }
208
+
func (s *WrapperStmt) node() {}
209
+
func (s *WrapperStmt) stmt() {}
210
+
211
+
// SetStmt represents a SET variable assignment
212
+
type SetStmt struct {
213
+
Position Position
214
+
Var string
215
+
Value Expr
216
+
}
217
+
218
+
func (s *SetStmt) Pos() Position { return s.Position }
219
+
func (s *SetStmt) node() {}
220
+
func (s *SetStmt) stmt() {}
+773
eval.go
+773
eval.go
···
1
+
package gott
2
+
3
+
import (
4
+
"fmt"
5
+
"reflect"
6
+
"sort"
7
+
"strconv"
8
+
"strings"
9
+
)
10
+
11
+
// Evaluator evaluates an AST with the given variables
12
+
type Evaluator struct {
13
+
renderer *Renderer // for filters, virtual methods, includes
14
+
vars map[string]any // current variable scope
15
+
blocks map[string]*BlockStmt // defined blocks
16
+
output strings.Builder // accumulated output
17
+
}
18
+
19
+
// NewEvaluator creates a new evaluator
20
+
func NewEvaluator(r *Renderer, vars map[string]any) *Evaluator {
21
+
if vars == nil {
22
+
vars = make(map[string]any)
23
+
}
24
+
return &Evaluator{
25
+
renderer: r,
26
+
vars: vars,
27
+
blocks: make(map[string]*BlockStmt),
28
+
}
29
+
}
30
+
31
+
// Eval evaluates the template and returns the output string
32
+
func (e *Evaluator) Eval(t *Template) (string, error) {
33
+
// First pass: collect block definitions
34
+
for _, node := range t.Nodes {
35
+
if block, ok := node.(*BlockStmt); ok {
36
+
e.blocks[block.Name] = block
37
+
}
38
+
}
39
+
40
+
// Second pass: evaluate nodes
41
+
for _, node := range t.Nodes {
42
+
// Skip block definitions in output (they're just definitions)
43
+
if _, ok := node.(*BlockStmt); ok {
44
+
continue
45
+
}
46
+
if err := e.evalNode(node); err != nil {
47
+
return "", err
48
+
}
49
+
}
50
+
51
+
return e.output.String(), nil
52
+
}
53
+
54
+
// evalNode evaluates a single AST node
55
+
func (e *Evaluator) evalNode(node Node) error {
56
+
switch n := node.(type) {
57
+
case *TextNode:
58
+
e.output.WriteString(n.Text)
59
+
60
+
case *OutputStmt:
61
+
val, err := e.evalExpr(n.Expr)
62
+
if err != nil {
63
+
return err
64
+
}
65
+
if val != nil {
66
+
e.output.WriteString(e.toString(val))
67
+
}
68
+
69
+
case *IfStmt:
70
+
return e.evalIf(n)
71
+
72
+
case *UnlessStmt:
73
+
return e.evalUnless(n)
74
+
75
+
case *ForeachStmt:
76
+
return e.evalForeach(n)
77
+
78
+
case *IncludeStmt:
79
+
return e.evalInclude(n)
80
+
81
+
case *WrapperStmt:
82
+
return e.evalWrapper(n)
83
+
84
+
case *SetStmt:
85
+
val, err := e.evalExpr(n.Value)
86
+
if err != nil {
87
+
return err
88
+
}
89
+
e.vars[n.Var] = val
90
+
91
+
case *BlockStmt:
92
+
// Block definitions are handled in first pass, skip here
93
+
}
94
+
95
+
return nil
96
+
}
97
+
98
+
// evalNodes evaluates a slice of nodes
99
+
func (e *Evaluator) evalNodes(nodes []Node) error {
100
+
for _, node := range nodes {
101
+
if err := e.evalNode(node); err != nil {
102
+
return err
103
+
}
104
+
}
105
+
return nil
106
+
}
107
+
108
+
// evalIf evaluates an IF statement
109
+
func (e *Evaluator) evalIf(n *IfStmt) error {
110
+
cond, err := e.evalExpr(n.Condition)
111
+
if err != nil {
112
+
return err
113
+
}
114
+
115
+
if e.isTruthy(cond) {
116
+
return e.evalNodes(n.Body)
117
+
}
118
+
119
+
// Check ELSIF chain
120
+
for _, elsif := range n.ElsIf {
121
+
cond, err := e.evalExpr(elsif.Condition)
122
+
if err != nil {
123
+
return err
124
+
}
125
+
if e.isTruthy(cond) {
126
+
return e.evalNodes(elsif.Body)
127
+
}
128
+
}
129
+
130
+
// Fall through to ELSE
131
+
if n.Else != nil {
132
+
return e.evalNodes(n.Else)
133
+
}
134
+
135
+
return nil
136
+
}
137
+
138
+
// evalUnless evaluates an UNLESS statement
139
+
func (e *Evaluator) evalUnless(n *UnlessStmt) error {
140
+
cond, err := e.evalExpr(n.Condition)
141
+
if err != nil {
142
+
return err
143
+
}
144
+
145
+
if !e.isTruthy(cond) {
146
+
return e.evalNodes(n.Body)
147
+
}
148
+
149
+
if n.Else != nil {
150
+
return e.evalNodes(n.Else)
151
+
}
152
+
153
+
return nil
154
+
}
155
+
156
+
// evalForeach evaluates a FOREACH loop
157
+
func (e *Evaluator) evalForeach(n *ForeachStmt) error {
158
+
list, err := e.evalExpr(n.ListExpr)
159
+
if err != nil {
160
+
return err
161
+
}
162
+
163
+
if list == nil {
164
+
return nil
165
+
}
166
+
167
+
rv := reflect.ValueOf(list)
168
+
169
+
switch rv.Kind() {
170
+
case reflect.Slice, reflect.Array:
171
+
for i := 0; i < rv.Len(); i++ {
172
+
// Create new scope with loop variable
173
+
loopEval := e.withVar(n.ItemVar, rv.Index(i).Interface())
174
+
if err := loopEval.evalNodes(n.Body); err != nil {
175
+
return err
176
+
}
177
+
e.output.WriteString(loopEval.output.String())
178
+
}
179
+
180
+
case reflect.Map:
181
+
// TT2-style: iterate as key/value pairs, sorted by key
182
+
keys := rv.MapKeys()
183
+
sort.Slice(keys, func(i, j int) bool {
184
+
return fmt.Sprintf("%v", keys[i].Interface()) < fmt.Sprintf("%v", keys[j].Interface())
185
+
})
186
+
187
+
for _, key := range keys {
188
+
// Each iteration gets a map with "key" and "value" fields
189
+
entry := map[string]any{
190
+
"key": key.Interface(),
191
+
"value": rv.MapIndex(key).Interface(),
192
+
}
193
+
194
+
loopEval := e.withVar(n.ItemVar, entry)
195
+
if err := loopEval.evalNodes(n.Body); err != nil {
196
+
return err
197
+
}
198
+
e.output.WriteString(loopEval.output.String())
199
+
}
200
+
201
+
default:
202
+
return &EvalError{
203
+
Pos: n.Position,
204
+
Message: fmt.Sprintf("cannot iterate over %T", list),
205
+
}
206
+
}
207
+
208
+
return nil
209
+
}
210
+
211
+
// evalInclude evaluates an INCLUDE directive
212
+
func (e *Evaluator) evalInclude(n *IncludeStmt) error {
213
+
// First check if it's a defined block
214
+
if block, ok := e.blocks[n.Name]; ok {
215
+
return e.evalNodes(block.Body)
216
+
}
217
+
218
+
// Otherwise, try to load from filesystem
219
+
content, err := e.renderer.loadFile(n.Name)
220
+
if err != nil {
221
+
e.output.WriteString(fmt.Sprintf("[Include '%s' not found]", n.Name))
222
+
return nil
223
+
}
224
+
225
+
// Parse and evaluate the included template
226
+
parser := NewParser(content)
227
+
tmpl, parseErrs := parser.Parse()
228
+
if len(parseErrs) > 0 {
229
+
return parseErrs[0]
230
+
}
231
+
232
+
// Evaluate with same scope
233
+
includeEval := NewEvaluator(e.renderer, e.copyVars())
234
+
// Copy blocks
235
+
for name, block := range e.blocks {
236
+
includeEval.blocks[name] = block
237
+
}
238
+
result, err := includeEval.Eval(tmpl)
239
+
if err != nil {
240
+
return err
241
+
}
242
+
e.output.WriteString(result)
243
+
244
+
return nil
245
+
}
246
+
247
+
// evalWrapper evaluates a WRAPPER directive
248
+
func (e *Evaluator) evalWrapper(n *WrapperStmt) error {
249
+
// First, evaluate the wrapped content
250
+
contentEval := NewEvaluator(e.renderer, e.copyVars())
251
+
for name, block := range e.blocks {
252
+
contentEval.blocks[name] = block
253
+
}
254
+
for _, node := range n.Content {
255
+
if err := contentEval.evalNode(node); err != nil {
256
+
return err
257
+
}
258
+
}
259
+
wrappedContent := contentEval.output.String()
260
+
261
+
// Load the wrapper template
262
+
var wrapperSource string
263
+
if block, ok := e.blocks[n.Name]; ok {
264
+
// Wrapper is a defined block - evaluate it
265
+
blockEval := NewEvaluator(e.renderer, e.copyVars())
266
+
for name, b := range e.blocks {
267
+
blockEval.blocks[name] = b
268
+
}
269
+
for _, node := range block.Body {
270
+
if err := blockEval.evalNode(node); err != nil {
271
+
return err
272
+
}
273
+
}
274
+
wrapperSource = blockEval.output.String()
275
+
} else {
276
+
content, err := e.renderer.loadFile(n.Name)
277
+
if err != nil {
278
+
e.output.WriteString(fmt.Sprintf("[Wrapper '%s' not found]", n.Name))
279
+
return nil
280
+
}
281
+
wrapperSource = content
282
+
}
283
+
284
+
// Parse the wrapper template
285
+
parser := NewParser(wrapperSource)
286
+
tmpl, parseErrs := parser.Parse()
287
+
if len(parseErrs) > 0 {
288
+
return parseErrs[0]
289
+
}
290
+
291
+
// Evaluate wrapper with "content" variable set to wrapped content
292
+
wrapperVars := e.copyVars()
293
+
wrapperVars["content"] = wrappedContent
294
+
295
+
wrapperEval := NewEvaluator(e.renderer, wrapperVars)
296
+
for name, block := range e.blocks {
297
+
wrapperEval.blocks[name] = block
298
+
}
299
+
result, err := wrapperEval.Eval(tmpl)
300
+
if err != nil {
301
+
return err
302
+
}
303
+
e.output.WriteString(result)
304
+
305
+
return nil
306
+
}
307
+
308
+
// evalExpr evaluates an expression and returns its value
309
+
func (e *Evaluator) evalExpr(expr Expr) (any, error) {
310
+
switch x := expr.(type) {
311
+
case *LiteralExpr:
312
+
return x.Value, nil
313
+
314
+
case *IdentExpr:
315
+
return e.resolveIdent(x.Parts)
316
+
317
+
case *BinaryExpr:
318
+
return e.evalBinary(x)
319
+
320
+
case *UnaryExpr:
321
+
return e.evalUnary(x)
322
+
323
+
case *CallExpr:
324
+
return e.evalCall(x)
325
+
326
+
case *FilterExpr:
327
+
return e.evalFilter(x)
328
+
329
+
case *DefaultExpr:
330
+
val, err := e.evalExpr(x.Expr)
331
+
if err != nil || !e.isDefined(val) {
332
+
return e.evalExpr(x.Default)
333
+
}
334
+
return val, nil
335
+
}
336
+
337
+
return nil, fmt.Errorf("unknown expression type: %T", expr)
338
+
}
339
+
340
+
// evalBinary evaluates a binary expression
341
+
func (e *Evaluator) evalBinary(b *BinaryExpr) (any, error) {
342
+
left, err := e.evalExpr(b.Left)
343
+
if err != nil {
344
+
return nil, err
345
+
}
346
+
right, err := e.evalExpr(b.Right)
347
+
if err != nil {
348
+
return nil, err
349
+
}
350
+
351
+
switch b.Op {
352
+
case TokenPlus:
353
+
// If either operand is a string, do string concatenation
354
+
if e.isString(left) || e.isString(right) {
355
+
return e.toString(left) + e.toString(right), nil
356
+
}
357
+
return e.toFloat(left) + e.toFloat(right), nil
358
+
359
+
case TokenMinus:
360
+
return e.toFloat(left) - e.toFloat(right), nil
361
+
362
+
case TokenMul:
363
+
return e.toFloat(left) * e.toFloat(right), nil
364
+
365
+
case TokenDiv:
366
+
r := e.toFloat(right)
367
+
if r == 0 {
368
+
return nil, &EvalError{Pos: b.Position, Message: "division by zero"}
369
+
}
370
+
return e.toFloat(left) / r, nil
371
+
372
+
case TokenMod:
373
+
return int(e.toFloat(left)) % int(e.toFloat(right)), nil
374
+
375
+
case TokenEq:
376
+
return e.equals(left, right), nil
377
+
378
+
case TokenNe:
379
+
return !e.equals(left, right), nil
380
+
381
+
case TokenLt:
382
+
return e.toFloat(left) < e.toFloat(right), nil
383
+
384
+
case TokenLe:
385
+
return e.toFloat(left) <= e.toFloat(right), nil
386
+
387
+
case TokenGt:
388
+
return e.toFloat(left) > e.toFloat(right), nil
389
+
390
+
case TokenGe:
391
+
return e.toFloat(left) >= e.toFloat(right), nil
392
+
393
+
case TokenAnd:
394
+
return e.isTruthy(left) && e.isTruthy(right), nil
395
+
396
+
case TokenOr:
397
+
return e.isTruthy(left) || e.isTruthy(right), nil
398
+
}
399
+
400
+
return nil, fmt.Errorf("unknown operator: %v", b.Op)
401
+
}
402
+
403
+
// evalUnary evaluates a unary expression
404
+
func (e *Evaluator) evalUnary(u *UnaryExpr) (any, error) {
405
+
val, err := e.evalExpr(u.X)
406
+
if err != nil {
407
+
return nil, err
408
+
}
409
+
410
+
switch u.Op {
411
+
case TokenMinus:
412
+
return -e.toFloat(val), nil
413
+
}
414
+
415
+
return nil, fmt.Errorf("unknown unary operator: %v", u.Op)
416
+
}
417
+
418
+
// evalCall evaluates a function call
419
+
func (e *Evaluator) evalCall(c *CallExpr) (any, error) {
420
+
fn, ok := e.vars[c.Func]
421
+
if !ok {
422
+
return nil, &EvalError{
423
+
Pos: c.Position,
424
+
Message: fmt.Sprintf("undefined function: %s", c.Func),
425
+
}
426
+
}
427
+
428
+
fnValue := reflect.ValueOf(fn)
429
+
if fnValue.Kind() != reflect.Func {
430
+
return nil, &EvalError{
431
+
Pos: c.Position,
432
+
Message: fmt.Sprintf("%s is not a function", c.Func),
433
+
}
434
+
}
435
+
436
+
var args []reflect.Value
437
+
for _, arg := range c.Args {
438
+
val, err := e.evalExpr(arg)
439
+
if err != nil {
440
+
return nil, err
441
+
}
442
+
args = append(args, reflect.ValueOf(val))
443
+
}
444
+
445
+
results := fnValue.Call(args)
446
+
if len(results) > 0 {
447
+
return results[0].Interface(), nil
448
+
}
449
+
return nil, nil
450
+
}
451
+
452
+
// evalFilter evaluates a filter expression
453
+
func (e *Evaluator) evalFilter(f *FilterExpr) (any, error) {
454
+
input, err := e.evalExpr(f.Input)
455
+
if err != nil {
456
+
return nil, err
457
+
}
458
+
459
+
inputStr := e.toString(input)
460
+
461
+
filter, ok := e.renderer.getFilter(f.Filter)
462
+
if !ok {
463
+
return inputStr + fmt.Sprintf("[Filter '%s' not found]", f.Filter), nil
464
+
}
465
+
466
+
var args []string
467
+
for _, arg := range f.Args {
468
+
val, err := e.evalExpr(arg)
469
+
if err != nil {
470
+
return nil, err
471
+
}
472
+
args = append(args, e.toString(val))
473
+
}
474
+
475
+
return filter(inputStr, args...), nil
476
+
}
477
+
478
+
// resolveIdent resolves a dot-notation identifier
479
+
func (e *Evaluator) resolveIdent(parts []string) (any, error) {
480
+
if len(parts) == 0 {
481
+
return nil, nil
482
+
}
483
+
484
+
// Get the root variable
485
+
val, ok := e.vars[parts[0]]
486
+
487
+
// Handle .exists virtual method at top level
488
+
if len(parts) == 2 && parts[1] == "exists" {
489
+
return ok, nil
490
+
}
491
+
492
+
// Handle .defined virtual method at top level
493
+
if len(parts) == 2 && parts[1] == "defined" {
494
+
if !ok {
495
+
return false, nil
496
+
}
497
+
return e.isDefined(val), nil
498
+
}
499
+
500
+
if !ok {
501
+
return nil, nil
502
+
}
503
+
504
+
if len(parts) == 1 {
505
+
return val, nil
506
+
}
507
+
508
+
// Navigate through the remaining parts
509
+
for i := 1; i < len(parts); i++ {
510
+
property := parts[i]
511
+
512
+
// Virtual methods
513
+
if property == "exists" {
514
+
return true, nil
515
+
}
516
+
if property == "defined" {
517
+
return e.isDefined(val), nil
518
+
}
519
+
520
+
// Check custom virtual methods
521
+
if method, ok := e.renderer.getVirtualMethod(property); ok {
522
+
result, _ := method(val)
523
+
val = result
524
+
continue
525
+
}
526
+
527
+
// Built-in virtual methods
528
+
switch property {
529
+
case "length", "size":
530
+
switch v := val.(type) {
531
+
case string:
532
+
return len(v), nil
533
+
case map[string]any:
534
+
return len(v), nil
535
+
default:
536
+
rv := reflect.ValueOf(val)
537
+
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
538
+
return rv.Len(), nil
539
+
}
540
+
return nil, nil
541
+
}
542
+
543
+
case "first":
544
+
rv := reflect.ValueOf(val)
545
+
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
546
+
if rv.Len() > 0 {
547
+
val = rv.Index(0).Interface()
548
+
continue
549
+
}
550
+
}
551
+
return nil, nil
552
+
553
+
case "last":
554
+
rv := reflect.ValueOf(val)
555
+
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
556
+
if rv.Len() > 0 {
557
+
val = rv.Index(rv.Len() - 1).Interface()
558
+
continue
559
+
}
560
+
}
561
+
return nil, nil
562
+
563
+
default:
564
+
// Try map access
565
+
if m, ok := val.(map[string]any); ok {
566
+
if v, exists := m[property]; exists {
567
+
val = v
568
+
continue
569
+
}
570
+
return nil, nil
571
+
}
572
+
573
+
// Try struct field access via reflection
574
+
rv := reflect.ValueOf(val)
575
+
if rv.Kind() == reflect.Ptr {
576
+
rv = rv.Elem()
577
+
}
578
+
if rv.Kind() == reflect.Struct {
579
+
field := rv.FieldByName(property)
580
+
if field.IsValid() {
581
+
val = field.Interface()
582
+
continue
583
+
}
584
+
}
585
+
586
+
return nil, nil
587
+
}
588
+
}
589
+
590
+
return val, nil
591
+
}
592
+
593
+
// ---- Helper methods ----
594
+
595
+
// withVar creates a new evaluator with an additional variable
596
+
func (e *Evaluator) withVar(name string, value any) *Evaluator {
597
+
newVars := e.copyVars()
598
+
newVars[name] = value
599
+
eval := NewEvaluator(e.renderer, newVars)
600
+
for k, v := range e.blocks {
601
+
eval.blocks[k] = v
602
+
}
603
+
return eval
604
+
}
605
+
606
+
// copyVars creates a shallow copy of the variables map
607
+
func (e *Evaluator) copyVars() map[string]any {
608
+
newVars := make(map[string]any, len(e.vars))
609
+
for k, v := range e.vars {
610
+
newVars[k] = v
611
+
}
612
+
return newVars
613
+
}
614
+
615
+
// isTruthy determines if a value is considered "true"
616
+
func (e *Evaluator) isTruthy(val any) bool {
617
+
if val == nil {
618
+
return false
619
+
}
620
+
621
+
switch v := val.(type) {
622
+
case bool:
623
+
return v
624
+
case string:
625
+
return v != ""
626
+
case int:
627
+
return v != 0
628
+
case int64:
629
+
return v != 0
630
+
case float64:
631
+
return v != 0
632
+
case float32:
633
+
return v != 0
634
+
default:
635
+
rv := reflect.ValueOf(val)
636
+
switch rv.Kind() {
637
+
case reflect.Slice, reflect.Array:
638
+
return rv.Len() > 0
639
+
case reflect.Map:
640
+
return rv.Len() > 0
641
+
}
642
+
return true
643
+
}
644
+
}
645
+
646
+
// isDefined checks if a value is "defined" (non-nil and non-empty for strings)
647
+
func (e *Evaluator) isDefined(val any) bool {
648
+
if val == nil {
649
+
return false
650
+
}
651
+
652
+
switch v := val.(type) {
653
+
case string:
654
+
return v != ""
655
+
case map[string]any:
656
+
return v != nil
657
+
default:
658
+
rv := reflect.ValueOf(val)
659
+
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
660
+
return !rv.IsNil()
661
+
}
662
+
return true
663
+
}
664
+
}
665
+
666
+
// isString checks if a value is a string
667
+
func (e *Evaluator) isString(v any) bool {
668
+
_, ok := v.(string)
669
+
return ok
670
+
}
671
+
672
+
// toString converts a value to string
673
+
func (e *Evaluator) toString(v any) string {
674
+
switch val := v.(type) {
675
+
case string:
676
+
return val
677
+
case nil:
678
+
return ""
679
+
case bool:
680
+
if val {
681
+
return "true"
682
+
}
683
+
return "false"
684
+
case float64:
685
+
if val == float64(int64(val)) {
686
+
return strconv.FormatInt(int64(val), 10)
687
+
}
688
+
return strconv.FormatFloat(val, 'f', -1, 64)
689
+
case int:
690
+
return strconv.Itoa(val)
691
+
case int64:
692
+
return strconv.FormatInt(val, 10)
693
+
default:
694
+
return fmt.Sprintf("%v", val)
695
+
}
696
+
}
697
+
698
+
// toFloat converts a value to float64
699
+
func (e *Evaluator) toFloat(v any) float64 {
700
+
switch val := v.(type) {
701
+
case float64:
702
+
return val
703
+
case float32:
704
+
return float64(val)
705
+
case int:
706
+
return float64(val)
707
+
case int64:
708
+
return float64(val)
709
+
case int32:
710
+
return float64(val)
711
+
case string:
712
+
f, _ := strconv.ParseFloat(val, 64)
713
+
return f
714
+
default:
715
+
return 0
716
+
}
717
+
}
718
+
719
+
// equals compares two values for equality
720
+
func (e *Evaluator) equals(left, right any) bool {
721
+
// Try numeric comparison
722
+
leftNum, leftOk := e.tryFloat(left)
723
+
rightNum, rightOk := e.tryFloat(right)
724
+
if leftOk && rightOk {
725
+
return leftNum == rightNum
726
+
}
727
+
728
+
// Fall back to string comparison
729
+
return fmt.Sprintf("%v", left) == fmt.Sprintf("%v", right)
730
+
}
731
+
732
+
// tryFloat attempts to convert a value to float64
733
+
func (e *Evaluator) tryFloat(v any) (float64, bool) {
734
+
switch val := v.(type) {
735
+
case float64:
736
+
return val, true
737
+
case float32:
738
+
return float64(val), true
739
+
case int:
740
+
return float64(val), true
741
+
case int64:
742
+
return float64(val), true
743
+
case int32:
744
+
return float64(val), true
745
+
case string:
746
+
if f, err := strconv.ParseFloat(val, 64); err == nil {
747
+
return f, true
748
+
}
749
+
}
750
+
return 0, false
751
+
}
752
+
753
+
// ---- Error type ----
754
+
755
+
// EvalError represents an error during evaluation
756
+
type EvalError struct {
757
+
Pos Position
758
+
Message string
759
+
}
760
+
761
+
func (e *EvalError) Error() string {
762
+
return fmt.Sprintf("line %d, column %d: %s", e.Pos.Line, e.Pos.Column, e.Message)
763
+
}
764
+
765
+
// ParseError represents an error during parsing
766
+
type ParseError struct {
767
+
Pos Position
768
+
Message string
769
+
}
770
+
771
+
func (e *ParseError) Error() string {
772
+
return fmt.Sprintf("parse error at line %d, column %d: %s", e.Pos.Line, e.Pos.Column, e.Message)
773
+
}
+5
go.mod
+5
go.mod
+215
gott.go
+215
gott.go
···
1
+
package gott
2
+
3
+
import (
4
+
"encoding/json"
5
+
"errors"
6
+
"html"
7
+
"io/fs"
8
+
"net/http"
9
+
"net/url"
10
+
"strings"
11
+
"sync"
12
+
)
13
+
14
+
// Renderer is the main template engine. It processes TT2-style templates
15
+
// with support for blocks, includes, filters, and virtual methods.
16
+
type Renderer struct {
17
+
mu sync.RWMutex
18
+
includePaths []fs.FS // filesystems for INCLUDE lookups, searched in order
19
+
filters map[string]func(string, ...string) string // text transformation filters
20
+
virtualMethods map[string]func(any) (any, bool) // custom virtual methods for variables
21
+
}
22
+
23
+
// Config holds initialization options for creating a new Renderer.
24
+
type Config struct {
25
+
IncludePaths []fs.FS // filesystems for template includes (e.g., os.DirFS, embed.FS)
26
+
Filters map[string]func(string, ...string) string // custom filters to register
27
+
}
28
+
29
+
var ErrTemplateNotFound = errors.New("template not found")
30
+
31
+
// New creates a Renderer with the given config. Registers default filters
32
+
// (upper, lower, html, uri) unless overridden in config.
33
+
func New(config *Config) *Renderer {
34
+
r := &Renderer{
35
+
filters: make(map[string]func(string, ...string) string),
36
+
virtualMethods: make(map[string]func(any) (any, bool)),
37
+
}
38
+
39
+
if config != nil {
40
+
r.includePaths = config.IncludePaths
41
+
if config.Filters != nil {
42
+
for k, v := range config.Filters {
43
+
r.filters[k] = v
44
+
}
45
+
}
46
+
}
47
+
48
+
// Default filters
49
+
if r.filters["upper"] == nil {
50
+
r.filters["upper"] = func(s string, args ...string) string {
51
+
return strings.ToUpper(s)
52
+
}
53
+
}
54
+
if r.filters["lower"] == nil {
55
+
r.filters["lower"] = func(s string, args ...string) string {
56
+
return strings.ToLower(s)
57
+
}
58
+
}
59
+
if r.filters["html"] == nil {
60
+
r.filters["html"] = func(s string, args ...string) string {
61
+
return html.EscapeString(s)
62
+
}
63
+
}
64
+
if r.filters["uri"] == nil {
65
+
r.filters["uri"] = func(s string, args ...string) string {
66
+
return url.QueryEscape(s)
67
+
}
68
+
}
69
+
70
+
return r
71
+
}
72
+
73
+
// AddFilter registers a custom filter function. Returns the Renderer for chaining.
74
+
// Note: This should only be called during initialization, not concurrently.
75
+
func (r *Renderer) AddFilter(name string, fn func(string, ...string) string) *Renderer {
76
+
r.mu.Lock()
77
+
defer r.mu.Unlock()
78
+
r.filters[name] = fn
79
+
return r
80
+
}
81
+
82
+
// AddVirtualMethod registers a custom virtual method for variable access.
83
+
// The function receives a value and returns (result, ok).
84
+
// Note: This should only be called during initialization, not concurrently.
85
+
func (r *Renderer) AddVirtualMethod(name string, fn func(any) (any, bool)) *Renderer {
86
+
r.mu.Lock()
87
+
defer r.mu.Unlock()
88
+
r.virtualMethods[name] = fn
89
+
return r
90
+
}
91
+
92
+
// getFilter safely retrieves a filter
93
+
func (r *Renderer) getFilter(name string) (func(string, ...string) string, bool) {
94
+
r.mu.RLock()
95
+
defer r.mu.RUnlock()
96
+
fn, ok := r.filters[name]
97
+
return fn, ok
98
+
}
99
+
100
+
// getVirtualMethod safely retrieves a virtual method
101
+
func (r *Renderer) getVirtualMethod(name string) (func(any) (any, bool), bool) {
102
+
r.mu.RLock()
103
+
defer r.mu.RUnlock()
104
+
fn, ok := r.virtualMethods[name]
105
+
return fn, ok
106
+
}
107
+
108
+
// loadFile attempts to load a template file from the configured filesystems.
109
+
// Searches each fs.FS in IncludePaths order, returning the first match.
110
+
// Returns ErrTemplateNotFound if not found in any filesystem.
111
+
func (r *Renderer) loadFile(name string) (string, error) {
112
+
r.mu.RLock()
113
+
paths := r.includePaths
114
+
r.mu.RUnlock()
115
+
116
+
for _, fsys := range paths {
117
+
data, err := fs.ReadFile(fsys, name)
118
+
if err == nil {
119
+
return string(data), nil
120
+
}
121
+
}
122
+
return "", ErrTemplateNotFound
123
+
}
124
+
125
+
// ProcessFile loads a template file and processes it with the given variables.
126
+
func (r *Renderer) ProcessFile(filename string, vars map[string]any) (string, error) {
127
+
content, err := r.loadFile(filename)
128
+
if err != nil {
129
+
return "", err
130
+
}
131
+
return r.Process(content, vars)
132
+
}
133
+
134
+
// Process processes a template string with the given variables.
135
+
// Uses an AST-based parser and evaluator for template processing.
136
+
func (r *Renderer) Process(input string, vars map[string]any) (string, error) {
137
+
// Parse the template into an AST
138
+
parser := NewParser(input)
139
+
tmpl, parseErrs := parser.Parse()
140
+
if len(parseErrs) > 0 {
141
+
return "", parseErrs[0]
142
+
}
143
+
144
+
// Evaluate the AST
145
+
eval := NewEvaluator(r, vars)
146
+
return eval.Eval(tmpl)
147
+
}
148
+
149
+
// Render processes a template file and writes the result to an http.ResponseWriter.
150
+
// Sets Content-Type to text/html and handles 404/500 errors appropriately.
151
+
func (r *Renderer) Render(w http.ResponseWriter, filename string, vars map[string]any) {
152
+
output, err := r.ProcessFile(filename, vars)
153
+
if err != nil {
154
+
if errors.Is(err, ErrTemplateNotFound) {
155
+
http.Error(w, "404 Not Found", http.StatusNotFound)
156
+
} else {
157
+
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
158
+
}
159
+
return
160
+
}
161
+
162
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
163
+
w.WriteHeader(http.StatusOK)
164
+
w.Write([]byte(output))
165
+
}
166
+
167
+
// RenderWithStatus processes a template file and writes it with a custom status code.
168
+
func (r *Renderer) RenderWithStatus(w http.ResponseWriter, statusCode int, filename string, vars map[string]any) {
169
+
output, err := r.ProcessFile(filename, vars)
170
+
if err != nil {
171
+
if errors.Is(err, ErrTemplateNotFound) {
172
+
http.Error(w, "404 Not Found", http.StatusNotFound)
173
+
} else {
174
+
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
175
+
}
176
+
return
177
+
}
178
+
179
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
180
+
w.WriteHeader(statusCode)
181
+
w.Write([]byte(output))
182
+
}
183
+
184
+
// Render204 writes a 204 No Content response.
185
+
func (r *Renderer) Render204(w http.ResponseWriter) {
186
+
w.WriteHeader(204)
187
+
w.Write([]byte(""))
188
+
}
189
+
190
+
// RenderJSON serializes json_data to JSON and writes it to the http.ResponseWriter.
191
+
// Sets Content-Type to application/json. Returns 500 error on serialization failure.
192
+
func (r *Renderer) RenderJSON(w http.ResponseWriter, json_data any) {
193
+
data, err := json.Marshal(json_data)
194
+
if err != nil {
195
+
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
196
+
return
197
+
}
198
+
199
+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
200
+
w.WriteHeader(http.StatusOK)
201
+
w.Write(data)
202
+
}
203
+
204
+
// RenderJSONWithStatus serializes json_data and writes it with a custom status code.
205
+
func (r *Renderer) RenderJSONWithStatus(w http.ResponseWriter, statusCode int, json_data any) {
206
+
data, err := json.Marshal(json_data)
207
+
if err != nil {
208
+
http.Error(w, "500 Internal Server Error", http.StatusInternalServerError)
209
+
return
210
+
}
211
+
212
+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
213
+
w.WriteHeader(statusCode)
214
+
w.Write(data)
215
+
}
+410
gott_test.go
+410
gott_test.go
···
1
+
package gott
2
+
3
+
import (
4
+
"testing"
5
+
)
6
+
7
+
// just a random struct for testing
8
+
type TestUser struct {
9
+
Handle string
10
+
DisplayName string
11
+
Image string
12
+
}
13
+
14
+
// TestNewFeatures tests ELSIF, arithmetic, string concatenation, and map iteration
15
+
func TestNewFeatures(t *testing.T) {
16
+
r := New(nil)
17
+
18
+
tests := []struct {
19
+
name string
20
+
template string
21
+
vars map[string]any
22
+
want string
23
+
}{
24
+
// ELSIF tests
25
+
{
26
+
name: "ELSIF - first branch",
27
+
template: "[% IF status == 'active' %]active[% ELSIF status == 'pending' %]pending[% ELSE %]other[% END %]",
28
+
vars: map[string]any{"status": "active"},
29
+
want: "active",
30
+
},
31
+
{
32
+
name: "ELSIF - second branch",
33
+
template: "[% IF status == 'active' %]active[% ELSIF status == 'pending' %]pending[% ELSE %]other[% END %]",
34
+
vars: map[string]any{"status": "pending"},
35
+
want: "pending",
36
+
},
37
+
{
38
+
name: "ELSIF - else branch",
39
+
template: "[% IF status == 'active' %]active[% ELSIF status == 'pending' %]pending[% ELSE %]other[% END %]",
40
+
vars: map[string]any{"status": "inactive"},
41
+
want: "other",
42
+
},
43
+
{
44
+
name: "ELSIF - multiple elsif",
45
+
template: "[% IF n == 1 %]one[% ELSIF n == 2 %]two[% ELSIF n == 3 %]three[% ELSE %]many[% END %]",
46
+
vars: map[string]any{"n": 3},
47
+
want: "three",
48
+
},
49
+
// Arithmetic tests
50
+
{
51
+
name: "arithmetic - addition",
52
+
template: "[% a + b %]",
53
+
vars: map[string]any{"a": 3, "b": 4},
54
+
want: "7",
55
+
},
56
+
{
57
+
name: "arithmetic - subtraction",
58
+
template: "[% a - b %]",
59
+
vars: map[string]any{"a": 10, "b": 3},
60
+
want: "7",
61
+
},
62
+
{
63
+
name: "arithmetic - multiplication",
64
+
template: "[% a * b %]",
65
+
vars: map[string]any{"a": 3, "b": 4},
66
+
want: "12",
67
+
},
68
+
{
69
+
name: "arithmetic - division",
70
+
template: "[% a / b %]",
71
+
vars: map[string]any{"a": 12, "b": 4},
72
+
want: "3",
73
+
},
74
+
{
75
+
name: "arithmetic - modulo",
76
+
template: "[% a % b %]",
77
+
vars: map[string]any{"a": 10, "b": 3},
78
+
want: "1",
79
+
},
80
+
{
81
+
name: "arithmetic - complex expression",
82
+
template: "[% (a + b) * c %]",
83
+
vars: map[string]any{"a": 2, "b": 3, "c": 4},
84
+
want: "20",
85
+
},
86
+
{
87
+
name: "arithmetic - in condition",
88
+
template: "[% IF count + 1 > 5 %]yes[% END %]",
89
+
vars: map[string]any{"count": 5},
90
+
want: "yes",
91
+
},
92
+
// String concatenation tests
93
+
{
94
+
name: "string concat - two strings",
95
+
template: "[% a + b %]",
96
+
vars: map[string]any{"a": "hello", "b": "world"},
97
+
want: "helloworld",
98
+
},
99
+
{
100
+
name: "string concat - string and literal",
101
+
template: `[% name + "-suffix" %]`,
102
+
vars: map[string]any{"name": "test"},
103
+
want: "test-suffix",
104
+
},
105
+
{
106
+
name: "string concat - number to string",
107
+
template: `[% "version-" + version %]`,
108
+
vars: map[string]any{"version": 3},
109
+
want: "version-3",
110
+
},
111
+
{
112
+
name: "string concat - in condition",
113
+
template: `[% IF name + "-dev" == "test-dev" %]match[% END %]`,
114
+
vars: map[string]any{"name": "test"},
115
+
want: "match",
116
+
},
117
+
{
118
+
name: "string concat - chained",
119
+
template: `[% "Hello, " + name + "!" %]`,
120
+
vars: map[string]any{"name": "World"},
121
+
want: "Hello, World!",
122
+
},
123
+
// String quote styles
124
+
{
125
+
name: "single quotes in comparison",
126
+
template: "[% IF status == 'active' %]yes[% END %]",
127
+
vars: map[string]any{"status": "active"},
128
+
want: "yes",
129
+
},
130
+
{
131
+
name: "double quotes in comparison",
132
+
template: `[% IF status == "active" %]yes[% END %]`,
133
+
vars: map[string]any{"status": "active"},
134
+
want: "yes",
135
+
},
136
+
// Map iteration tests
137
+
{
138
+
name: "map iteration - key and value",
139
+
template: "[% FOREACH entry IN config %][% entry.key %]=[% entry.value %];[% END %]",
140
+
vars: map[string]any{"config": map[string]any{"host": "localhost", "port": 8080}},
141
+
want: "host=localhost;port=8080;",
142
+
},
143
+
{
144
+
name: "map iteration - sorted by key",
145
+
template: "[% FOREACH e IN data %][% e.key %][% END %]",
146
+
vars: map[string]any{"data": map[string]any{"c": 3, "a": 1, "b": 2}},
147
+
want: "abc",
148
+
},
149
+
// SET directive tests
150
+
{
151
+
name: "SET - simple assignment",
152
+
template: "[% SET x = 42 %][% x %]",
153
+
vars: map[string]any{},
154
+
want: "42",
155
+
},
156
+
{
157
+
name: "SET - string assignment",
158
+
template: `[% SET greeting = "Hello" %][% greeting %]`,
159
+
vars: map[string]any{},
160
+
want: "Hello",
161
+
},
162
+
{
163
+
name: "SET - expression",
164
+
template: "[% SET total = a + b %][% total %]",
165
+
vars: map[string]any{"a": 10, "b": 5},
166
+
want: "15",
167
+
},
168
+
{
169
+
name: "SET - use in condition",
170
+
template: "[% SET threshold = 10 %][% IF count > threshold %]high[% ELSE %]low[% END %]",
171
+
vars: map[string]any{"count": 15},
172
+
want: "high",
173
+
},
174
+
// Default value tests
175
+
{
176
+
name: "default - with undefined var",
177
+
template: `[% name || "guest" %]`,
178
+
vars: map[string]any{},
179
+
want: "guest",
180
+
},
181
+
{
182
+
name: "default - with defined var",
183
+
template: `[% name || "guest" %]`,
184
+
vars: map[string]any{"name": "alice"},
185
+
want: "alice",
186
+
},
187
+
// Filter tests
188
+
{
189
+
name: "filter - upper",
190
+
template: "[% name | upper %]",
191
+
vars: map[string]any{"name": "hello"},
192
+
want: "HELLO",
193
+
},
194
+
{
195
+
name: "filter - chain",
196
+
template: "[% text | lower | upper %]",
197
+
vars: map[string]any{"text": "Hello"},
198
+
want: "HELLO",
199
+
},
200
+
// FOREACH with slice
201
+
{
202
+
name: "foreach - slice",
203
+
template: "[% FOREACH item IN items %][% item %],[% END %]",
204
+
vars: map[string]any{"items": []string{"a", "b", "c"}},
205
+
want: "a,b,c,",
206
+
},
207
+
// Virtual methods
208
+
{
209
+
name: "vmethod - length",
210
+
template: "[% items.length %]",
211
+
vars: map[string]any{"items": []int{1, 2, 3, 4, 5}},
212
+
want: "5",
213
+
},
214
+
{
215
+
name: "vmethod - first",
216
+
template: "[% items.first %]",
217
+
vars: map[string]any{"items": []string{"a", "b", "c"}},
218
+
want: "a",
219
+
},
220
+
{
221
+
name: "vmethod - last",
222
+
template: "[% items.last %]",
223
+
vars: map[string]any{"items": []string{"a", "b", "c"}},
224
+
want: "c",
225
+
},
226
+
}
227
+
228
+
for _, tt := range tests {
229
+
t.Run(tt.name, func(t *testing.T) {
230
+
got, err := r.Process(tt.template, tt.vars)
231
+
if err != nil {
232
+
t.Fatalf("Process() error = %v", err)
233
+
}
234
+
if got != tt.want {
235
+
t.Errorf("Process() = %q, want %q", got, tt.want)
236
+
}
237
+
})
238
+
}
239
+
}
240
+
241
+
func TestSimpleIfCondition(t *testing.T) {
242
+
r := New(nil)
243
+
244
+
tests := []struct {
245
+
name string
246
+
template string
247
+
vars map[string]any
248
+
want string
249
+
}{
250
+
{
251
+
name: "simple IF true",
252
+
template: "[% IF user %]yes[% END %]",
253
+
vars: map[string]any{"user": "test"},
254
+
want: "yes",
255
+
},
256
+
{
257
+
name: "simple IF false - missing var",
258
+
template: "[% IF user %]yes[% END %]",
259
+
vars: map[string]any{},
260
+
want: "",
261
+
},
262
+
{
263
+
name: "IF with defined",
264
+
template: "[% IF user.defined %]yes[% END %]",
265
+
vars: map[string]any{"user": "test"},
266
+
want: "yes",
267
+
},
268
+
{
269
+
name: "IF with nested defined",
270
+
template: "[% IF user.Image.defined %]yes[% END %]",
271
+
vars: map[string]any{"user": map[string]any{"Image": "http://example.com/img.jpg"}},
272
+
want: "yes",
273
+
},
274
+
{
275
+
name: "IF with AND - both true",
276
+
template: "[% IF a && b %]yes[% END %]",
277
+
vars: map[string]any{"a": true, "b": true},
278
+
want: "yes",
279
+
},
280
+
{
281
+
name: "IF with AND - one false",
282
+
template: "[% IF a && b %]yes[% END %]",
283
+
vars: map[string]any{"a": true, "b": false},
284
+
want: "",
285
+
},
286
+
{
287
+
name: "IF with OR - both true",
288
+
template: "[% IF a || b %]yes[% END %]",
289
+
vars: map[string]any{"a": true, "b": true},
290
+
want: "yes",
291
+
},
292
+
{
293
+
name: "IF with OR - one true",
294
+
template: "[% IF a || b %]yes[% END %]",
295
+
vars: map[string]any{"a": false, "b": true},
296
+
want: "yes",
297
+
},
298
+
{
299
+
name: "IF with struct user",
300
+
template: "[% IF user %]yes[% END %]",
301
+
vars: map[string]any{"user": TestUser{Handle: "test"}},
302
+
want: "yes",
303
+
},
304
+
{
305
+
name: "IF with struct Image.defined",
306
+
template: "[% IF user.Image.defined %]yes[% END %]",
307
+
vars: map[string]any{"user": TestUser{Image: "http://example.com/img.jpg"}},
308
+
want: "yes",
309
+
},
310
+
{
311
+
name: "IF with struct Image.defined - empty",
312
+
template: "[% IF user.Image.defined %]yes[% END %]",
313
+
vars: map[string]any{"user": TestUser{}},
314
+
want: "",
315
+
},
316
+
// Comparison operators
317
+
{
318
+
name: "IF greater than - true",
319
+
template: "[% IF count > 0 %]yes[% END %]",
320
+
vars: map[string]any{"count": 5},
321
+
want: "yes",
322
+
},
323
+
{
324
+
name: "IF greater than - false",
325
+
template: "[% IF count > 0 %]yes[% END %]",
326
+
vars: map[string]any{"count": 0},
327
+
want: "",
328
+
},
329
+
{
330
+
name: "IF equals string",
331
+
template: "[% IF status == 'active' %]yes[% END %]",
332
+
vars: map[string]any{"status": "active"},
333
+
want: "yes",
334
+
},
335
+
{
336
+
name: "IF equals string - false",
337
+
template: "[% IF status == 'active' %]yes[% END %]",
338
+
vars: map[string]any{"status": "inactive"},
339
+
want: "",
340
+
},
341
+
{
342
+
name: "IF not equals",
343
+
template: "[% IF status != 'error' %]yes[% END %]",
344
+
vars: map[string]any{"status": "ok"},
345
+
want: "yes",
346
+
},
347
+
{
348
+
name: "IF greater than or equal",
349
+
template: "[% IF count >= 5 %]yes[% END %]",
350
+
vars: map[string]any{"count": 5},
351
+
want: "yes",
352
+
},
353
+
{
354
+
name: "IF less than",
355
+
template: "[% IF count < 10 %]yes[% END %]",
356
+
vars: map[string]any{"count": 5},
357
+
want: "yes",
358
+
},
359
+
{
360
+
name: "IF combined AND with comparison",
361
+
template: "[% IF enabled && count > 0 %]yes[% END %]",
362
+
vars: map[string]any{"enabled": true, "count": 5},
363
+
want: "yes",
364
+
},
365
+
{
366
+
name: "nested IF with defined",
367
+
template: "[% IF user.defined %]outer[% IF user.Image.defined %]inner[% END %][% END %]",
368
+
vars: map[string]any{"user": map[string]any{"Image": "test.jpg"}},
369
+
want: "outerinner",
370
+
},
371
+
{
372
+
name: "IF with ELSE",
373
+
template: "[% IF user.Image.defined %]has-image[% ELSE %]no-image[% END %]",
374
+
vars: map[string]any{"user": map[string]any{"Image": ""}},
375
+
want: "no-image",
376
+
},
377
+
{
378
+
name: "multiline nested IF like user_detail",
379
+
template: `<div>
380
+
[% IF user.defined %]
381
+
<div class="outer">
382
+
[% IF user.Image.defined %]
383
+
<img src="[% user.Image %]">
384
+
[% ELSE %]
385
+
<div>placeholder</div>
386
+
[% END %]
387
+
</div>
388
+
[% END %]
389
+
</div>`,
390
+
vars: map[string]any{"user": map[string]any{"Image": "test.jpg"}},
391
+
want: "<div>\n \n <div class=\"outer\">\n \n <img src=\"test.jpg\">\n \n </div>\n \n</div>",
392
+
},
393
+
}
394
+
395
+
for _, tt := range tests {
396
+
t.Run(tt.name, func(t *testing.T) {
397
+
got, err := r.Process(tt.template, tt.vars)
398
+
if err != nil {
399
+
t.Fatalf("Process() error = %v", err)
400
+
}
401
+
if got != tt.want {
402
+
t.Errorf("Process() = %q, want %q", got, tt.want)
403
+
if tt.name == "multiline nested IF like user_detail" {
404
+
t.Logf("Template:\n%s", tt.template)
405
+
t.Logf("Result:\n%s", got)
406
+
}
407
+
}
408
+
})
409
+
}
410
+
}
+384
lexer.go
+384
lexer.go
···
1
+
package gott
2
+
3
+
import (
4
+
"strings"
5
+
"unicode"
6
+
"unicode/utf8"
7
+
)
8
+
9
+
const eof = -1
10
+
11
+
// Lexer tokenizes a template string into a sequence of tokens
12
+
type Lexer struct {
13
+
input string // the string being scanned
14
+
start int // start position of current token
15
+
pos int // current position in input
16
+
width int // width of last rune read
17
+
line int // current line number (1-based)
18
+
linePos int // position of start of current line
19
+
tokens chan Token // channel of scanned tokens
20
+
}
21
+
22
+
// stateFn represents a lexer state function; returns the next state
23
+
type stateFn func(*Lexer) stateFn
24
+
25
+
// NewLexer creates a new lexer for the given input and starts scanning
26
+
func NewLexer(input string) *Lexer {
27
+
l := &Lexer{
28
+
input: input,
29
+
line: 1,
30
+
linePos: 0,
31
+
tokens: make(chan Token, 2),
32
+
}
33
+
go l.run()
34
+
return l
35
+
}
36
+
37
+
// run executes the state machine
38
+
func (l *Lexer) run() {
39
+
for state := lexText; state != nil; {
40
+
state = state(l)
41
+
}
42
+
close(l.tokens)
43
+
}
44
+
45
+
// NextToken returns the next token from the lexer
46
+
func (l *Lexer) NextToken() Token {
47
+
return <-l.tokens
48
+
}
49
+
50
+
// Tokens returns all tokens as a slice (consumes the channel)
51
+
func (l *Lexer) Tokens() []Token {
52
+
var tokens []Token
53
+
for tok := range l.tokens {
54
+
tokens = append(tokens, tok)
55
+
}
56
+
return tokens
57
+
}
58
+
59
+
// emit sends a token to the tokens channel
60
+
func (l *Lexer) emit(t TokenType) {
61
+
l.tokens <- Token{
62
+
Type: t,
63
+
Value: l.input[l.start:l.pos],
64
+
Pos: l.currentPos(),
65
+
}
66
+
l.start = l.pos
67
+
}
68
+
69
+
// emitValue sends a token with a specific value
70
+
func (l *Lexer) emitValue(t TokenType, value string) {
71
+
l.tokens <- Token{
72
+
Type: t,
73
+
Value: value,
74
+
Pos: l.currentPos(),
75
+
}
76
+
l.start = l.pos
77
+
}
78
+
79
+
// currentPos returns the current position for error reporting
80
+
func (l *Lexer) currentPos() Position {
81
+
return Position{
82
+
Line: l.line,
83
+
Column: l.start - l.linePos + 1,
84
+
Offset: l.start,
85
+
}
86
+
}
87
+
88
+
// next returns the next rune in the input and advances the position
89
+
func (l *Lexer) next() rune {
90
+
if l.pos >= len(l.input) {
91
+
l.width = 0
92
+
return eof
93
+
}
94
+
r, w := utf8.DecodeRuneInString(l.input[l.pos:])
95
+
l.width = w
96
+
l.pos += l.width
97
+
if r == '\n' {
98
+
l.line++
99
+
l.linePos = l.pos
100
+
}
101
+
return r
102
+
}
103
+
104
+
// backup steps back one rune (can only be called once per next call)
105
+
func (l *Lexer) backup() {
106
+
l.pos -= l.width
107
+
// If we backed up over a newline, decrement line count
108
+
if l.pos < len(l.input) && l.input[l.pos] == '\n' {
109
+
l.line--
110
+
// Find previous line start
111
+
l.linePos = strings.LastIndex(l.input[:l.pos], "\n") + 1
112
+
}
113
+
}
114
+
115
+
// peek returns the next rune without advancing
116
+
func (l *Lexer) peek() rune {
117
+
r := l.next()
118
+
l.backup()
119
+
return r
120
+
}
121
+
122
+
// ignore skips over the pending input
123
+
func (l *Lexer) ignore() {
124
+
l.start = l.pos
125
+
}
126
+
127
+
// errorf emits an error token and terminates scanning
128
+
func (l *Lexer) errorf(format string, args ...any) stateFn {
129
+
l.tokens <- Token{
130
+
Type: TokenError,
131
+
Value: sprintf(format, args...),
132
+
Pos: l.currentPos(),
133
+
}
134
+
return nil
135
+
}
136
+
137
+
// sprintf is a simple formatter (avoiding fmt import in hot path)
138
+
func sprintf(format string, args ...any) string {
139
+
// Simple implementation - for errors only
140
+
result := format
141
+
for _, arg := range args {
142
+
if s, ok := arg.(string); ok {
143
+
result = strings.Replace(result, "%s", s, 1)
144
+
result = strings.Replace(result, "%q", "'"+s+"'", 1)
145
+
}
146
+
}
147
+
return result
148
+
}
149
+
150
+
// skipWhitespace advances past any whitespace characters
151
+
func (l *Lexer) skipWhitespace() {
152
+
for {
153
+
r := l.next()
154
+
if r == eof || !unicode.IsSpace(r) {
155
+
l.backup()
156
+
break
157
+
}
158
+
}
159
+
l.ignore()
160
+
}
161
+
162
+
// hasPrefix checks if the remaining input starts with the given prefix
163
+
func (l *Lexer) hasPrefix(prefix string) bool {
164
+
return strings.HasPrefix(l.input[l.pos:], prefix)
165
+
}
166
+
167
+
// ---- State Functions ----
168
+
169
+
// lexText scans text outside of tags until [% or EOF
170
+
func lexText(l *Lexer) stateFn {
171
+
for {
172
+
if l.hasPrefix("[%") {
173
+
if l.pos > l.start {
174
+
l.emit(TokenText)
175
+
}
176
+
return lexTagOpen
177
+
}
178
+
if l.next() == eof {
179
+
break
180
+
}
181
+
}
182
+
// Emit any remaining text
183
+
if l.pos > l.start {
184
+
l.emit(TokenText)
185
+
}
186
+
l.emit(TokenEOF)
187
+
return nil
188
+
}
189
+
190
+
// lexTagOpen scans the [% opening delimiter
191
+
func lexTagOpen(l *Lexer) stateFn {
192
+
l.pos += 2 // skip [%
193
+
l.emit(TokenTagOpen)
194
+
return lexInsideTag
195
+
}
196
+
197
+
// lexInsideTag scans inside a [% ... %] tag
198
+
func lexInsideTag(l *Lexer) stateFn {
199
+
l.skipWhitespace()
200
+
201
+
// Check for closing tag
202
+
if l.hasPrefix("%]") {
203
+
l.pos += 2
204
+
l.emit(TokenTagClose)
205
+
return lexText
206
+
}
207
+
208
+
// Check for two-character operators first
209
+
twoCharOps := []struct {
210
+
str string
211
+
tok TokenType
212
+
}{
213
+
{"==", TokenEq},
214
+
{"!=", TokenNe},
215
+
{">=", TokenGe},
216
+
{"<=", TokenLe},
217
+
{"&&", TokenAnd},
218
+
{"||", TokenOr},
219
+
}
220
+
for _, op := range twoCharOps {
221
+
if l.hasPrefix(op.str) {
222
+
l.pos += 2
223
+
l.emit(op.tok)
224
+
return lexInsideTag
225
+
}
226
+
}
227
+
228
+
// Check for single-character operators/delimiters
229
+
r := l.next()
230
+
switch r {
231
+
case eof:
232
+
return l.errorf("unclosed tag")
233
+
case '>':
234
+
l.emit(TokenGt)
235
+
return lexInsideTag
236
+
case '<':
237
+
l.emit(TokenLt)
238
+
return lexInsideTag
239
+
case '+':
240
+
l.emit(TokenPlus)
241
+
return lexInsideTag
242
+
case '-':
243
+
// Could be minus or negative number
244
+
if unicode.IsDigit(l.peek()) {
245
+
l.backup()
246
+
return lexNumber
247
+
}
248
+
l.emit(TokenMinus)
249
+
return lexInsideTag
250
+
case '*':
251
+
l.emit(TokenMul)
252
+
return lexInsideTag
253
+
case '/':
254
+
l.emit(TokenDiv)
255
+
return lexInsideTag
256
+
case '%':
257
+
// Check if this is %] (tag close) or % (modulo)
258
+
if l.peek() == ']' {
259
+
l.backup()
260
+
l.pos += 2
261
+
l.emit(TokenTagClose)
262
+
return lexText
263
+
}
264
+
l.emit(TokenMod)
265
+
return lexInsideTag
266
+
case '.':
267
+
l.emit(TokenDot)
268
+
return lexInsideTag
269
+
case '|':
270
+
l.emit(TokenPipe)
271
+
return lexInsideTag
272
+
case '(':
273
+
l.emit(TokenLParen)
274
+
return lexInsideTag
275
+
case ')':
276
+
l.emit(TokenRParen)
277
+
return lexInsideTag
278
+
case ',':
279
+
l.emit(TokenComma)
280
+
return lexInsideTag
281
+
case '=':
282
+
l.emit(TokenAssign)
283
+
return lexInsideTag
284
+
case '"', '\'':
285
+
l.backup()
286
+
return lexString
287
+
}
288
+
289
+
// Check for number
290
+
if unicode.IsDigit(r) {
291
+
l.backup()
292
+
return lexNumber
293
+
}
294
+
295
+
// Must be identifier or keyword
296
+
if isAlpha(r) || r == '_' {
297
+
l.backup()
298
+
return lexIdentifier
299
+
}
300
+
301
+
return l.errorf("unexpected character: %s", string(r))
302
+
}
303
+
304
+
// lexString scans a quoted string literal (single or double quotes)
305
+
func lexString(l *Lexer) stateFn {
306
+
quote := l.next() // consume opening quote
307
+
l.ignore() // don't include quote in value
308
+
309
+
for {
310
+
r := l.next()
311
+
if r == eof {
312
+
return l.errorf("unterminated string")
313
+
}
314
+
if r == '\\' {
315
+
// Skip escaped character
316
+
l.next()
317
+
continue
318
+
}
319
+
if r == quote {
320
+
// Don't include closing quote in value
321
+
l.backup()
322
+
l.emit(TokenString)
323
+
l.next() // consume closing quote
324
+
l.ignore()
325
+
return lexInsideTag
326
+
}
327
+
}
328
+
}
329
+
330
+
// lexNumber scans a number (integer or float, possibly negative)
331
+
func lexNumber(l *Lexer) stateFn {
332
+
// Optional leading minus
333
+
if l.peek() == '-' {
334
+
l.next()
335
+
}
336
+
337
+
// Integer part
338
+
digits := false
339
+
for unicode.IsDigit(l.peek()) {
340
+
l.next()
341
+
digits = true
342
+
}
343
+
344
+
if !digits {
345
+
return l.errorf("expected digits in number")
346
+
}
347
+
348
+
// Optional decimal part
349
+
if l.peek() == '.' {
350
+
l.next()
351
+
for unicode.IsDigit(l.peek()) {
352
+
l.next()
353
+
}
354
+
}
355
+
356
+
l.emit(TokenNumber)
357
+
return lexInsideTag
358
+
}
359
+
360
+
// lexIdentifier scans an identifier or keyword
361
+
func lexIdentifier(l *Lexer) stateFn {
362
+
for {
363
+
r := l.next()
364
+
if !isAlphaNumeric(r) && r != '_' {
365
+
l.backup()
366
+
break
367
+
}
368
+
}
369
+
370
+
word := l.input[l.start:l.pos]
371
+
tokType := LookupKeyword(word)
372
+
l.emit(tokType)
373
+
return lexInsideTag
374
+
}
375
+
376
+
// isAlpha returns true if r is an alphabetic character
377
+
func isAlpha(r rune) bool {
378
+
return unicode.IsLetter(r)
379
+
}
380
+
381
+
// isAlphaNumeric returns true if r is alphanumeric
382
+
func isAlphaNumeric(r rune) bool {
383
+
return unicode.IsLetter(r) || unicode.IsDigit(r)
384
+
}
+686
parser.go
+686
parser.go
···
1
+
package gott
2
+
3
+
import (
4
+
"strconv"
5
+
)
6
+
7
+
// Parser parses a token stream into an AST
8
+
type Parser struct {
9
+
lexer *Lexer
10
+
token Token // current token
11
+
peekToken Token // lookahead token
12
+
errors []error // accumulated parse errors
13
+
}
14
+
15
+
// NewParser creates a new parser for the given input
16
+
func NewParser(input string) *Parser {
17
+
p := &Parser{
18
+
lexer: NewLexer(input),
19
+
}
20
+
// Load first two tokens
21
+
p.advance()
22
+
p.advance()
23
+
return p
24
+
}
25
+
26
+
// advance moves to the next token
27
+
func (p *Parser) advance() {
28
+
p.token = p.peekToken
29
+
p.peekToken = p.lexer.NextToken()
30
+
}
31
+
32
+
// expect checks that the current token is of the expected type and advances
33
+
func (p *Parser) expect(t TokenType) bool {
34
+
if p.token.Type != t {
35
+
p.errorf("expected %s, got %s", t, p.token.Type)
36
+
return false
37
+
}
38
+
p.advance()
39
+
return true
40
+
}
41
+
42
+
// errorf records a parse error
43
+
func (p *Parser) errorf(format string, args ...any) {
44
+
err := &ParseError{
45
+
Pos: p.token.Pos,
46
+
Message: sprintf(format, args...),
47
+
}
48
+
p.errors = append(p.errors, err)
49
+
}
50
+
51
+
// Parse parses the input and returns the AST
52
+
func (p *Parser) Parse() (*Template, []error) {
53
+
t := &Template{
54
+
Position: Position{Line: 1, Column: 1, Offset: 0},
55
+
Nodes: []Node{},
56
+
}
57
+
58
+
for p.token.Type != TokenEOF && p.token.Type != TokenError {
59
+
node := p.parseNode()
60
+
if node != nil {
61
+
t.Nodes = append(t.Nodes, node)
62
+
}
63
+
}
64
+
65
+
if p.token.Type == TokenError {
66
+
p.errors = append(p.errors, &ParseError{
67
+
Pos: p.token.Pos,
68
+
Message: p.token.Value,
69
+
})
70
+
}
71
+
72
+
return t, p.errors
73
+
}
74
+
75
+
// parseNode parses a single node (text or tag)
76
+
func (p *Parser) parseNode() Node {
77
+
switch p.token.Type {
78
+
case TokenText:
79
+
return p.parseText()
80
+
case TokenTagOpen:
81
+
return p.parseTag()
82
+
default:
83
+
p.errorf("unexpected token: %s", p.token.Type)
84
+
p.advance()
85
+
return nil
86
+
}
87
+
}
88
+
89
+
// parseText parses a text node
90
+
func (p *Parser) parseText() Node {
91
+
node := &TextNode{
92
+
Position: p.token.Pos,
93
+
Text: p.token.Value,
94
+
}
95
+
p.advance()
96
+
return node
97
+
}
98
+
99
+
// parseTag parses a [% ... %] tag
100
+
func (p *Parser) parseTag() Node {
101
+
p.expect(TokenTagOpen)
102
+
103
+
switch p.token.Type {
104
+
case TokenIF:
105
+
return p.parseIf()
106
+
case TokenUNLESS:
107
+
return p.parseUnless()
108
+
case TokenFOREACH:
109
+
return p.parseForeach()
110
+
case TokenBLOCK:
111
+
return p.parseBlock()
112
+
case TokenINCLUDE:
113
+
return p.parseInclude()
114
+
case TokenWRAPPER:
115
+
return p.parseWrapper()
116
+
case TokenSET:
117
+
return p.parseSet()
118
+
default:
119
+
// Expression output: [% expr %]
120
+
return p.parseOutput()
121
+
}
122
+
}
123
+
124
+
// parseIf parses an IF statement with optional ELSIF and ELSE
125
+
func (p *Parser) parseIf() *IfStmt {
126
+
pos := p.token.Pos
127
+
p.expect(TokenIF)
128
+
129
+
cond := p.parseExpr()
130
+
if !p.expect(TokenTagClose) {
131
+
return nil
132
+
}
133
+
134
+
stmt := &IfStmt{
135
+
Position: pos,
136
+
Condition: cond,
137
+
}
138
+
139
+
// Parse body until ELSIF, ELSE, or END
140
+
stmt.Body = p.parseBody(TokenELSIF, TokenELSE, TokenEND)
141
+
142
+
// Parse ELSIF chain
143
+
for p.token.Type == TokenTagOpen && p.peekToken.Type == TokenELSIF {
144
+
p.expect(TokenTagOpen)
145
+
elsifPos := p.token.Pos
146
+
p.expect(TokenELSIF)
147
+
148
+
elsifCond := p.parseExpr()
149
+
if !p.expect(TokenTagClose) {
150
+
return nil
151
+
}
152
+
153
+
elsifBody := p.parseBody(TokenELSIF, TokenELSE, TokenEND)
154
+
155
+
stmt.ElsIf = append(stmt.ElsIf, &ElsIfClause{
156
+
Position: elsifPos,
157
+
Condition: elsifCond,
158
+
Body: elsifBody,
159
+
})
160
+
}
161
+
162
+
// Parse optional ELSE
163
+
if p.token.Type == TokenTagOpen && p.peekToken.Type == TokenELSE {
164
+
p.expect(TokenTagOpen)
165
+
p.expect(TokenELSE)
166
+
p.expect(TokenTagClose)
167
+
stmt.Else = p.parseBody(TokenEND)
168
+
}
169
+
170
+
// Expect END
171
+
p.expectEndTag()
172
+
173
+
return stmt
174
+
}
175
+
176
+
// parseUnless parses an UNLESS statement
177
+
func (p *Parser) parseUnless() *UnlessStmt {
178
+
pos := p.token.Pos
179
+
p.expect(TokenUNLESS)
180
+
181
+
cond := p.parseExpr()
182
+
if !p.expect(TokenTagClose) {
183
+
return nil
184
+
}
185
+
186
+
stmt := &UnlessStmt{
187
+
Position: pos,
188
+
Condition: cond,
189
+
}
190
+
191
+
stmt.Body = p.parseBody(TokenELSE, TokenEND)
192
+
193
+
// Parse optional ELSE
194
+
if p.token.Type == TokenTagOpen && p.peekToken.Type == TokenELSE {
195
+
p.expect(TokenTagOpen)
196
+
p.expect(TokenELSE)
197
+
p.expect(TokenTagClose)
198
+
stmt.Else = p.parseBody(TokenEND)
199
+
}
200
+
201
+
p.expectEndTag()
202
+
203
+
return stmt
204
+
}
205
+
206
+
// parseForeach parses a FOREACH loop
207
+
func (p *Parser) parseForeach() *ForeachStmt {
208
+
pos := p.token.Pos
209
+
p.expect(TokenFOREACH)
210
+
211
+
if p.token.Type != TokenIdent {
212
+
p.errorf("expected identifier, got %s", p.token.Type)
213
+
return nil
214
+
}
215
+
itemVar := p.token.Value
216
+
p.advance()
217
+
218
+
if !p.expect(TokenIN) {
219
+
return nil
220
+
}
221
+
222
+
listExpr := p.parseExpr()
223
+
if !p.expect(TokenTagClose) {
224
+
return nil
225
+
}
226
+
227
+
body := p.parseBody(TokenEND)
228
+
p.expectEndTag()
229
+
230
+
return &ForeachStmt{
231
+
Position: pos,
232
+
ItemVar: itemVar,
233
+
ListExpr: listExpr,
234
+
Body: body,
235
+
}
236
+
}
237
+
238
+
// parseBlock parses a BLOCK definition
239
+
func (p *Parser) parseBlock() *BlockStmt {
240
+
pos := p.token.Pos
241
+
p.expect(TokenBLOCK)
242
+
243
+
if p.token.Type != TokenIdent {
244
+
p.errorf("expected block name, got %s", p.token.Type)
245
+
return nil
246
+
}
247
+
name := p.token.Value
248
+
p.advance()
249
+
250
+
if !p.expect(TokenTagClose) {
251
+
return nil
252
+
}
253
+
254
+
body := p.parseBody(TokenEND)
255
+
p.expectEndTag()
256
+
257
+
return &BlockStmt{
258
+
Position: pos,
259
+
Name: name,
260
+
Body: body,
261
+
}
262
+
}
263
+
264
+
// parseInclude parses an INCLUDE directive
265
+
func (p *Parser) parseInclude() *IncludeStmt {
266
+
pos := p.token.Pos
267
+
p.expect(TokenINCLUDE)
268
+
269
+
var name string
270
+
if p.token.Type == TokenString {
271
+
name = p.token.Value
272
+
p.advance()
273
+
} else if p.token.Type == TokenIdent {
274
+
// Could be a simple name or a path like "partials/header"
275
+
name = p.token.Value
276
+
p.advance()
277
+
// Handle path-like includes: partials/header.html
278
+
for p.token.Type == TokenDiv || p.token.Type == TokenDot {
279
+
name += p.token.Value
280
+
p.advance()
281
+
if p.token.Type == TokenIdent {
282
+
name += p.token.Value
283
+
p.advance()
284
+
}
285
+
}
286
+
} else {
287
+
p.errorf("expected include name, got %s", p.token.Type)
288
+
return nil
289
+
}
290
+
291
+
p.expect(TokenTagClose)
292
+
293
+
return &IncludeStmt{
294
+
Position: pos,
295
+
Name: name,
296
+
}
297
+
}
298
+
299
+
// parseWrapper parses a WRAPPER directive
300
+
func (p *Parser) parseWrapper() *WrapperStmt {
301
+
pos := p.token.Pos
302
+
p.expect(TokenWRAPPER)
303
+
304
+
var name string
305
+
if p.token.Type == TokenString {
306
+
name = p.token.Value
307
+
p.advance()
308
+
} else if p.token.Type == TokenIdent {
309
+
name = p.token.Value
310
+
p.advance()
311
+
// Handle path-like wrappers
312
+
for p.token.Type == TokenDiv || p.token.Type == TokenDot {
313
+
name += p.token.Value
314
+
p.advance()
315
+
if p.token.Type == TokenIdent {
316
+
name += p.token.Value
317
+
p.advance()
318
+
}
319
+
}
320
+
} else {
321
+
p.errorf("expected wrapper name, got %s", p.token.Type)
322
+
return nil
323
+
}
324
+
325
+
if !p.expect(TokenTagClose) {
326
+
return nil
327
+
}
328
+
329
+
content := p.parseBody(TokenEND)
330
+
p.expectEndTag()
331
+
332
+
return &WrapperStmt{
333
+
Position: pos,
334
+
Name: name,
335
+
Content: content,
336
+
}
337
+
}
338
+
339
+
// parseSet parses a SET directive
340
+
func (p *Parser) parseSet() *SetStmt {
341
+
pos := p.token.Pos
342
+
p.expect(TokenSET)
343
+
344
+
if p.token.Type != TokenIdent {
345
+
p.errorf("expected variable name, got %s", p.token.Type)
346
+
return nil
347
+
}
348
+
varName := p.token.Value
349
+
p.advance()
350
+
351
+
if !p.expect(TokenAssign) {
352
+
return nil
353
+
}
354
+
355
+
value := p.parseExpr()
356
+
p.expect(TokenTagClose)
357
+
358
+
return &SetStmt{
359
+
Position: pos,
360
+
Var: varName,
361
+
Value: value,
362
+
}
363
+
}
364
+
365
+
// parseOutput parses an expression output: [% expr %]
366
+
func (p *Parser) parseOutput() *OutputStmt {
367
+
pos := p.token.Pos
368
+
expr := p.parseExpr()
369
+
p.expect(TokenTagClose)
370
+
371
+
return &OutputStmt{
372
+
Position: pos,
373
+
Expr: expr,
374
+
}
375
+
}
376
+
377
+
// parseBody parses nodes until one of the stop tokens is seen as the next keyword
378
+
func (p *Parser) parseBody(stopTokens ...TokenType) []Node {
379
+
var nodes []Node
380
+
381
+
for {
382
+
// Check for EOF
383
+
if p.token.Type == TokenEOF {
384
+
break
385
+
}
386
+
387
+
// Check if next tag starts with a stop token
388
+
if p.token.Type == TokenTagOpen {
389
+
for _, stop := range stopTokens {
390
+
if p.peekToken.Type == stop {
391
+
return nodes
392
+
}
393
+
}
394
+
}
395
+
396
+
node := p.parseNode()
397
+
if node != nil {
398
+
nodes = append(nodes, node)
399
+
}
400
+
}
401
+
402
+
return nodes
403
+
}
404
+
405
+
// expectEndTag expects [% END %]
406
+
func (p *Parser) expectEndTag() {
407
+
if p.token.Type == TokenTagOpen && p.peekToken.Type == TokenEND {
408
+
p.expect(TokenTagOpen)
409
+
p.expect(TokenEND)
410
+
p.expect(TokenTagClose)
411
+
} else {
412
+
p.errorf("expected [%% END %%], got %s", p.token.Type)
413
+
}
414
+
}
415
+
416
+
// ---- Expression Parsing (with precedence) ----
417
+
418
+
// parseExpr is the entry point for expression parsing
419
+
func (p *Parser) parseExpr() Expr {
420
+
return p.parseOr()
421
+
}
422
+
423
+
// parseOr handles || (logical OR) and || (default value)
424
+
// When || is followed by a literal and left is an identifier/filter expr, treat as default
425
+
func (p *Parser) parseOr() Expr {
426
+
left := p.parseAnd()
427
+
428
+
for p.token.Type == TokenOr {
429
+
pos := p.token.Pos
430
+
p.advance()
431
+
right := p.parseAnd()
432
+
433
+
// Check if this looks like a default value expression:
434
+
// left is identifier/filter, right is a literal
435
+
if isDefaultCandidate(left) && isLiteralExpr(right) {
436
+
left = &DefaultExpr{
437
+
Position: pos,
438
+
Expr: left,
439
+
Default: right,
440
+
}
441
+
} else {
442
+
left = &BinaryExpr{
443
+
Position: pos,
444
+
Op: TokenOr,
445
+
Left: left,
446
+
Right: right,
447
+
}
448
+
}
449
+
}
450
+
451
+
return left
452
+
}
453
+
454
+
// isDefaultCandidate returns true if the expression can have a default value
455
+
func isDefaultCandidate(e Expr) bool {
456
+
switch e.(type) {
457
+
case *IdentExpr, *FilterExpr:
458
+
return true
459
+
}
460
+
return false
461
+
}
462
+
463
+
// isLiteralExpr returns true if the expression is a literal
464
+
func isLiteralExpr(e Expr) bool {
465
+
_, ok := e.(*LiteralExpr)
466
+
return ok
467
+
}
468
+
469
+
// parseAnd handles && (logical AND)
470
+
func (p *Parser) parseAnd() Expr {
471
+
left := p.parseComparison()
472
+
473
+
for p.token.Type == TokenAnd {
474
+
op := p.token.Type
475
+
pos := p.token.Pos
476
+
p.advance()
477
+
right := p.parseComparison()
478
+
left = &BinaryExpr{
479
+
Position: pos,
480
+
Op: op,
481
+
Left: left,
482
+
Right: right,
483
+
}
484
+
}
485
+
486
+
return left
487
+
}
488
+
489
+
// parseComparison handles ==, !=, <, <=, >, >=
490
+
func (p *Parser) parseComparison() Expr {
491
+
left := p.parseAdditive()
492
+
493
+
if isComparisonOp(p.token.Type) {
494
+
op := p.token.Type
495
+
pos := p.token.Pos
496
+
p.advance()
497
+
right := p.parseAdditive()
498
+
return &BinaryExpr{
499
+
Position: pos,
500
+
Op: op,
501
+
Left: left,
502
+
Right: right,
503
+
}
504
+
}
505
+
506
+
return left
507
+
}
508
+
509
+
// parseAdditive handles + and -
510
+
func (p *Parser) parseAdditive() Expr {
511
+
left := p.parseMultiplicative()
512
+
513
+
for p.token.Type == TokenPlus || p.token.Type == TokenMinus {
514
+
op := p.token.Type
515
+
pos := p.token.Pos
516
+
p.advance()
517
+
right := p.parseMultiplicative()
518
+
left = &BinaryExpr{
519
+
Position: pos,
520
+
Op: op,
521
+
Left: left,
522
+
Right: right,
523
+
}
524
+
}
525
+
526
+
return left
527
+
}
528
+
529
+
// parseMultiplicative handles *, /, %
530
+
func (p *Parser) parseMultiplicative() Expr {
531
+
left := p.parseUnary()
532
+
533
+
for p.token.Type == TokenMul || p.token.Type == TokenDiv || p.token.Type == TokenMod {
534
+
op := p.token.Type
535
+
pos := p.token.Pos
536
+
p.advance()
537
+
right := p.parseUnary()
538
+
left = &BinaryExpr{
539
+
Position: pos,
540
+
Op: op,
541
+
Left: left,
542
+
Right: right,
543
+
}
544
+
}
545
+
546
+
return left
547
+
}
548
+
549
+
// parseUnary handles unary - (negation)
550
+
func (p *Parser) parseUnary() Expr {
551
+
if p.token.Type == TokenMinus {
552
+
pos := p.token.Pos
553
+
p.advance()
554
+
return &UnaryExpr{
555
+
Position: pos,
556
+
Op: TokenMinus,
557
+
X: p.parseUnary(),
558
+
}
559
+
}
560
+
return p.parsePrimary()
561
+
}
562
+
563
+
// parsePrimary handles literals, identifiers, function calls, and parentheses
564
+
func (p *Parser) parsePrimary() Expr {
565
+
switch p.token.Type {
566
+
case TokenNumber:
567
+
val, _ := strconv.ParseFloat(p.token.Value, 64)
568
+
expr := &LiteralExpr{
569
+
Position: p.token.Pos,
570
+
Value: val,
571
+
}
572
+
p.advance()
573
+
return expr
574
+
575
+
case TokenString:
576
+
expr := &LiteralExpr{
577
+
Position: p.token.Pos,
578
+
Value: p.token.Value,
579
+
}
580
+
p.advance()
581
+
return expr
582
+
583
+
case TokenIdent:
584
+
return p.parseIdentOrCall()
585
+
586
+
case TokenLParen:
587
+
p.advance()
588
+
expr := p.parseExpr()
589
+
p.expect(TokenRParen)
590
+
return expr
591
+
}
592
+
593
+
p.errorf("unexpected token in expression: %s", p.token.Type)
594
+
return &LiteralExpr{Position: p.token.Pos, Value: ""}
595
+
}
596
+
597
+
// parseIdentOrCall parses an identifier, possibly with dots, function calls, or filters
598
+
func (p *Parser) parseIdentOrCall() Expr {
599
+
pos := p.token.Pos
600
+
601
+
// Collect dot-separated parts: foo.bar.baz
602
+
parts := []string{p.token.Value}
603
+
p.advance()
604
+
605
+
for p.token.Type == TokenDot {
606
+
p.advance()
607
+
if p.token.Type != TokenIdent {
608
+
p.errorf("expected identifier after '.', got %s", p.token.Type)
609
+
break
610
+
}
611
+
parts = append(parts, p.token.Value)
612
+
p.advance()
613
+
}
614
+
615
+
var expr Expr = &IdentExpr{
616
+
Position: pos,
617
+
Parts: parts,
618
+
}
619
+
620
+
// Check for function call: func(args)
621
+
if p.token.Type == TokenLParen && len(parts) == 1 {
622
+
p.advance()
623
+
args := p.parseArgList()
624
+
p.expect(TokenRParen)
625
+
expr = &CallExpr{
626
+
Position: pos,
627
+
Func: parts[0],
628
+
Args: args,
629
+
}
630
+
}
631
+
632
+
// Check for filter chain: expr | filter | filter(args)
633
+
for p.token.Type == TokenPipe {
634
+
p.advance()
635
+
if p.token.Type != TokenIdent {
636
+
p.errorf("expected filter name after '|', got %s", p.token.Type)
637
+
break
638
+
}
639
+
filterName := p.token.Value
640
+
filterPos := p.token.Pos
641
+
p.advance()
642
+
643
+
var filterArgs []Expr
644
+
if p.token.Type == TokenLParen {
645
+
p.advance()
646
+
filterArgs = p.parseArgList()
647
+
p.expect(TokenRParen)
648
+
}
649
+
650
+
expr = &FilterExpr{
651
+
Position: filterPos,
652
+
Input: expr,
653
+
Filter: filterName,
654
+
Args: filterArgs,
655
+
}
656
+
}
657
+
658
+
return expr
659
+
}
660
+
661
+
// parseArgList parses a comma-separated list of expressions
662
+
func (p *Parser) parseArgList() []Expr {
663
+
var args []Expr
664
+
665
+
if p.token.Type == TokenRParen {
666
+
return args
667
+
}
668
+
669
+
args = append(args, p.parseExpr())
670
+
671
+
for p.token.Type == TokenComma {
672
+
p.advance()
673
+
args = append(args, p.parseExpr())
674
+
}
675
+
676
+
return args
677
+
}
678
+
679
+
// isComparisonOp returns true if the token is a comparison operator
680
+
func isComparisonOp(t TokenType) bool {
681
+
switch t {
682
+
case TokenEq, TokenNe, TokenLt, TokenLe, TokenGt, TokenGe:
683
+
return true
684
+
}
685
+
return false
686
+
}
+175
token.go
+175
token.go
···
1
+
package gott
2
+
3
+
// TokenType represents the type of a lexical token
4
+
type TokenType int
5
+
6
+
const (
7
+
TokenError TokenType = iota // error occurred
8
+
TokenEOF // end of input
9
+
10
+
// Literals
11
+
TokenText // raw text outside [% %]
12
+
TokenIdent // identifier (variable name, block name)
13
+
TokenString // "string" or 'string'
14
+
TokenNumber // 123 or 45.67
15
+
16
+
// Delimiters
17
+
TokenTagOpen // [%
18
+
TokenTagClose // %]
19
+
TokenLParen // (
20
+
TokenRParen // )
21
+
TokenDot // .
22
+
TokenPipe // |
23
+
TokenComma // ,
24
+
TokenAssign // =
25
+
26
+
// Operators
27
+
TokenOr // ||
28
+
TokenAnd // &&
29
+
TokenEq // ==
30
+
TokenNe // !=
31
+
TokenLt // <
32
+
TokenLe // <=
33
+
TokenGt // >
34
+
TokenGe // >=
35
+
TokenPlus // +
36
+
TokenMinus // -
37
+
TokenMul // *
38
+
TokenDiv // /
39
+
TokenMod // % (modulo, only inside tags)
40
+
41
+
// Keywords
42
+
TokenIF
43
+
TokenELSIF
44
+
TokenELSE
45
+
TokenUNLESS
46
+
TokenEND
47
+
TokenFOREACH
48
+
TokenIN
49
+
TokenBLOCK
50
+
TokenINCLUDE
51
+
TokenWRAPPER
52
+
TokenSET
53
+
)
54
+
55
+
// String returns a human-readable name for the token type
56
+
func (t TokenType) String() string {
57
+
switch t {
58
+
case TokenError:
59
+
return "Error"
60
+
case TokenEOF:
61
+
return "EOF"
62
+
case TokenText:
63
+
return "Text"
64
+
case TokenIdent:
65
+
return "Ident"
66
+
case TokenString:
67
+
return "String"
68
+
case TokenNumber:
69
+
return "Number"
70
+
case TokenTagOpen:
71
+
return "[%"
72
+
case TokenTagClose:
73
+
return "%]"
74
+
case TokenLParen:
75
+
return "("
76
+
case TokenRParen:
77
+
return ")"
78
+
case TokenDot:
79
+
return "."
80
+
case TokenPipe:
81
+
return "|"
82
+
case TokenComma:
83
+
return ","
84
+
case TokenAssign:
85
+
return "="
86
+
case TokenOr:
87
+
return "||"
88
+
case TokenAnd:
89
+
return "&&"
90
+
case TokenEq:
91
+
return "=="
92
+
case TokenNe:
93
+
return "!="
94
+
case TokenLt:
95
+
return "<"
96
+
case TokenLe:
97
+
return "<="
98
+
case TokenGt:
99
+
return ">"
100
+
case TokenGe:
101
+
return ">="
102
+
case TokenPlus:
103
+
return "+"
104
+
case TokenMinus:
105
+
return "-"
106
+
case TokenMul:
107
+
return "*"
108
+
case TokenDiv:
109
+
return "/"
110
+
case TokenMod:
111
+
return "%"
112
+
case TokenIF:
113
+
return "IF"
114
+
case TokenELSIF:
115
+
return "ELSIF"
116
+
case TokenELSE:
117
+
return "ELSE"
118
+
case TokenUNLESS:
119
+
return "UNLESS"
120
+
case TokenEND:
121
+
return "END"
122
+
case TokenFOREACH:
123
+
return "FOREACH"
124
+
case TokenIN:
125
+
return "IN"
126
+
case TokenBLOCK:
127
+
return "BLOCK"
128
+
case TokenINCLUDE:
129
+
return "INCLUDE"
130
+
case TokenWRAPPER:
131
+
return "WRAPPER"
132
+
case TokenSET:
133
+
return "SET"
134
+
default:
135
+
return "Unknown"
136
+
}
137
+
}
138
+
139
+
// Position represents a location in the source template
140
+
type Position struct {
141
+
Line int // 1-based line number
142
+
Column int // 1-based column number
143
+
Offset int // byte offset from start of input
144
+
}
145
+
146
+
// Token represents a lexical token with its type, value, and position
147
+
type Token struct {
148
+
Type TokenType
149
+
Value string // literal value for idents, strings, numbers, text, errors
150
+
Pos Position // source position for error reporting
151
+
}
152
+
153
+
// keywords maps keyword strings to their token types
154
+
var keywords = map[string]TokenType{
155
+
"IF": TokenIF,
156
+
"ELSIF": TokenELSIF,
157
+
"ELSE": TokenELSE,
158
+
"UNLESS": TokenUNLESS,
159
+
"END": TokenEND,
160
+
"FOREACH": TokenFOREACH,
161
+
"IN": TokenIN,
162
+
"BLOCK": TokenBLOCK,
163
+
"INCLUDE": TokenINCLUDE,
164
+
"WRAPPER": TokenWRAPPER,
165
+
"SET": TokenSET,
166
+
}
167
+
168
+
// LookupKeyword returns the token type for an identifier,
169
+
// returning TokenIdent if it's not a keyword
170
+
func LookupKeyword(ident string) TokenType {
171
+
if tok, ok := keywords[ident]; ok {
172
+
return tok
173
+
}
174
+
return TokenIdent
175
+
}