A go template renderer based on Perl's Template Toolkit

Initial commit, rejoice!

+2
.gitignore
···
··· 1 + dev/ 2 + .claude
+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 "&gt;foo&lt;" 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
···
··· 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
···
··· 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
···
··· 1 + module tangled.org/angrydutchman.peedee.es/gott 2 + 3 + go 1.25 4 + toolchain go1.25.1 5 +
+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
···
··· 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
···
··· 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
···
··· 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
···
··· 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 + }