├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── bench_test.go ├── cli └── xslate │ └── xslate.go ├── compiler ├── compiler.go ├── compiler_test.go ├── interface.go └── optimizer.go ├── functions ├── array │ └── array.go ├── depot.go ├── hash │ └── hash.go └── time │ └── time.go ├── internal ├── frame │ ├── frame.go │ └── frame_test.go ├── rbpool │ └── rbpool.go ├── rvpool │ └── rvpool.go └── stack │ ├── stack.go │ └── stack_test.go ├── kolonish_test.go ├── loader ├── cache.go ├── file.go ├── http.go ├── http_test.go ├── interface.go ├── loader.go ├── reader.go └── string.go ├── node ├── interface.go ├── node.go ├── node_test.go └── nodetype_string.go ├── parser ├── ast.go ├── builder.go ├── interface.go ├── kolonish │ ├── kolonish.go │ └── lexer_test.go ├── lexer.go ├── lexer_test.go ├── symbols.go └── tterse │ ├── lexer_test.go │ ├── parser_test.go │ └── tterse.go ├── test └── test.go ├── tterse_test.go ├── vm ├── bytecode.go ├── bytecode_test.go ├── interface.go ├── op.go ├── ops.go ├── state.go ├── util.go ├── util_test.go ├── vars.go ├── vars_test.go ├── vm.go └── vm_test.go ├── xslate.go └── xslate_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - 1.9.x 5 | - 1.10.x 6 | - tip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 lestrrat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | INTERNAL_BIN_DIR=_internal_bin 2 | GOVERSION=$(shell go version) 3 | GOOS=$(word 1,$(subst /, ,$(lastword $(GOVERSION)))) 4 | GOARCH=$(word 2,$(subst /, ,$(lastword $(GOVERSION)))) 5 | RELEASE_DIR=releases 6 | SRC_FILES = $(wildcard *.go internal/*/*.go) 7 | HAVE_GLIDE:=$(shell which glide) 8 | 9 | .PHONY: test 10 | 11 | $(INTERNAL_BIN_DIR)/$(GOOS)/$(GOARCH)/glide: 12 | ifndef HAVE_GLIDE 13 | @echo "Installing glide for $(GOOS)/$(GOARCH)..." 14 | @mkdir -p $(INTERNAL_BIN_DIR)/$(GOOS)/$(GOARCH) 15 | @wget -q -O - https://github.com/Masterminds/glide/releases/download/0.10.2/glide-0.10.2-$(GOOS)-$(GOARCH).tar.gz | tar xvz 16 | @mv $(GOOS)-$(GOARCH)/glide $(INTERNAL_BIN_DIR)/$(GOOS)/$(GOARCH)/glide 17 | @rm -rf $(GOOS)-$(GOARCH) 18 | endif 19 | 20 | glide: $(INTERNAL_BIN_DIR)/$(GOOS)/$(GOARCH)/glide 21 | 22 | installdeps: glide $(SRC_FILES) 23 | @echo "Installing dependencies..." 24 | @PATH=$(INTERNAL_BIN_DIR)/$(GOOS)/$(GOARCH):$(PATH) glide install 25 | 26 | test: installdeps 27 | @echo "Running tests..." 28 | @PATH=$(INTERNAL_BIN_DIR)/$(GOOS)/$(GOARCH):$(PATH) go test -v $(shell glide nv) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | xslate 2 | ========= 3 | 4 | Attempt to port Perl5's Text::Xslate to Go 5 | 6 | [](https://travis-ci.org/lestrrat-go/xslate) 7 | 8 | [](https://godoc.org/github.com/lestrrat-go/xslate) 9 | 10 | Play with it! 11 | ============= 12 | 13 | [Go-Xslate Playground](http://play-go-xslate.appspot.com) is a little toy that allows you to try out Xslate template rendering. Note that for obvious reasons you cannot use directives that use external templates, such as WRAPPEr and INCLUDE 14 | 15 | If you find templates that you think should work but doesn't, please file an issue using this service. You need to first "share" the current template that you're using, and then copy that new generated URL to file the issue. 16 | 17 | Description 18 | =========== 19 | 20 | This is an attempt to port [Text::Xslate](https://github.com/xslate/p5-Text-Xslate) from Perl5 to Go. 21 | 22 | Xslate is an extremely powerful virtual machine based template engine. 23 | 24 | Why Would I Choose xslate over text/template? 25 | ============================================= 26 | 27 | I believe there are at least two reasons you would choose Xslate over the basic 28 | text/template or html/template packages: 29 | 30 | *Template flexibility* 31 | 32 | IMHO, the default TTerse syntax is much more expressive and flexible. 33 | With WRAPPERs and INCLUDEs, it is possible to write a very module set of 34 | templates. YMMV 35 | 36 | *Dynamic/Automatic Reloading* 37 | 38 | By default Xslate expects that your template live in the file system -- i.e. 39 | outside of your go code. While text/template expects that you manage loading 40 | of templates yourself. Xslate handles all this for you. It searches for 41 | templates in the specified path, does the compilation, and handles caching, 42 | both on memory and on file system. 43 | 44 | Xslate is also designed to allow you to customize this behavior: It should be 45 | easy to create a template loader that loads from databases and cache into 46 | memcached and the like. 47 | 48 | Current Status 49 | ======= 50 | 51 | Currently: 52 | 53 | * I'm aiming for port of most of TTerse syntax 54 | * See [VM Progress](https://github.com/lestrrat-go/xslate/wiki/VM-Progress) for what the this xslate virtual machine can handle 55 | * VM TODO: cleanup, optimization 56 | * Parser is about 90% finished. 57 | * Compiler is about 90% finished. 58 | * Pluggable syntax isn't implemented at all. 59 | * Need to come up with ways to register functions. 60 | 61 | For simple templates, you can already do: 62 | 63 | ```go 64 | package main 65 | 66 | import ( 67 | "log" 68 | "github.com/lestrrat-go/xslate" 69 | ) 70 | 71 | func main() { 72 | xt, err := xslate.New() 73 | if err != nil { // xslate.New may barf because it by default tries to 74 | // initialize stuff that may want to access the filesystem 75 | log.Fatalf("Failed to create xslate: %s", err) 76 | } 77 | 78 | // This uses RenderString() -- which is fine in itself and as an example, 79 | // but the real use case for Xslate is loading templates from other 80 | // locations, such as the filesystem. For this case yo uprobably 81 | // want to use RenderInto() or Render() 82 | template := `Hello World, [% name %]!` 83 | output, err := xt.RenderString(template, xslate.Vars { "name": "Bob" }) 84 | if err != nil { 85 | log.Fatalf("Failed to render template: %s", err) 86 | } 87 | log.Printf(output) 88 | } 89 | ``` 90 | 91 | See [Supported Syntax (TTerse)](https://github.com/lestrrat-go/xslate/wiki/Supported-Syntax-(TTerse)) for what's currently available 92 | 93 | Debugging 94 | ========= 95 | 96 | Currently the [error reporting is a bit weak](https://github.com/lestrrat-go/xslate/issues/4). What you can do when you debug or send me bug reports is to give me a stack trace, and also while you're at it, run your templates with XSLATE_DEBUG=1 environment variable. This will print out the AST and ByteCode structure that is being executed. 97 | 98 | Caveats 99 | ======= 100 | 101 | Functions 102 | --------- 103 | 104 | In Go, functions that are not part of current package namespace must be 105 | qualified with a package name, e.g.: 106 | 107 | time.Now() 108 | 109 | This works fine because you can specify this at compile time, but you can't 110 | resolve this at runtime... which is a problem for templates. The way to solve 111 | this is to register these functions as variables: 112 | 113 | template = ` 114 | [% now() %] 115 | ` 116 | tx.RenderString(template, xslate.Vars { "now": time.Now }) 117 | 118 | But this forces you to register these functions every time, as well as 119 | having to take the extra care to make names globally unique. 120 | 121 | tx := xslate.New( 122 | functions: map[string]FuncDepot { 123 | // TODO: create pre-built "bundle" of these FuncDepot's 124 | "time": FuncDepot { "Now": time.Now } 125 | } 126 | ) 127 | template := ` 128 | [% time.Now() %] 129 | ` 130 | tx.RenderString(template, ...) 131 | 132 | 133 | Comparison Operators 134 | -------------------- 135 | 136 | The original xslate, written for Perl5, has comparison operators for both 137 | numeric and string ("eq" vs "==", "ne" vs "!=", etc). In go-xslate, there's 138 | no distinction. Both are translated to the same opcode (XXX "we plan to", that is) 139 | 140 | So these are the same: 141 | 142 | [% IF x == 1 %]...[% END %] 143 | [% IF x eq 1 %]...[% END %] 144 | 145 | 146 | Accessing Fields 147 | ---------------- 148 | 149 | Only public struc fields are accessible from templates. This is a limitation of the Go language itself. 150 | However, in order to allow smooth(er) migration from p5-Text-Xslate to go-xslate, go-xslate automatically changes the field name's first character to uppercase. 151 | 152 | So given a struct like this: 153 | 154 | ```go 155 | x struct { Value int } 156 | ``` 157 | 158 | You can access `Value` via `value`, which is common in p5-Text-Xslate 159 | 160 | ``` 161 | [% x.value # same as x.Value %] 162 | ``` 163 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package xslate 2 | 3 | import ( 4 | "bytes" 5 | ht "html/template" 6 | "testing" 7 | tt "text/template" 8 | ) 9 | 10 | func BenchmarkXslateHelloWorld(b *testing.B) { 11 | c := newTestCtx(b) 12 | defer c.Cleanup() 13 | 14 | c.File("xslate/hello.tx").WriteString(`Hello World, [% name %]!`) 15 | 16 | lcfg, _ := c.XslateArgs.Get("Loader") 17 | lcfg.(Args)["CacheLevel"] = 2 18 | tx := c.CreateTx() 19 | 20 | vars := Vars{"name": "Bob"} 21 | buf := bytes.Buffer{} 22 | b.ResetTimer() 23 | for i := 0; i < b.N; i++ { 24 | buf.Reset() 25 | tx.RenderInto(&buf, "xslate/hello.tx", vars) 26 | } 27 | } 28 | 29 | func BenchmarkHTMLTemplateHelloWorld(b *testing.B) { 30 | t, err := ht.New("hello").Parse(`{{define "T"}}Hello World, {{.}}!{{end}}`) 31 | if err != nil { 32 | b.Fatalf("Failed to parse template: %s", err) 33 | } 34 | 35 | b.ResetTimer() 36 | for i := 0; i < b.N; i++ { 37 | buf := &bytes.Buffer{} 38 | t.ExecuteTemplate(buf, "T", "Bob") 39 | } 40 | } 41 | 42 | func BenchmarkTextTemplateHelloWorld(b *testing.B) { 43 | t, err := tt.New("hello").Parse(`{{define "T"}}Hello World, {{.}}!{{end}}`) 44 | if err != nil { 45 | b.Fatalf("Failed to parse template: %s", err) 46 | } 47 | 48 | b.ResetTimer() 49 | for i := 0; i < b.N; i++ { 50 | buf := &bytes.Buffer{} 51 | t.ExecuteTemplate(buf, "T", "Bob") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cli/xslate/xslate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/lestrrat-go/xslate" 7 | "os" 8 | ) 9 | 10 | func usage() { 11 | fmt.Fprintf(os.Stderr, "usage: xslate [options...] [input-files]\n") 12 | flag.PrintDefaults() 13 | os.Exit(2) 14 | } 15 | 16 | func main() { 17 | flag.Usage = usage 18 | flag.Parse() 19 | 20 | args := flag.Args() 21 | if len(args) < 1 { 22 | fmt.Fprintf(os.Stderr, "Input file is missing.\n") 23 | os.Exit(1) 24 | } 25 | 26 | // TODO: Accept --path arguments 27 | tx, err := xslate.New() 28 | if err != nil { 29 | fmt.Fprintf(os.Stderr, "Failed to create Xslate instance: %s", err) 30 | os.Exit(1) 31 | } 32 | 33 | for _, file := range args { 34 | output, err := tx.Render(file, nil) 35 | if err != nil { 36 | fmt.Fprintf(os.Stderr, "Failed to render %s: %s\n", file, err) 37 | os.Exit(1) 38 | } 39 | fmt.Fprintf(os.Stdout, output) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /compiler/compiler.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/lestrrat-go/xslate/node" 8 | "github.com/lestrrat-go/xslate/parser" 9 | "github.com/lestrrat-go/xslate/vm" 10 | ) 11 | 12 | // AppendOp creates and appends a new op to the current set of ByteCode 13 | func (ctx *context) AppendOp(o vm.OpType, args ...interface{}) vm.Op { 14 | return ctx.ByteCode.AppendOp(o, args...) 15 | } 16 | 17 | // New creates a new BasicCompiler instance 18 | func New() *BasicCompiler { 19 | return &BasicCompiler{} 20 | } 21 | 22 | // Compile satisfies the compiler.Compiler interface. It accepts an AST 23 | // created by parser.Parser, and returns vm.ByteCode or an error 24 | func (c *BasicCompiler) Compile(ast *parser.AST) (*vm.ByteCode, error) { 25 | ctx := &context{ 26 | ByteCode: vm.NewByteCode(), 27 | } 28 | for _, n := range ast.Root.Nodes { 29 | compile(ctx, n) 30 | } 31 | 32 | // When we're done compiling, always append an END op 33 | ctx.ByteCode.AppendOp(vm.TXOPEnd) 34 | 35 | opt := &NaiveOptimizer{} 36 | opt.Optimize(ctx.ByteCode) 37 | 38 | ctx.ByteCode.Name = ast.Name 39 | return ctx.ByteCode, nil 40 | } 41 | 42 | func compile(ctx *context, n node.Node) { 43 | switch n.Type() { 44 | case node.Int, node.Text: 45 | compileLiteral(ctx, n) 46 | case node.FetchSymbol: 47 | compileFetchSymbol(ctx, n.(*node.TextNode)) 48 | case node.FetchField: 49 | compileFetchField(ctx, n.(*node.FetchFieldNode)) 50 | case node.FetchArrayElement: 51 | compileFetchArrayElement(ctx, n.(*node.BinaryNode)) 52 | case node.LocalVar: 53 | compileLoadLvar(ctx, n.(*node.LocalVarNode)) 54 | case node.Assignment: 55 | compileAssignment(ctx, n.(*node.AssignmentNode)) 56 | case node.Print: 57 | compilePrint(ctx, n.(*node.ListNode)) 58 | case node.PrintRaw: 59 | compilePrintRaw(ctx, n.(*node.ListNode)) 60 | case node.Foreach: 61 | compileForeach(ctx, n.(*node.ForeachNode)) 62 | case node.While: 63 | compileWhile(ctx, n.(*node.WhileNode)) 64 | case node.If: 65 | compileIf(ctx, n.(*node.IfNode)) 66 | case node.Else: 67 | compileElse(ctx, n.(*node.ElseNode)) 68 | case node.MakeArray: 69 | compileMakeArray(ctx, n.(*node.UnaryNode)) 70 | case node.Range: 71 | compileRange(ctx, n.(*node.BinaryNode)) 72 | case node.List: 73 | compileList(ctx, n.(*node.ListNode)) 74 | case node.FunCall: 75 | compileFunCall(ctx, n.(*node.FunCallNode)) 76 | case node.MethodCall: 77 | compileMethodCall(ctx, n.(*node.MethodCallNode)) 78 | case node.Include: 79 | compileInclude(ctx, n.(*node.IncludeNode)) 80 | case node.Group: 81 | compile(ctx, n.(*node.UnaryNode).Child) 82 | case node.Equals, node.NotEquals, node.LT, node.GT: 83 | compileComparison(ctx, n.(*node.BinaryNode)) 84 | case node.Plus, node.Minus, node.Mul, node.Div: 85 | compileBinaryArithmetic(ctx, n.(*node.BinaryNode)) 86 | case node.Filter: 87 | compileFilter(ctx, n.(*node.FilterNode)) 88 | case node.Wrapper: 89 | compileWrapper(ctx, n.(*node.WrapperNode)) 90 | case node.Macro: 91 | compileMacro(ctx, n.(*node.MacroNode)) 92 | default: 93 | fmt.Printf("Unknown node: %s\n", n.Type()) 94 | } 95 | } 96 | 97 | func compileComparison(ctx *context, n *node.BinaryNode) { 98 | compileBinaryOperands(ctx, n) 99 | switch n.Type() { 100 | case node.Equals: 101 | ctx.AppendOp(vm.TXOPEquals) 102 | case node.NotEquals: 103 | ctx.AppendOp(vm.TXOPNotEquals) 104 | case node.LT: 105 | ctx.AppendOp(vm.TXOPLessThan) 106 | case node.GT: 107 | ctx.AppendOp(vm.TXOPGreaterThan) 108 | default: 109 | panic("Unknown operator") 110 | } 111 | } 112 | 113 | func compileFetchArrayElement(ctx *context, n *node.BinaryNode) { 114 | ctx.AppendOp(vm.TXOPPushmark).SetComment("fetch array element") 115 | compile(ctx, n.Right) 116 | ctx.AppendOp(vm.TXOPPush) 117 | compile(ctx, n.Left) 118 | ctx.AppendOp(vm.TXOPPush) 119 | ctx.AppendOp(vm.TXOPFetchArrayElement) 120 | ctx.AppendOp(vm.TXOPPopmark) 121 | } 122 | 123 | func compileFetchField(ctx *context, n *node.FetchFieldNode) { 124 | compile(ctx, n.Container) 125 | ctx.AppendOp(vm.TXOPFetchFieldSymbol, n.FieldName) 126 | } 127 | 128 | func compileFetchSymbol(ctx *context, n *node.TextNode) { 129 | ctx.AppendOp(vm.TXOPFetchSymbol, n.Text) 130 | } 131 | 132 | func compileFilter(ctx *context, n *node.FilterNode) { 133 | compile(ctx, n.Child) 134 | ctx.AppendOp(vm.TXOPFilter, n.Name) 135 | } 136 | 137 | func compileFunCall(ctx *context, n *node.FunCallNode) { 138 | if len(n.Args.Nodes) > 0 { 139 | ctx.AppendOp(vm.TXOPNoop).SetComment("Setting up function arguments") 140 | for _, child := range n.Args.Nodes { 141 | compile(ctx, child) 142 | ctx.AppendOp(vm.TXOPPush) 143 | } 144 | } 145 | 146 | if inv := n.Invocant; inv != nil { 147 | compile(ctx, inv) 148 | } 149 | ctx.AppendOp(vm.TXOPFunCallOmni) 150 | } 151 | 152 | func compileMakeArray(ctx *context, n *node.UnaryNode) { 153 | compile(ctx, n.Child) 154 | ctx.AppendOp(vm.TXOPMakeArray) 155 | } 156 | 157 | func compileMethodCall(ctx *context, n *node.MethodCallNode) { 158 | ctx.AppendOp(vm.TXOPPushmark).SetComment("Begin method call") 159 | compile(ctx, n.Invocant) 160 | ctx.AppendOp(vm.TXOPPush).SetComment("Push method invocant") 161 | for _, child := range n.Args.Nodes { 162 | compile(ctx, child) 163 | ctx.AppendOp(vm.TXOPPush) 164 | } 165 | ctx.AppendOp(vm.TXOPMethodCall, n.MethodName) 166 | ctx.AppendOp(vm.TXOPPopmark).SetComment("End method call") 167 | } 168 | 169 | func compilePrint(ctx *context, n *node.ListNode) { 170 | compile(ctx, n.Nodes[0]) 171 | ctx.AppendOp(vm.TXOPPrint) 172 | } 173 | 174 | func compilePrintRaw(ctx *context, n *node.ListNode) { 175 | compile(ctx, n.Nodes[0]) 176 | ctx.AppendOp(vm.TXOPPrintRaw) 177 | } 178 | 179 | func compileRange(ctx *context, n *node.BinaryNode) { 180 | compile(ctx, n.Right) 181 | ctx.AppendOp(vm.TXOPPush) 182 | compile(ctx, n.Left) 183 | ctx.AppendOp(vm.TXOPMoveToSb) 184 | ctx.AppendOp(vm.TXOPPop) 185 | ctx.AppendOp(vm.TXOPRange) 186 | } 187 | 188 | func compileList(ctx *context, n *node.ListNode) { 189 | ctx.AppendOp(vm.TXOPNoop).SetComment("BEGIN list") 190 | for _, v := range n.Nodes { 191 | compile(ctx, v) 192 | if v.Type() != node.Range { 193 | ctx.AppendOp(vm.TXOPPush) 194 | } 195 | } 196 | ctx.AppendOp(vm.TXOPNoop).SetComment("END list") 197 | } 198 | 199 | func compileIf(ctx *context, n *node.IfNode) { 200 | ctx.AppendOp(vm.TXOPPushmark).SetComment("BEGIN IF") 201 | compile(ctx, n.BooleanExpression) 202 | ifop := ctx.AppendOp(vm.TXOPAnd, 0) 203 | pos := ctx.ByteCode.Len() 204 | 205 | var elseNode node.Node 206 | children := n.ListNode.Nodes 207 | for _, child := range children { 208 | if child.Type() == node.Else { 209 | elseNode = child 210 | } else { 211 | compile(ctx, child) 212 | } 213 | } 214 | 215 | if elseNode == nil { 216 | ifop.SetArg(ctx.ByteCode.Len() - pos + 1) 217 | ifop.SetComment("Jump to end of IF at " + strconv.Itoa(ctx.ByteCode.Len()+1) + " when condition fails") 218 | } else { 219 | // If we have an else, we need to put this AFTER the goto 220 | // that's generated by else 221 | ifop.SetArg(ctx.ByteCode.Len() - pos + 2) 222 | ifop.SetComment("Jump to ELSE at " + strconv.Itoa(ctx.ByteCode.Len()+2) + " when condition fails") 223 | compile(ctx, elseNode) 224 | } 225 | ctx.AppendOp(vm.TXOPPopmark).SetComment("END IF") 226 | } 227 | 228 | func compileElse(ctx *context, n *node.ElseNode) { 229 | gotoOp := ctx.AppendOp(vm.TXOPGoto, 0) 230 | pos := ctx.ByteCode.Len() 231 | for _, child := range n.ListNode.Nodes { 232 | compile(ctx, child) 233 | } 234 | gotoOp.SetArg(ctx.ByteCode.Len() - pos + 1) 235 | } 236 | 237 | func compileBinaryOperands(ctx *context, x *node.BinaryNode) { 238 | if x.Right.Type() == node.Group { 239 | // Grouped node 240 | compile(ctx, x.Right) 241 | ctx.AppendOp(vm.TXOPPush) 242 | compile(ctx, x.Left) 243 | ctx.AppendOp(vm.TXOPMoveToSb) 244 | ctx.AppendOp(vm.TXOPPop) 245 | } else { 246 | compile(ctx, x.Left) 247 | ctx.AppendOp(vm.TXOPMoveToSb) 248 | compile(ctx, x.Right) 249 | } 250 | } 251 | 252 | func compileAssignmentNodes(ctx *context, assignnodes []node.Node) { 253 | if len(assignnodes) <= 0 { 254 | return 255 | } 256 | ctx.AppendOp(vm.TXOPPushmark) 257 | for _, nv := range assignnodes { 258 | v := nv.(*node.AssignmentNode) 259 | ctx.AppendOp(vm.TXOPLiteral, v.Assignee.Name) 260 | ctx.AppendOp(vm.TXOPPush) 261 | compile(ctx, v.Expression) 262 | ctx.AppendOp(vm.TXOPPush) 263 | } 264 | ctx.AppendOp(vm.TXOPMakeHash) 265 | ctx.AppendOp(vm.TXOPMoveToSb) 266 | ctx.AppendOp(vm.TXOPPopmark) 267 | } 268 | 269 | func compileForeach(ctx *context, x *node.ForeachNode) { 270 | ctx.AppendOp(vm.TXOPPushmark).SetComment("BEGIN FOREACH") 271 | ctx.AppendOp(vm.TXOPPushFrame).SetComment("BEGIN new scope") 272 | compile(ctx, x.List) 273 | ctx.AppendOp(vm.TXOPForStart, x.IndexVarIdx) 274 | ctx.AppendOp(vm.TXOPLiteral, x.IndexVarIdx) 275 | 276 | iter := ctx.AppendOp(vm.TXOPForIter, 0) 277 | pos := ctx.ByteCode.Len() 278 | 279 | children := x.Nodes 280 | for _, v := range children { 281 | compile(ctx, v) 282 | } 283 | 284 | ctx.AppendOp(vm.TXOPGoto, -1*(ctx.ByteCode.Len()-pos+2)).SetComment("Jump back to for_iter at " + strconv.Itoa(pos)) 285 | ctx.AppendOp(vm.TXOPPopFrame).SetComment("END scope") 286 | 287 | // Tell for iter to jump to this position when 288 | // the loop is done. 289 | iter.SetArg(ctx.ByteCode.Len() - pos) 290 | iter.SetComment("Jump to end of scope at " + strconv.Itoa(ctx.ByteCode.Len()) + " when we're done") 291 | ctx.AppendOp(vm.TXOPPopmark).SetComment("END FOREACH") 292 | } 293 | 294 | func compileWhile(ctx *context, x *node.WhileNode) { 295 | ctx.AppendOp(vm.TXOPPushmark) 296 | ctx.AppendOp(vm.TXOPPushFrame) 297 | ctx.AppendOp(vm.TXOPLiteral, 0) 298 | ctx.AppendOp(vm.TXOPSaveToLvar, 0) 299 | 300 | condPos := ctx.ByteCode.Len() + 1 301 | 302 | // compile the boolean expression 303 | compile(ctx, x.Condition) 304 | 305 | // we might as well use the equivalent of If here! 306 | ifop := ctx.AppendOp(vm.TXOPAnd, 0) 307 | ifPos := ctx.ByteCode.Len() 308 | 309 | children := x.Nodes 310 | for _, v := range children { 311 | compile(ctx, v) 312 | } 313 | 314 | // Go back to condPos 315 | ctx.AppendOp(vm.TXOPGoto, -1*(ctx.ByteCode.Len()-condPos+1)).SetComment("Jump to " + strconv.Itoa(condPos)) 316 | ifop.SetArg(ctx.ByteCode.Len() - ifPos + 1) 317 | ifop.SetComment("Jump to " + strconv.Itoa(ctx.ByteCode.Len()+1)) 318 | ctx.AppendOp(vm.TXOPPopmark) 319 | } 320 | 321 | func compileWrapper(ctx *context, x *node.WrapperNode) { 322 | // Save the current io.Writer to the stack 323 | // This also creates pushes a bytes.Buffer into the stack 324 | // so that following operations write to that buffer 325 | ctx.AppendOp(vm.TXOPSaveWriter) 326 | 327 | // From this place on, executed opcodes will write to a temporary 328 | // new output 329 | for _, v := range x.ListNode.Nodes { 330 | compile(ctx, v) 331 | } 332 | 333 | // Pop the original writer, and place it back to the output 334 | // Also push the output onto the stack 335 | ctx.AppendOp(vm.TXOPRestoreWriter) 336 | 337 | // Arguments to include (WITH foo = "bar") need to be evaulated 338 | // in the OUTER context, but the variables need to be set in the 339 | // include context 340 | compileAssignmentNodes(ctx, x.AssignmentNodes) 341 | 342 | // Popt the "content" 343 | ctx.AppendOp(vm.TXOPPop) 344 | ctx.AppendOp(vm.TXOPPushmark) 345 | ctx.AppendOp(vm.TXOPWrapper, x.WrapperName) 346 | ctx.AppendOp(vm.TXOPPopmark) 347 | } 348 | 349 | func compileMacro(ctx *context, x *node.MacroNode) { 350 | // The VM is responsible for passing arguments, which do not need 351 | // to be declared as variables in the template. n.Arguments exists, 352 | // but it's left untouched 353 | 354 | // This goto effectively forces the VM to "ignore" this block of 355 | // MACRO definition. 356 | gotoOp := ctx.AppendOp(vm.TXOPGoto, 0) 357 | start := ctx.ByteCode.Len() 358 | 359 | // This is the actual "entry point" 360 | ctx.AppendOp(vm.TXOPPushmark) 361 | entryPoint := ctx.ByteCode.Len() - 1 362 | 363 | for _, child := range x.Nodes { 364 | compile(ctx, child) 365 | } 366 | ctx.AppendOp(vm.TXOPPopmark) 367 | ctx.AppendOp(vm.TXOPEnd) // This END forces termination 368 | gotoOp.SetArg(ctx.ByteCode.Len() - start + 1) 369 | 370 | // Now remember about this definition 371 | ctx.AppendOp(vm.TXOPLiteral, entryPoint) 372 | ctx.AppendOp(vm.TXOPSaveToLvar, x.LocalVar.Offset) 373 | } 374 | 375 | func compileInclude(ctx *context, x *node.IncludeNode) { 376 | compile(ctx, x.IncludeTarget) 377 | ctx.AppendOp(vm.TXOPPush) 378 | // Arguments to include (WITH foo = "bar") need to be evaulated 379 | // in the OUTER context, but the variables need to be set in the 380 | // include context 381 | compileAssignmentNodes(ctx, x.AssignmentNodes) 382 | ctx.AppendOp(vm.TXOPPop) 383 | ctx.AppendOp(vm.TXOPPushmark) 384 | ctx.AppendOp(vm.TXOPInclude) 385 | ctx.AppendOp(vm.TXOPPopmark) 386 | } 387 | 388 | func compileBinaryArithmetic(ctx *context, n *node.BinaryNode) { 389 | var optype vm.OpType 390 | switch n.Type() { 391 | case node.Plus: 392 | optype = vm.TXOPAdd 393 | case node.Minus: 394 | optype = vm.TXOPSub 395 | case node.Mul: 396 | optype = vm.TXOPMul 397 | case node.Div: 398 | optype = vm.TXOPDiv 399 | default: 400 | panic("Unknown arithmetic") 401 | } 402 | ctx.AppendOp(vm.TXOPNoop).SetComment("BEGIN " + optype.String()) 403 | compileBinaryOperands(ctx, n) 404 | ctx.AppendOp(optype).SetComment("Execute " + optype.String() + " on registers sa and sb") 405 | } 406 | 407 | func compileLiteral(ctx *context, n node.Node) { 408 | var op vm.Op 409 | switch n.Type() { 410 | case node.Int: 411 | op = ctx.AppendOp(vm.TXOPLiteral, n.(*node.NumberNode).Value.Int()) 412 | case node.Text: 413 | op = ctx.AppendOp(vm.TXOPLiteral, n.(*node.TextNode).Text) 414 | default: 415 | panic("unknown literal value") 416 | } 417 | op.SetComment("Save literal to sa") 418 | } 419 | 420 | func compileAssignment(ctx *context, n *node.AssignmentNode) { 421 | compile(ctx, n.Expression) 422 | // XXX this 0 must be pre-computed 423 | ctx.AppendOp(vm.TXOPSaveToLvar, 0).SetComment("Saving to local var 0") 424 | } 425 | 426 | func compileLoadLvar(ctx *context, n *node.LocalVarNode) { 427 | ctx.AppendOp(vm.TXOPLoadLvar, n).SetComment("Load variable '" + n.Name + "' to sa") 428 | } 429 | -------------------------------------------------------------------------------- /compiler/compiler_test.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "github.com/lestrrat-go/xslate/parser/tterse" 5 | "github.com/lestrrat-go/xslate/test" 6 | "github.com/lestrrat-go/xslate/vm" 7 | "testing" 8 | ) 9 | 10 | func compile(t *testing.T, tmpl string) *vm.ByteCode { 11 | p := tterse.New() 12 | ast, err := p.ParseString(tmpl, tmpl) 13 | if err != nil { 14 | t.Fatalf("Failed to parse template: %s", err) 15 | } 16 | 17 | c := New() 18 | bc, err := c.Compile(ast) 19 | if err != nil { 20 | t.Fatalf("Failed to compile ast: %s", err) 21 | } 22 | 23 | t.Logf("-> %+v", bc) 24 | 25 | return bc 26 | } 27 | 28 | func TestCompiler(t *testing.T) { 29 | c := New() 30 | if c == nil { 31 | t.Fatalf("Failed to instanticate compiler") 32 | } 33 | } 34 | 35 | func TestCompile_RawText(t *testing.T) { 36 | compile(t, `Hello, World!`) 37 | } 38 | 39 | func TestCompile_LocalVar(t *testing.T) { 40 | compile(t, `[% s %]`) 41 | } 42 | 43 | func TestCompile_Wrapper(t *testing.T) { 44 | c := test.NewCtx(t) 45 | defer c.Cleanup() 46 | 47 | index := c.File("index.tx") 48 | index.WriteString(`[% WRAPPER "wrapper.tx" %]Hello[% END %]`) 49 | c.File("wrapper.tx").WriteString(`Hello, [% content %], Hello`) 50 | 51 | p := tterse.New() 52 | ast, err := p.Parse("index.tx", index.Read()) 53 | if err != nil { 54 | t.Fatalf("Failed to parse template: %s", err) 55 | } 56 | 57 | comp := New() 58 | bc, err := comp.Compile(ast) 59 | if err != nil { 60 | t.Fatalf("Failed to compile ast: %s", err) 61 | } 62 | 63 | t.Logf("-> %+v", bc) 64 | } 65 | -------------------------------------------------------------------------------- /compiler/interface.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "github.com/lestrrat-go/xslate/parser" 5 | "github.com/lestrrat-go/xslate/vm" 6 | ) 7 | 8 | // Compiler is the interface to objects that can convert AST trees to 9 | // actual Xslate Virtual Machine bytecode (see vm.ByteCode) 10 | type Compiler interface { 11 | Compile(*parser.AST) (*vm.ByteCode, error) 12 | } 13 | 14 | type context struct { 15 | ByteCode *vm.ByteCode 16 | } 17 | 18 | // BasicCompiler is the default compiler used by Xslate 19 | type BasicCompiler struct{} 20 | 21 | // Optimizer is the interface of things that can optimize the ByteCode 22 | type Optimizer interface { 23 | Optimize(*vm.ByteCode) error 24 | } 25 | 26 | // NaiveOptimizer is the default ByteCode optimizer 27 | type NaiveOptimizer struct{} 28 | -------------------------------------------------------------------------------- /compiler/optimizer.go: -------------------------------------------------------------------------------- 1 | package compiler 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/lestrrat-go/xslate/vm" 7 | ) 8 | 9 | // Optimize modifies the ByteCode in place to an optimized version 10 | func (o *NaiveOptimizer) Optimize(bc *vm.ByteCode) error { 11 | for i := 0; i < bc.Len(); i++ { 12 | op := bc.Get(i) 13 | if op == nil { 14 | return errors.New("failed to fetch op '" + op.String() + "'") 15 | } 16 | switch op.Type() { 17 | case vm.TXOPLiteral: 18 | if i+1 < bc.Len() && bc.Get(i+1).Type() == vm.TXOPPrintRaw { 19 | bc.OpList[i] = vm.NewOp(vm.TXOPPrintRawConst, op.ArgString()) 20 | bc.OpList[i+1] = vm.NewOp(vm.TXOPNoop) 21 | i++ 22 | } 23 | } 24 | } 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /functions/array/array.go: -------------------------------------------------------------------------------- 1 | package array 2 | 3 | import ( 4 | "github.com/lestrrat-go/xslate/functions" 5 | ) 6 | 7 | var depot = functions.NewFuncDepot("array") 8 | 9 | func init() { 10 | depot.Set("Item", Item) 11 | depot.Set("Size", Size) 12 | } 13 | 14 | // Item returns the `i`-th item in the list 15 | func Item(l []interface{}, i int) interface{} { 16 | return l[i] 17 | } 18 | 19 | // Size returns the size of the list 20 | func Size(l []interface{}) int { 21 | return len(l) 22 | } 23 | 24 | // First returns the first element 25 | func First(l []interface{}) interface{} { 26 | return l[0] 27 | } 28 | 29 | // Last returns the last element 30 | func Last(l []interface{}) interface{} { 31 | return l[len(l)-1] 32 | } 33 | 34 | // Depot returns the FuncDepot for "array" 35 | func Depot() *functions.FuncDepot { 36 | return depot 37 | } 38 | -------------------------------------------------------------------------------- /functions/depot.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // FuncDepot is a map of function name to it's real content 8 | // wrapped in reflect.ValueOf() 9 | type FuncDepot struct { 10 | namespace string 11 | depot map[string]reflect.Value 12 | } 13 | 14 | // NewFuncDepot creates a new FuncDepot under the given `namespace` 15 | func NewFuncDepot(namespace string) *FuncDepot { 16 | return &FuncDepot{namespace, make(map[string]reflect.Value)} 17 | } 18 | 19 | // Map returns the map of string to function 20 | func (fc *FuncDepot) Map() map[string]reflect.Value { 21 | return fc.depot 22 | } 23 | 24 | // Get returns the function associated with the given key. The function 25 | // is wrapped as reflect.Value so reflection can be used to determine 26 | // attributes about this function 27 | func (fc *FuncDepot) Get(key string) (reflect.Value, bool) { 28 | f, ok := fc.depot[key] 29 | return f, ok 30 | } 31 | 32 | // Set stores the function under the name `key` 33 | func (fc *FuncDepot) Set(key string, v interface{}) { 34 | fc.depot[key] = reflect.ValueOf(v) 35 | } 36 | -------------------------------------------------------------------------------- /functions/hash/hash.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "github.com/lestrrat-go/xslate/functions" 5 | ) 6 | 7 | var depot = functions.NewFuncDepot("hash") 8 | 9 | func init() { 10 | depot.Set("Keys", Keys) 11 | } 12 | 13 | // Keys returns the list of keys in this map. You can use this from a template 14 | // like so `[% FOREACH key IN hash.Keys(mymap) %]...[% END %]` or 15 | func Keys(m map[interface{}]interface{}) []interface{} { 16 | l := make([]interface{}, len(m)) 17 | i := 0 18 | for k := range m { 19 | l[i] = k 20 | i++ 21 | } 22 | return l 23 | } 24 | 25 | // Depot returns the Depot for hash package 26 | func Depot() *functions.FuncDepot { 27 | return depot 28 | } 29 | -------------------------------------------------------------------------------- /functions/time/time.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | "github.com/lestrrat-go/xslate/functions" 5 | "time" 6 | ) 7 | 8 | var depot = functions.NewFuncDepot("time") 9 | 10 | func init() { 11 | depot.Set("After", time.After) 12 | depot.Set("Sleep", time.Sleep) 13 | depot.Set("Since", time.Since) 14 | depot.Set("Now", time.Now) 15 | depot.Set("ParseDuration", time.ParseDuration) 16 | depot.Set("Since", time.Since) 17 | } 18 | 19 | // Depot returns the FuncDepot in the "time" namespace 20 | func Depot() *functions.FuncDepot { 21 | return depot 22 | } 23 | -------------------------------------------------------------------------------- /internal/frame/frame.go: -------------------------------------------------------------------------------- 1 | package frame 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/lestrrat-go/xslate/internal/stack" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // Frame represents a single stack frame. It has a reference to the main 11 | // stack where the actual data resides. Frame is just a convenient 12 | // wrapper to remember when the Frame started 13 | type Frame struct { 14 | stack stack.Stack 15 | mark int 16 | } 17 | 18 | // New creates a new Frame instance. 19 | func New(s stack.Stack) *Frame { 20 | return &Frame{ 21 | mark: 0, 22 | stack: s, 23 | } 24 | } 25 | 26 | func (f Frame) Stack() stack.Stack { 27 | return f.stack 28 | } 29 | 30 | // SetMark sets the offset from which this frame's variables may be stored 31 | func (f *Frame) SetMark(v int) { 32 | f.mark = v 33 | } 34 | 35 | // Mark returns the current mark index 36 | func (f *Frame) Mark() int { 37 | return f.mark 38 | } 39 | 40 | // DeclareVar puts a new variable in the stack, and returns the 41 | // index where it now resides 42 | func (f *Frame) DeclareVar(v interface{}) int { 43 | f.stack.Push(v) 44 | return f.stack.Size() - 1 45 | } 46 | 47 | // GetLvar gets the frame local variable at position i 48 | func (f *Frame) GetLvar(i int) (interface{}, error) { 49 | v, err := f.stack.Get(i) 50 | if err != nil { 51 | return nil, errors.Wrap(err, "failed to get local variable at "+strconv.Itoa(i+f.mark)) 52 | } 53 | return v, nil 54 | } 55 | 56 | // SetLvar sets the frame local variable at position i 57 | func (f *Frame) SetLvar(i int, v interface{}) { 58 | f.stack.Set(i, v) 59 | } 60 | 61 | // LastLvarIndex returns the index of the last element in our stack. 62 | func (f *Frame) LastLvarIndex() int { 63 | return f.stack.Size() 64 | } 65 | -------------------------------------------------------------------------------- /internal/frame/frame_test.go: -------------------------------------------------------------------------------- 1 | package frame 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lestrrat-go/xslate/internal/stack" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFrame_Lvar(t *testing.T) { 11 | f := New(stack.New(5)) 12 | f.SetLvar(0, 1) 13 | x, err := f.GetLvar(0) 14 | if !assert.NoError(t, err, "f.GetLvar(0) should succeed") { 15 | return 16 | } 17 | 18 | if !assert.Equal(t, 1, x, "f.GetLvar(0) should be 1") { 19 | return 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/rbpool/rbpool.go: -------------------------------------------------------------------------------- 1 | package rbpool 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | // render buffer pool 9 | var pool = sync.Pool{ 10 | New: allocRenderBuffer, 11 | } 12 | 13 | func allocRenderBuffer() interface{} { 14 | return &bytes.Buffer{} 15 | } 16 | 17 | func Get() *bytes.Buffer { 18 | return pool.Get().(*bytes.Buffer) 19 | } 20 | 21 | func Release(buf *bytes.Buffer) { 22 | buf.Reset() 23 | pool.Put(buf) 24 | } 25 | -------------------------------------------------------------------------------- /internal/rvpool/rvpool.go: -------------------------------------------------------------------------------- 1 | package rvpool 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // render vars pool 8 | var pool = sync.Pool{ 9 | New: allocRenderVars, 10 | } 11 | 12 | func allocRenderVars() interface{} { 13 | return map[string]interface{}{} 14 | } 15 | 16 | func Get() map[string]interface{} { 17 | return pool.Get().(map[string]interface{}) 18 | } 19 | 20 | func Release(m map[string]interface{}) { 21 | pool.Put(m) 22 | } 23 | -------------------------------------------------------------------------------- /internal/stack/stack.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | ) 9 | 10 | // Stack is a simple structure to hold various data 11 | type Stack []interface{} 12 | 13 | // Reset clears the contents of the stack and pushes back the cursor 14 | // as if nothing is in the stack 15 | func (s *Stack) Reset() { 16 | *s = (*s)[:0] 17 | } 18 | 19 | func calcNewSize(base int) int { 20 | if base < 100 { 21 | return base * 2 22 | } 23 | return int(float64(base) * 1.5) 24 | } 25 | 26 | // NewStack creates a new Stack of initial size `size`. 27 | func New(size int) Stack { 28 | return Stack(make([]interface{}, 0, size)) 29 | } 30 | 31 | // Top returns the element at the top of the stack or an error if stack is empty 32 | func (s *Stack) Top() (interface{}, error) { 33 | if len(*s) == 0 { 34 | return nil, errors.New("nothing on stack") 35 | } 36 | return (*s)[len(*s)-1], nil 37 | } 38 | 39 | // BufferSize returns the length of the underlying buffer 40 | func (s *Stack) BufferSize() int { 41 | return cap(*s) 42 | } 43 | 44 | // Size returns the number of elements stored in this stack 45 | func (s *Stack) Size() int { 46 | return len(*s) 47 | } 48 | 49 | // Resize changes the size of the underlying buffer 50 | func (s *Stack) Resize(size int) { 51 | newl := make([]interface{}, len(*s), size) 52 | copy(newl, *s) 53 | *s = newl 54 | } 55 | 56 | // Extend changes the size of the underlying buffer, extending it by `extendBy` 57 | func (s *Stack) Extend(extendBy int) { 58 | s.Resize(s.Size() + extendBy) 59 | } 60 | 61 | // Grow automatically grows the underlying buffer so that it can hold at 62 | // least `min` elements 63 | func (s *Stack) Grow(min int) { 64 | // Automatically grow the stack to some long-enough length 65 | if min <= s.BufferSize() { 66 | // we have enough 67 | return 68 | } 69 | 70 | s.Resize(calcNewSize(min)) 71 | } 72 | 73 | // Get returns the element at position `i` 74 | func (s *Stack) Get(i int) (interface{}, error) { 75 | if i < 0 || i >= len(*s) { 76 | return nil, errors.New(strconv.Itoa(i) + " is out of range") 77 | } 78 | 79 | return (*s)[i], nil 80 | } 81 | 82 | // Set sets the element at position `i` to `v`. The stack size is automatically 83 | // adjusted. 84 | func (s *Stack) Set(i int, v interface{}) error { 85 | if i < 0 { 86 | return errors.New("invalid index into stack") 87 | } 88 | 89 | if i >= s.BufferSize() { 90 | s.Resize(calcNewSize(i)) 91 | } 92 | 93 | for len(*s) < i + 1 { 94 | *s = append(*s, nil) 95 | } 96 | 97 | (*s)[i] = v 98 | return nil 99 | } 100 | 101 | // Push adds an element at the end of the stack 102 | func (s *Stack) Push(v interface{}) { 103 | if len(*s) >= s.BufferSize() { 104 | s.Resize(calcNewSize(cap(*s))) 105 | } 106 | 107 | *s = append(*s, v) 108 | } 109 | 110 | // Pop removes and returns the item at the end of the stack 111 | func (s *Stack) Pop() interface{} { 112 | l := len(*s) 113 | if l == 0 { 114 | return nil 115 | } 116 | 117 | v := (*s)[l-1] 118 | *s = (*s)[:l-1] 119 | return v 120 | } 121 | 122 | // String returns the textual representation of the stack 123 | func (s *Stack) String() string { 124 | buf := bytes.Buffer{} 125 | for k, v := range *s { 126 | fmt.Fprintf(&buf, "%03d: %q\n", k, v) 127 | } 128 | return buf.String() 129 | } 130 | -------------------------------------------------------------------------------- /internal/stack/stack_test.go: -------------------------------------------------------------------------------- 1 | package stack 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type IntWrap struct{ i int } 10 | 11 | func TestStack_Grow(t *testing.T) { 12 | s := New(5) 13 | for i := 0; i < 10; i++ { 14 | if i%2 == 0 { 15 | s.Push(i) 16 | } else { 17 | s.Push(IntWrap{i}) 18 | } 19 | } 20 | 21 | for i := 0; i < 10; i++ { 22 | x, err := s.Get(i) 23 | if !assert.NoError(t, err, "s.Get(%d) should succeed", i) { 24 | return 25 | } 26 | 27 | if i%2 == 0 { 28 | if !assert.Equal(t, i, x, "s.Get(%d) should be %d (got %v)", i, i, x) { 29 | t.Logf("%s", s) 30 | return 31 | } 32 | } else { 33 | if !assert.Equal(t, IntWrap{i}, x, "s.Get(%d) should be IntWrap{%d} (got %v)", i, i, x) { 34 | t.Logf("%s", s) 35 | return 36 | } 37 | } 38 | } 39 | 40 | for i := 9; i > -1; i-- { 41 | x := s.Pop() 42 | if i%2 == 0 { 43 | if x.(int) != i { 44 | t.Errorf("Pop(%d): Expected %d, got %s\n", i, i, x) 45 | } 46 | } else { 47 | if x.(IntWrap).i != i { 48 | t.Errorf("Get(%d): Expected %d, got %s\n", i, x.(IntWrap).i, x) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /kolonish_test.go: -------------------------------------------------------------------------------- 1 | package xslate 2 | 3 | import ( 4 | "github.com/lestrrat-go/xslate/test" 5 | "testing" 6 | ) 7 | 8 | func newKolonCtx(t test.Tester) *testctx { 9 | c := newTestCtx(t) 10 | pargs := c.XslateArgs["Parser"].(Args) 11 | pargs["Syntax"] = "Kolon" 12 | 13 | return c 14 | } 15 | 16 | func TestKolonish_SimpleString(t *testing.T) { 17 | c := newKolonCtx(t) 18 | defer c.Cleanup() 19 | 20 | c.renderStringAndCompare(`Hello, World!`, nil, `Hello, World!`) 21 | c.renderStringAndCompare(` <:- "Hello, World!" :>`, nil, `Hello, World!`) 22 | c.renderStringAndCompare(`<: "Hello, World!" -:> `, nil, `Hello, World!`) 23 | } 24 | 25 | func TestKolonish_Comments(t *testing.T) { 26 | c := newKolonCtx(t) 27 | defer c.Cleanup() 28 | 29 | // XXX TODO 30 | // c.renderStringAndCompare(`:# This is a comment`, nil, ``) 31 | c.renderStringAndCompare(` <:- "Hello, World!" :>`, nil, `Hello, World!`) 32 | c.renderStringAndCompare(`<: "Hello, World!" -:> `, nil, `Hello, World!`) 33 | } 34 | -------------------------------------------------------------------------------- /loader/cache.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "bufio" 5 | "encoding/gob" 6 | "fmt" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/lestrrat-go/xslate/compiler" 12 | "github.com/lestrrat-go/xslate/parser" 13 | "github.com/lestrrat-go/xslate/vm" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | // NewCachedByteCodeLoader creates a new CachedByteCodeLoader 18 | func NewCachedByteCodeLoader( 19 | cache Cache, 20 | cacheLevel CacheStrategy, 21 | fetcher TemplateFetcher, 22 | parser parser.Parser, 23 | compiler compiler.Compiler, 24 | ) *CachedByteCodeLoader { 25 | return &CachedByteCodeLoader{ 26 | NewStringByteCodeLoader(parser, compiler), 27 | NewReaderByteCodeLoader(parser, compiler), 28 | fetcher, 29 | []Cache{MemoryCache{}, cache}, 30 | cacheLevel, 31 | } 32 | } 33 | 34 | func (l *CachedByteCodeLoader) DumpAST(v bool) { 35 | l.StringByteCodeLoader.DumpAST(v) 36 | l.ReaderByteCodeLoader.DumpAST(v) 37 | } 38 | 39 | func (l *CachedByteCodeLoader) DumpByteCode(v bool) { 40 | l.StringByteCodeLoader.DumpByteCode(v) 41 | l.ReaderByteCodeLoader.DumpByteCode(v) 42 | } 43 | 44 | func (l *CachedByteCodeLoader) ShouldDumpAST() bool { 45 | return l.StringByteCodeLoader.ShouldDumpAST() || l.ReaderByteCodeLoader.ShouldDumpAST() 46 | } 47 | 48 | func (l *CachedByteCodeLoader) ShouldDumpByteCode() bool { 49 | return l.StringByteCodeLoader.ShouldDumpByteCode() || l.ReaderByteCodeLoader.ShouldDumpByteCode() 50 | } 51 | 52 | // Load loads the ByteCode for template specified by `key`, which, for this 53 | // ByteCodeLoader, is the path to the template we want. 54 | // If cached vm.ByteCode struct is found, it is loaded and its last modified 55 | // time is compared against that of the template file. If the template is 56 | // newer, it's compiled. Otherwise the cached version is used, saving us the 57 | // time to parse and compile the template. 58 | func (l *CachedByteCodeLoader) Load(key string) (bc *vm.ByteCode, err error) { 59 | defer func() { 60 | if bc != nil && err == nil && l.ShouldDumpByteCode() { 61 | fmt.Fprintf(os.Stderr, "%s\n", bc.String()) 62 | } 63 | }() 64 | 65 | var source TemplateSource 66 | if l.CacheLevel > CacheNone { 67 | var entity *CacheEntity 68 | for _, cache := range l.Caches { 69 | entity, err = cache.Get(key) 70 | if err == nil { 71 | break 72 | } 73 | } 74 | 75 | if err == nil { 76 | if l.CacheLevel == CacheNoVerify { 77 | return entity.ByteCode, nil 78 | } 79 | 80 | t, err := entity.Source.LastModified() 81 | if err != nil { 82 | return nil, errors.Wrap(err, "failed to get last-modified from source") 83 | } 84 | 85 | if t.Before(entity.ByteCode.GeneratedOn) { 86 | return entity.ByteCode, nil 87 | } 88 | 89 | // ByteCode validation failed, but we can still re-use source 90 | source = entity.Source 91 | } 92 | } 93 | 94 | if source == nil { 95 | source, err = l.Fetcher.FetchTemplate(key) 96 | if err != nil { 97 | return nil, errors.Wrap(err, "failed to fetch template") 98 | } 99 | } 100 | 101 | rdr, err := source.Reader() 102 | if err != nil { 103 | return nil, errors.Wrap(err, "failed to get the reader") 104 | } 105 | 106 | bc, err = l.LoadReader(key, rdr) 107 | if err != nil { 108 | return nil, errors.Wrap(err, "failed to read byte code") 109 | } 110 | 111 | entity := &CacheEntity{bc, source} 112 | for _, cache := range l.Caches { 113 | cache.Set(key, entity) 114 | } 115 | 116 | return bc, nil 117 | } 118 | 119 | // NewFileCache creates a new FileCache which stores caches underneath 120 | // the directory specified by `dir` 121 | func NewFileCache(dir string) (*FileCache, error) { 122 | f := &FileCache{dir} 123 | return f, nil 124 | } 125 | 126 | // GetCachePath creates a string describing where a given template key 127 | // would be cached in the file system 128 | func (c *FileCache) GetCachePath(key string) string { 129 | // What's the best, portable way to remove make an absolute path into 130 | // a relative path? 131 | key = filepath.Clean(key) 132 | key = strings.TrimPrefix(key, "/") 133 | return filepath.Join(c.Dir, key) 134 | } 135 | 136 | // Get returns the cached vm.ByteCode, if available 137 | func (c *FileCache) Get(key string) (*CacheEntity, error) { 138 | path := c.GetCachePath(key) 139 | 140 | // Need to avoid race condition 141 | file, err := os.Open(path) 142 | if err != nil { 143 | return nil, errors.Wrap(err, "failed to open cache file '"+path+"'") 144 | } 145 | defer file.Close() 146 | 147 | var entity CacheEntity 148 | dec := gob.NewDecoder(file) 149 | if err = dec.Decode(&entity); err != nil { 150 | return nil, errors.Wrap(err, "failed to gob decode from cache file '"+path+"'") 151 | } 152 | 153 | return &entity, nil 154 | } 155 | 156 | // Set creates a new cache file to store the ByteCode. 157 | func (c *FileCache) Set(key string, entity *CacheEntity) error { 158 | path := c.GetCachePath(key) 159 | if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil { 160 | return errors.Wrap(err, "failed to create directory for cache file") 161 | } 162 | 163 | // Need to avoid race condition 164 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666) 165 | if err != nil { 166 | return errors.Wrap(err, "failed to open/create a cache file") 167 | } 168 | defer file.Close() 169 | 170 | f := bufio.NewWriter(file) 171 | defer f.Flush() 172 | enc := gob.NewEncoder(f) 173 | if err = enc.Encode(entity); err != nil { 174 | return errors.Wrap(err, "failed to encode Entity via gob") 175 | } 176 | 177 | return nil 178 | } 179 | 180 | // Delete deletes the cache 181 | func (c *FileCache) Delete(key string) error { 182 | return errors.Wrap(os.Remove(c.GetCachePath(key)), "failed to remove file cache file") 183 | } 184 | 185 | // Get returns the cached ByteCode 186 | func (c MemoryCache) Get(key string) (*CacheEntity, error) { 187 | bc, ok := c[key] 188 | if !ok { 189 | return nil, errors.New("cache miss") 190 | } 191 | return bc, nil 192 | } 193 | 194 | // Set stores the ByteCode 195 | func (c MemoryCache) Set(key string, bc *CacheEntity) error { 196 | c[key] = bc 197 | return nil 198 | } 199 | 200 | // Delete deletes the ByteCode 201 | func (c MemoryCache) Delete(key string) error { 202 | delete(c, key) 203 | return nil 204 | } 205 | -------------------------------------------------------------------------------- /loader/file.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | ) 11 | 12 | // ErrAbsolutePathNotAllowed is returned when the given path is not a 13 | // relative path. As of this writing, Xslate does not allow you to load 14 | // templates by absolute path, but this probably should be configurable 15 | var ErrAbsolutePathNotAllowed = errors.New("error: Absolute paths are not allowed") 16 | 17 | // NewFileTemplateFetcher creates a new struct. `paths` must give us the 18 | // directories for us to look the templates in 19 | func NewFileTemplateFetcher(paths []string) (*FileTemplateFetcher, error) { 20 | l := &FileTemplateFetcher{ 21 | Paths: make([]string, len(paths)), 22 | } 23 | for k, v := range paths { 24 | abs, err := filepath.Abs(v) 25 | if err != nil { 26 | return nil, err 27 | } 28 | l.Paths[k] = abs 29 | } 30 | return l, nil 31 | } 32 | 33 | // FetchTemplate returns a TemplateSource representing the template at path 34 | // `path`. Paths are searched relative to the paths given to NewFileTemplateFetcher() 35 | func (l *FileTemplateFetcher) FetchTemplate(path string) (TemplateSource, error) { 36 | if filepath.IsAbs(path) { 37 | return nil, ErrAbsolutePathNotAllowed 38 | } 39 | 40 | for _, dir := range l.Paths { 41 | fullpath := filepath.Join(dir, path) 42 | 43 | _, err := os.Stat(fullpath) 44 | if err != nil { 45 | continue 46 | } 47 | 48 | return NewFileSource(fullpath), nil 49 | } 50 | return nil, ErrTemplateNotFound 51 | } 52 | 53 | // LastModified returns time when the target template file was last modified 54 | func (s *FileSource) LastModified() (time.Time, error) { 55 | // Calling os.Stat() for *every* Render of the same source is a waste 56 | // Only call os.Stat() if we haven't done so in the last 1 second 57 | if time.Since(s.LastStat) < time.Second { 58 | // A-ha! it's not that long ago we calculated this value, just return 59 | // the same thing as our last call 60 | return s.LastStatResult.ModTime(), nil 61 | } 62 | 63 | // If we got here, our previous check was too old or this is the first 64 | // time we're checking for os.Stat() 65 | fi, err := os.Stat(s.Path) 66 | if err != nil { 67 | return time.Time{}, err 68 | } 69 | 70 | // Save these for later... 71 | s.LastStat = time.Now() 72 | s.LastStatResult = fi 73 | 74 | return s.LastStatResult.ModTime(), nil 75 | } 76 | 77 | // Reader returns the io.Reader instance for the file source 78 | func (s *FileSource) Reader() (io.Reader, error) { 79 | fh, err := os.Open(s.Path) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return fh, nil 84 | } 85 | 86 | // Bytes returns the bytes in teh template file 87 | func (s *FileSource) Bytes() ([]byte, error) { 88 | rdr, err := s.Reader() 89 | if err != nil { 90 | return nil, err 91 | } 92 | return ioutil.ReadAll(rdr) 93 | } 94 | -------------------------------------------------------------------------------- /loader/http.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "time" 11 | ) 12 | 13 | // NewHTTPTemplateFetcher creates a new struct. `urls` must give us the 14 | // base HTTP urls for us to look the templates in (note: do not use trailing slashes) 15 | func NewHTTPTemplateFetcher(urls []string) (*HTTPTemplateFetcher, error) { 16 | f := &HTTPTemplateFetcher{ 17 | URLs: make([]string, len(urls)), 18 | } 19 | for k, v := range urls { 20 | u, err := url.Parse(v) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | if !u.IsAbs() { 26 | return nil, fmt.Errorf("url %s is not an absolute url", v) 27 | } 28 | f.URLs[k] = u.String() 29 | } 30 | return f, nil 31 | } 32 | 33 | // FetchTemplate returns a TemplateSource representing the template at path 34 | // `path`. Paths are searched relative to the urls given to NewHTTPTemplateFetcher() 35 | func (l *HTTPTemplateFetcher) FetchTemplate(path string) (TemplateSource, error) { 36 | u, err := url.Parse(path) 37 | 38 | if err != nil { 39 | return nil, fmt.Errorf("error parsing given path as url: %s", err) 40 | } 41 | 42 | if u.IsAbs() { 43 | return nil, ErrAbsolutePathNotAllowed 44 | } 45 | 46 | // XXX Consider caching! 47 | for _, base := range l.URLs { 48 | u := base + "/" + path 49 | res, err := http.Get(u) 50 | if err != nil { 51 | continue 52 | } 53 | 54 | return NewHTTPSource(res) 55 | } 56 | return nil, ErrTemplateNotFound 57 | } 58 | 59 | // NewHTTPSource creates a new HTTPSource instance 60 | func NewHTTPSource(r *http.Response) (*HTTPSource, error) { 61 | body, err := ioutil.ReadAll(r.Body) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | s := &HTTPSource{ 67 | bytes.NewBuffer(body), 68 | time.Time{}, 69 | } 70 | 71 | if lastmodStr := r.Header.Get("Last-Modified"); lastmodStr != "" { 72 | t, err := time.Parse(http.TimeFormat, lastmodStr) 73 | if err != nil { 74 | fmt.Printf("failed to parse: %s\n", err) 75 | t = time.Now() 76 | } 77 | s.LastModifiedTime = t 78 | } else { 79 | s.LastModifiedTime = time.Now() 80 | } 81 | 82 | return s, nil 83 | } 84 | 85 | // LastModified returns the last modified date of this template 86 | func (s *HTTPSource) LastModified() (time.Time, error) { 87 | return s.LastModifiedTime, nil 88 | } 89 | 90 | // Reader returns the io.Reader for the template 91 | func (s *HTTPSource) Reader() (io.Reader, error) { 92 | return s.Buffer, nil 93 | } 94 | 95 | // Bytes returns the bytes in the template file 96 | func (s *HTTPSource) Bytes() ([]byte, error) { 97 | return s.Buffer.Bytes(), nil 98 | } 99 | -------------------------------------------------------------------------------- /loader/http_test.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestHTTPFetcher(t *testing.T) { 12 | content := `Hello, World!` 13 | modtime := time.Now().Add(-1 * time.Hour).UTC() 14 | modtime = modtime.Add(-1 * time.Duration(modtime.Nanosecond())) 15 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 16 | switch r.URL.Path { 17 | case "/hello.tx": 18 | w.Header().Set("Last-Modified", modtime.Format(http.TimeFormat)) 19 | fmt.Fprintf(w, content) 20 | default: 21 | http.Error(w, "Not Found", http.StatusNotFound) 22 | return 23 | } 24 | })) 25 | defer ts.Close() 26 | 27 | f, err := NewHTTPTemplateFetcher([]string{ts.URL}) 28 | if err != nil { 29 | t.Fatalf("failed to instantiate fetcher: %s", err) 30 | } 31 | 32 | s, err := f.FetchTemplate("hello.tx") 33 | if err != nil { 34 | t.Fatalf("failed to fetch template 'hello.tx': %s", err) 35 | } 36 | 37 | if lastmod, err := s.LastModified(); err != nil || lastmod != modtime { 38 | t.Errorf("last-modified does not match. got '%s', expected '%s'", lastmod, modtime) 39 | } 40 | 41 | if b, err := s.Bytes(); err != nil || string(b) != content { 42 | t.Errorf("content does not match. got '%s'", b) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /loader/interface.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "os" 8 | "time" 9 | 10 | "github.com/lestrrat-go/xslate/compiler" 11 | "github.com/lestrrat-go/xslate/vm" 12 | "github.com/lestrrat-go/xslate/parser" 13 | ) 14 | 15 | // CacheStrategy specifies how the cache should be checked 16 | type CacheStrategy int 17 | 18 | const ( 19 | // CacheNone flag specifies that cache checking and setting hould be skipped 20 | CacheNone CacheStrategy = iota 21 | // CacheVerify flag specifies that cached ByteCode generation time should be 22 | // verified against the source's last modified time. If new, the source is 23 | // re-parsed and re-compiled even on a cache hit. 24 | CacheVerify 25 | // CacheNoVerify flag specifies that if we have a cache hit, the ByteCode 26 | // is not verified against the source. If there's a cache hit, it is 27 | // used regardless of updates to the original template on file system 28 | CacheNoVerify 29 | ) 30 | 31 | // CacheEntity contains all the othings required to perform calculations 32 | // necessary to validate a template 33 | type CacheEntity struct { 34 | ByteCode *vm.ByteCode 35 | Source TemplateSource 36 | } 37 | 38 | // Cache defines the interface for things that can cache generated ByteCode 39 | type Cache interface { 40 | Get(string) (*CacheEntity, error) 41 | Set(string, *CacheEntity) error 42 | Delete(string) error 43 | } 44 | 45 | // CachedByteCodeLoader is the default ByteCodeLoader that loads templates 46 | // from the file system and caches in the file system, too 47 | type CachedByteCodeLoader struct { 48 | *StringByteCodeLoader // gives us LoadString 49 | *ReaderByteCodeLoader // gives us LoadReader 50 | Fetcher TemplateFetcher 51 | Caches []Cache 52 | CacheLevel CacheStrategy 53 | } 54 | 55 | // FileCache is Cache implementation that stores caches in the file system 56 | type FileCache struct { 57 | Dir string 58 | } 59 | 60 | // MemoryCache is what's used store cached ByteCode in memory for maximum 61 | // speed. As of this writing this cache never freed. We may need to 62 | // introduce LRU in the future 63 | type MemoryCache map[string]*CacheEntity 64 | 65 | // FileTemplateFetcher is a TemplateFetcher that loads template strings 66 | // in the file system. 67 | type FileTemplateFetcher struct { 68 | Paths []string 69 | } 70 | 71 | // NewFileSource creates a new FileSource 72 | func NewFileSource(path string) *FileSource { 73 | return &FileSource{path, time.Time{}, nil} 74 | } 75 | 76 | // FileSource is a TemplateSource variant that holds template information 77 | // in a file. 78 | type FileSource struct { 79 | Path string 80 | LastStat time.Time 81 | LastStatResult os.FileInfo 82 | } 83 | 84 | // HTTPTemplateFetcher is a proof of concept loader that fetches templates 85 | // from external http servers. Probably not a good thing to use in 86 | // your production environment 87 | type HTTPTemplateFetcher struct { 88 | URLs []string 89 | } 90 | 91 | // HTTPSource represents a template source fetched via HTTP 92 | type HTTPSource struct { 93 | Buffer *bytes.Buffer 94 | LastModifiedTime time.Time 95 | } 96 | 97 | // ReaderByteCodeLoader is a fancy name for objects that can "given a template 98 | // string, parse and compile it". This is one of the most common operations 99 | // that users want to do, but it needs to be separate from other loaders 100 | // because there's no sane way to cache intermediate results, and therefore 101 | // has significant performance penalty 102 | type ReaderByteCodeLoader struct { 103 | *Flags 104 | Parser parser.Parser 105 | Compiler compiler.Compiler 106 | } 107 | 108 | // Mask... set of constants are used as flags to denote Debug modes. 109 | // If you're using these you should be reading the source code 110 | const ( 111 | MaskDumpByteCode = 1 << iota 112 | MaskDumpAST 113 | ) 114 | 115 | // Flags holds the flags to indicate if certain debug operations should be 116 | // performed during load time 117 | type Flags struct{ flags int32 } 118 | 119 | // DebugDumper defines interface that an object able to dump debug informatin 120 | // during load time must fulfill 121 | type DebugDumper interface { 122 | DumpAST(bool) 123 | DumpByteCode(bool) 124 | ShouldDumpAST() bool 125 | ShouldDumpByteCode() bool 126 | } 127 | 128 | // ByteCodeLoader defines the interface for objects that can load 129 | // ByteCode specified by a key 130 | type ByteCodeLoader interface { 131 | DebugDumper 132 | LoadString(string, string) (*vm.ByteCode, error) 133 | Load(string) (*vm.ByteCode, error) 134 | } 135 | 136 | // TemplateFetcher defines the interface for objects that can load 137 | // TemplateSource specified by a key 138 | type TemplateFetcher interface { 139 | FetchTemplate(string) (TemplateSource, error) 140 | } 141 | 142 | // TemplateSource is an abstraction over the actual template, which may live 143 | // on a file system, cache, database, whatever. 144 | // It needs to be able to give us the actual template string AND its 145 | // last modified time 146 | type TemplateSource interface { 147 | LastModified() (time.Time, error) 148 | Bytes() ([]byte, error) 149 | Reader() (io.Reader, error) 150 | } 151 | 152 | // ErrTemplateNotFound is returned whenever one of the loaders failed to 153 | // find a suitable template 154 | var ErrTemplateNotFound = errors.New("error: Specified template was not found") 155 | 156 | // StringByteCodeLoader is a fancy name for objects that can "given a template 157 | // string, parse and compile it". This is one of the most common operations 158 | // that users want to do, but it needs to be separate from other loaders 159 | // because there's no sane way to cache intermediate results, and therefore 160 | // has significant performance penalty 161 | type StringByteCodeLoader struct { 162 | *Flags 163 | Parser parser.Parser 164 | Compiler compiler.Compiler 165 | } 166 | -------------------------------------------------------------------------------- /loader/loader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package loader abstracts the top-level xslate package from the job of 3 | loading the bytecode from a key value. 4 | */ 5 | package loader 6 | 7 | // NewFlags creates a new Flags struct initialized to 0 8 | func NewFlags() *Flags { 9 | return &Flags{0} 10 | } 11 | 12 | // DumpAST sets the bitmask for DumpAST debug flag 13 | func (f *Flags) DumpAST(b bool) { 14 | if b { 15 | f.flags |= MaskDumpAST 16 | } else { 17 | f.flags &= ^MaskDumpAST 18 | } 19 | } 20 | 21 | // DumpByteCode sets the bitmask for DumpByteCode debug flag 22 | func (f *Flags) DumpByteCode(b bool) { 23 | if b { 24 | f.flags |= MaskDumpByteCode 25 | } else { 26 | f.flags &= ^MaskDumpByteCode 27 | } 28 | } 29 | 30 | // ShouldDumpAST returns true if the DumpAST debug flag is set 31 | func (f *Flags) ShouldDumpAST() bool { 32 | return f.flags&MaskDumpAST == MaskDumpAST 33 | } 34 | 35 | // ShouldDumpByteCode returns true if the DumpByteCode debug flag is set 36 | func (f Flags) ShouldDumpByteCode() bool { 37 | return f.flags&MaskDumpByteCode == 1 38 | } 39 | -------------------------------------------------------------------------------- /loader/reader.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/lestrrat-go/xslate/compiler" 9 | "github.com/lestrrat-go/xslate/parser" 10 | "github.com/lestrrat-go/xslate/vm" 11 | ) 12 | 13 | // NewReaderByteCodeLoader creates a new object 14 | func NewReaderByteCodeLoader(p parser.Parser, c compiler.Compiler) *ReaderByteCodeLoader { 15 | return &ReaderByteCodeLoader{NewFlags(), p, c} 16 | } 17 | 18 | // LoadReader takes a io.Reader and compiles it into vm.ByteCode 19 | func (l *ReaderByteCodeLoader) LoadReader(name string, rdr io.Reader) (*vm.ByteCode, error) { 20 | ast, err := l.Parser.ParseReader(name, rdr) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | if l.ShouldDumpAST() { 26 | fmt.Fprintf(os.Stderr, "AST:\n%s\n", ast) 27 | } 28 | 29 | bc, err := l.Compiler.Compile(ast) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return bc, nil 35 | } 36 | -------------------------------------------------------------------------------- /loader/string.go: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import ( 4 | "fmt" 5 | "github.com/lestrrat-go/xslate/compiler" 6 | "github.com/lestrrat-go/xslate/parser" 7 | "github.com/lestrrat-go/xslate/vm" 8 | "os" 9 | ) 10 | 11 | // NewStringByteCodeLoader creates a new object 12 | func NewStringByteCodeLoader(p parser.Parser, c compiler.Compiler) *StringByteCodeLoader { 13 | return &StringByteCodeLoader{NewFlags(), p, c} 14 | } 15 | 16 | // LoadString takes a template string and compiles it into vm.ByteCode 17 | func (l *StringByteCodeLoader) LoadString(name string, template string) (*vm.ByteCode, error) { 18 | ast, err := l.Parser.ParseString(name, template) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | if l.ShouldDumpAST() { 24 | fmt.Fprintf(os.Stderr, "AST:\n%s\n", ast) 25 | } 26 | 27 | bc, err := l.Compiler.Compile(ast) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | if l.ShouldDumpByteCode() { 33 | fmt.Fprintf(os.Stderr, "ByteCode:\n%s\n", bc) 34 | } 35 | 36 | return bc, nil 37 | } 38 | -------------------------------------------------------------------------------- /node/interface.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import "reflect" 4 | 5 | const DefaultMaxIterations = 1000 6 | 7 | // NodeType is used to distinguish each AST node 8 | type NodeType int 9 | 10 | // Node defines the interface for an AST node 11 | type Node interface { 12 | Type() NodeType 13 | Copy() Node 14 | Pos() int 15 | Visit(chan Node) 16 | } 17 | 18 | type Appender interface { 19 | Node 20 | Append(Node) 21 | } 22 | 23 | const ( 24 | Noop NodeType = iota 25 | Root 26 | Text 27 | Number 28 | Int 29 | Float 30 | If 31 | Else 32 | List 33 | Foreach 34 | While 35 | Wrapper 36 | Include 37 | Assignment 38 | LocalVar 39 | FetchField 40 | FetchArrayElement 41 | MethodCall 42 | FunCall 43 | Print 44 | PrintRaw 45 | FetchSymbol 46 | Range 47 | Plus 48 | Minus 49 | Mul 50 | Div 51 | Equals 52 | NotEquals 53 | LT 54 | GT 55 | MakeArray 56 | Group 57 | Filter 58 | Macro 59 | Max 60 | ) 61 | 62 | // BaseNode is the most basic node with no extra data attached to it 63 | type BaseNode struct { 64 | NodeType // String() is delegated here 65 | pos int 66 | } 67 | 68 | type ListNode struct { 69 | BaseNode 70 | Nodes []Node 71 | } 72 | 73 | type NumberNode struct { 74 | BaseNode 75 | Value reflect.Value 76 | } 77 | 78 | type TextNode struct { 79 | BaseNode 80 | Text []byte 81 | } 82 | 83 | type WrapperNode struct { 84 | *ListNode 85 | WrapperName string 86 | // XXX need to make this configurable. currently it's only "content" 87 | // WrapInto string 88 | AssignmentNodes []Node 89 | } 90 | 91 | type AssignmentNode struct { 92 | BaseNode 93 | Assignee *LocalVarNode 94 | Expression Node 95 | } 96 | 97 | type LocalVarNode struct { 98 | BaseNode 99 | Name string 100 | Offset int 101 | } 102 | 103 | type LoopNode struct { 104 | *ListNode // Body of the loop 105 | Condition Node // 106 | MaxIteration int // Max number of iterations 107 | } 108 | 109 | type ForeachNode struct { 110 | *LoopNode 111 | IndexVarName string 112 | IndexVarIdx int 113 | List Node 114 | } 115 | 116 | type WhileNode struct { 117 | *LoopNode 118 | } 119 | 120 | type MethodCallNode struct { 121 | BaseNode 122 | Invocant Node 123 | MethodName string 124 | Args *ListNode 125 | } 126 | 127 | type FunCallNode struct { 128 | BaseNode 129 | Invocant Node 130 | Args *ListNode 131 | } 132 | 133 | type FetchFieldNode struct { 134 | BaseNode 135 | Container Node 136 | FieldName string 137 | } 138 | 139 | type IfNode struct { 140 | *ListNode 141 | BooleanExpression Node 142 | } 143 | 144 | type ElseNode struct { 145 | *ListNode 146 | IfNode Node 147 | } 148 | 149 | type UnaryNode struct { 150 | BaseNode 151 | Child Node 152 | } 153 | 154 | type BinaryNode struct { 155 | BaseNode 156 | Left Node 157 | Right Node 158 | } 159 | 160 | type FilterNode struct { 161 | *UnaryNode 162 | Name string 163 | } 164 | 165 | type MacroNode struct { 166 | *ListNode 167 | Name string 168 | LocalVar *LocalVarNode 169 | Arguments []*LocalVarNode 170 | } 171 | -------------------------------------------------------------------------------- /node/node.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // Type returns the current node type 9 | func (n NodeType) Type() NodeType { 10 | return n 11 | } 12 | 13 | // Pos returns the position of this node in the document 14 | func (n *BaseNode) Pos() int { 15 | return n.pos 16 | } 17 | 18 | func (n *BaseNode) Copy() Node { 19 | return &BaseNode{n.NodeType, n.pos} 20 | } 21 | 22 | func (n *BaseNode) Visit(c chan Node) { 23 | c <- n 24 | } 25 | 26 | // Noop nodes don't need to be distinct 27 | var noop = &BaseNode{Noop, 0} 28 | 29 | // NewNoopNode returns a op that does nothing 30 | func NewNoopNode() *BaseNode { 31 | return noop 32 | } 33 | 34 | func (l *ListNode) Visit(c chan Node) { 35 | c <- l 36 | for _, child := range l.Nodes { 37 | child.Visit(c) 38 | } 39 | } 40 | 41 | func NewListNode(pos int) *ListNode { 42 | return &ListNode{ 43 | BaseNode{List, pos}, 44 | []Node{}, 45 | } 46 | } 47 | 48 | func (l *ListNode) Copy() Node { 49 | n := NewListNode(l.pos) 50 | n.Nodes = make([]Node, len(l.Nodes)) 51 | for k, v := range l.Nodes { 52 | n.Nodes[k] = v.Copy() 53 | } 54 | return n 55 | } 56 | 57 | func (l *ListNode) Append(n Node) { 58 | l.Nodes = append(l.Nodes, n) 59 | } 60 | 61 | func NewTextNode(pos int, arg string) *TextNode { 62 | return &TextNode{ 63 | BaseNode{Text, pos}, 64 | []byte(arg), 65 | } 66 | } 67 | 68 | func (n *TextNode) Copy() Node { 69 | return NewTextNode(n.pos, string(n.Text)) 70 | } 71 | 72 | func (n *TextNode) String() string { 73 | return fmt.Sprintf("%s %q", n.NodeType, n.Text) 74 | } 75 | 76 | func (n *TextNode) Visit(c chan Node) { 77 | c <- n 78 | } 79 | 80 | func NewWrapperNode(pos int, template string) *WrapperNode { 81 | n := &WrapperNode{ 82 | NewListNode(pos), 83 | template, 84 | []Node{}, 85 | } 86 | n.NodeType = Wrapper 87 | return n 88 | } 89 | 90 | func (n *WrapperNode) AppendAssignment(a Node) { 91 | n.AssignmentNodes = append(n.AssignmentNodes, a) 92 | } 93 | 94 | func (n *WrapperNode) Copy() Node { 95 | anodes := make([]Node, len(n.AssignmentNodes)) 96 | for i, v := range n.AssignmentNodes { 97 | anodes[i] = v.Copy() 98 | } 99 | return &WrapperNode{ 100 | n.ListNode.Copy().(*ListNode), 101 | n.WrapperName, 102 | anodes, 103 | } 104 | } 105 | 106 | func (n *WrapperNode) Visit(c chan Node) { 107 | c <- n 108 | for _, v := range n.AssignmentNodes { 109 | v.Visit(c) 110 | } 111 | n.ListNode.Visit(c) 112 | } 113 | 114 | func NewAssignmentNode(pos int, symbol string) *AssignmentNode { 115 | n := &AssignmentNode{ 116 | BaseNode{Assignment, pos}, 117 | NewLocalVarNode(pos, symbol, 0), // TODO 118 | nil, 119 | } 120 | return n 121 | } 122 | 123 | func (n *AssignmentNode) Copy() Node { 124 | x := &AssignmentNode{ 125 | BaseNode{Assignment, n.pos}, 126 | n.Assignee, 127 | n.Expression, 128 | } 129 | return x 130 | } 131 | 132 | func (n *AssignmentNode) Visit(c chan Node) { 133 | c <- n 134 | c <- n.Assignee 135 | c <- n.Expression 136 | } 137 | 138 | func NewLocalVarNode(pos int, symbol string, idx int) *LocalVarNode { 139 | n := &LocalVarNode{ 140 | BaseNode{LocalVar, pos}, 141 | symbol, 142 | idx, 143 | } 144 | return n 145 | } 146 | 147 | func (n *LocalVarNode) Copy() Node { 148 | return NewLocalVarNode(n.pos, n.Name, n.Offset) 149 | } 150 | 151 | func (n *LocalVarNode) Visit(c chan Node) { 152 | c <- n 153 | } 154 | 155 | func (n *LocalVarNode) String() string { 156 | return fmt.Sprintf("%s %s (%d)", n.NodeType, n.Name, n.Offset) 157 | } 158 | 159 | func NewLoopNode(pos int) *LoopNode { 160 | maxiter := DefaultMaxIterations 161 | return &LoopNode{ 162 | ListNode: NewListNode(pos), 163 | MaxIteration: maxiter, 164 | } 165 | } 166 | 167 | func NewForeachNode(pos int, symbol string) *ForeachNode { 168 | n := &ForeachNode{ 169 | IndexVarName: symbol, 170 | IndexVarIdx: 0, 171 | List: nil, 172 | LoopNode: NewLoopNode(pos), 173 | } 174 | n.NodeType = Foreach 175 | return n 176 | } 177 | 178 | func (n *ForeachNode) Visit(c chan Node) { 179 | c <- n 180 | // Skip the list node that we contain 181 | for _, child := range n.ListNode.Nodes { 182 | child.Visit(c) 183 | } 184 | } 185 | 186 | func (n *ForeachNode) Copy() Node { 187 | x := &ForeachNode{ 188 | IndexVarName: n.IndexVarName, 189 | IndexVarIdx: n.IndexVarIdx, 190 | List: n.List.Copy(), 191 | LoopNode: n.LoopNode.Copy().(*LoopNode), 192 | } 193 | x.NodeType = Foreach 194 | return n 195 | } 196 | 197 | func (n *ForeachNode) String() string { 198 | return fmt.Sprintf("%s %s (%d)", n.NodeType, n.IndexVarName, n.IndexVarIdx) 199 | } 200 | 201 | func NewWhileNode(pos int, n Node) *WhileNode { 202 | x := &WhileNode{ 203 | LoopNode: NewLoopNode(pos), 204 | } 205 | x.Condition = n 206 | x.NodeType = While 207 | return x 208 | } 209 | 210 | func (n *WhileNode) Copy() Node { 211 | return &WhileNode{ 212 | LoopNode: n.LoopNode.Copy().(*LoopNode), 213 | } 214 | } 215 | 216 | func (n *WhileNode) Visit(c chan Node) { 217 | c <- n 218 | n.Condition.Visit(c) 219 | n.ListNode.Visit(c) 220 | } 221 | 222 | func NewMethodCallNode(pos int, invocant Node, method string, args *ListNode) *MethodCallNode { 223 | return &MethodCallNode{ 224 | BaseNode{MethodCall, pos}, 225 | invocant, 226 | method, 227 | args, 228 | } 229 | } 230 | 231 | func (n *MethodCallNode) Copy() Node { 232 | return NewMethodCallNode(n.pos, n.Invocant, n.MethodName, n.Args) 233 | } 234 | 235 | func (n *MethodCallNode) Visit(c chan Node) { 236 | c <- n 237 | n.Invocant.Visit(c) 238 | n.Args.Visit(c) 239 | } 240 | 241 | func NewFunCallNode(pos int, invocant Node, args *ListNode) *FunCallNode { 242 | return &FunCallNode{ 243 | BaseNode{FunCall, pos}, 244 | invocant, 245 | args, 246 | } 247 | } 248 | 249 | func (n *FunCallNode) Copy() Node { 250 | return NewFunCallNode(n.pos, n.Invocant, n.Args) 251 | } 252 | 253 | func (n *FunCallNode) Visit(c chan Node) { 254 | c <- n 255 | n.Invocant.Visit(c) 256 | n.Args.Visit(c) 257 | } 258 | 259 | func NewFetchFieldNode(pos int, container Node, field string) *FetchFieldNode { 260 | n := &FetchFieldNode{ 261 | BaseNode{FetchField, pos}, 262 | container, 263 | field, 264 | } 265 | return n 266 | } 267 | 268 | func (n *FetchFieldNode) Copy() Node { 269 | return &FetchFieldNode{ 270 | BaseNode{FetchField, n.pos}, 271 | n.Container.Copy(), 272 | n.FieldName, 273 | } 274 | } 275 | 276 | func (n *FetchFieldNode) Visit(c chan Node) { 277 | c <- n 278 | n.Container.Visit(c) 279 | } 280 | 281 | func NewRootNode() *ListNode { 282 | n := NewListNode(0) 283 | n.NodeType = Root 284 | return n 285 | } 286 | 287 | func NewNumberNode(pos int, num reflect.Value) *NumberNode { 288 | return &NumberNode{ 289 | BaseNode{Number, pos}, 290 | num, 291 | } 292 | } 293 | 294 | func (n *NumberNode) Copy() Node { 295 | x := NewNumberNode(n.pos, n.Value) 296 | x.NodeType = n.NodeType 297 | return x 298 | } 299 | 300 | func (n *NumberNode) Visit(c chan Node) { 301 | c <- n 302 | } 303 | 304 | func NewIntNode(pos int, v int64) *NumberNode { 305 | n := NewNumberNode(pos, reflect.ValueOf(v)) 306 | n.NodeType = Int 307 | return n 308 | } 309 | 310 | func NewFloatNode(pos int, v float64) *NumberNode { 311 | n := NewNumberNode(pos, reflect.ValueOf(v)) 312 | n.NodeType = Float 313 | return n 314 | } 315 | 316 | func NewPrintNode(pos int, arg Node) *ListNode { 317 | n := NewListNode(pos) 318 | n.NodeType = Print 319 | n.Append(arg) 320 | return n 321 | } 322 | 323 | func NewPrintRawNode(pos int) *ListNode { 324 | n := NewListNode(pos) 325 | n.NodeType = PrintRaw 326 | return n 327 | } 328 | 329 | func NewFetchSymbolNode(pos int, symbol string) *TextNode { 330 | n := NewTextNode(pos, symbol) 331 | n.NodeType = FetchSymbol 332 | return n 333 | } 334 | 335 | func NewIfNode(pos int, exp Node) *IfNode { 336 | n := &IfNode{ 337 | NewListNode(pos), 338 | exp, 339 | } 340 | n.NodeType = If 341 | return n 342 | } 343 | 344 | func (n *IfNode) Copy() Node { 345 | x := &IfNode{ 346 | n.ListNode.Copy().(*ListNode), 347 | nil, 348 | } 349 | if e := n.BooleanExpression; e != nil { 350 | x.BooleanExpression = e.Copy() 351 | } 352 | 353 | x.ListNode = n.ListNode.Copy().(*ListNode) 354 | 355 | return x 356 | } 357 | 358 | func (n *IfNode) Visit(c chan Node) { 359 | c <- n 360 | c <- n.BooleanExpression 361 | for _, child := range n.ListNode.Nodes { 362 | c <- child 363 | } 364 | } 365 | 366 | func NewElseNode(pos int) *ElseNode { 367 | n := &ElseNode{ 368 | NewListNode(pos), 369 | nil, 370 | } 371 | n.NodeType = Else 372 | return n 373 | } 374 | 375 | func NewRangeNode(pos int, start, end Node) *BinaryNode { 376 | return &BinaryNode{ 377 | BaseNode{Range, pos}, 378 | start, 379 | end, 380 | } 381 | } 382 | 383 | func (n *UnaryNode) Visit(c chan Node) { 384 | c <- n 385 | n.Child.Visit(c) 386 | } 387 | 388 | func (n *UnaryNode) Copy() Node { 389 | return &UnaryNode{ 390 | BaseNode{n.NodeType, n.pos}, 391 | n.Child.Copy(), 392 | } 393 | } 394 | 395 | func NewMakeArrayNode(pos int, child Node) *UnaryNode { 396 | return &UnaryNode{ 397 | BaseNode{MakeArray, pos}, 398 | child, 399 | } 400 | } 401 | 402 | type IncludeNode struct { 403 | BaseNode 404 | IncludeTarget Node 405 | AssignmentNodes []Node 406 | } 407 | 408 | func NewIncludeNode(pos int, include Node) *IncludeNode { 409 | return &IncludeNode{ 410 | BaseNode{Include, pos}, 411 | include, 412 | []Node{}, 413 | } 414 | } 415 | 416 | func (n *IncludeNode) AppendAssignment(a Node) { 417 | n.AssignmentNodes = append(n.AssignmentNodes, a) 418 | } 419 | 420 | func (n *IncludeNode) Copy() Node { 421 | return NewIncludeNode(n.pos, n.IncludeTarget) 422 | } 423 | 424 | func (n *IncludeNode) Visit(c chan Node) { 425 | c <- n 426 | c <- n.IncludeTarget 427 | } 428 | 429 | func NewPlusNode(pos int) *BinaryNode { 430 | return &BinaryNode{ 431 | BaseNode{Plus, pos}, 432 | nil, 433 | nil, 434 | } 435 | } 436 | 437 | func NewMinusNode(pos int) *BinaryNode { 438 | return &BinaryNode{ 439 | BaseNode{Minus, pos}, 440 | nil, 441 | nil, 442 | } 443 | } 444 | 445 | func NewMulNode(pos int) *BinaryNode { 446 | return &BinaryNode{ 447 | BaseNode{Mul, pos}, 448 | nil, 449 | nil, 450 | } 451 | } 452 | 453 | func NewDivNode(pos int) *BinaryNode { 454 | return &BinaryNode{ 455 | BaseNode{Div, pos}, 456 | nil, 457 | nil, 458 | } 459 | } 460 | 461 | func NewEqualsNode(pos int) *BinaryNode { 462 | return &BinaryNode{ 463 | BaseNode{Equals, pos}, 464 | nil, 465 | nil, 466 | } 467 | } 468 | 469 | func NewNotEqualsNode(pos int) *BinaryNode { 470 | return &BinaryNode{ 471 | BaseNode{NotEquals, pos}, 472 | nil, 473 | nil, 474 | } 475 | } 476 | 477 | func NewLTNode(pos int) *BinaryNode { 478 | return &BinaryNode{ 479 | BaseNode{LT, pos}, 480 | nil, 481 | nil, 482 | } 483 | } 484 | 485 | func NewGTNode(pos int) *BinaryNode { 486 | return &BinaryNode{ 487 | BaseNode{GT, pos}, 488 | nil, 489 | nil, 490 | } 491 | } 492 | 493 | func (n *BinaryNode) Copy() Node { 494 | return &BinaryNode{ 495 | BaseNode{n.NodeType, n.pos}, 496 | n.Left.Copy(), 497 | n.Right.Copy(), 498 | } 499 | } 500 | 501 | func (n *BinaryNode) Visit(c chan Node) { 502 | c <- n 503 | n.Left.Visit(c) 504 | n.Right.Visit(c) 505 | } 506 | 507 | func NewGroupNode(pos int) *UnaryNode { 508 | return &UnaryNode{ 509 | BaseNode{Group, pos}, 510 | nil, 511 | } 512 | } 513 | 514 | func NewFilterNode(pos int, name string, child Node) *FilterNode { 515 | return &FilterNode{ 516 | &UnaryNode{ 517 | BaseNode{Filter, pos}, 518 | child, 519 | }, 520 | name, 521 | } 522 | } 523 | 524 | func (n *FilterNode) Copy() Node { 525 | return &FilterNode{ 526 | &UnaryNode{ 527 | BaseNode{Filter, n.pos}, 528 | n.Child.Copy(), 529 | }, 530 | n.Name, 531 | } 532 | } 533 | 534 | func (n *FilterNode) Visit(c chan Node) { 535 | c <- n 536 | n.UnaryNode.Visit(c) 537 | } 538 | 539 | func NewFetchArrayElementNode(pos int) *BinaryNode { 540 | return &BinaryNode{ 541 | BaseNode{FetchArrayElement, pos}, 542 | nil, 543 | nil, 544 | } 545 | } 546 | 547 | func NewMacroNode(pos int, name string) *MacroNode { 548 | n := &MacroNode{ 549 | NewListNode(pos), 550 | name, 551 | nil, 552 | []*LocalVarNode{}, 553 | } 554 | n.NodeType = Macro 555 | return n 556 | } 557 | 558 | func (n *MacroNode) AppendArg(arg *LocalVarNode) { 559 | n.Arguments = append(n.Arguments, arg) 560 | } 561 | -------------------------------------------------------------------------------- /node/node_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestNode_String(t *testing.T) { 9 | for i := 0; i < int(Max); i++ { 10 | nt := NodeType(i) 11 | if strings.HasPrefix(nt.String(), "Unknown") { 12 | t.Errorf("%#v does not have String() implemented", nt) 13 | } 14 | } 15 | } 16 | 17 | func TestTextNode(t *testing.T) { 18 | n := NewTextNode(0, "foo") 19 | c := make(chan Node) 20 | go func() { 21 | n.Visit(c) 22 | close(c) 23 | }() 24 | for v := range c { 25 | if _, ok := v.(*TextNode); !ok { 26 | t.Errorf("expected TextNode, got %v", v) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /node/nodetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=NodeType node/node.go"; DO NOT EDIT 2 | 3 | package node 4 | 5 | import "fmt" 6 | 7 | const _NodeType_name = "NoopRootTextNumberIntFloatIfElseListForeachWhileWrapperIncludeAssignmentLocalVarFetchFieldFetchArrayElementMethodCallFunCallPrintPrintRawFetchSymbolRangePlusMinusMulDivEqualsNotEqualsLTGTMakeArrayGroupFilterMacroMax" 8 | 9 | var _NodeType_index = [...]uint8{0, 4, 8, 12, 18, 21, 26, 28, 32, 36, 43, 48, 55, 62, 72, 80, 90, 107, 117, 124, 129, 137, 148, 153, 157, 162, 165, 168, 174, 183, 185, 187, 196, 201, 207, 212, 215} 10 | 11 | func (i NodeType) String() string { 12 | if i < 0 || i >= NodeType(len(_NodeType_index)-1) { 13 | return fmt.Sprintf("NodeType(%d)", i) 14 | } 15 | return _NodeType_name[_NodeType_index[i]:_NodeType_index[i+1]] 16 | } 17 | -------------------------------------------------------------------------------- /parser/ast.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/lestrrat-go/xslate/internal/rbpool" 7 | "github.com/lestrrat-go/xslate/node" 8 | ) 9 | 10 | // Visit returns a channel which you can receive Node structs in order that 11 | // that they would be processed 12 | func (ast *AST) Visit() <-chan node.Node { 13 | c := make(chan node.Node) 14 | go func() { 15 | defer close(c) 16 | ast.Root.Visit(c) 17 | }() 18 | return c 19 | } 20 | 21 | // String returns the textual representation of this AST 22 | func (ast *AST) String() string { 23 | buf := rbpool.Get() 24 | defer rbpool.Release(buf) 25 | 26 | c := ast.Visit() 27 | k := 0 28 | for v := range c { 29 | k++ 30 | fmt.Fprintf(buf, "%03d. %s\n", k, v) 31 | } 32 | return buf.String() 33 | } 34 | -------------------------------------------------------------------------------- /parser/builder.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/lestrrat-go/lex" 9 | "github.com/lestrrat-go/xslate/internal/frame" 10 | "github.com/lestrrat-go/xslate/internal/stack" 11 | "github.com/lestrrat-go/xslate/node" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | func NewFrame(s stack.Stack) *Frame { 16 | f := &Frame{ 17 | frame.New(s), 18 | nil, 19 | make(map[string]int), 20 | } 21 | return f 22 | } 23 | 24 | type builderCtx struct { 25 | ParseName string 26 | Text string 27 | Line int 28 | Lexer lex.Lexer 29 | Root *node.ListNode 30 | PeekCount int 31 | Tokens [3]lex.LexItem 32 | CurrentStackTop int 33 | PostChomp bool 34 | FrameStack stack.Stack 35 | Frames stack.Stack 36 | Error error 37 | } 38 | 39 | func NewBuilder() *Builder { 40 | return &Builder{} 41 | } 42 | 43 | func (b *Builder) Parse(name string, l lex.Lexer) (ast *AST, err error) { 44 | ctx := &builderCtx{ 45 | ParseName: name, 46 | Lexer: l, 47 | Root: node.NewRootNode(), 48 | Tokens: [3]lex.LexItem{}, 49 | FrameStack: stack.New(5), 50 | Frames: stack.New(5), 51 | } 52 | 53 | defer func() { 54 | if ctx.Error != nil { 55 | err = ctx.Error 56 | ast = nil 57 | // don't let the panic propagate 58 | recover() 59 | } 60 | }() 61 | 62 | b.Start(ctx) 63 | b.ParseStatements(ctx) 64 | return &AST{ 65 | Name: name, 66 | Root: ctx.Root, 67 | }, nil 68 | } 69 | 70 | func (b *Builder) Start(ctx *builderCtx) { 71 | go ctx.Lexer.Run() 72 | } 73 | 74 | func (b *Builder) Backup(ctx *builderCtx) { 75 | ctx.PeekCount++ 76 | } 77 | 78 | func (b *Builder) Peek(ctx *builderCtx) lex.LexItem { 79 | if ctx.PeekCount > 0 { 80 | return ctx.Tokens[ctx.PeekCount-1] 81 | } 82 | ctx.PeekCount = 1 83 | ctx.Tokens[0] = ctx.Lexer.NextItem() 84 | return ctx.Tokens[0] 85 | } 86 | 87 | func (b *Builder) PeekNonSpace(ctx *builderCtx) lex.LexItem { 88 | var token lex.LexItem 89 | for { 90 | token = b.Next(ctx) 91 | if token.Type() != ItemSpace { 92 | break 93 | } 94 | } 95 | b.Backup(ctx) 96 | return token 97 | } 98 | 99 | func (b *Builder) Next(ctx *builderCtx) lex.LexItem { 100 | if ctx.PeekCount > 0 { 101 | ctx.PeekCount-- 102 | } else { 103 | ctx.Tokens[0] = ctx.Lexer.NextItem() 104 | } 105 | return ctx.Tokens[ctx.PeekCount] 106 | } 107 | 108 | func (b *Builder) NextNonSpace(ctx *builderCtx) lex.LexItem { 109 | var token lex.LexItem 110 | for { 111 | token = b.Next(ctx) 112 | ctx.Line = token.Line() 113 | if token.Type() != ItemSpace { 114 | break 115 | } 116 | } 117 | return token 118 | } 119 | 120 | func (b *Builder) Backup2(ctx *builderCtx, t1 lex.LexItem) { 121 | ctx.Tokens[1] = t1 122 | ctx.PeekCount = 2 123 | } 124 | 125 | func (ctx *builderCtx) HasLocalVar(symbol string) (pos int, ok bool) { 126 | for i := ctx.Frames.Size() - 1; i >= 0; i-- { 127 | f, _ := ctx.Frames.Get(i) 128 | pos, ok = f.(*Frame).LvarNames[symbol] 129 | if ok { 130 | return 131 | } 132 | } 133 | return 0, false 134 | } 135 | 136 | func (ctx *builderCtx) DeclareLocalVar(symbol string) int { 137 | f := ctx.CurrentFrame() 138 | i := f.DeclareVar(symbol) 139 | f.LvarNames[symbol] = i 140 | return i 141 | } 142 | 143 | func (ctx *builderCtx) PushFrame() *Frame { 144 | f := NewFrame(ctx.FrameStack) 145 | ctx.Frames.Push(f) 146 | f.SetMark(ctx.Frames.Size()) 147 | return f 148 | } 149 | 150 | func (ctx *builderCtx) PopFrame() *Frame { 151 | x := ctx.Frames.Pop() 152 | if x == nil { 153 | return nil 154 | } 155 | 156 | f := x.(*Frame) 157 | for i := ctx.FrameStack.Size() - 1; i > f.Mark(); i-- { 158 | ctx.FrameStack.Pop() 159 | } 160 | return f 161 | } 162 | 163 | func (ctx *builderCtx) CurrentFrame() *Frame { 164 | x, err := ctx.Frames.Top() 165 | if err != nil { 166 | return nil 167 | } 168 | return x.(*Frame) 169 | } 170 | 171 | func (ctx *builderCtx) PushParentNode(n node.Appender) { 172 | f := ctx.PushFrame() 173 | f.Node = n 174 | } 175 | 176 | func (ctx *builderCtx) PopParentNode() node.Appender { 177 | f := ctx.PopFrame() 178 | if f != nil { 179 | return f.Node 180 | } 181 | return nil 182 | } 183 | 184 | func (ctx *builderCtx) CurrentParentNode() node.Appender { 185 | f := ctx.CurrentFrame() 186 | if f != nil { 187 | return f.Node 188 | } 189 | return nil 190 | } 191 | 192 | func (b *Builder) ParseStatements(ctx *builderCtx) node.Node { 193 | ctx.PushParentNode(ctx.Root) 194 | for b.Peek(ctx).Type() != ItemEOF { 195 | n := b.ParseTemplateOrText(ctx) 196 | if n != nil { 197 | ctx.CurrentParentNode().Append(n) 198 | } 199 | } 200 | return nil 201 | } 202 | 203 | func (b *Builder) ParseTemplateOrText(ctx *builderCtx) node.Node { 204 | token := b.PeekNonSpace(ctx) 205 | switch token.Type() { 206 | case ItemRawString: 207 | return b.ParseRawString(ctx) 208 | case ItemTagStart: 209 | return b.ParseTemplate(ctx) 210 | default: 211 | panic(fmt.Sprintf("Unexpected token: %s", token)) 212 | } 213 | } 214 | 215 | func (b *Builder) ParseRawString(ctx *builderCtx) node.Node { 216 | const whiteSpace = " \t\r\n" 217 | token := b.NextNonSpace(ctx) 218 | if token.Type() != ItemRawString { 219 | b.Unexpected(ctx, "Expected raw string, got %s", token) 220 | } 221 | 222 | value := token.Value() 223 | 224 | if ctx.PostChomp { 225 | value = strings.TrimLeft(value, whiteSpace) 226 | ctx.PostChomp = false 227 | } 228 | 229 | // Look for signs of pre-chomp 230 | if b.PeekNonSpace(ctx).Type() == ItemTagStart { 231 | start := b.NextNonSpace(ctx) 232 | next := b.PeekNonSpace(ctx) 233 | b.Backup2(ctx, start) 234 | if next.Type() == ItemMinus { 235 | // prechomp! 236 | value = strings.TrimRight(value, whiteSpace) 237 | } 238 | } 239 | 240 | n := node.NewPrintRawNode(token.Pos()) 241 | n.Append(node.NewTextNode(token.Pos(), value)) 242 | 243 | return n 244 | } 245 | 246 | func (b *Builder) Unexpected(ctx *builderCtx, format string, args ...interface{}) { 247 | msg := fmt.Sprintf( 248 | "Unexpected token found: %s in %s at line %d", 249 | fmt.Sprintf(format, args...), 250 | ctx.ParseName, 251 | ctx.Line, 252 | ) 253 | ctx.Error = errors.New(msg) 254 | panic(msg) 255 | } 256 | 257 | func (b *Builder) ParseTemplate(ctx *builderCtx) node.Node { 258 | // consume tagstart 259 | start := b.NextNonSpace(ctx) 260 | if start.Type() != ItemTagStart { 261 | b.Unexpected(ctx, "Expected TagStart, got %s", start) 262 | } 263 | ctx.PostChomp = false 264 | 265 | if b.PeekNonSpace(ctx).Type() == ItemMinus { 266 | b.NextNonSpace(ctx) 267 | } 268 | 269 | var tmpl node.Node 270 | switch b.PeekNonSpace(ctx).Type() { 271 | case ItemEnd: 272 | b.NextNonSpace(ctx) 273 | for keepPopping := true; keepPopping; { 274 | parent := ctx.PopParentNode() 275 | switch parent.Type() { 276 | case node.Root: 277 | b.Unexpected(ctx, "Unexpected END") 278 | case node.Else: 279 | // no op 280 | default: 281 | keepPopping = false 282 | } 283 | } 284 | case ItemComment: 285 | b.NextNonSpace(ctx) 286 | // no op 287 | case ItemCall: 288 | b.NextNonSpace(ctx) 289 | tmpl = b.ParseExpressionOrAssignment(ctx, false) 290 | case ItemSet: 291 | b.NextNonSpace(ctx) // Consume SET 292 | tmpl = b.ParseAssignment(ctx) 293 | case ItemMacro: 294 | tmpl = b.ParseMacro(ctx) 295 | case ItemWrapper: 296 | tmpl = b.ParseWrapper(ctx) 297 | case ItemForeach: 298 | tmpl = b.ParseForeach(ctx) 299 | case ItemWhile: 300 | tmpl = b.ParseWhile(ctx) 301 | case ItemInclude: 302 | tmpl = b.ParseInclude(ctx) 303 | case ItemTagEnd: // Silly, but possible 304 | b.NextNonSpace(ctx) 305 | tmpl = node.NewNoopNode() 306 | case ItemIdentifier, ItemNumber, ItemDoubleQuotedString, ItemSingleQuotedString, ItemOpenParen: 307 | tmpl = b.ParseExpressionOrAssignment(ctx, true) 308 | case ItemIf: 309 | tmpl = b.ParseIf(ctx) 310 | case ItemElse: 311 | tmpl = b.ParseElse(ctx) 312 | default: 313 | b.Unexpected(ctx, "%s", b.PeekNonSpace(ctx)) 314 | } 315 | 316 | for b.PeekNonSpace(ctx).Type() == ItemComment { 317 | b.NextNonSpace(ctx) 318 | } 319 | 320 | if b.PeekNonSpace(ctx).Type() == ItemMinus { 321 | b.NextNonSpace(ctx) 322 | ctx.PostChomp = true 323 | } 324 | 325 | // Consume tag end 326 | end := b.NextNonSpace(ctx) 327 | if end.Type() != ItemTagEnd { 328 | b.Unexpected(ctx, "Expected TagEnd, got %s", end) 329 | } 330 | return tmpl 331 | } 332 | 333 | func (b *Builder) ParseExpressionOrAssignment(ctx *builderCtx, canPrint bool) node.Node { 334 | // There's a special case for assignment where SET is omitted 335 | // [% foo = ... %] instead of [% SET foo = ... %] 336 | next := b.NextNonSpace(ctx) 337 | following := b.PeekNonSpace(ctx) 338 | b.Backup2(ctx, next) 339 | 340 | var n node.Node 341 | if next.Type() == ItemIdentifier { 342 | switch following.Type() { 343 | case ItemAssign, ItemAssignAdd, ItemAssignSub, ItemAssignMul, ItemAssignDiv: 344 | // This is a simple assignment! 345 | n = b.ParseAssignment(ctx) 346 | default: 347 | n = b.ParseExpression(ctx, canPrint) 348 | } 349 | } else { 350 | n = b.ParseExpression(ctx, canPrint) 351 | } 352 | 353 | return n 354 | } 355 | 356 | func (b *Builder) ParseWrapper(ctx *builderCtx) node.Node { 357 | wrapper := b.Next(ctx) 358 | if wrapper.Type() != ItemWrapper { 359 | panic("fuck") 360 | } 361 | 362 | tmpl := b.NextNonSpace(ctx) 363 | var template string 364 | switch tmpl.Type() { 365 | case ItemDoubleQuotedString, ItemSingleQuotedString: 366 | template = tmpl.Value() 367 | template = template[1 : len(template)-1] 368 | default: 369 | b.Unexpected(ctx, "Expected identifier, got %s", tmpl) 370 | } 371 | 372 | n := node.NewWrapperNode(wrapper.Pos(), template) 373 | ctx.CurrentParentNode().Append(n) 374 | ctx.PushParentNode(n) 375 | 376 | ctx.PushFrame() 377 | 378 | // If we have parameters, we have WITH. otherwise we want TagEnd 379 | if token := b.PeekNonSpace(ctx); token.Type() != ItemWith { 380 | ctx.PopFrame() 381 | return nil 382 | } 383 | b.NextNonSpace(ctx) // WITH 384 | LOOP: 385 | for { 386 | a := b.ParseAssignment(ctx) 387 | n.AppendAssignment(a) 388 | next := b.PeekNonSpace(ctx) 389 | switch next.Type() { 390 | case ItemComma, ItemTagEnd: 391 | break LOOP 392 | case ItemMinus: 393 | cur := b.NextNonSpace(ctx) 394 | next := b.PeekNonSpace(ctx) 395 | b.Backup2(ctx, cur) 396 | if next.Type() == ItemTagEnd { 397 | break LOOP 398 | } 399 | } 400 | b.NextNonSpace(ctx) 401 | } 402 | ctx.PopFrame() 403 | 404 | return nil 405 | } 406 | 407 | func (b *Builder) ParseAssignment(ctx *builderCtx) node.Node { 408 | symbol := b.NextNonSpace(ctx) 409 | if symbol.Type() != ItemIdentifier { 410 | b.Unexpected(ctx, "Expected identifier, got %s", symbol) 411 | } 412 | 413 | b.DeclareLocalVarIfNew(ctx, symbol) 414 | n := node.NewAssignmentNode(symbol.Pos(), symbol.Value()) 415 | 416 | eq := b.NextNonSpace(ctx) 417 | switch eq.Type() { 418 | case ItemAssign: 419 | n.Expression = b.ParseExpression(ctx, false) 420 | case ItemAssignAdd: 421 | add := node.NewPlusNode(symbol.Pos()) 422 | add.Left = b.LocalVarOrFetchSymbol(ctx, symbol) 423 | add.Right = b.ParseExpression(ctx, false) 424 | n.Expression = add 425 | case ItemAssignSub: 426 | sub := node.NewMinusNode(symbol.Pos()) 427 | sub.Left = b.LocalVarOrFetchSymbol(ctx, symbol) 428 | sub.Right = b.ParseExpression(ctx, false) 429 | n.Expression = sub 430 | case ItemAssignMul: 431 | mul := node.NewMulNode(symbol.Pos()) 432 | mul.Left = b.LocalVarOrFetchSymbol(ctx, symbol) 433 | mul.Right = b.ParseExpression(ctx, false) 434 | n.Expression = mul 435 | case ItemAssignDiv: 436 | div := node.NewDivNode(symbol.Pos()) 437 | div.Left = b.LocalVarOrFetchSymbol(ctx, symbol) 438 | div.Right = b.ParseExpression(ctx, false) 439 | n.Expression = div 440 | default: 441 | b.Unexpected(ctx, "Expected assign, got %s", eq) 442 | } 443 | 444 | return n 445 | } 446 | 447 | func (b *Builder) DeclareLocalVarIfNew(ctx *builderCtx, symbol lex.LexItem) { 448 | _, ok := ctx.HasLocalVar(symbol.Value()) 449 | if !ok { 450 | ctx.DeclareLocalVar(symbol.Value()) 451 | } 452 | } 453 | 454 | func (b *Builder) LocalVarOrFetchSymbol(ctx *builderCtx, token lex.LexItem) node.Node { 455 | if idx, ok := ctx.HasLocalVar(token.Value()); ok { 456 | return node.NewLocalVarNode(token.Pos(), token.Value(), idx) 457 | } 458 | return node.NewFetchSymbolNode(token.Pos(), token.Value()) 459 | } 460 | 461 | func (b *Builder) ParseTerm(ctx *builderCtx) node.Node { 462 | token := b.NextNonSpace(ctx) 463 | switch token.Type() { 464 | case ItemIdentifier: 465 | return b.LocalVarOrFetchSymbol(ctx, token) 466 | case ItemNumber, ItemDoubleQuotedString, ItemSingleQuotedString: 467 | b.Backup(ctx) 468 | return b.ParseLiteral(ctx) 469 | default: 470 | b.Backup(ctx) 471 | return nil 472 | } 473 | } 474 | 475 | func (b *Builder) ParseFunCall(ctx *builderCtx, invocant node.Node) node.Node { 476 | next := b.NextNonSpace(ctx) 477 | if next.Type() != ItemOpenParen { 478 | b.Unexpected(ctx, "Expected '(', got %s", next.Type()) 479 | } 480 | 481 | args := b.ParseList(ctx) 482 | closeParen := b.NextNonSpace(ctx) 483 | if closeParen.Type() != ItemCloseParen { 484 | b.Unexpected(ctx, "Expected ')', got %s", closeParen.Type()) 485 | } 486 | 487 | return node.NewFunCallNode(invocant.Pos(), invocant, args.(*node.ListNode)) 488 | } 489 | 490 | func (b *Builder) ParseMethodCallOrMapLookup(ctx *builderCtx, invocant node.Node) node.Node { 491 | // We have already seen identifier followed by a period 492 | symbol := b.NextNonSpace(ctx) 493 | if symbol.Type() != ItemIdentifier { 494 | b.Unexpected(ctx, "Expected identifier for method call or map lookup, got %s", symbol.Type()) 495 | } 496 | 497 | var n node.Node 498 | next := b.NextNonSpace(ctx) 499 | if next.Type() != ItemOpenParen { 500 | // it's a map lookup. Put back that extra token we read 501 | b.Backup(ctx) 502 | n = node.NewFetchFieldNode(invocant.Pos(), invocant, symbol.Value()) 503 | } else { 504 | // It's a method call! Parse the list 505 | args := b.ParseList(ctx) 506 | closeParen := b.NextNonSpace(ctx) 507 | if closeParen.Type() != ItemCloseParen { 508 | b.Unexpected(ctx, "Expected ')', got %s", closeParen.Type()) 509 | } 510 | n = node.NewMethodCallNode(invocant.Pos(), invocant, symbol.Value(), args.(*node.ListNode)) 511 | } 512 | 513 | // If we are followed by another period, we are going to have to 514 | // check for another level of methodcall / lookup 515 | if b.PeekNonSpace(ctx).Type() == ItemPeriod { 516 | b.NextNonSpace(ctx) // consume period 517 | return b.ParseMethodCallOrMapLookup(ctx, n) 518 | } 519 | return n 520 | } 521 | 522 | func (b *Builder) ParseArrayElementFetch(ctx *builderCtx, invocant node.Node) node.Node { 523 | openBracket := b.NextNonSpace(ctx) 524 | if openBracket.Type() != ItemOpenSquareBracket { 525 | b.Unexpected(ctx, "Expected '[', got %s", openBracket) 526 | } 527 | 528 | index := b.ParseExpression(ctx, false) 529 | 530 | n := node.NewFetchArrayElementNode(openBracket.Pos()) 531 | n.Left = invocant 532 | n.Right = index 533 | 534 | closeBracket := b.NextNonSpace(ctx) 535 | if closeBracket.Type() != ItemCloseSquareBracket { 536 | b.Unexpected(ctx, "Expected ']', got %s", closeBracket) 537 | } 538 | 539 | return n 540 | } 541 | 542 | func (b *Builder) ParseExpression(ctx *builderCtx, canPrint bool) (n node.Node) { 543 | defer func() { 544 | if n != nil && canPrint { 545 | n = node.NewPrintNode(n.Pos(), n) 546 | } 547 | }() 548 | 549 | switch b.PeekNonSpace(ctx).Type() { 550 | case ItemOpenParen: 551 | // Looks like a group of something 552 | n = b.ParseGroup(ctx) 553 | case ItemOpenSquareBracket: 554 | // Looks like an inline list def 555 | n = b.ParseMakeArray(ctx) 556 | default: 557 | // Otherwise it's a straight forward ... something 558 | n = b.ParseTerm(ctx) 559 | if n == nil { 560 | panic(fmt.Sprintf("Expected term but could not parse. Next is %s\n", b.PeekNonSpace(ctx))) 561 | } 562 | } 563 | 564 | next := b.PeekNonSpace(ctx) 565 | 566 | switch n.Type() { 567 | case node.LocalVar, node.FetchSymbol: 568 | switch next.Type() { 569 | case ItemPeriod: 570 | // It's either a method call, or a map lookup 571 | b.NextNonSpace(ctx) 572 | n = b.ParseMethodCallOrMapLookup(ctx, n) 573 | case ItemOpenSquareBracket: 574 | n = b.ParseArrayElementFetch(ctx, n) 575 | case ItemOpenParen: 576 | // A variable followed by an open paren is a function call 577 | n = b.ParseFunCall(ctx, n) 578 | } 579 | } 580 | 581 | next = b.NextNonSpace(ctx) 582 | switch next.Type() { 583 | case ItemPlus: 584 | tmp := node.NewPlusNode(next.Pos()) 585 | tmp.Left = n 586 | tmp.Right = b.ParseExpression(ctx, false) 587 | n = tmp 588 | return 589 | case ItemMinus: 590 | // This is special... 591 | following := b.PeekNonSpace(ctx) 592 | if following.Type() == ItemTagEnd { 593 | b.Backup2(ctx, next) 594 | // Postchomp! not arithmetic! 595 | return 596 | } 597 | tmp := node.NewMinusNode(next.Pos()) 598 | tmp.Left = n 599 | tmp.Right = b.ParseExpression(ctx, false) 600 | n = tmp 601 | return 602 | case ItemAsterisk: 603 | tmp := node.NewMulNode(next.Pos()) 604 | tmp.Left = n 605 | tmp.Right = b.ParseExpression(ctx, false) 606 | n = tmp 607 | return 608 | case ItemSlash: 609 | tmp := node.NewDivNode(next.Pos()) 610 | tmp.Left = n 611 | tmp.Right = b.ParseExpression(ctx, false) 612 | n = tmp 613 | return 614 | case ItemEquals: 615 | tmp := node.NewEqualsNode(next.Pos()) 616 | tmp.Left = n 617 | tmp.Right = b.ParseExpression(ctx, false) 618 | n = tmp 619 | case ItemNotEquals: 620 | tmp := node.NewNotEqualsNode(next.Pos()) 621 | tmp.Left = n 622 | tmp.Right = b.ParseExpression(ctx, false) 623 | n = tmp 624 | case ItemLT: 625 | tmp := node.NewLTNode(next.Pos()) 626 | tmp.Left = n 627 | tmp.Right = b.ParseExpression(ctx, false) 628 | n = tmp 629 | return 630 | case ItemGT: 631 | tmp := node.NewGTNode(next.Pos()) 632 | tmp.Left = n 633 | tmp.Right = b.ParseExpression(ctx, false) 634 | n = tmp 635 | case ItemVerticalSlash: 636 | b.Backup(ctx) 637 | n = b.ParseFilter(ctx, n) 638 | default: 639 | b.Backup(ctx) 640 | } 641 | 642 | return 643 | } 644 | 645 | func (b *Builder) ParseFilter(ctx *builderCtx, n node.Node) node.Node { 646 | vslash := b.NextNonSpace(ctx) 647 | if vslash.Type() != ItemVerticalSlash { 648 | b.Unexpected(ctx, "Expected '|', got %s", vslash.Type()) 649 | } 650 | 651 | id := b.NextNonSpace(ctx) 652 | if id.Type() != ItemIdentifier { 653 | b.Unexpected(ctx, "Expected idenfitier, got %s", id.Type()) 654 | } 655 | 656 | filter := node.NewFilterNode(id.Pos(), id.Value(), n) 657 | 658 | if b.PeekNonSpace(ctx).Type() == ItemVerticalSlash { 659 | filter = b.ParseFilter(ctx, filter).(*node.FilterNode) 660 | } 661 | 662 | return filter 663 | } 664 | 665 | func (b *Builder) ParseLiteral(ctx *builderCtx) node.Node { 666 | t := b.NextNonSpace(ctx) 667 | switch t.Type() { 668 | case ItemDoubleQuotedString, ItemSingleQuotedString: 669 | v := t.Value() 670 | return node.NewTextNode(t.Pos(), v[1:len(v)-1]) 671 | case ItemNumber: 672 | v := t.Value() 673 | // XXX TODO: parse hex/oct/bin 674 | if strings.Contains(v, ".") { 675 | f, err := strconv.ParseFloat(v, 64) 676 | if err != nil { // shouldn't happen, as we were able to lex it 677 | b.Unexpected(ctx, "Could not parse number: %s", err) 678 | } 679 | return node.NewFloatNode(t.Pos(), f) 680 | } 681 | i, err := strconv.ParseInt(v, 10, 64) 682 | if err != nil { 683 | b.Unexpected(ctx, "Could not parse number: %s", err) 684 | } 685 | return node.NewIntNode(t.Pos(), i) 686 | default: 687 | b.Unexpected(ctx, "Expected literal value, got %s", t) 688 | } 689 | return nil 690 | } 691 | 692 | func (b *Builder) ParseForeach(ctx *builderCtx) node.Node { 693 | foreach := b.NextNonSpace(ctx) 694 | if foreach.Type() != ItemForeach { 695 | b.Unexpected(ctx, "Expected FOREACH, got %s", foreach) 696 | } 697 | 698 | localsym := b.NextNonSpace(ctx) 699 | if localsym.Type() != ItemIdentifier { 700 | b.Unexpected(ctx, "Expected identifier, got %s", localsym) 701 | } 702 | 703 | forNode := node.NewForeachNode(foreach.Pos(), localsym.Value()) 704 | // use current frame mark 705 | forNode.IndexVarIdx = ctx.CurrentFrame().Mark() 706 | 707 | in := b.NextNonSpace(ctx) 708 | if in.Type() != ItemIn { 709 | b.Unexpected(ctx, "Expected IN, got %s", in) 710 | } 711 | 712 | forNode.List = b.ParseListVariableOrMakeArray(ctx) 713 | 714 | ctx.CurrentParentNode().Append(forNode) 715 | ctx.PushParentNode(forNode) 716 | ctx.DeclareLocalVar(localsym.Value()) 717 | ctx.DeclareLocalVar("loop") 718 | 719 | return nil 720 | } 721 | 722 | func (b *Builder) ParseWhile(ctx *builderCtx) node.Node { 723 | while := b.NextNonSpace(ctx) 724 | if while.Type() != ItemWhile { 725 | b.Unexpected(ctx, "Expected WHILE, got %s", while) 726 | } 727 | 728 | condition := b.ParseExpression(ctx, false) 729 | whileNode := node.NewWhileNode(while.Pos(), condition) 730 | 731 | ctx.CurrentParentNode().Append(whileNode) 732 | ctx.PushParentNode(whileNode) 733 | ctx.DeclareLocalVar("loop") 734 | 735 | return nil 736 | } 737 | 738 | func (b *Builder) ParseRange(ctx *builderCtx) node.Node { 739 | start := b.ParseTerm(ctx) 740 | if start == nil { 741 | b.Unexpected(ctx, "Expected term (start range), got %s", b.PeekNonSpace(ctx).Value()) 742 | } 743 | 744 | rangeOp := b.NextNonSpace(ctx) 745 | if rangeOp.Type() != ItemRange { 746 | b.Unexpected(ctx, "Expected range, got %s", rangeOp.Value()) 747 | } 748 | 749 | end := b.ParseTerm(ctx) 750 | if end == nil { 751 | b.Unexpected(ctx, "Expected term (end range), got %s", b.PeekNonSpace(ctx).Value()) 752 | } 753 | 754 | return node.NewRangeNode(start.Pos(), start, end) 755 | } 756 | 757 | func (b *Builder) ParseListVariableOrMakeArray(ctx *builderCtx) node.Node { 758 | list := b.PeekNonSpace(ctx) 759 | 760 | var n node.Node 761 | switch list.Type() { 762 | case ItemIdentifier: 763 | b.NextNonSpace(ctx) 764 | if idx, ok := ctx.HasLocalVar(list.Value()); ok { 765 | n = node.NewLocalVarNode(list.Pos(), list.Value(), idx) 766 | } else { 767 | n = node.NewFetchSymbolNode(list.Pos(), list.Value()) 768 | } 769 | if b.PeekNonSpace(ctx).Type() == ItemPeriod { 770 | b.NextNonSpace(ctx) 771 | n = b.ParseMethodCallOrMapLookup(ctx, n) 772 | } 773 | case ItemOpenSquareBracket: 774 | n = b.ParseMakeArray(ctx) 775 | default: 776 | panic("fuck") 777 | } 778 | return n 779 | } 780 | 781 | func (b *Builder) ParseMakeArray(ctx *builderCtx) node.Node { 782 | openB := b.NextNonSpace(ctx) 783 | if openB.Type() != ItemOpenSquareBracket { 784 | b.Unexpected(ctx, "Expected '[', got %s", openB.Value()) 785 | } 786 | 787 | child := b.ParseList(ctx) 788 | 789 | closeB := b.NextNonSpace(ctx) 790 | if closeB.Type() != ItemCloseSquareBracket { 791 | b.Unexpected(ctx, "Expected ']', got %s", closeB.Value()) 792 | } 793 | 794 | return node.NewMakeArrayNode(openB.Pos(), child) 795 | } 796 | 797 | func (b *Builder) ParseList(ctx *builderCtx) node.Node { 798 | n := node.NewListNode(b.PeekNonSpace(ctx).Pos()) 799 | OUTER: 800 | for { 801 | // At the beginning of this loop, we must see an 802 | // identifier or a literal 803 | switch item := b.PeekNonSpace(ctx); item.Type() { 804 | case ItemIdentifier, ItemNumber, ItemDoubleQuotedString, ItemSingleQuotedString: 805 | // okay, proceed 806 | default: 807 | break OUTER 808 | } 809 | item := b.NextNonSpace(ctx) 810 | 811 | // Depending on the next item, we have range operator or a literal list 812 | var child node.Node 813 | switch nextN := b.PeekNonSpace(ctx); nextN.Type() { 814 | case ItemRange: 815 | b.Backup2(ctx, item) 816 | child = b.ParseRange(ctx) 817 | default: 818 | b.Backup2(ctx, item) 819 | child = b.ParseExpression(ctx, false) 820 | } 821 | 822 | n.Append(child) 823 | 824 | // Then, we must be followed by either a comma, or the it's the end of the 825 | // list section 826 | if b.PeekNonSpace(ctx).Type() == ItemComma { 827 | b.NextNonSpace(ctx) 828 | } 829 | } 830 | return n 831 | } 832 | 833 | func (b *Builder) ParseIf(ctx *builderCtx) node.Node { 834 | ifToken := b.NextNonSpace(ctx) 835 | if ifToken.Type() != ItemIf { 836 | b.Unexpected(ctx, "Expected if, got %s", ifToken) 837 | } 838 | 839 | // parenthesis are optional 840 | expectCloseParen := false 841 | if b.PeekNonSpace(ctx).Type() == ItemOpenParen { 842 | b.NextNonSpace(ctx) 843 | expectCloseParen = true 844 | } 845 | 846 | exp := b.ParseExpression(ctx, false) 847 | ifNode := node.NewIfNode(ifToken.Pos(), exp) 848 | 849 | if expectCloseParen { 850 | closeParenToken := b.NextNonSpace(ctx) 851 | if closeParenToken.Type() != ItemCloseParen { 852 | b.Unexpected(ctx, "Expected close parenthesis, got %s", closeParenToken) 853 | } 854 | } 855 | 856 | ctx.CurrentParentNode().Append(ifNode) 857 | ctx.PushParentNode(ifNode) 858 | 859 | return nil 860 | } 861 | 862 | func (b *Builder) ParseElse(ctx *builderCtx) node.Node { 863 | elseToken := b.NextNonSpace(ctx) 864 | if elseToken.Type() != ItemElse { 865 | b.Unexpected(ctx, "Expected else, got %s", elseToken) 866 | } 867 | 868 | // CurrentParentNode must be "If" in order for "else" to work 869 | if ctx.CurrentParentNode().Type() != node.If { 870 | b.Unexpected(ctx, "Found else without if") 871 | } 872 | 873 | elseNode := node.NewElseNode(elseToken.Pos()) 874 | elseNode.IfNode = ctx.CurrentParentNode() 875 | ctx.CurrentParentNode().Append(elseNode) 876 | ctx.PushParentNode(elseNode) 877 | 878 | return nil 879 | } 880 | 881 | func (b *Builder) ParseInclude(ctx *builderCtx) node.Node { 882 | incToken := b.NextNonSpace(ctx) 883 | if incToken.Type() != ItemInclude { 884 | b.Unexpected(ctx, "Expected include, got %s", incToken) 885 | } 886 | 887 | // Next thing must be the name of the included template 888 | n := b.ParseExpression(ctx, false) 889 | x := node.NewIncludeNode(incToken.Pos(), n) 890 | ctx.PushFrame() 891 | 892 | if b.PeekNonSpace(ctx).Type() != ItemWith { 893 | ctx.PopFrame() 894 | return x 895 | } 896 | 897 | b.NextNonSpace(ctx) 898 | for { 899 | a := b.ParseAssignment(ctx) 900 | x.AppendAssignment(a) 901 | next := b.PeekNonSpace(ctx) 902 | if next.Type() != ItemComma { 903 | break 904 | } else if next.Type() == ItemTagEnd { 905 | break 906 | } 907 | b.NextNonSpace(ctx) 908 | } 909 | ctx.PopFrame() 910 | 911 | return x 912 | } 913 | 914 | func (b *Builder) ParseGroup(ctx *builderCtx) node.Node { 915 | openParenToken := b.NextNonSpace(ctx) 916 | if openParenToken.Type() != ItemOpenParen { 917 | b.Unexpected(ctx, "Expected '(', got %s", openParenToken) 918 | } 919 | 920 | n := node.NewGroupNode(openParenToken.Pos()) 921 | n.Child = b.ParseExpression(ctx, false) 922 | 923 | closeParenToken := b.NextNonSpace(ctx) 924 | if closeParenToken.Type() != ItemCloseParen { 925 | b.Unexpected(ctx, "Expected ')', got %s", closeParenToken) 926 | } 927 | 928 | return n 929 | } 930 | 931 | func (b *Builder) ParseMacro(ctx *builderCtx) node.Node { 932 | macroToken := b.NextNonSpace(ctx) 933 | if macroToken.Type() != ItemMacro { 934 | b.Unexpected(ctx, "Expected 'MACRO', got %s", macroToken) 935 | } 936 | 937 | // Parse name, and arguments. 938 | nameToken := b.NextNonSpace(ctx) 939 | if nameToken.Type() != ItemIdentifier { 940 | b.Unexpected(ctx, "Expected identifier, got %s", nameToken) 941 | } 942 | 943 | idx := ctx.DeclareLocalVar(nameToken.Value()) 944 | 945 | macro := node.NewMacroNode(nameToken.Pos(), nameToken.Value()) 946 | macro.LocalVar = node.NewLocalVarNode(nameToken.Pos(), nameToken.Value(), idx) 947 | ctx.CurrentParentNode().Append(macro) 948 | ctx.PushParentNode(macro) 949 | 950 | // either a '(' followed by argument list, or BLOCK 951 | if b.PeekNonSpace(ctx).Type() == ItemOpenParen { 952 | b.NextNonSpace(ctx) // discard open paren 953 | // Can't use ParseList() here, because we want a list of only identifiers 954 | for { 955 | next := b.NextNonSpace(ctx) 956 | if next.Type() != ItemIdentifier { 957 | b.Backup(ctx) 958 | break 959 | } 960 | 961 | // idx := ctx.DeclareLocalVar(next.Value()) 962 | // macro.AppendArg(node.NewLocalVarNode(next.Pos(), next.Value(), idx)) 963 | 964 | next = b.NextNonSpace(ctx) 965 | if next.Type() != ItemComma { 966 | b.Backup(ctx) 967 | break 968 | } 969 | } 970 | if closeParen := b.NextNonSpace(ctx); closeParen.Type() != ItemCloseParen { 971 | b.Unexpected(ctx, "Expected ')', got %s", closeParen) 972 | } 973 | } 974 | 975 | // Then we need a BLOCK 976 | if block := b.NextNonSpace(ctx); block.Type() != ItemBlock { 977 | b.Unexpected(ctx, "Expected BLOCK, got %s", block) 978 | } 979 | 980 | return nil 981 | } 982 | -------------------------------------------------------------------------------- /parser/interface.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "io" 5 | "time" 6 | 7 | "github.com/lestrrat-go/lex" 8 | "github.com/lestrrat-go/xslate/internal/frame" 9 | "github.com/lestrrat-go/xslate/node" 10 | ) 11 | 12 | const ( 13 | ItemError lex.ItemType = lex.ItemDefaultMax + 1 + iota 14 | ItemEOF 15 | ItemRawString 16 | ItemComment 17 | ItemNumber 18 | ItemComplex 19 | ItemChar 20 | ItemSpace 21 | ItemTagStart 22 | ItemTagEnd 23 | ItemSymbol 24 | ItemIdentifier 25 | ItemDoubleQuotedString 26 | ItemSingleQuotedString 27 | ItemBool 28 | ItemField 29 | ItemComma 30 | ItemOpenParen // '(' 31 | ItemCloseParen // ')' 32 | ItemOpenSquareBracket // '[' 33 | ItemCloseSquareBracket // ']' 34 | ItemPeriod // '.' 35 | ItemKeyword // Delimiter 36 | ItemCall // CALL 37 | ItemGet // GET 38 | ItemSet // SET 39 | ItemMacro // MACRO 40 | ItemBlock // BLOCK 41 | ItemForeach // FOREACH 42 | ItemWhile // WHILE 43 | ItemIn // IN 44 | ItemInclude // INCLUDE 45 | ItemWith // WITH 46 | ItemIf // IF 47 | ItemElse // ELSE 48 | ItemElseIf // ELSIF 49 | ItemUnless // UNLESS 50 | ItemSwitch // SWITCH 51 | ItemCase // CASE 52 | ItemWrapper // WRAPPER 53 | ItemDefault // DEFAULT 54 | ItemEnd // END 55 | ItemOperator // Delimiter 56 | ItemRange // .. 57 | ItemEquals // == 58 | ItemNotEquals // != 59 | ItemGT // > 60 | ItemLT // < 61 | ItemCmp // <=> 62 | ItemLE // <= 63 | ItemGE // >= 64 | ItemShiftLeft // << 65 | ItemShiftRight // >> 66 | ItemAssignAdd // += 67 | ItemAssignSub // -= 68 | ItemAssignMul // *= 69 | ItemAssignDiv // /= 70 | ItemAssignMod // %= 71 | ItemAnd // && 72 | ItemOr // || 73 | ItemFatComma // => 74 | ItemIncr // ++ 75 | ItemDecr // -- 76 | ItemPlus 77 | ItemMinus 78 | ItemAsterisk 79 | ItemSlash 80 | ItemVerticalSlash 81 | ItemMod 82 | ItemAssign // = 83 | 84 | DefaultItemTypeMax 85 | ) 86 | 87 | // AST is represents the syntax tree for an Xslate template 88 | type AST struct { 89 | Name string // name of the template 90 | ParseName string // name of the top-level template during parsing 91 | Root *node.ListNode // root of the tree 92 | Timestamp time.Time // last-modified date of this template 93 | text string 94 | } 95 | 96 | type Builder struct { 97 | } 98 | 99 | // Frame is the frame struct used during parsing, which has a bit of 100 | // extension over the common Frame struct. 101 | type Frame struct { 102 | *frame.Frame 103 | Node node.Appender 104 | 105 | // This contains names of local variables, mapped to their 106 | // respective location in the framestack 107 | LvarNames map[string]int 108 | } 109 | 110 | type Lexer struct { 111 | lex.Lexer 112 | tagStart string 113 | tagEnd string 114 | symbols *LexSymbolSet 115 | } 116 | 117 | // LexSymbol holds the pre-defined symbols to be lexed 118 | type LexSymbol struct { 119 | Name string 120 | Type lex.ItemType 121 | Priority float32 122 | } 123 | 124 | // LexSymbolList a list of LexSymbols. Normally you do not need to use it. 125 | // This is mainly only useful for sorting LexSymbols 126 | type LexSymbolList []LexSymbol 127 | 128 | // LexSymbolSorter sorts a list of LexSymbols by priority 129 | type LexSymbolSorter struct { 130 | list LexSymbolList 131 | } 132 | 133 | // LexSymbolSet is the container for symbols. 134 | type LexSymbolSet struct { 135 | Map map[string]LexSymbol 136 | SortedList LexSymbolList 137 | } 138 | 139 | // Parser defines the interface for Xslate parsers 140 | type Parser interface { 141 | Parse(string, []byte) (*AST, error) 142 | ParseString(string, string) (*AST, error) 143 | ParseReader(string, io.Reader) (*AST, error) 144 | } 145 | -------------------------------------------------------------------------------- /parser/kolonish/kolonish.go: -------------------------------------------------------------------------------- 1 | package kolonish 2 | 3 | import ( 4 | "github.com/lestrrat-go/lex" 5 | "github.com/lestrrat-go/xslate/parser" 6 | "io" 7 | ) 8 | 9 | const ( 10 | ItemDollar lex.ItemType = parser.DefaultItemTypeMax + 1 11 | ) 12 | 13 | var SymbolSet = parser.DefaultSymbolSet.Copy() 14 | 15 | func init() { 16 | SymbolSet.Set("$", ItemDollar) 17 | } 18 | 19 | // Kolonish is the main parser for Kolonish 20 | type Kolonish struct{} 21 | 22 | // NewStringLexer creates a new lexer 23 | func NewStringLexer(template string) *parser.Lexer { 24 | l := parser.NewStringLexer(template, SymbolSet) 25 | l.SetTagStart("<:") 26 | l.SetTagEnd(":>") 27 | 28 | return l 29 | } 30 | 31 | // NewReaderLexer creates a new lexer 32 | func NewReaderLexer(rdr io.Reader) *parser.Lexer { 33 | l := parser.NewReaderLexer(rdr, SymbolSet) 34 | l.SetTagStart("<:") 35 | l.SetTagEnd(":>") 36 | 37 | return l 38 | } 39 | 40 | // New creates a new Kolonish parser 41 | func New() *Kolonish { 42 | return &Kolonish{} 43 | } 44 | 45 | func NewLexer(template string) *parser.Lexer { 46 | l := parser.NewStringLexer(template, SymbolSet) 47 | l.SetTagStart("<:") 48 | l.SetTagEnd(":>") 49 | 50 | return l 51 | } 52 | 53 | // Parse parses the given template and creates an AST 54 | func (p *Kolonish) Parse(name string, template []byte) (*parser.AST, error) { 55 | return p.ParseString(name, string(template)) 56 | } 57 | 58 | // ParseString is the same as Parse, but receives a string instead of []byte 59 | func (p *Kolonish) ParseString(name, template string) (*parser.AST, error) { 60 | b := parser.NewBuilder() 61 | lex := NewStringLexer(template) 62 | return b.Parse(name, lex) 63 | } 64 | 65 | // ParseReader gets the template content from an io.Reader type 66 | func (p *Kolonish) ParseReader(name string, rdr io.Reader) (*parser.AST, error) { 67 | b := parser.NewBuilder() 68 | lex := NewReaderLexer(rdr) 69 | return b.Parse(name, lex) 70 | } 71 | -------------------------------------------------------------------------------- /parser/kolonish/lexer_test.go: -------------------------------------------------------------------------------- 1 | package kolonish 2 | 3 | import ( 4 | "github.com/lestrrat-go/lex" 5 | "github.com/lestrrat-go/xslate/parser" 6 | "testing" 7 | ) 8 | 9 | func makeItem(t lex.ItemType, p, line int, v string) lex.LexItem { 10 | return lex.NewItem(t, p, line, v) 11 | } 12 | 13 | func makeLexer(input string) *parser.Lexer { 14 | l := NewLexer(input) 15 | return l 16 | } 17 | 18 | func lexit(input string) *parser.Lexer { 19 | l := makeLexer(input) 20 | go l.Run() 21 | return l 22 | } 23 | 24 | func compareLex(t *testing.T, expected []lex.LexItem, l *parser.Lexer) { 25 | for n := 0; n < len(expected); n++ { 26 | i := l.NextItem() 27 | 28 | e := expected[n] 29 | if e.Type() != i.Type() { 30 | t.Errorf("Expected type %s, got %s", e.Type(), i.Type()) 31 | t.Logf(" -> expected %s got %s", e, i) 32 | } 33 | if e.Type() == parser.ItemIdentifier || e.Type() == parser.ItemRawString { 34 | if e.Value() != i.Value() { 35 | t.Errorf("Expected.Value()ue %s, got %s", e.Value(), i.Value()) 36 | t.Logf(" -> expected %s got %s", e, i) 37 | } 38 | } 39 | } 40 | 41 | i := l.NextItem() 42 | if i.Type() != parser.ItemEOF { 43 | t.Errorf("Expected EOF, got %s", i) 44 | } 45 | 46 | } 47 | 48 | func TestGetImplicit(t *testing.T) { 49 | tmpl := `<: $foo :>` 50 | l := lexit(tmpl) 51 | expected := []lex.LexItem{ 52 | makeItem(parser.ItemTagStart, 0, 1, "<:"), 53 | makeItem(parser.ItemSpace, 2, 1, " "), 54 | makeItem(ItemDollar, 3, 1, "$"), 55 | makeItem(parser.ItemIdentifier, 4, 1, "foo"), 56 | makeItem(parser.ItemSpace, 7, 1, " "), 57 | makeItem(parser.ItemTagEnd, 8, 1, ":>"), 58 | } 59 | compareLex(t, expected, l) 60 | } 61 | 62 | func TestLinewiseCode(t *testing.T) { 63 | tmpl := ` 64 | : "foo\n" 65 | : for list -> i { 66 | : i 67 | : } 68 | ` 69 | _ = lexit(tmpl) 70 | 71 | } 72 | -------------------------------------------------------------------------------- /parser/lexer.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | /* 4 | 5 | Lexer for TTerse, based on http://golang.org/src/pkg/text/template/parse/lex.go 6 | 7 | Anything up to a tagStart('[%') is treated as RawText, and therefore does not 8 | need any real lexing. 9 | 10 | Once tagStart is found, real lexing starts. 11 | 12 | */ 13 | 14 | import ( 15 | "io" 16 | "unicode" 17 | "unicode/utf8" 18 | 19 | "github.com/lestrrat-go/lex" 20 | ) 21 | 22 | func init() { 23 | lex.TypeNames[ItemError] = "Error" 24 | lex.TypeNames[ItemRawString] = "RawString" 25 | lex.TypeNames[ItemEOF] = "EOF" 26 | lex.TypeNames[ItemComment] = "Comment" 27 | lex.TypeNames[ItemComplex] = "Complex" // may not need this 28 | lex.TypeNames[ItemChar] = "Char" 29 | lex.TypeNames[ItemSpace] = "Space" 30 | lex.TypeNames[ItemNumber] = "Number" 31 | lex.TypeNames[ItemSymbol] = "Symbol" 32 | lex.TypeNames[ItemIdentifier] = "Identifier" 33 | lex.TypeNames[ItemTagStart] = "TagStart" 34 | lex.TypeNames[ItemTagEnd] = "TagEnd" 35 | lex.TypeNames[ItemBool] = "Bool" 36 | lex.TypeNames[ItemField] = "Field" 37 | lex.TypeNames[ItemSet] = "Set" 38 | lex.TypeNames[ItemPlus] = "Plus" 39 | lex.TypeNames[ItemMinus] = "Minus" 40 | lex.TypeNames[ItemAsterisk] = "Asterisk" 41 | lex.TypeNames[ItemSlash] = "Slash" 42 | lex.TypeNames[ItemVerticalSlash] = "VerticalSlash" 43 | lex.TypeNames[ItemAssign] = "Assign" 44 | lex.TypeNames[ItemOpenSquareBracket] = "OpenSquareBracket" 45 | lex.TypeNames[ItemCloseSquareBracket] = "CloseSquareBracket" 46 | lex.TypeNames[ItemWrapper] = "Wrapper" 47 | lex.TypeNames[ItemComma] = "Comma" 48 | lex.TypeNames[ItemOpenParen] = "OpenParen" 49 | lex.TypeNames[ItemCloseParen] = "CloseParen" 50 | lex.TypeNames[ItemPeriod] = "Period" 51 | lex.TypeNames[ItemKeyword] = "Keyword" 52 | lex.TypeNames[ItemGet] = "GET" 53 | lex.TypeNames[ItemMacro] = "Macro" 54 | lex.TypeNames[ItemBlock] = "Block" 55 | lex.TypeNames[ItemDoubleQuotedString] = "DoubleQuotedString" 56 | lex.TypeNames[ItemSingleQuotedString] = "SingleQuotedString" 57 | lex.TypeNames[ItemWith] = "With" 58 | lex.TypeNames[ItemForeach] = "Foreach" 59 | lex.TypeNames[ItemWhile] = "While" 60 | lex.TypeNames[ItemIn] = "In" 61 | lex.TypeNames[ItemInclude] = "Include" 62 | lex.TypeNames[ItemIf] = "If" 63 | lex.TypeNames[ItemElse] = "Else" 64 | lex.TypeNames[ItemElseIf] = "ElseIf" 65 | lex.TypeNames[ItemUnless] = "Unless" 66 | lex.TypeNames[ItemSwitch] = "Switch" 67 | lex.TypeNames[ItemCase] = "Case" 68 | lex.TypeNames[ItemDefault] = "Default" 69 | lex.TypeNames[ItemCall] = "Call" 70 | lex.TypeNames[ItemOperator] = "Operator (INTERNAL)" 71 | lex.TypeNames[ItemRange] = "Range" 72 | lex.TypeNames[ItemEquals] = "Equals" 73 | lex.TypeNames[ItemNotEquals] = "NotEquals" 74 | lex.TypeNames[ItemCmp] = "Cmp" 75 | lex.TypeNames[ItemGT] = "GreaterThan" 76 | lex.TypeNames[ItemLT] = "LessThan" 77 | lex.TypeNames[ItemLE] = "LessThanEquals" 78 | lex.TypeNames[ItemGE] = "GreterThanEquals" 79 | lex.TypeNames[ItemShiftLeft] = "ShiftLeft" 80 | lex.TypeNames[ItemShiftRight] = "ShiftRight" 81 | lex.TypeNames[ItemAssignAdd] = "AssignAdd" 82 | lex.TypeNames[ItemAssignSub] = "AssignSub" 83 | lex.TypeNames[ItemAssignMul] = "AssignMul" 84 | lex.TypeNames[ItemAssignDiv] = "AssignDiv" 85 | lex.TypeNames[ItemAssignMod] = "AssignMod" 86 | lex.TypeNames[ItemAnd] = "And" 87 | lex.TypeNames[ItemOr] = "Or" 88 | lex.TypeNames[ItemFatComma] = "FatComma" 89 | lex.TypeNames[ItemIncr] = "Incr" 90 | lex.TypeNames[ItemDecr] = "Decr" 91 | lex.TypeNames[ItemMod] = "Mod" 92 | lex.TypeNames[ItemEnd] = "End" 93 | } 94 | 95 | func (l *Lexer) SetTagStart(s string) { 96 | l.tagStart = s 97 | } 98 | 99 | func (l *Lexer) SetTagEnd(s string) { 100 | l.tagEnd = s 101 | } 102 | 103 | func isSpace(r rune) bool { 104 | return r == ' ' || r == '\t' 105 | } 106 | 107 | func isAlphaNumeric(r rune) bool { 108 | return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) 109 | } 110 | 111 | func isEndOfLine(r rune) bool { 112 | return r == '\r' || r == '\n' 113 | } 114 | 115 | func isChar(r rune) bool { 116 | return r <= unicode.MaxASCII && unicode.IsPrint(r) 117 | } 118 | 119 | func isNumeric(r rune) bool { 120 | return '0' <= r && r <= '9' 121 | } 122 | 123 | func NewStringLexer(template string, ss *LexSymbolSet) *Lexer { 124 | l := &Lexer{ 125 | nil, 126 | "", 127 | "", 128 | ss, 129 | } 130 | l.Lexer = lex.NewStringLexer(template, l.lexRawString) 131 | return l 132 | } 133 | 134 | func NewReaderLexer(rdr io.Reader, ss *LexSymbolSet) *Lexer { 135 | l := &Lexer{ 136 | nil, 137 | "", 138 | "", 139 | ss, 140 | } 141 | l.Lexer = lex.NewReaderLexer(rdr, l.lexRawString) 142 | return l 143 | } 144 | 145 | func (sl *Lexer) lexRawString(l lex.Lexer) lex.LexFn { 146 | for { 147 | if sl.PeekString(sl.tagStart) { 148 | if len(l.BufferString()) > 0 { 149 | sl.Emit(ItemRawString) 150 | } 151 | return sl.lexTagStart 152 | } 153 | if sl.Next() == lex.EOF { 154 | break 155 | } 156 | } 157 | 158 | if len(sl.BufferString()) > 0 { 159 | sl.Emit(ItemRawString) 160 | } 161 | sl.Emit(ItemEOF) 162 | return nil 163 | } 164 | 165 | func (sl *Lexer) lexSpace(l lex.Lexer) lex.LexFn { 166 | guard := lex.Mark("lexSpace") 167 | defer guard() 168 | 169 | count := 0 170 | for { 171 | r := l.Peek() 172 | if !isSpace(r) { 173 | break 174 | } 175 | count++ 176 | l.Next() 177 | } 178 | 179 | if count > 0 { 180 | l.Emit(ItemSpace) 181 | } 182 | return sl.lexInsideTag 183 | } 184 | 185 | func (sl *Lexer) lexTagStart(l lex.Lexer) lex.LexFn { 186 | if !sl.AcceptString(sl.tagStart) { 187 | sl.EmitErrorf("Expected tag start (%s)", sl.tagStart) 188 | } 189 | sl.Emit(ItemTagStart) 190 | return sl.lexInsideTag 191 | } 192 | 193 | func (sl *Lexer) lexTagEnd(l lex.Lexer) lex.LexFn { 194 | if !sl.AcceptString(sl.tagEnd) { 195 | sl.EmitErrorf("Expected tag end (%s)", sl.tagEnd) 196 | } 197 | sl.Emit(ItemTagEnd) 198 | return sl.lexRawString 199 | } 200 | 201 | func (sl *Lexer) lexIdentifier(l lex.Lexer) lex.LexFn { 202 | Loop: 203 | for { 204 | switch r := sl.Next(); { 205 | case isAlphaNumeric(r): 206 | default: 207 | sl.Backup() 208 | word := sl.BufferString() 209 | if !sl.atTerminator() { 210 | return sl.EmitErrorf("bad character %#U", r) 211 | } 212 | 213 | if sym := sl.symbols.Get(word); sym.Type > ItemKeyword { 214 | sl.Emit(sym.Type) 215 | } else { 216 | switch { 217 | case word[0] == '.': 218 | sl.Emit(ItemField) 219 | case word == "true", word == "false": 220 | sl.Emit(ItemBool) 221 | default: 222 | sl.Emit(ItemIdentifier) 223 | } 224 | } 225 | break Loop 226 | } 227 | } 228 | return sl.lexInsideTag 229 | } 230 | 231 | func (l *Lexer) atTerminator() bool { 232 | r := l.Peek() 233 | if isSpace(r) || isEndOfLine(r) { 234 | return true 235 | } 236 | switch r { 237 | case lex.EOF, '.', ',', '|', ':', ')', '(', '[', ']': 238 | return true 239 | } 240 | // Does r start the delimiter? This can be ambiguous (with delim=="//", $x/2 will 241 | // succeed but should fail) but only in extremely rare cases caused by willfully 242 | // bad choice of delimiter. 243 | if rd, _ := utf8.DecodeRuneInString(l.tagEnd); rd == r { 244 | return true 245 | } 246 | return false 247 | } 248 | 249 | func (sl *Lexer) lexRange(l lex.Lexer) lex.LexFn { 250 | for i := 0; i < 2; i++ { 251 | if sl.Peek() != '.' { 252 | return sl.EmitErrorf("bad range syntax: %q", sl.BufferString()) 253 | } 254 | sl.Next() 255 | } 256 | sl.Emit(ItemRange) 257 | 258 | if isNumeric(sl.Peek()) { 259 | return sl.lexInteger 260 | } 261 | return sl.lexIdentifier 262 | } 263 | 264 | func (sl *Lexer) lexInteger(l lex.Lexer) lex.LexFn { 265 | if sl.scanInteger() { 266 | sl.Emit(ItemNumber) 267 | } else { 268 | return sl.EmitErrorf("bad integer syntax: %q", sl.BufferString()) 269 | } 270 | return sl.lexInsideTag 271 | } 272 | 273 | func (sl *Lexer) lexNumber(l lex.Lexer) lex.LexFn { 274 | if !sl.scanNumber() { 275 | return sl.EmitErrorf("bad number syntax: %q", sl.BufferString()) 276 | } 277 | 278 | /* XXX Remove complex number support 279 | if sign := sl.Peek(); sign == '+' || sign == '-' { 280 | // Complex: 1+2i. No spaces, must end in 'i'. 281 | if !sl.scanNumber() || sl.PrevByte() != 'i' { 282 | return sl.EmitErrorf("bad number syntax: %q", sl.BufferString()) 283 | } 284 | sl.Emit(ItemComplex) 285 | } else 286 | */ 287 | if dot := sl.Peek(); dot == '.' { 288 | sl.Emit(ItemNumber) 289 | return sl.lexRange 290 | } 291 | sl.Emit(ItemNumber) 292 | return sl.lexInsideTag 293 | } 294 | 295 | func (l *Lexer) scanInteger() bool { 296 | l.AcceptAny("+-") 297 | digits := "0123456789" 298 | ret := l.AcceptRun(digits) 299 | // l.backup() 300 | return ret 301 | } 302 | 303 | func (l *Lexer) scanNumber() bool { 304 | // Optional leading sign. 305 | l.AcceptAny("+-") 306 | // Is it hex? 307 | digits := "0123456789" 308 | if l.AcceptAny("0") && l.AcceptAny("xX") { 309 | digits = "0123456789abcdefABCDEF" 310 | } 311 | l.AcceptRun(digits) 312 | if l.AcceptString(".") { 313 | if !l.AcceptRun(digits) { 314 | l.Backup() 315 | } 316 | return true 317 | } 318 | if l.AcceptAny("eE") { 319 | l.AcceptAny("+-") 320 | l.AcceptRun("0123456789") 321 | } 322 | // Is it imaginary? 323 | l.AcceptString("i") 324 | // Next thing mustn't be alphanumeric. 325 | if isAlphaNumeric(l.Peek()) { 326 | l.Next() 327 | return false 328 | } 329 | return true 330 | } 331 | 332 | func (sl *Lexer) lexComment(l lex.Lexer) lex.LexFn { 333 | for { 334 | if sl.PeekString(sl.tagEnd) { 335 | sl.Emit(ItemComment) 336 | return sl.lexTagEnd 337 | } 338 | if isEndOfLine(sl.Next()) { 339 | sl.Emit(ItemComment) 340 | return sl.lexTagEnd 341 | } 342 | } 343 | } 344 | 345 | func (sl *Lexer) lexQuotedString(l lex.Lexer, quote rune, t lex.ItemType) lex.LexFn { 346 | for { 347 | if sl.PeekString(sl.tagEnd) { 348 | return sl.EmitErrorf("unexpected end of quoted string") 349 | } 350 | 351 | r := sl.Next() 352 | switch r { 353 | case quote: 354 | sl.Emit(t) 355 | return sl.lexInsideTag 356 | case lex.EOF: 357 | return sl.EmitErrorf("unexpected end of quoted string") 358 | } 359 | } 360 | } 361 | 362 | func (sl *Lexer) lexDoubleQuotedString(l lex.Lexer) lex.LexFn { 363 | return sl.lexQuotedString(l, '"', ItemDoubleQuotedString) 364 | } 365 | 366 | func (sl *Lexer) lexSingleQuotedString(l lex.Lexer) lex.LexFn { 367 | return sl.lexQuotedString(l, '\'', ItemSingleQuotedString) 368 | } 369 | 370 | func (l *Lexer) getSortedSymbols() LexSymbolList { 371 | return l.symbols.GetSortedList() 372 | } 373 | 374 | func (sl *Lexer) lexInsideTag(l lex.Lexer) lex.LexFn { 375 | guard := lex.Mark("lexInsideTag") 376 | defer guard() 377 | 378 | if sl.PeekString(sl.tagEnd) { 379 | return sl.lexTagEnd 380 | } 381 | 382 | // Find registered symbols 383 | for _, sym := range sl.getSortedSymbols() { 384 | if sl.AcceptString(sym.Name) { 385 | sl.Emit(sym.Type) 386 | return sl.lexInsideTag 387 | } 388 | } 389 | 390 | r := sl.Next() 391 | lex.Trace("r = '%q'\n", r) 392 | switch { 393 | case r == lex.EOF: 394 | return sl.EmitErrorf("unclosed tag") 395 | case r == '#': 396 | return sl.lexComment 397 | case isSpace(r): 398 | sl.Backup() 399 | return sl.lexSpace 400 | case isNumeric(r): 401 | sl.Backup() 402 | return sl.lexNumber 403 | case r == '"': 404 | return sl.lexDoubleQuotedString 405 | case r == '\'': 406 | return sl.lexSingleQuotedString 407 | case isAlphaNumeric(r): 408 | sl.Backup() 409 | return sl.lexIdentifier 410 | default: 411 | return sl.EmitErrorf("unrecognized character in tag: %#U", r) 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /parser/lexer_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/lestrrat-go/lex" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestItem_String(t *testing.T) { 10 | for i := lex.ItemDefaultMax + 1; i < DefaultItemTypeMax; i++ { 11 | it := lex.ItemType(i) 12 | if strings.HasPrefix(it.String(), "Unknown") { 13 | t.Errorf("%#v does not have String() implemented", it) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /parser/symbols.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "github.com/lestrrat-go/lex" 5 | "sort" 6 | ) 7 | 8 | // DefaultSymbolSet is the LexSymbolSet for symbols that are common to 9 | // all syntax 10 | var DefaultSymbolSet = NewLexSymbolSet() 11 | 12 | func init() { 13 | DefaultSymbolSet.Set("==", ItemEquals, 1.0) 14 | DefaultSymbolSet.Set("eq", ItemEquals, 0.0) 15 | DefaultSymbolSet.Set("!=", ItemNotEquals, 1.0) 16 | DefaultSymbolSet.Set("ne", ItemNotEquals, 0.0) 17 | DefaultSymbolSet.Set("+=", ItemAssignAdd, 1.0) 18 | DefaultSymbolSet.Set("-=", ItemAssignSub, 1.0) 19 | DefaultSymbolSet.Set("*=", ItemAssignMul, 1.0) 20 | DefaultSymbolSet.Set("/=", ItemAssignDiv, 1.0) 21 | DefaultSymbolSet.Set("(", ItemOpenParen, 0.0) 22 | DefaultSymbolSet.Set(")", ItemCloseParen, 0.0) 23 | DefaultSymbolSet.Set("[", ItemOpenSquareBracket, 0.0) 24 | DefaultSymbolSet.Set("]", ItemCloseSquareBracket, 0.0) 25 | DefaultSymbolSet.Set("..", ItemRange, 1.0) 26 | DefaultSymbolSet.Set(".", ItemPeriod, 0.0) 27 | DefaultSymbolSet.Set(",", ItemComma, 0.0) 28 | DefaultSymbolSet.Set("|", ItemVerticalSlash, 0.0) 29 | DefaultSymbolSet.Set("=", ItemAssign, 0.0) 30 | DefaultSymbolSet.Set(">", ItemGT, 0.0) 31 | DefaultSymbolSet.Set("<", ItemLT, 0.0) 32 | DefaultSymbolSet.Set("+", ItemPlus, 0.0) 33 | DefaultSymbolSet.Set("-", ItemMinus, 0.0) 34 | DefaultSymbolSet.Set("*", ItemAsterisk, 0.0) 35 | DefaultSymbolSet.Set("/", ItemSlash, 0.0) 36 | } 37 | 38 | // Sort returns a sorted list of LexSymbols, sorted by Priority 39 | func (list LexSymbolList) Sort() LexSymbolList { 40 | sorter := LexSymbolSorter{ 41 | list: list, 42 | } 43 | sort.Sort(sorter) 44 | return sorter.list 45 | } 46 | 47 | // Len returns the length of the list 48 | func (s LexSymbolSorter) Len() int { 49 | return len(s.list) 50 | } 51 | 52 | // Less returns true if the i-th element's Priority is less than the j-th element 53 | func (s LexSymbolSorter) Less(i, j int) bool { 54 | return s.list[i].Priority > s.list[j].Priority 55 | } 56 | 57 | // Swap swaps the elements at i and j 58 | func (s LexSymbolSorter) Swap(i, j int) { 59 | s.list[i], s.list[j] = s.list[j], s.list[i] 60 | } 61 | 62 | // NewLexSymbolSet creates a new LexSymbolSet 63 | func NewLexSymbolSet() *LexSymbolSet { 64 | return &LexSymbolSet{ 65 | make(map[string]LexSymbol), 66 | nil, 67 | } 68 | } 69 | 70 | // Copy creates a new copy of the given LexSymbolSet 71 | func (l *LexSymbolSet) Copy() *LexSymbolSet { 72 | c := NewLexSymbolSet() 73 | for k, v := range l.Map { 74 | c.Map[k] = LexSymbol{v.Name, v.Type, v.Priority} 75 | } 76 | return c 77 | } 78 | 79 | // Count returns the number of symbols registered 80 | func (l *LexSymbolSet) Count() int { 81 | return len(l.Map) 82 | } 83 | 84 | // Get returns the LexSymbol associated with `name` 85 | func (l *LexSymbolSet) Get(name string) LexSymbol { 86 | return l.Map[name] 87 | } 88 | 89 | // Set creates and sets a new LexItem to `name` 90 | func (l *LexSymbolSet) Set(name string, typ lex.ItemType, prio ...float32) { 91 | var x float32 92 | if len(prio) < 1 { 93 | x = 1.0 94 | } else { 95 | x = prio[0] 96 | } 97 | l.Map[name] = LexSymbol{name, typ, x} 98 | l.SortedList = nil // reset 99 | } 100 | 101 | // GetSortedList returns the lsit of LexSymbols in order that they should 102 | // be searched for in the tempalte 103 | func (l *LexSymbolSet) GetSortedList() LexSymbolList { 104 | // Because symbols are parsed automatically in a loop, we need to make 105 | // sure that we search starting with the longest term (e.g., "INCLUDE" 106 | // must come before "IN") 107 | // However, simply sorting the symbols using alphabetical sort then 108 | // max-length forces us to make more comparisons than necessary. 109 | // To get the best of both world, we allow passing a floating point 110 | // "priority" parameter to sort the symbols 111 | if l.SortedList != nil { 112 | return l.SortedList 113 | } 114 | 115 | num := len(l.Map) 116 | list := make(LexSymbolList, num) 117 | i := 0 118 | for _, v := range l.Map { 119 | list[i] = v 120 | i++ 121 | } 122 | l.SortedList = list.Sort() 123 | 124 | return l.SortedList 125 | } 126 | -------------------------------------------------------------------------------- /parser/tterse/lexer_test.go: -------------------------------------------------------------------------------- 1 | package tterse 2 | 3 | import ( 4 | "github.com/lestrrat-go/lex" 5 | "github.com/lestrrat-go/xslate/parser" 6 | "testing" 7 | ) 8 | 9 | func makeItem(t lex.ItemType, p, l int, v string) lex.Item { 10 | return lex.NewItem(t, p, l, v) 11 | } 12 | 13 | var space = makeItem(parser.ItemSpace, 0, 1, " ") 14 | var tagStart = makeItem(parser.ItemTagStart, 0, 1, "[%") 15 | var tagEnd = makeItem(parser.ItemTagEnd, 0, 1, "[%") 16 | 17 | func makeLexer(input string) *parser.Lexer { 18 | l := NewStringLexer(input) 19 | return l 20 | } 21 | 22 | func lexit(input string) *parser.Lexer { 23 | l := makeLexer(input) 24 | go l.Run() 25 | return l 26 | } 27 | 28 | func compareLex(t *testing.T, expected []lex.LexItem, l *parser.Lexer) { 29 | for n := 0; n < len(expected); n++ { 30 | i := l.NextItem() 31 | 32 | e := expected[n] 33 | if e.Type() != i.Type() { 34 | t.Errorf("Expected type %s, got %s", e.Type(), i.Type()) 35 | t.Logf(" -> expected %s got %s", e, i) 36 | } 37 | if e.Type() == parser.ItemIdentifier || e.Type() == parser.ItemRawString { 38 | if e.Value() != i.Value() { 39 | t.Errorf("Expected.Value()ue %s, got %s", e.Value(), i.Value()) 40 | t.Logf(" -> expected %s got %s", e, i) 41 | } 42 | } 43 | } 44 | 45 | i := l.NextItem() 46 | if i.Type() != parser.ItemEOF { 47 | t.Errorf("Expected EOF, got %s", i) 48 | } 49 | 50 | } 51 | 52 | func TestLexRawString(t *testing.T) { 53 | tmpl := `This is a raw string 日本語もはいるよ!` 54 | l := lexit(tmpl) 55 | 56 | for { 57 | i := l.NextItem() 58 | if i.Type() == parser.ItemEOF || i.Type() == parser.ItemError { 59 | break 60 | } 61 | 62 | if i.Type() != parser.ItemRawString { 63 | t.Errorf("Expected type RawString, got %s", i) 64 | } 65 | 66 | if i.Value() != tmpl { 67 | t.Errorf("Expected.Value()ue '%s', got '%s'", tmpl, i.Value()) 68 | } 69 | } 70 | } 71 | 72 | func TestLexSet(t *testing.T) { 73 | tmpl := `[% SET foo = bar + 1 %]` 74 | l := lexit(tmpl) 75 | expected := []lex.LexItem{ 76 | tagStart, 77 | space, 78 | makeItem(parser.ItemSet, 0, 1, ""), 79 | space, 80 | makeItem(parser.ItemIdentifier, 0, 1, "foo"), 81 | space, 82 | makeItem(parser.ItemAssign, 0, 1, ""), 83 | space, 84 | makeItem(parser.ItemIdentifier, 0, 1, "bar"), 85 | space, 86 | makeItem(parser.ItemPlus, 0, 1, ""), 87 | space, 88 | makeItem(parser.ItemNumber, 0, 1, "1"), 89 | space, 90 | tagEnd, 91 | } 92 | compareLex(t, expected, l) 93 | } 94 | 95 | func TestLexGet(t *testing.T) { 96 | tmpl := `[% GET foo %]` 97 | l := lexit(tmpl) 98 | expected := []lex.LexItem{ 99 | tagStart, 100 | space, 101 | makeItem(parser.ItemGet, 0, 1, ""), 102 | space, 103 | makeItem(parser.ItemIdentifier, 0, 1, "foo"), 104 | space, 105 | tagEnd, 106 | } 107 | compareLex(t, expected, l) 108 | } 109 | 110 | func TestLexForeach(t *testing.T) { 111 | tmpl := `[% FOREACH i IN list %][% i %][% END %]` 112 | l := lexit(tmpl) 113 | expected := []lex.LexItem{ 114 | tagStart, 115 | space, 116 | makeItem(parser.ItemForeach, 0, 1, ""), 117 | space, 118 | makeItem(parser.ItemIdentifier, 0, 1, "i"), 119 | space, 120 | makeItem(parser.ItemIn, 0, 1, ""), 121 | space, 122 | makeItem(parser.ItemIdentifier, 0, 1, "list"), 123 | space, 124 | tagEnd, 125 | tagStart, 126 | space, 127 | makeItem(parser.ItemIdentifier, 0, 1, "i"), 128 | space, 129 | tagEnd, 130 | tagStart, 131 | space, 132 | makeItem(parser.ItemEnd, 0, 1, ""), 133 | space, 134 | tagEnd, 135 | } 136 | 137 | compareLex(t, expected, l) 138 | } 139 | 140 | func TestLexMacro(t *testing.T) { 141 | tmpl := `[% MACRO foo BLOCK %]foo bar[% baz %][% END %]` 142 | l := lexit(tmpl) 143 | expected := []lex.LexItem{ 144 | tagStart, 145 | space, 146 | makeItem(parser.ItemMacro, 0, 1, ""), 147 | space, 148 | makeItem(parser.ItemIdentifier, 0, 1, "foo"), 149 | space, 150 | makeItem(parser.ItemBlock, 0, 1, ""), 151 | space, 152 | tagEnd, 153 | makeItem(parser.ItemRawString, 0, 1, "foo bar"), 154 | tagStart, 155 | space, 156 | makeItem(parser.ItemIdentifier, 0, 1, "baz"), 157 | space, 158 | tagEnd, 159 | tagStart, 160 | space, 161 | makeItem(parser.ItemEnd, 0, 1, ""), 162 | space, 163 | tagEnd, 164 | } 165 | 166 | compareLex(t, expected, l) 167 | } 168 | 169 | func TestLexConditional(t *testing.T) { 170 | tmpl := `[% IF foo %][% IF (bar) %]baz[% END %][% ELSIF quux %]hoge[% ELSE %]fuga[% END %][% UNLESS moge %]bababa[% END %]` 171 | l := lexit(tmpl) 172 | expected := []lex.LexItem{ 173 | tagStart, 174 | space, 175 | makeItem(parser.ItemIf, 0, 1, ""), 176 | space, 177 | makeItem(parser.ItemIdentifier, 0, 1, "foo"), 178 | space, 179 | tagEnd, 180 | tagStart, 181 | space, 182 | makeItem(parser.ItemIf, 0, 1, ""), 183 | space, 184 | makeItem(parser.ItemOpenParen, 0, 1, ""), 185 | makeItem(parser.ItemIdentifier, 0, 1, "bar"), 186 | makeItem(parser.ItemCloseParen, 0, 1, ""), 187 | space, 188 | tagEnd, 189 | makeItem(parser.ItemRawString, 0, 1, "baz"), 190 | tagStart, 191 | space, 192 | makeItem(parser.ItemEnd, 0, 1, ""), 193 | space, 194 | tagEnd, 195 | tagStart, 196 | space, 197 | makeItem(parser.ItemElseIf, 0, 1, ""), 198 | space, 199 | makeItem(parser.ItemIdentifier, 0, 1, "quux"), 200 | space, 201 | tagEnd, 202 | makeItem(parser.ItemRawString, 0, 1, "hoge"), 203 | tagStart, 204 | space, 205 | makeItem(parser.ItemElse, 0, 1, ""), 206 | space, 207 | tagEnd, 208 | makeItem(parser.ItemRawString, 0, 1, "fuga"), 209 | tagStart, 210 | space, 211 | makeItem(parser.ItemEnd, 0, 1, ""), 212 | space, 213 | tagEnd, 214 | tagStart, 215 | space, 216 | makeItem(parser.ItemUnless, 0, 1, ""), 217 | space, 218 | makeItem(parser.ItemIdentifier, 0, 1, "moge"), 219 | space, 220 | tagEnd, 221 | makeItem(parser.ItemRawString, 0, 1, "bababa"), 222 | tagStart, 223 | space, 224 | makeItem(parser.ItemEnd, 0, 1, ""), 225 | space, 226 | tagEnd, 227 | } 228 | compareLex(t, expected, l) 229 | } 230 | 231 | func TestVariableAccess(t *testing.T) { 232 | tmpl := `[% foo.bar %][% foo.bar.baz() %]` 233 | l := lexit(tmpl) 234 | 235 | expected := []lex.LexItem{ 236 | tagStart, 237 | space, 238 | makeItem(parser.ItemIdentifier, 0, 1, "foo"), 239 | makeItem(parser.ItemPeriod, 0, 1, ""), 240 | makeItem(parser.ItemIdentifier, 0, 1, "bar"), 241 | space, 242 | tagEnd, 243 | tagStart, 244 | space, 245 | makeItem(parser.ItemIdentifier, 0, 1, "foo"), 246 | makeItem(parser.ItemPeriod, 0, 1, ""), 247 | makeItem(parser.ItemIdentifier, 0, 1, "bar"), 248 | makeItem(parser.ItemPeriod, 0, 1, ""), 249 | makeItem(parser.ItemIdentifier, 0, 1, "baz"), 250 | makeItem(parser.ItemOpenParen, 0, 1, ""), 251 | makeItem(parser.ItemCloseParen, 0, 1, ""), 252 | space, 253 | tagEnd, 254 | } 255 | 256 | compareLex(t, expected, l) 257 | } 258 | 259 | func TestBareQuotedString(t *testing.T) { 260 | tmpl := `[% "hello, double quote" %][% 'hello, single quote' %]` 261 | l := lexit(tmpl) 262 | 263 | expected := []lex.LexItem{ 264 | tagStart, 265 | space, 266 | makeItem(parser.ItemDoubleQuotedString, 0, 1, "hello, double quote"), 267 | space, 268 | tagEnd, 269 | tagStart, 270 | space, 271 | makeItem(parser.ItemSingleQuotedString, 0, 1, "hello, single quote"), 272 | space, 273 | tagEnd, 274 | } 275 | compareLex(t, expected, l) 276 | } 277 | -------------------------------------------------------------------------------- /parser/tterse/parser_test.go: -------------------------------------------------------------------------------- 1 | package tterse 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/lestrrat-go/xslate/node" 7 | "github.com/lestrrat-go/xslate/parser" 8 | ) 9 | 10 | func parse(t *testing.T, tmpl string) *parser.AST { 11 | p := New() 12 | ast, err := p.ParseString(tmpl, tmpl) 13 | if err != nil { 14 | t.Fatalf("Failed to parse template: %s", err) 15 | } 16 | return ast 17 | } 18 | 19 | func matchNodeTypes(t *testing.T, ast *parser.AST, expected []node.NodeType) { 20 | i := 0 21 | for n := range ast.Visit() { 22 | t.Logf("n -> %s", n.Type()) 23 | 24 | if len(expected) <= i { 25 | t.Fatalf("Got extra nodes after %d nodes", i) 26 | } 27 | 28 | if n.Type() != expected[i] { 29 | t.Fatalf("Expected node type %s, got %s", expected[i], n.Type()) 30 | } 31 | i++ 32 | } 33 | 34 | if i < len(expected) { 35 | t.Fatalf("Expected %d nodes, but only got %d", len(expected), i) 36 | } 37 | } 38 | 39 | func TestRawString(t *testing.T) { 40 | tmpl := `Hello, World!` 41 | ast := parse(t, tmpl) 42 | 43 | // Expect nodes to be in this order: 44 | expected := []node.NodeType{ 45 | node.Root, 46 | node.PrintRaw, 47 | node.Text, 48 | } 49 | matchNodeTypes(t, ast, expected) 50 | } 51 | 52 | func TestGetLocalVariable(t *testing.T) { 53 | tmpl := `[% SET name = "Bob" %]Hello World, [% name %]` 54 | ast := parse(t, tmpl) 55 | 56 | expected := []node.NodeType{ 57 | node.Root, 58 | node.Assignment, 59 | node.LocalVar, 60 | node.Text, 61 | node.PrintRaw, 62 | node.Text, 63 | node.Print, 64 | node.LocalVar, 65 | } 66 | matchNodeTypes(t, ast, expected) 67 | } 68 | 69 | func TestForeachLoop(t *testing.T) { 70 | tmpl := `[% FOREACH x IN list %]Hello World, [% x %][% END %]` 71 | ast := parse(t, tmpl) 72 | expected := []node.NodeType{ 73 | node.Root, 74 | node.Foreach, 75 | node.PrintRaw, 76 | node.Text, 77 | node.Print, 78 | node.LocalVar, 79 | } 80 | matchNodeTypes(t, ast, expected) 81 | } 82 | 83 | func TestBasic(t *testing.T) { 84 | tmpl := ` 85 | [% WRAPPER "hoge.tx" WITH foo = "bar" %] 86 | [% FOREACH x IN list %] 87 | [% loop.index %]. x is [% x %] 88 | [% END %] 89 | [% END %] 90 | ` 91 | p := New() 92 | ast, err := p.ParseString(tmpl, tmpl) 93 | if err != nil { 94 | t.Errorf("Error during parse: %s", err) 95 | } 96 | 97 | if len(ast.Root.Nodes) == 1 { 98 | t.Errorf("Expected Root node to have 1 child, got %d", len(ast.Root.Nodes)) 99 | } 100 | } 101 | 102 | func TestSimpleAssign(t *testing.T) { 103 | tmpl := `[% SET s = 1 %][% s %]` 104 | ast := parse(t, tmpl) 105 | 106 | expected := []node.NodeType{ 107 | node.Root, 108 | node.Assignment, 109 | node.LocalVar, 110 | node.Int, 111 | node.Print, 112 | node.LocalVar, 113 | } 114 | 115 | matchNodeTypes(t, ast, expected) 116 | } 117 | -------------------------------------------------------------------------------- /parser/tterse/tterse.go: -------------------------------------------------------------------------------- 1 | package tterse 2 | 3 | import ( 4 | "github.com/lestrrat-go/xslate/parser" 5 | "io" 6 | ) 7 | 8 | // SymbolSet contains TTerse specific symbols 9 | var SymbolSet = parser.DefaultSymbolSet.Copy() 10 | 11 | func init() { 12 | // "In" must come before Include 13 | SymbolSet.Set("INCLUDE", parser.ItemInclude, 2.0) 14 | SymbolSet.Set("IN", parser.ItemIn, 1.5) 15 | SymbolSet.Set("WITH", parser.ItemWith) 16 | SymbolSet.Set("CALL", parser.ItemCall) 17 | SymbolSet.Set("END", parser.ItemEnd) 18 | SymbolSet.Set("WRAPPER", parser.ItemWrapper) 19 | SymbolSet.Set("SET", parser.ItemSet) 20 | SymbolSet.Set("GET", parser.ItemGet) 21 | SymbolSet.Set("IF", parser.ItemIf) 22 | SymbolSet.Set("ELSIF", parser.ItemElseIf) 23 | SymbolSet.Set("ELSE", parser.ItemElse) 24 | SymbolSet.Set("UNLESS", parser.ItemUnless) 25 | SymbolSet.Set("FOREACH", parser.ItemForeach) 26 | SymbolSet.Set("WHILE", parser.ItemWhile) 27 | SymbolSet.Set("MACRO", parser.ItemMacro) 28 | SymbolSet.Set("BLOCK", parser.ItemBlock) 29 | SymbolSet.Set("END", parser.ItemEnd) 30 | } 31 | 32 | // TTerse is the main parser for TTerse 33 | type TTerse struct{} 34 | 35 | // NewStringLexer creates a new lexer 36 | func NewStringLexer(template string) *parser.Lexer { 37 | l := parser.NewStringLexer(template, SymbolSet) 38 | l.SetTagStart("[%") 39 | l.SetTagEnd("%]") 40 | 41 | return l 42 | } 43 | 44 | // NewReaderLexer creates a new lexer 45 | func NewReaderLexer(rdr io.Reader) *parser.Lexer { 46 | l := parser.NewReaderLexer(rdr, SymbolSet) 47 | l.SetTagStart("[%") 48 | l.SetTagEnd("%]") 49 | 50 | return l 51 | } 52 | 53 | // New creates a new TTerse parser 54 | func New() *TTerse { 55 | return &TTerse{} 56 | } 57 | 58 | // Parse parses the given template and creates an AST 59 | func (p *TTerse) Parse(name string, template []byte) (*parser.AST, error) { 60 | return p.ParseString(name, string(template)) 61 | } 62 | 63 | // ParseString is the same as Parse, but receives a string instead of []byte 64 | func (p *TTerse) ParseString(name, template string) (*parser.AST, error) { 65 | b := parser.NewBuilder() 66 | lex := NewStringLexer(template) 67 | return b.Parse(name, lex) 68 | } 69 | 70 | // ParseReader gets the template content from an io.Reader type 71 | func (p *TTerse) ParseReader(name string, rdr io.Reader) (*parser.AST, error) { 72 | b := parser.NewBuilder() 73 | lex := NewReaderLexer(rdr) 74 | return b.Parse(name, lex) 75 | } 76 | -------------------------------------------------------------------------------- /test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | type Ctx struct { 10 | Tester 11 | BaseDir string 12 | } 13 | 14 | type Tester interface { 15 | Errorf(string, ...interface{}) 16 | Fatalf(string, ...interface{}) 17 | Logf(string, ...interface{}) 18 | } 19 | 20 | /* 21 | 22 | NewCtx creates a new Context 23 | 24 | ctx := test.NewCtx() 25 | defer ctx.Clean() 26 | ctx.File(...).WriteSTring 27 | 28 | */ 29 | func NewCtx(t Tester) *Ctx { 30 | dir, err := ioutil.TempDir("", "xslate-test-") 31 | if err != nil { 32 | panic("Failed to create temporary directory!") 33 | } 34 | 35 | return &Ctx{t, dir} 36 | } 37 | 38 | func (c *Ctx) Cleanup() { 39 | os.RemoveAll(c.BaseDir) 40 | } 41 | 42 | type File struct { 43 | *Ctx 44 | Path string 45 | } 46 | 47 | func (c *Ctx) File(name string) *File { 48 | return &File{c, name} 49 | } 50 | 51 | func (c *Ctx) Mkpath(name string) string { 52 | return filepath.Join(c.BaseDir, name) 53 | } 54 | 55 | func (f *File) FullPath() string { 56 | return f.Mkpath(f.Path) 57 | } 58 | 59 | func (f *File) Mkdir() { 60 | fullpath := f.FullPath() 61 | dir := filepath.Dir(fullpath) 62 | 63 | _, err := os.Stat(dir) 64 | if err != nil { // non-existent 65 | err = os.MkdirAll(dir, 0777) 66 | if err != nil { 67 | f.Fatalf("error: Mkdir %s failed: %s", dir, err) 68 | } 69 | } 70 | } 71 | 72 | func (f *File) WriteString(body string) { 73 | f.Mkdir() 74 | 75 | fullpath := f.FullPath() 76 | fh, err := os.OpenFile(fullpath, os.O_CREATE|os.O_WRONLY, 0666) 77 | if err != nil { 78 | f.Fatalf("error: Failed to open file %s for writing: %s", fullpath, err) 79 | } 80 | defer fh.Close() 81 | 82 | _, err = fh.WriteString(body) 83 | if err != nil { 84 | f.Fatalf("error: Failed to write to file %s: %s", fullpath, err) 85 | } 86 | } 87 | 88 | func (f *File) Read() []byte { 89 | fh, err := os.Open(f.FullPath()) 90 | if err != nil { 91 | f.Fatalf("error: Failed to open file %s for reading: %s", f.FullPath(), err) 92 | } 93 | defer fh.Close() 94 | 95 | buf, err := ioutil.ReadAll(fh) 96 | if err != nil { 97 | f.Fatalf("error: Failed to read from file %s: %s", f.FullPath(), err) 98 | } 99 | return buf 100 | } 101 | -------------------------------------------------------------------------------- /tterse_test.go: -------------------------------------------------------------------------------- 1 | package xslate 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestTTerse_SimpleString(t *testing.T) { 13 | c := newTestCtx(t) 14 | defer c.Cleanup() 15 | c.renderStringAndCompare(`Hello, World!`, nil, `Hello, World!`) 16 | c.renderStringAndCompare(` [%- "Hello, World!" %]`, nil, `Hello, World!`) 17 | c.renderStringAndCompare(`[% "Hello, World!" -%] `, nil, `Hello, World!`) 18 | } 19 | 20 | func TestTTerse_SimpleHTMLString(t *testing.T) { 21 | c := newTestCtx(t) 22 | defer c.Cleanup() 23 | c.renderStringAndCompare(`