├── .gitmodules ├── .travis.yml ├── BENCHMARKS.md ├── CHANGELOG.md ├── LICENSE ├── README.md ├── VERSION ├── ast ├── node.go └── print.go ├── base_test.go ├── benchmark_test.go ├── data_frame.go ├── escape.go ├── escape_test.go ├── eval.go ├── eval_test.go ├── handlebars ├── base_test.go ├── basic_test.go ├── blocks_test.go ├── builtins_test.go ├── data_test.go ├── doc.go ├── helpers_test.go ├── partials_test.go ├── subexpressions_test.go └── whitespace_test.go ├── helper.go ├── helper_test.go ├── lexer ├── lexer.go ├── lexer_test.go └── token.go ├── mustache_test.go ├── parser ├── parser.go ├── parser_test.go └── whitespace.go ├── partial.go ├── raymond.go ├── raymond.png ├── raymond_test.go ├── string.go ├── string_test.go ├── template.go ├── template_test.go ├── utils.go └── utils_test.go /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "mustache"] 2 | path = mustache 3 | url = git://github.com/mustache/spec.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | 4 | go: 5 | - 1.3 6 | - 1.4 7 | - 1.5 8 | - 1.6 9 | - 1.7 10 | - tip 11 | -------------------------------------------------------------------------------- /BENCHMARKS.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | Hardware: MacBookPro11,1 - Intel Core i5 - 2,6 GHz - 8 Go RAM 4 | 5 | With: 6 | 7 | - handlebars.js #8cba84df119c317fcebc49fb285518542ca9c2d0 8 | - raymond #7bbaaf50ed03c96b56687d7fa6c6e04e02375a98 9 | 10 | 11 | ## handlebars.js (ops/ms) 12 | 13 | arguments 198 ±4 (5) 14 | array-each 568 ±23 (5) 15 | array-mustache 522 ±18 (4) 16 | complex 71 ±7 (3) 17 | data 67 ±2 (3) 18 | depth-1 47 ±2 (3) 19 | depth-2 14 ±1 (2) 20 | object-mustache 1099 ±47 (5) 21 | object 907 ±58 (4) 22 | partial-recursion 46 ±3 (4) 23 | partial 68 ±3 (3) 24 | paths 1650 ±50 (3) 25 | string 2552 ±157 (3) 26 | subexpression 141 ±2 (4) 27 | variables 2671 ±83 (4) 28 | 29 | 30 | ## raymond 31 | 32 | BenchmarkArguments 200000 6642 ns/op 151 ops/ms 33 | BenchmarkArrayEach 100000 19584 ns/op 51 ops/ms 34 | BenchmarkArrayMustache 100000 17305 ns/op 58 ops/ms 35 | BenchmarkComplex 30000 50270 ns/op 20 ops/ms 36 | BenchmarkData 50000 25551 ns/op 39 ops/ms 37 | BenchmarkDepth1 100000 20162 ns/op 50 ops/ms 38 | BenchmarkDepth2 30000 47782 ns/op 21 ops/ms 39 | BenchmarkObjectMustache 200000 7668 ns/op 130 ops/ms 40 | BenchmarkObject 200000 8843 ns/op 113 ops/ms 41 | BenchmarkPartialRecursion 50000 23139 ns/op 43 ops/ms 42 | BenchmarkPartial 50000 31015 ns/op 32 ops/ms 43 | BenchmarkPath 200000 8997 ns/op 111 ops/ms 44 | BenchmarkString 1000000 1879 ns/op 532 ops/ms 45 | BenchmarkSubExpression 300000 4935 ns/op 203 ops/ms 46 | BenchmarkVariables 200000 6478 ns/op 154 ops/ms 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Raymond Changelog 2 | 3 | ### HEAD 4 | 5 | - [IMPROVEMENT] Add `RemoveHelper` and `RemoveAllHelpers` functions 6 | 7 | ### Raymond 2.0.2 _(March 22, 2018)_ 8 | 9 | - [IMPROVEMENT] Add the #equal helper (#7) 10 | - [IMPROVEMENT] Add struct tag template variable support (#8) 11 | 12 | ### Raymond 2.0.1 _(June 01, 2016)_ 13 | 14 | - [BUGFIX] Removes data races [#3](https://github.com/aymerick/raymond/issues/3) - Thanks [@markbates](https://github.com/markbates) 15 | 16 | ### Raymond 2.0.0 _(May 01, 2016)_ 17 | 18 | - [BUGFIX] Fixes passing of context in helper options [#2](https://github.com/aymerick/raymond/issues/2) - Thanks [@GhostRussia](https://github.com/GhostRussia) 19 | - [BREAKING] Renames and unexports constants: 20 | 21 | - `handlebars.DUMP_TPL` 22 | - `lexer.ESCAPED_ESCAPED_OPEN_MUSTACHE` 23 | - `lexer.ESCAPED_OPEN_MUSTACHE` 24 | - `lexer.OPEN_MUSTACHE` 25 | - `lexer.CLOSE_MUSTACHE` 26 | - `lexer.CLOSE_STRIP_MUSTACHE` 27 | - `lexer.CLOSE_UNESCAPED_STRIP_MUSTACHE` 28 | - `lexer.DUMP_TOKEN_POS` 29 | - `lexer.DUMP_ALL_TOKENS_VAL` 30 | 31 | 32 | ### Raymond 1.1.0 _(June 15, 2015)_ 33 | 34 | - Permits templates references with lowercase versions of struct fields. 35 | - Adds `ParseFile()` function. 36 | - Adds `RegisterPartialFile()`, `RegisterPartialFiles()` and `Clone()` methods on `Template`. 37 | - Helpers can now be struct methods. 38 | - Ensures safe concurrent access to helpers and partials. 39 | 40 | ### Raymond 1.0.0 _(June 09, 2015)_ 41 | 42 | - This is the first release. Raymond supports almost all handlebars features. See https://github.com/aymerick/raymond#limitations for a list of differences with the javascript implementation. 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Aymerick JEHANNE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.0.2 2 | -------------------------------------------------------------------------------- /ast/print.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // printVisitor implements the Visitor interface to print a AST. 9 | type printVisitor struct { 10 | buf string 11 | depth int 12 | 13 | original bool 14 | inBlock bool 15 | } 16 | 17 | func newPrintVisitor() *printVisitor { 18 | return &printVisitor{} 19 | } 20 | 21 | // Print returns a string representation of given AST, that can be used for debugging purpose. 22 | func Print(node Node) string { 23 | visitor := newPrintVisitor() 24 | node.Accept(visitor) 25 | return visitor.output() 26 | } 27 | 28 | func (v *printVisitor) output() string { 29 | return v.buf 30 | } 31 | 32 | func (v *printVisitor) indent() { 33 | for i := 0; i < v.depth; { 34 | v.buf += " " 35 | i++ 36 | } 37 | } 38 | 39 | func (v *printVisitor) str(val string) { 40 | v.buf += val 41 | } 42 | 43 | func (v *printVisitor) nl() { 44 | v.str("\n") 45 | } 46 | 47 | func (v *printVisitor) line(val string) { 48 | v.indent() 49 | v.str(val) 50 | v.nl() 51 | } 52 | 53 | // 54 | // Visitor interface 55 | // 56 | 57 | // Statements 58 | 59 | // VisitProgram implements corresponding Visitor interface method 60 | func (v *printVisitor) VisitProgram(node *Program) interface{} { 61 | if len(node.BlockParams) > 0 { 62 | v.line("BLOCK PARAMS: [ " + strings.Join(node.BlockParams, " ") + " ]") 63 | } 64 | 65 | for _, n := range node.Body { 66 | n.Accept(v) 67 | } 68 | 69 | return nil 70 | } 71 | 72 | // VisitMustache implements corresponding Visitor interface method 73 | func (v *printVisitor) VisitMustache(node *MustacheStatement) interface{} { 74 | v.indent() 75 | v.str("{{ ") 76 | 77 | node.Expression.Accept(v) 78 | 79 | v.str(" }}") 80 | v.nl() 81 | 82 | return nil 83 | } 84 | 85 | // VisitBlock implements corresponding Visitor interface method 86 | func (v *printVisitor) VisitBlock(node *BlockStatement) interface{} { 87 | v.inBlock = true 88 | 89 | v.line("BLOCK:") 90 | v.depth++ 91 | 92 | node.Expression.Accept(v) 93 | 94 | if node.Program != nil { 95 | v.line("PROGRAM:") 96 | v.depth++ 97 | node.Program.Accept(v) 98 | v.depth-- 99 | } 100 | 101 | if node.Inverse != nil { 102 | // if node.Program != nil { 103 | // v.depth++ 104 | // } 105 | 106 | v.line("{{^}}") 107 | v.depth++ 108 | node.Inverse.Accept(v) 109 | v.depth-- 110 | 111 | // if node.Program != nil { 112 | // v.depth-- 113 | // } 114 | } 115 | 116 | v.inBlock = false 117 | 118 | return nil 119 | } 120 | 121 | // VisitPartial implements corresponding Visitor interface method 122 | func (v *printVisitor) VisitPartial(node *PartialStatement) interface{} { 123 | v.indent() 124 | v.str("{{> PARTIAL:") 125 | 126 | v.original = true 127 | node.Name.Accept(v) 128 | v.original = false 129 | 130 | if len(node.Params) > 0 { 131 | v.str(" ") 132 | node.Params[0].Accept(v) 133 | } 134 | 135 | // hash 136 | if node.Hash != nil { 137 | v.str(" ") 138 | node.Hash.Accept(v) 139 | } 140 | 141 | v.str(" }}") 142 | v.nl() 143 | 144 | return nil 145 | } 146 | 147 | // VisitContent implements corresponding Visitor interface method 148 | func (v *printVisitor) VisitContent(node *ContentStatement) interface{} { 149 | v.line("CONTENT[ '" + node.Value + "' ]") 150 | 151 | return nil 152 | } 153 | 154 | // VisitComment implements corresponding Visitor interface method 155 | func (v *printVisitor) VisitComment(node *CommentStatement) interface{} { 156 | v.line("{{! '" + node.Value + "' }}") 157 | 158 | return nil 159 | } 160 | 161 | // Expressions 162 | 163 | // VisitExpression implements corresponding Visitor interface method 164 | func (v *printVisitor) VisitExpression(node *Expression) interface{} { 165 | if v.inBlock { 166 | v.indent() 167 | } 168 | 169 | // path 170 | node.Path.Accept(v) 171 | 172 | // params 173 | v.str(" [") 174 | for i, n := range node.Params { 175 | if i > 0 { 176 | v.str(", ") 177 | } 178 | n.Accept(v) 179 | } 180 | v.str("]") 181 | 182 | // hash 183 | if node.Hash != nil { 184 | v.str(" ") 185 | node.Hash.Accept(v) 186 | } 187 | 188 | if v.inBlock { 189 | v.nl() 190 | } 191 | 192 | return nil 193 | } 194 | 195 | // VisitSubExpression implements corresponding Visitor interface method 196 | func (v *printVisitor) VisitSubExpression(node *SubExpression) interface{} { 197 | node.Expression.Accept(v) 198 | 199 | return nil 200 | } 201 | 202 | // VisitPath implements corresponding Visitor interface method 203 | func (v *printVisitor) VisitPath(node *PathExpression) interface{} { 204 | if v.original { 205 | v.str(node.Original) 206 | } else { 207 | path := strings.Join(node.Parts, "/") 208 | 209 | result := "" 210 | if node.Data { 211 | result += "@" 212 | } 213 | 214 | v.str(result + "PATH:" + path) 215 | } 216 | 217 | return nil 218 | } 219 | 220 | // Literals 221 | 222 | // VisitString implements corresponding Visitor interface method 223 | func (v *printVisitor) VisitString(node *StringLiteral) interface{} { 224 | if v.original { 225 | v.str(node.Value) 226 | } else { 227 | v.str("\"" + node.Value + "\"") 228 | } 229 | 230 | return nil 231 | } 232 | 233 | // VisitBoolean implements corresponding Visitor interface method 234 | func (v *printVisitor) VisitBoolean(node *BooleanLiteral) interface{} { 235 | if v.original { 236 | v.str(node.Original) 237 | } else { 238 | v.str(fmt.Sprintf("BOOLEAN{%s}", node.Canonical())) 239 | } 240 | 241 | return nil 242 | } 243 | 244 | // VisitNumber implements corresponding Visitor interface method 245 | func (v *printVisitor) VisitNumber(node *NumberLiteral) interface{} { 246 | if v.original { 247 | v.str(node.Original) 248 | } else { 249 | v.str(fmt.Sprintf("NUMBER{%s}", node.Canonical())) 250 | } 251 | 252 | return nil 253 | } 254 | 255 | // Miscellaneous 256 | 257 | // VisitHash implements corresponding Visitor interface method 258 | func (v *printVisitor) VisitHash(node *Hash) interface{} { 259 | v.str("HASH{") 260 | 261 | for i, p := range node.Pairs { 262 | if i > 0 { 263 | v.str(", ") 264 | } 265 | p.Accept(v) 266 | } 267 | 268 | v.str("}") 269 | 270 | return nil 271 | } 272 | 273 | // VisitHashPair implements corresponding Visitor interface method 274 | func (v *printVisitor) VisitHashPair(node *HashPair) interface{} { 275 | v.str(node.Key + "=") 276 | node.Val.Accept(v) 277 | 278 | return nil 279 | } 280 | -------------------------------------------------------------------------------- /base_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | ) 8 | 9 | type Test struct { 10 | name string 11 | input string 12 | data interface{} 13 | privData map[string]interface{} 14 | helpers map[string]interface{} 15 | partials map[string]string 16 | output interface{} 17 | } 18 | 19 | func launchTests(t *testing.T, tests []Test) { 20 | // NOTE: TestMustache() makes Parallel testing fail 21 | // t.Parallel() 22 | 23 | for _, test := range tests { 24 | var err error 25 | var tpl *Template 26 | 27 | // parse template 28 | tpl, err = Parse(test.input) 29 | if err != nil { 30 | t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err) 31 | } else { 32 | if len(test.helpers) > 0 { 33 | // register helpers 34 | tpl.RegisterHelpers(test.helpers) 35 | } 36 | 37 | if len(test.partials) > 0 { 38 | // register partials 39 | tpl.RegisterPartials(test.partials) 40 | } 41 | 42 | // setup private data frame 43 | var privData *DataFrame 44 | if test.privData != nil { 45 | privData = NewDataFrame() 46 | for k, v := range test.privData { 47 | privData.Set(k, v) 48 | } 49 | } 50 | 51 | // render template 52 | output, err := tpl.ExecWith(test.data, privData) 53 | if err != nil { 54 | t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\nerror:\n\t%s\nAST:\n\t%s", test.name, test.input, Str(test.data), err, tpl.PrintAST()) 55 | } else { 56 | // check output 57 | var expectedArr []string 58 | expectedArr, ok := test.output.([]string) 59 | if ok { 60 | match := false 61 | for _, expectedStr := range expectedArr { 62 | if expectedStr == output { 63 | match = true 64 | break 65 | } 66 | } 67 | 68 | if !match { 69 | t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, Str(test.data), Str(test.partials), expectedArr, output, tpl.PrintAST()) 70 | } 71 | } else { 72 | expectedStr, ok := test.output.(string) 73 | if !ok { 74 | panic(fmt.Errorf("Erroneous test output description: %q", test.output)) 75 | } 76 | 77 | if expectedStr != output { 78 | t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, Str(test.data), Str(test.partials), expectedStr, output, tpl.PrintAST()) 79 | } 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | func launchErrorTests(t *testing.T, tests []Test) { 87 | t.Parallel() 88 | 89 | for _, test := range tests { 90 | var err error 91 | var tpl *Template 92 | 93 | // parse template 94 | tpl, err = Parse(test.input) 95 | if err != nil { 96 | t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err) 97 | } else { 98 | if len(test.helpers) > 0 { 99 | // register helpers 100 | tpl.RegisterHelpers(test.helpers) 101 | } 102 | 103 | if len(test.partials) > 0 { 104 | // register partials 105 | tpl.RegisterPartials(test.partials) 106 | } 107 | 108 | // setup private data frame 109 | var privData *DataFrame 110 | if test.privData != nil { 111 | privData := NewDataFrame() 112 | for k, v := range test.privData { 113 | privData.Set(k, v) 114 | } 115 | } 116 | 117 | // render template 118 | output, err := tpl.ExecWith(test.data, privData) 119 | if err == nil { 120 | t.Errorf("Test '%s' failed - Error expected\ninput:\n\t'%s'\ngot\n\t%q\nAST:\n%q", test.name, test.input, output, tpl.PrintAST()) 121 | } else { 122 | var errMatch error 123 | match := false 124 | 125 | // check output 126 | var expectedArr []string 127 | expectedArr, ok := test.output.([]string) 128 | if ok { 129 | if len(expectedArr) > 0 { 130 | for _, expectedStr := range expectedArr { 131 | match, errMatch = regexp.MatchString(regexp.QuoteMeta(expectedStr), fmt.Sprint(err)) 132 | if errMatch != nil { 133 | panic("Failed to match regexp") 134 | } 135 | 136 | if match { 137 | break 138 | } 139 | } 140 | } else { 141 | // nothing to test 142 | match = true 143 | } 144 | } else { 145 | expectedStr, ok := test.output.(string) 146 | if !ok { 147 | panic(fmt.Errorf("Erroneous test output description: %q", test.output)) 148 | } 149 | 150 | if expectedStr != "" { 151 | match, errMatch = regexp.MatchString(regexp.QuoteMeta(expectedStr), fmt.Sprint(err)) 152 | if errMatch != nil { 153 | panic("Failed to match regexp") 154 | } 155 | } else { 156 | // nothing to test 157 | match = true 158 | } 159 | } 160 | 161 | if !match { 162 | t.Errorf("Test '%s' failed - Incorrect error returned\ninput:\n\t'%s'\ndata:\n\t%s\nexpected\n\t%q\ngot\n\t%q", test.name, test.input, Str(test.data), test.output, err) 163 | } 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import "testing" 4 | 5 | // 6 | // Those tests come from: 7 | // https://github.com/wycats/handlebars.js/blob/master/bench/ 8 | // 9 | // Note that handlebars.js does NOT benchmark template compilation, it only benchmarks evaluation. 10 | // 11 | 12 | func BenchmarkArguments(b *testing.B) { 13 | source := `{{foo person "person" 1 true foo=bar foo="person" foo=1 foo=true}}` 14 | 15 | ctx := map[string]bool{ 16 | "bar": true, 17 | } 18 | 19 | tpl := MustParse(source) 20 | tpl.RegisterHelper("foo", func(a, b, c, d interface{}) string { return "" }) 21 | 22 | b.ResetTimer() 23 | for i := 0; i < b.N; i++ { 24 | tpl.MustExec(ctx) 25 | } 26 | } 27 | 28 | func BenchmarkArrayEach(b *testing.B) { 29 | source := `{{#each names}}{{name}}{{/each}}` 30 | 31 | ctx := map[string][]map[string]string{ 32 | "names": { 33 | {"name": "Moe"}, 34 | {"name": "Larry"}, 35 | {"name": "Curly"}, 36 | {"name": "Shemp"}, 37 | }, 38 | } 39 | 40 | tpl := MustParse(source) 41 | 42 | b.ResetTimer() 43 | for i := 0; i < b.N; i++ { 44 | tpl.MustExec(ctx) 45 | } 46 | } 47 | 48 | func BenchmarkArrayMustache(b *testing.B) { 49 | source := `{{#names}}{{name}}{{/names}}` 50 | 51 | ctx := map[string][]map[string]string{ 52 | "names": { 53 | {"name": "Moe"}, 54 | {"name": "Larry"}, 55 | {"name": "Curly"}, 56 | {"name": "Shemp"}, 57 | }, 58 | } 59 | 60 | tpl := MustParse(source) 61 | 62 | b.ResetTimer() 63 | for i := 0; i < b.N; i++ { 64 | tpl.MustExec(ctx) 65 | } 66 | } 67 | 68 | func BenchmarkComplex(b *testing.B) { 69 | source := `

{{header}}

70 | {{#if items}} 71 | 80 | {{^}} 81 |

The list is empty.

82 | {{/if}} 83 | ` 84 | 85 | ctx := map[string]interface{}{ 86 | "header": func() string { return "Colors" }, 87 | "hasItems": true, 88 | "items": []map[string]interface{}{ 89 | {"name": "red", "current": true, "url": "#Red"}, 90 | {"name": "green", "current": false, "url": "#Green"}, 91 | {"name": "blue", "current": false, "url": "#Blue"}, 92 | }, 93 | } 94 | 95 | tpl := MustParse(source) 96 | 97 | b.ResetTimer() 98 | for i := 0; i < b.N; i++ { 99 | tpl.MustExec(ctx) 100 | } 101 | } 102 | 103 | func BenchmarkData(b *testing.B) { 104 | source := `{{#each names}}{{@index}}{{name}}{{/each}}` 105 | 106 | ctx := map[string][]map[string]string{ 107 | "names": { 108 | {"name": "Moe"}, 109 | {"name": "Larry"}, 110 | {"name": "Curly"}, 111 | {"name": "Shemp"}, 112 | }, 113 | } 114 | 115 | tpl := MustParse(source) 116 | 117 | b.ResetTimer() 118 | for i := 0; i < b.N; i++ { 119 | tpl.MustExec(ctx) 120 | } 121 | } 122 | 123 | func BenchmarkDepth1(b *testing.B) { 124 | source := `{{#each names}}{{../foo}}{{/each}}` 125 | 126 | ctx := map[string]interface{}{ 127 | "names": []map[string]string{ 128 | {"name": "Moe"}, 129 | {"name": "Larry"}, 130 | {"name": "Curly"}, 131 | {"name": "Shemp"}, 132 | }, 133 | "foo": "bar", 134 | } 135 | 136 | tpl := MustParse(source) 137 | 138 | b.ResetTimer() 139 | for i := 0; i < b.N; i++ { 140 | tpl.MustExec(ctx) 141 | } 142 | } 143 | 144 | func BenchmarkDepth2(b *testing.B) { 145 | source := `{{#each names}}{{#each name}}{{../bat}}{{../../foo}}{{/each}}{{/each}}` 146 | 147 | ctx := map[string]interface{}{ 148 | "names": []map[string]interface{}{ 149 | {"bat": "foo", "name": []string{"Moe"}}, 150 | {"bat": "foo", "name": []string{"Larry"}}, 151 | {"bat": "foo", "name": []string{"Curly"}}, 152 | {"bat": "foo", "name": []string{"Shemp"}}, 153 | }, 154 | "foo": "bar", 155 | } 156 | 157 | tpl := MustParse(source) 158 | 159 | b.ResetTimer() 160 | for i := 0; i < b.N; i++ { 161 | tpl.MustExec(ctx) 162 | } 163 | } 164 | 165 | func BenchmarkObjectMustache(b *testing.B) { 166 | source := `{{#person}}{{name}}{{age}}{{/person}}` 167 | 168 | ctx := map[string]interface{}{ 169 | "person": map[string]interface{}{ 170 | "name": "Larry", 171 | "age": 45, 172 | }, 173 | } 174 | 175 | tpl := MustParse(source) 176 | 177 | b.ResetTimer() 178 | for i := 0; i < b.N; i++ { 179 | tpl.MustExec(ctx) 180 | } 181 | } 182 | 183 | func BenchmarkObject(b *testing.B) { 184 | source := `{{#with person}}{{name}}{{age}}{{/with}}` 185 | 186 | ctx := map[string]interface{}{ 187 | "person": map[string]interface{}{ 188 | "name": "Larry", 189 | "age": 45, 190 | }, 191 | } 192 | 193 | tpl := MustParse(source) 194 | 195 | b.ResetTimer() 196 | for i := 0; i < b.N; i++ { 197 | tpl.MustExec(ctx) 198 | } 199 | } 200 | 201 | func BenchmarkPartialRecursion(b *testing.B) { 202 | source := `{{name}}{{#each kids}}{{>recursion}}{{/each}}` 203 | 204 | ctx := map[string]interface{}{ 205 | "name": 1, 206 | "kids": []map[string]interface{}{ 207 | { 208 | "name": "1.1", 209 | "kids": []map[string]interface{}{ 210 | { 211 | "name": "1.1.1", 212 | "kids": []map[string]interface{}{}, 213 | }, 214 | }, 215 | }, 216 | }, 217 | } 218 | 219 | tpl := MustParse(source) 220 | 221 | partial := MustParse(`{{name}}{{#each kids}}{{>recursion}}{{/each}}`) 222 | tpl.RegisterPartialTemplate("recursion", partial) 223 | 224 | b.ResetTimer() 225 | for i := 0; i < b.N; i++ { 226 | tpl.MustExec(ctx) 227 | } 228 | } 229 | 230 | func BenchmarkPartial(b *testing.B) { 231 | source := `{{#each peeps}}{{>variables}}{{/each}}` 232 | 233 | ctx := map[string]interface{}{ 234 | "peeps": []map[string]interface{}{ 235 | {"name": "Moe", "count": 15}, 236 | {"name": "Moe", "count": 5}, 237 | {"name": "Curly", "count": 1}, 238 | }, 239 | } 240 | 241 | tpl := MustParse(source) 242 | 243 | partial := MustParse(`Hello {{name}}! You have {{count}} new messages.`) 244 | tpl.RegisterPartialTemplate("variables", partial) 245 | 246 | b.ResetTimer() 247 | for i := 0; i < b.N; i++ { 248 | tpl.MustExec(ctx) 249 | } 250 | } 251 | 252 | func BenchmarkPath(b *testing.B) { 253 | source := `{{person.name.bar.baz}}{{person.age}}{{person.foo}}{{animal.age}}` 254 | 255 | ctx := map[string]interface{}{ 256 | "person": map[string]interface{}{ 257 | "name": map[string]interface{}{ 258 | "bar": map[string]string{ 259 | "baz": "Larry", 260 | }, 261 | }, 262 | "age": 45, 263 | }, 264 | } 265 | 266 | tpl := MustParse(source) 267 | 268 | b.ResetTimer() 269 | for i := 0; i < b.N; i++ { 270 | tpl.MustExec(ctx) 271 | } 272 | } 273 | 274 | func BenchmarkString(b *testing.B) { 275 | source := `Hello world` 276 | 277 | tpl := MustParse(source) 278 | 279 | b.ResetTimer() 280 | for i := 0; i < b.N; i++ { 281 | tpl.MustExec(nil) 282 | } 283 | } 284 | 285 | func BenchmarkSubExpression(b *testing.B) { 286 | source := `{{echo (header)}}` 287 | 288 | ctx := map[string]interface{}{} 289 | 290 | tpl := MustParse(source) 291 | tpl.RegisterHelpers(map[string]interface{}{ 292 | "echo": func(v string) string { return "foo " + v }, 293 | "header": func() string { return "Colors" }, 294 | }) 295 | 296 | b.ResetTimer() 297 | for i := 0; i < b.N; i++ { 298 | tpl.MustExec(ctx) 299 | } 300 | } 301 | 302 | func BenchmarkVariables(b *testing.B) { 303 | source := `Hello {{name}}! You have {{count}} new messages.` 304 | 305 | ctx := map[string]interface{}{ 306 | "name": "Mick", 307 | "count": 30, 308 | } 309 | 310 | tpl := MustParse(source) 311 | 312 | b.ResetTimer() 313 | for i := 0; i < b.N; i++ { 314 | tpl.MustExec(ctx) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /data_frame.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import "reflect" 4 | 5 | // DataFrame represents a private data frame. 6 | // 7 | // Cf. private variables documentation at: http://handlebarsjs.com/block_helpers.html 8 | type DataFrame struct { 9 | parent *DataFrame 10 | data map[string]interface{} 11 | } 12 | 13 | // NewDataFrame instanciates a new private data frame. 14 | func NewDataFrame() *DataFrame { 15 | return &DataFrame{ 16 | data: make(map[string]interface{}), 17 | } 18 | } 19 | 20 | // Copy instanciates a new private data frame with receiver as parent. 21 | func (p *DataFrame) Copy() *DataFrame { 22 | result := NewDataFrame() 23 | 24 | for k, v := range p.data { 25 | result.data[k] = v 26 | } 27 | 28 | result.parent = p 29 | 30 | return result 31 | } 32 | 33 | // newIterDataFrame instanciates a new private data frame with receiver as parent and with iteration data set (@index, @key, @first, @last) 34 | func (p *DataFrame) newIterDataFrame(length int, i int, key interface{}) *DataFrame { 35 | result := p.Copy() 36 | 37 | result.Set("index", i) 38 | result.Set("key", key) 39 | result.Set("first", i == 0) 40 | result.Set("last", i == length-1) 41 | 42 | return result 43 | } 44 | 45 | // Set sets a data value. 46 | func (p *DataFrame) Set(key string, val interface{}) { 47 | p.data[key] = val 48 | } 49 | 50 | // Get gets a data value. 51 | func (p *DataFrame) Get(key string) interface{} { 52 | return p.find([]string{key}) 53 | } 54 | 55 | // find gets a deep data value 56 | // 57 | // @todo This is NOT consistent with the way we resolve data in template (cf. `evalDataPathExpression()`) ! FIX THAT ! 58 | func (p *DataFrame) find(parts []string) interface{} { 59 | data := p.data 60 | 61 | for i, part := range parts { 62 | val := data[part] 63 | if val == nil { 64 | return nil 65 | } 66 | 67 | if i == len(parts)-1 { 68 | // found 69 | return val 70 | } 71 | 72 | valValue := reflect.ValueOf(val) 73 | if valValue.Kind() != reflect.Map { 74 | // not found 75 | return nil 76 | } 77 | 78 | // continue 79 | data = mapStringInterface(valValue) 80 | } 81 | 82 | // not found 83 | return nil 84 | } 85 | 86 | // mapStringInterface converts any `map` to `map[string]interface{}` 87 | func mapStringInterface(value reflect.Value) map[string]interface{} { 88 | result := make(map[string]interface{}) 89 | 90 | for _, key := range value.MapKeys() { 91 | result[strValue(key)] = value.MapIndex(key).Interface() 92 | } 93 | 94 | return result 95 | } 96 | -------------------------------------------------------------------------------- /escape.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | // 9 | // That whole file is borrowed from https://github.com/golang/go/tree/master/src/html/escape.go 10 | // 11 | // With changes: 12 | // ' => ' 13 | // " => " 14 | // 15 | // To stay in sync with JS implementation, and make mustache tests pass. 16 | // 17 | 18 | type writer interface { 19 | WriteString(string) (int, error) 20 | } 21 | 22 | const escapedChars = `&'<>"` 23 | 24 | func escape(w writer, s string) error { 25 | i := strings.IndexAny(s, escapedChars) 26 | for i != -1 { 27 | if _, err := w.WriteString(s[:i]); err != nil { 28 | return err 29 | } 30 | var esc string 31 | switch s[i] { 32 | case '&': 33 | esc = "&" 34 | case '\'': 35 | esc = "'" 36 | case '<': 37 | esc = "<" 38 | case '>': 39 | esc = ">" 40 | case '"': 41 | esc = """ 42 | default: 43 | panic("unrecognized escape character") 44 | } 45 | s = s[i+1:] 46 | if _, err := w.WriteString(esc); err != nil { 47 | return err 48 | } 49 | i = strings.IndexAny(s, escapedChars) 50 | } 51 | _, err := w.WriteString(s) 52 | return err 53 | } 54 | 55 | // Escape escapes special HTML characters. 56 | // 57 | // It can be used by helpers that return a SafeString and that need to escape some content by themselves. 58 | func Escape(s string) string { 59 | if strings.IndexAny(s, escapedChars) == -1 { 60 | return s 61 | } 62 | var buf bytes.Buffer 63 | escape(&buf, s) 64 | return buf.String() 65 | } 66 | -------------------------------------------------------------------------------- /escape_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import "fmt" 4 | 5 | func ExampleEscape() { 6 | tpl := MustParse("{{link url text}}") 7 | 8 | tpl.RegisterHelper("link", func(url string, text string) SafeString { 9 | return SafeString("" + Escape(text) + "") 10 | }) 11 | 12 | ctx := map[string]string{ 13 | "url": "http://www.aymerick.com/", 14 | "text": "This is a cool website", 15 | } 16 | 17 | result := tpl.MustExec(ctx) 18 | fmt.Print(result) 19 | // Output: This is a <em>cool</em> website 20 | } 21 | -------------------------------------------------------------------------------- /eval_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import "testing" 4 | 5 | var evalTests = []Test{ 6 | { 7 | "only content", 8 | "this is content", 9 | nil, nil, nil, nil, 10 | "this is content", 11 | }, 12 | { 13 | "checks path in parent contexts", 14 | "{{#a}}{{one}}{{#b}}{{one}}{{two}}{{one}}{{/b}}{{/a}}", 15 | map[string]interface{}{"a": map[string]int{"one": 1}, "b": map[string]int{"two": 2}}, 16 | nil, nil, nil, 17 | "1121", 18 | }, 19 | { 20 | "block params", 21 | "{{#foo as |bar|}}{{bar}}{{/foo}}{{bar}}", 22 | map[string]string{"foo": "baz", "bar": "bat"}, 23 | nil, nil, nil, 24 | "bazbat", 25 | }, 26 | { 27 | "block params on array", 28 | "{{#foo as |bar i|}}{{i}}.{{bar}} {{/foo}}", 29 | map[string][]string{"foo": {"baz", "bar", "bat"}}, 30 | nil, nil, nil, 31 | "0.baz 1.bar 2.bat ", 32 | }, 33 | { 34 | "nested block params", 35 | "{{#foos as |foo iFoo|}}{{#wats as |wat iWat|}}{{iFoo}}.{{iWat}}.{{foo}}-{{wat}} {{/wats}}{{/foos}}", 36 | map[string][]string{"foos": {"baz", "bar"}, "wats": {"the", "phoque"}}, 37 | nil, nil, nil, 38 | "0.0.baz-the 0.1.baz-phoque 1.0.bar-the 1.1.bar-phoque ", 39 | }, 40 | { 41 | "block params with path reference", 42 | "{{#foo as |bar|}}{{bar.baz}}{{/foo}}", 43 | map[string]map[string]string{"foo": {"baz": "bat"}}, 44 | nil, nil, nil, 45 | "bat", 46 | }, 47 | { 48 | "falsy block evaluation", 49 | "{{#foo}}bar{{/foo}} baz", 50 | map[string]interface{}{"foo": false}, 51 | nil, nil, nil, 52 | " baz", 53 | }, 54 | { 55 | "block helper returns a SafeString", 56 | "{{title}} - {{#bold}}{{body}}{{/bold}}", 57 | map[string]string{ 58 | "title": "My new blog post", 59 | "body": "I have so many things to say!", 60 | }, 61 | nil, 62 | map[string]interface{}{"bold": func(options *Options) SafeString { 63 | return SafeString(`
` + options.Fn() + "
") 64 | }}, 65 | nil, 66 | `My new blog post -
I have so many things to say!
`, 67 | }, 68 | { 69 | "chained blocks", 70 | "{{#if a}}A{{else if b}}B{{else}}C{{/if}}", 71 | map[string]interface{}{"b": false}, 72 | nil, nil, nil, 73 | "C", 74 | }, 75 | 76 | // @todo Test with a "../../path" (depth 2 path) while context is only depth 1 77 | } 78 | 79 | func TestEval(t *testing.T) { 80 | t.Parallel() 81 | 82 | launchTests(t, evalTests) 83 | } 84 | 85 | var evalErrors = []Test{ 86 | { 87 | "functions with wrong number of arguments", 88 | `{{foo "bar"}}`, 89 | map[string]interface{}{"foo": func(a string, b string) string { return "foo" }}, 90 | nil, nil, nil, 91 | "Helper 'foo' called with wrong number of arguments, needed 2 but got 1", 92 | }, 93 | { 94 | "functions with wrong number of returned values (1)", 95 | "{{foo}}", 96 | map[string]interface{}{"foo": func() {}}, 97 | nil, nil, nil, 98 | "Helper function must return a string or a SafeString", 99 | }, 100 | { 101 | "functions with wrong number of returned values (2)", 102 | "{{foo}}", 103 | map[string]interface{}{"foo": func() (string, bool, string) { return "foo", true, "bar" }}, 104 | nil, nil, nil, 105 | "Helper function must return a string or a SafeString", 106 | }, 107 | } 108 | 109 | func TestEvalErrors(t *testing.T) { 110 | launchErrorTests(t, evalErrors) 111 | } 112 | 113 | func TestEvalStruct(t *testing.T) { 114 | t.Parallel() 115 | 116 | source := `
117 |

By {{author.FirstName}} {{Author.lastName}}

118 |
{{Body}}
119 | 120 |

Comments

121 | 122 | {{#each comments}} 123 |

By {{Author.FirstName}} {{author.LastName}}

124 |
{{body}}
125 | {{/each}} 126 |
` 127 | 128 | expected := `
129 |

By Jean Valjean

130 |
Life is difficult
131 | 132 |

Comments

133 | 134 |

By Marcel Beliveau

135 |
LOL!
136 |
` 137 | 138 | type Person struct { 139 | FirstName string 140 | LastName string 141 | } 142 | 143 | type Comment struct { 144 | Author Person 145 | Body string 146 | } 147 | 148 | type Post struct { 149 | Author Person 150 | Body string 151 | Comments []Comment 152 | } 153 | 154 | ctx := Post{ 155 | Person{"Jean", "Valjean"}, 156 | "Life is difficult", 157 | []Comment{ 158 | Comment{ 159 | Person{"Marcel", "Beliveau"}, 160 | "LOL!", 161 | }, 162 | }, 163 | } 164 | 165 | output := MustRender(source, ctx) 166 | if output != expected { 167 | t.Errorf("Failed to evaluate with struct context") 168 | } 169 | } 170 | 171 | func TestEvalStructTag(t *testing.T) { 172 | t.Parallel() 173 | 174 | source := `
175 |

{{real-name}}

176 | 181 | {{#each other-names}} 182 |

{{alias-name}}

183 | {{/each}} 184 |
` 185 | 186 | expected := `
187 |

Lebowski

188 | 193 |

his dudeness

194 |

el duderino

195 |
` 196 | 197 | type Alias struct { 198 | Name string `handlebars:"alias-name"` 199 | } 200 | 201 | type CharacterInfo struct { 202 | City string `handlebars:"location"` 203 | Rug string `handlebars:"r.u.g"` 204 | Activity string `handlebars:"not-activity"` 205 | } 206 | 207 | type Character struct { 208 | RealName string `handlebars:"real-name"` 209 | Info CharacterInfo 210 | Aliases []Alias `handlebars:"other-names"` 211 | } 212 | 213 | ctx := Character{ 214 | "Lebowski", 215 | CharacterInfo{"Venice", "Tied The Room Together", "Bowling"}, 216 | []Alias{ 217 | {"his dudeness"}, 218 | {"el duderino"}, 219 | }, 220 | } 221 | 222 | output := MustRender(source, ctx) 223 | if output != expected { 224 | t.Errorf("Failed to evaluate with struct tag context") 225 | } 226 | } 227 | 228 | type TestFoo struct { 229 | } 230 | 231 | func (t *TestFoo) Subject() string { 232 | return "foo" 233 | } 234 | 235 | func TestEvalMethod(t *testing.T) { 236 | t.Parallel() 237 | 238 | source := `Subject is {{subject}}! YES I SAID {{Subject}}!` 239 | expected := `Subject is foo! YES I SAID foo!` 240 | 241 | ctx := &TestFoo{} 242 | 243 | output := MustRender(source, ctx) 244 | if output != expected { 245 | t.Errorf("Failed to evaluate struct method: %s", output) 246 | } 247 | } 248 | 249 | type TestBar struct { 250 | } 251 | 252 | func (t *TestBar) Subject() interface{} { 253 | return testBar 254 | } 255 | 256 | func testBar() string { 257 | return "bar" 258 | } 259 | 260 | func TestEvalMethodReturningFunc(t *testing.T) { 261 | t.Parallel() 262 | 263 | source := `Subject is {{subject}}! YES I SAID {{Subject}}!` 264 | expected := `Subject is bar! YES I SAID bar!` 265 | 266 | ctx := &TestBar{} 267 | 268 | output := MustRender(source, ctx) 269 | if output != expected { 270 | t.Errorf("Failed to evaluate struct method: %s", output) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /handlebars/base_test.go: -------------------------------------------------------------------------------- 1 | package handlebars 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "path" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/aymerick/raymond" 11 | ) 12 | 13 | // cf. https://github.com/aymerick/go-fuzz-tests/raymond 14 | const dumpTpl = false 15 | 16 | var dumpTplNb = 0 17 | 18 | type Test struct { 19 | name string 20 | input string 21 | data interface{} 22 | privData map[string]interface{} 23 | helpers map[string]interface{} 24 | partials map[string]string 25 | output interface{} 26 | } 27 | 28 | func launchTests(t *testing.T, tests []Test) { 29 | t.Parallel() 30 | 31 | for _, test := range tests { 32 | var err error 33 | var tpl *raymond.Template 34 | 35 | if dumpTpl { 36 | filename := strconv.Itoa(dumpTplNb) 37 | if err := ioutil.WriteFile(path.Join(".", "dump_tpl", filename), []byte(test.input), 0644); err != nil { 38 | panic(err) 39 | } 40 | dumpTplNb++ 41 | } 42 | 43 | // parse template 44 | tpl, err = raymond.Parse(test.input) 45 | if err != nil { 46 | t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err) 47 | } else { 48 | if len(test.helpers) > 0 { 49 | // register helpers 50 | tpl.RegisterHelpers(test.helpers) 51 | } 52 | 53 | if len(test.partials) > 0 { 54 | // register partials 55 | tpl.RegisterPartials(test.partials) 56 | } 57 | 58 | // setup private data frame 59 | var privData *raymond.DataFrame 60 | if test.privData != nil { 61 | privData = raymond.NewDataFrame() 62 | for k, v := range test.privData { 63 | privData.Set(k, v) 64 | } 65 | } 66 | 67 | // render template 68 | output, err := tpl.ExecWith(test.data, privData) 69 | if err != nil { 70 | t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\nerror:\n\t%s\nAST:\n\t%s", test.name, test.input, raymond.Str(test.data), err, tpl.PrintAST()) 71 | } else { 72 | // check output 73 | var expectedArr []string 74 | expectedArr, ok := test.output.([]string) 75 | if ok { 76 | match := false 77 | for _, expectedStr := range expectedArr { 78 | if expectedStr == output { 79 | match = true 80 | break 81 | } 82 | } 83 | 84 | if !match { 85 | t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, raymond.Str(test.data), raymond.Str(test.partials), expectedArr, output, tpl.PrintAST()) 86 | } 87 | } else { 88 | expectedStr, ok := test.output.(string) 89 | if !ok { 90 | panic(fmt.Errorf("Erroneous test output description: %q", test.output)) 91 | } 92 | 93 | if expectedStr != output { 94 | t.Errorf("Test '%s' failed\ninput:\n\t'%s'\ndata:\n\t%s\npartials:\n\t%s\nexpected\n\t%q\ngot\n\t%q\nAST:\n%s", test.name, test.input, raymond.Str(test.data), raymond.Str(test.partials), expectedStr, output, tpl.PrintAST()) 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /handlebars/basic_test.go: -------------------------------------------------------------------------------- 1 | package handlebars 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/aymerick/raymond" 9 | ) 10 | 11 | // 12 | // Those tests come from: 13 | // https://github.com/wycats/handlebars.js/blob/master/spec/basic.js 14 | // 15 | var basicTests = []Test{ 16 | { 17 | "most basic", 18 | "{{foo}}", 19 | map[string]string{"foo": "foo"}, 20 | nil, nil, nil, 21 | "foo", 22 | }, 23 | { 24 | "escaping (1)", 25 | "\\{{foo}}", 26 | map[string]string{"foo": "food"}, 27 | nil, nil, nil, 28 | "{{foo}}", 29 | }, 30 | { 31 | "escaping (2)", 32 | "content \\{{foo}}", 33 | map[string]string{}, 34 | nil, nil, nil, 35 | "content {{foo}}", 36 | }, 37 | { 38 | "escaping (3)", 39 | "\\\\{{foo}}", 40 | map[string]string{"foo": "food"}, 41 | nil, nil, nil, 42 | "\\food", 43 | }, 44 | { 45 | "escaping (4)", 46 | "content \\\\{{foo}}", 47 | map[string]string{"foo": "food"}, 48 | nil, nil, nil, 49 | "content \\food", 50 | }, 51 | { 52 | "escaping (5)", 53 | "\\\\ {{foo}}", 54 | map[string]string{"foo": "food"}, 55 | nil, nil, nil, 56 | "\\\\ food", 57 | }, 58 | { 59 | "compiling with a basic context", 60 | "Goodbye\n{{cruel}}\n{{world}}!", 61 | map[string]string{"cruel": "cruel", "world": "world"}, 62 | nil, nil, nil, 63 | "Goodbye\ncruel\nworld!", 64 | }, 65 | { 66 | "compiling with an undefined context (1)", 67 | "Goodbye\n{{cruel}}\n{{world.bar}}!", 68 | nil, nil, nil, nil, 69 | "Goodbye\n\n!", 70 | }, 71 | { 72 | "compiling with an undefined context (2)", 73 | "{{#unless foo}}Goodbye{{../test}}{{test2}}{{/unless}}", 74 | nil, nil, nil, nil, 75 | "Goodbye", 76 | }, 77 | { 78 | "comments (1)", 79 | "{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!", 80 | map[string]string{"cruel": "cruel", "world": "world"}, 81 | nil, nil, nil, 82 | "Goodbye\ncruel\nworld!", 83 | }, 84 | { 85 | "comments (2)", 86 | " {{~! comment ~}} blah", 87 | nil, nil, nil, nil, 88 | "blah", 89 | }, 90 | { 91 | "comments (3)", 92 | " {{~!-- long-comment --~}} blah", 93 | nil, nil, nil, nil, 94 | "blah", 95 | }, 96 | { 97 | "comments (4)", 98 | " {{! comment ~}} blah", 99 | nil, nil, nil, nil, 100 | " blah", 101 | }, 102 | { 103 | "comments (5)", 104 | " {{!-- long-comment --~}} blah", 105 | nil, nil, nil, nil, 106 | " blah", 107 | }, 108 | { 109 | "comments (6)", 110 | " {{~! comment}} blah", 111 | nil, nil, nil, nil, 112 | " blah", 113 | }, 114 | { 115 | "comments (7)", 116 | " {{~!-- long-comment --}} blah", 117 | nil, nil, nil, nil, 118 | " blah", 119 | }, 120 | { 121 | "boolean (1)", 122 | "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!", 123 | map[string]interface{}{"goodbye": true, "world": "world"}, 124 | nil, nil, nil, 125 | "GOODBYE cruel world!", 126 | }, 127 | { 128 | "boolean (2)", 129 | "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!", 130 | map[string]interface{}{"goodbye": false, "world": "world"}, 131 | nil, nil, nil, 132 | "cruel world!", 133 | }, 134 | { 135 | "zeros (1)", 136 | "num1: {{num1}}, num2: {{num2}}", 137 | map[string]interface{}{"num1": 42, "num2": 0}, 138 | nil, nil, nil, 139 | "num1: 42, num2: 0", 140 | }, 141 | { 142 | "zeros (2)", 143 | "num: {{.}}", 144 | 0, 145 | nil, nil, nil, 146 | "num: 0", 147 | }, 148 | { 149 | "zeros (3)", 150 | "num: {{num1/num2}}", 151 | map[string]map[string]interface{}{"num1": {"num2": 0}}, 152 | nil, nil, nil, 153 | "num: 0", 154 | }, 155 | { 156 | "false (1)", 157 | "val1: {{val1}}, val2: {{val2}}", 158 | map[string]interface{}{"val1": false, "val2": false}, 159 | nil, nil, nil, 160 | "val1: false, val2: false", 161 | }, 162 | { 163 | "false (2)", 164 | "val: {{.}}", 165 | false, 166 | nil, nil, nil, 167 | "val: false", 168 | }, 169 | { 170 | "false (3)", 171 | "val: {{val1/val2}}", 172 | map[string]map[string]interface{}{"val1": {"val2": false}}, 173 | nil, nil, nil, 174 | "val: false", 175 | }, 176 | { 177 | "false (4)", 178 | "val1: {{{val1}}}, val2: {{{val2}}}", 179 | map[string]interface{}{"val1": false, "val2": false}, 180 | nil, nil, nil, 181 | "val1: false, val2: false", 182 | }, 183 | { 184 | "false (5)", 185 | "val: {{{val1/val2}}}", 186 | map[string]map[string]interface{}{"val1": {"val2": false}}, 187 | nil, nil, nil, 188 | "val: false", 189 | }, 190 | { 191 | "newlines (1)", 192 | "Alan's\nTest", 193 | nil, nil, nil, nil, 194 | "Alan's\nTest", 195 | }, 196 | { 197 | "newlines (2)", 198 | "Alan's\rTest", 199 | nil, nil, nil, nil, 200 | "Alan's\rTest", 201 | }, 202 | { 203 | "escaping text (1)", 204 | "Awesome's", 205 | map[string]string{}, 206 | nil, nil, nil, 207 | "Awesome's", 208 | }, 209 | { 210 | "escaping text (2)", 211 | "Awesome\\", 212 | map[string]string{}, 213 | nil, nil, nil, 214 | "Awesome\\", 215 | }, 216 | { 217 | "escaping text (3)", 218 | "Awesome\\\\ foo", 219 | map[string]string{}, 220 | nil, nil, nil, 221 | "Awesome\\\\ foo", 222 | }, 223 | { 224 | "escaping text (4)", 225 | "Awesome {{foo}}", 226 | map[string]string{"foo": "\\"}, 227 | nil, nil, nil, 228 | "Awesome \\", 229 | }, 230 | { 231 | "escaping text (5)", 232 | " ' ' ", 233 | map[string]string{}, 234 | nil, nil, nil, 235 | " ' ' ", 236 | }, 237 | { 238 | "escaping expressions (6)", 239 | "{{{awesome}}}", 240 | map[string]string{"awesome": "&'\\<>"}, 241 | nil, nil, nil, 242 | "&'\\<>", 243 | }, 244 | { 245 | "escaping expressions (7)", 246 | "{{&awesome}}", 247 | map[string]string{"awesome": "&'\\<>"}, 248 | nil, nil, nil, 249 | "&'\\<>", 250 | }, 251 | { 252 | "escaping expressions (8)", 253 | "{{awesome}}", 254 | map[string]string{"awesome": "&\"'`\\<>"}, 255 | nil, nil, nil, 256 | "&"'`\\<>", 257 | }, 258 | { 259 | "escaping expressions (9)", 260 | "{{awesome}}", 261 | map[string]string{"awesome": "Escaped, looks like: <b>"}, 262 | nil, nil, nil, 263 | "Escaped, <b> looks like: &lt;b&gt;", 264 | }, 265 | { 266 | "functions returning safestrings shouldn't be escaped", 267 | "{{awesome}}", 268 | map[string]interface{}{"awesome": func() raymond.SafeString { return raymond.SafeString("&'\\<>") }}, 269 | nil, nil, nil, 270 | "&'\\<>", 271 | }, 272 | { 273 | "functions (1)", 274 | "{{awesome}}", 275 | map[string]interface{}{"awesome": func() string { return "Awesome" }}, 276 | nil, nil, nil, 277 | "Awesome", 278 | }, 279 | { 280 | "functions (2)", 281 | "{{awesome}}", 282 | map[string]interface{}{"awesome": func(options *raymond.Options) string { 283 | return options.ValueStr("more") 284 | }, "more": "More awesome"}, 285 | nil, nil, nil, 286 | "More awesome", 287 | }, 288 | { 289 | "functions with context argument", 290 | "{{awesome frank}}", 291 | map[string]interface{}{"awesome": func(context string) string { 292 | return context 293 | }, "frank": "Frank"}, 294 | nil, nil, nil, 295 | "Frank", 296 | }, 297 | { 298 | "pathed functions with context argument", 299 | "{{bar.awesome frank}}", 300 | map[string]interface{}{"bar": map[string]interface{}{"awesome": func(context string) string { 301 | return context 302 | }}, "frank": "Frank"}, 303 | nil, nil, nil, 304 | "Frank", 305 | }, 306 | { 307 | "depthed functions with context argument", 308 | "{{#with frank}}{{../awesome .}}{{/with}}", 309 | map[string]interface{}{"awesome": func(context string) string { 310 | return context 311 | }, "frank": "Frank"}, 312 | nil, nil, nil, 313 | "Frank", 314 | }, 315 | { 316 | "block functions with context argument", 317 | "{{#awesome 1}}inner {{.}}{{/awesome}}", 318 | map[string]interface{}{"awesome": func(context interface{}, options *raymond.Options) string { 319 | return options.FnWith(context) 320 | }}, 321 | nil, nil, nil, 322 | "inner 1", 323 | }, 324 | { 325 | "depthed block functions with context argument", 326 | "{{#with value}}{{#../awesome 1}}inner {{.}}{{/../awesome}}{{/with}}", 327 | map[string]interface{}{ 328 | "awesome": func(context interface{}, options *raymond.Options) string { 329 | return options.FnWith(context) 330 | }, 331 | "value": true, 332 | }, 333 | nil, nil, nil, 334 | "inner 1", 335 | }, 336 | { 337 | "block functions without context argument", 338 | "{{#awesome}}inner{{/awesome}}", 339 | map[string]interface{}{ 340 | "awesome": func(options *raymond.Options) string { 341 | return options.Fn() 342 | }, 343 | }, 344 | nil, nil, nil, 345 | "inner", 346 | }, 347 | // // @note I don't even understand why this test passes with the JS implementation... it should be 348 | // // the responsability of the function to evaluate the block 349 | // { 350 | // "pathed block functions without context argument", 351 | // "{{#foo.awesome}}inner{{/foo.awesome}}", 352 | // map[string]map[string]interface{}{ 353 | // "foo": { 354 | // "awesome": func(options *raymond.Options) interface{} { 355 | // return options.Ctx() 356 | // }, 357 | // }, 358 | // }, 359 | // nil, nil, nil, 360 | // "inner", 361 | // }, 362 | // // @note I don't even understand why this test passes with the JS implementation... it should be 363 | // // the responsability of the function to evaluate the block 364 | // { 365 | // "depthed block functions without context argument", 366 | // "{{#with value}}{{#../awesome}}inner{{/../awesome}}{{/with}}", 367 | // map[string]interface{}{ 368 | // "value": true, 369 | // "awesome": func(options *raymond.Options) interface{} { 370 | // return options.Ctx() 371 | // }, 372 | // }, 373 | // nil, nil, nil, 374 | // "inner", 375 | // }, 376 | { 377 | "paths with hyphens (1)", 378 | "{{foo-bar}}", 379 | map[string]string{"foo-bar": "baz"}, 380 | nil, nil, nil, 381 | "baz", 382 | }, 383 | { 384 | "paths with hyphens (2)", 385 | "{{foo.foo-bar}}", 386 | map[string]map[string]string{"foo": {"foo-bar": "baz"}}, 387 | nil, nil, nil, 388 | "baz", 389 | }, 390 | { 391 | "paths with hyphens (3)", 392 | "{{foo/foo-bar}}", 393 | map[string]map[string]string{"foo": {"foo-bar": "baz"}}, 394 | nil, nil, nil, 395 | "baz", 396 | }, 397 | { 398 | "nested paths", 399 | "Goodbye {{alan/expression}} world!", 400 | map[string]map[string]string{"alan": {"expression": "beautiful"}}, 401 | nil, nil, nil, 402 | "Goodbye beautiful world!", 403 | }, 404 | { 405 | "nested paths with empty string value", 406 | "Goodbye {{alan/expression}} world!", 407 | map[string]map[string]string{"alan": {"expression": ""}}, 408 | nil, nil, nil, 409 | "Goodbye world!", 410 | }, 411 | { 412 | "literal paths (1)", 413 | "Goodbye {{[@alan]/expression}} world!", 414 | map[string]map[string]string{"@alan": {"expression": "beautiful"}}, 415 | nil, nil, nil, 416 | "Goodbye beautiful world!", 417 | }, 418 | { 419 | "literal paths (2)", 420 | "Goodbye {{[foo bar]/expression}} world!", 421 | map[string]map[string]string{"foo bar": {"expression": "beautiful"}}, 422 | nil, nil, nil, 423 | "Goodbye beautiful world!", 424 | }, 425 | { 426 | "literal references", 427 | "Goodbye {{[foo bar]}} world!", 428 | map[string]string{"foo bar": "beautiful"}, 429 | nil, nil, nil, 430 | "Goodbye beautiful world!", 431 | }, 432 | // @note MMm ok, well... no... I don't see the purpose of that test 433 | { 434 | "that current context path ({{.}}) doesn't hit helpers", 435 | "test: {{.}}", 436 | nil, nil, 437 | map[string]interface{}{"helper": func() string { 438 | panic("fail") 439 | }}, 440 | nil, 441 | "test: ", 442 | }, 443 | { 444 | "complex but empty paths (1)", 445 | "{{person/name}}", 446 | map[string]map[string]interface{}{"person": {"name": nil}}, 447 | nil, nil, nil, 448 | "", 449 | }, 450 | { 451 | "complex but empty paths (2)", 452 | "{{person/name}}", 453 | map[string]map[string]string{"person": {}}, 454 | nil, nil, nil, 455 | "", 456 | }, 457 | { 458 | "this keyword in paths (1)", 459 | "{{#goodbyes}}{{this}}{{/goodbyes}}", 460 | map[string]interface{}{"goodbyes": []string{"goodbye", "Goodbye", "GOODBYE"}}, 461 | nil, nil, nil, 462 | "goodbyeGoodbyeGOODBYE", 463 | }, 464 | { 465 | "this keyword in paths (2)", 466 | "{{#hellos}}{{this/text}}{{/hellos}}", 467 | map[string]interface{}{"hellos": []interface{}{ 468 | map[string]string{"text": "hello"}, 469 | map[string]string{"text": "Hello"}, 470 | map[string]string{"text": "HELLO"}, 471 | }}, 472 | nil, nil, nil, 473 | "helloHelloHELLO", 474 | }, 475 | { 476 | "this keyword nested inside path' (1)", 477 | "{{[this]}}", 478 | map[string]string{"this": "bar"}, 479 | nil, nil, nil, 480 | "bar", 481 | }, 482 | { 483 | "this keyword nested inside path' (2)", 484 | "{{text/[this]}}", 485 | map[string]map[string]string{"text": {"this": "bar"}}, 486 | nil, nil, nil, 487 | "bar", 488 | }, 489 | { 490 | "this keyword in helpers (1)", 491 | "{{#goodbyes}}{{foo this}}{{/goodbyes}}", 492 | map[string]interface{}{"goodbyes": []string{"goodbye", "Goodbye", "GOODBYE"}}, 493 | nil, 494 | map[string]interface{}{"foo": barSuffixHelper}, 495 | nil, 496 | "bar goodbyebar Goodbyebar GOODBYE", 497 | }, 498 | { 499 | "this keyword in helpers (2)", 500 | "{{#hellos}}{{foo this/text}}{{/hellos}}", 501 | map[string]interface{}{"hellos": []map[string]string{{"text": "hello"}, {"text": "Hello"}, {"text": "HELLO"}}}, 502 | nil, 503 | map[string]interface{}{"foo": barSuffixHelper}, 504 | nil, 505 | "bar hellobar Hellobar HELLO", 506 | }, 507 | { 508 | "this keyword nested inside helpers param (1)", 509 | "{{foo [this]}}", 510 | map[string]interface{}{"this": "bar"}, 511 | nil, 512 | map[string]interface{}{"foo": echoHelper}, 513 | nil, 514 | "bar", 515 | }, 516 | { 517 | "this keyword nested inside helpers param (2)", 518 | "{{foo text/[this]}}", 519 | map[string]map[string]string{"text": {"this": "bar"}}, 520 | nil, 521 | map[string]interface{}{"foo": echoHelper}, 522 | nil, 523 | "bar", 524 | }, 525 | { 526 | "pass string literals (1)", 527 | `{{"foo"}}`, 528 | map[string]string{}, 529 | nil, nil, nil, 530 | "", 531 | }, 532 | { 533 | "pass string literals (2)", 534 | `{{"foo"}}`, 535 | map[string]string{"foo": "bar"}, 536 | nil, nil, nil, 537 | "bar", 538 | }, 539 | { 540 | "pass string literals (3)", 541 | `{{#"foo"}}{{.}}{{/"foo"}}`, 542 | map[string]interface{}{"foo": []string{"bar", "baz"}}, 543 | nil, nil, nil, 544 | "barbaz", 545 | }, 546 | { 547 | "pass number literals (1)", 548 | "{{12}}", 549 | map[string]string{}, 550 | nil, nil, nil, 551 | "", 552 | }, 553 | { 554 | "pass number literals (2)", 555 | "{{12}}", 556 | map[string]string{"12": "bar"}, 557 | nil, nil, nil, 558 | "bar", 559 | }, 560 | { 561 | "pass number literals (3)", 562 | "{{12.34}}", 563 | map[string]string{}, 564 | nil, nil, nil, 565 | "", 566 | }, 567 | { 568 | "pass number literals (4)", 569 | "{{12.34}}", 570 | map[string]string{"12.34": "bar"}, 571 | nil, nil, nil, 572 | "bar", 573 | }, 574 | { 575 | "pass number literals (5)", 576 | "{{12.34 1}}", 577 | map[string]interface{}{"12.34": func(context string) string { 578 | return "bar" + context 579 | }}, 580 | nil, nil, nil, 581 | "bar1", 582 | }, 583 | { 584 | "pass boolean literals (1)", 585 | "{{true}}", 586 | map[string]string{}, 587 | nil, nil, nil, 588 | "", 589 | }, 590 | { 591 | "pass boolean literals (2)", 592 | "{{true}}", 593 | map[string]string{"": "foo"}, 594 | nil, nil, nil, 595 | "", 596 | }, 597 | { 598 | "pass boolean literals (3)", 599 | "{{false}}", 600 | map[string]string{"false": "foo"}, 601 | nil, nil, nil, 602 | "foo", 603 | }, 604 | { 605 | "should handle literals in subexpression", 606 | "{{foo (false)}}", 607 | map[string]interface{}{"false": func() string { return "bar" }}, 608 | nil, 609 | map[string]interface{}{"foo": func(context string) string { 610 | return context 611 | }}, 612 | nil, 613 | "bar", 614 | }, 615 | } 616 | 617 | func TestBasic(t *testing.T) { 618 | launchTests(t, basicTests) 619 | } 620 | 621 | func TestBasicErrors(t *testing.T) { 622 | t.Parallel() 623 | 624 | var err error 625 | 626 | inputs := []string{ 627 | // this keyword nested inside path 628 | "{{#hellos}}{{text/this/foo}}{{/hellos}}", 629 | // this keyword nested inside helpers param 630 | "{{#hellos}}{{foo text/this/foo}}{{/hellos}}", 631 | } 632 | 633 | expectedError := regexp.QuoteMeta("Invalid path: text/this") 634 | 635 | for _, input := range inputs { 636 | _, err = raymond.Parse(input) 637 | if err == nil { 638 | t.Errorf("Test failed - Error expected") 639 | } 640 | 641 | match, errMatch := regexp.MatchString(expectedError, fmt.Sprint(err)) 642 | if errMatch != nil { 643 | panic("Failed to match regexp") 644 | } 645 | 646 | if !match { 647 | t.Errorf("Test failed - Expected error:\n\t%s\n\nGot:\n\t%s", expectedError, err) 648 | } 649 | } 650 | } 651 | -------------------------------------------------------------------------------- /handlebars/blocks_test.go: -------------------------------------------------------------------------------- 1 | package handlebars 2 | 3 | import "testing" 4 | 5 | // 6 | // Those tests come from: 7 | // https://github.com/wycats/handlebars.js/blob/master/spec/blocks.js 8 | // 9 | var blocksTests = []Test{ 10 | { 11 | "array (1) - Arrays iterate over the contents when not empty", 12 | "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!", 13 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"}, 14 | nil, nil, nil, 15 | "goodbye! Goodbye! GOODBYE! cruel world!", 16 | }, 17 | { 18 | "array (2) - Arrays ignore the contents when empty", 19 | "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!", 20 | map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"}, 21 | nil, nil, nil, 22 | "cruel world!", 23 | }, 24 | { 25 | "array without data", 26 | "{{#goodbyes}}{{text}}{{/goodbyes}} {{#goodbyes}}{{text}}{{/goodbyes}}", 27 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"}, 28 | nil, nil, nil, 29 | "goodbyeGoodbyeGOODBYE goodbyeGoodbyeGOODBYE", 30 | }, 31 | { 32 | "array with @index - The @index variable is used", 33 | "{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!", 34 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"}, 35 | nil, nil, nil, 36 | "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", 37 | }, 38 | { 39 | "empty block (1) - Arrays iterate over the contents when not empty", 40 | "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!", 41 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"}, 42 | nil, nil, nil, 43 | "cruel world!", 44 | }, 45 | { 46 | "empty block (1) - Arrays ignore the contents when empty", 47 | "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!", 48 | map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"}, 49 | nil, nil, nil, 50 | "cruel world!", 51 | }, 52 | { 53 | "block with complex lookup - Templates can access variables in contexts up the stack with relative path syntax", 54 | "{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}", 55 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "name": "Alan"}, 56 | nil, nil, nil, 57 | "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! ", 58 | }, 59 | { 60 | "multiple blocks with complex lookup", 61 | "{{#goodbyes}}{{../name}}{{../name}}{{/goodbyes}}", 62 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "name": "Alan"}, 63 | nil, nil, nil, 64 | "AlanAlanAlanAlanAlanAlan", 65 | }, 66 | 67 | // @todo "{{#goodbyes}}{{text}} cruel {{foo/../name}}! {{/goodbyes}}" should throw error 68 | 69 | { 70 | "block with deep nested complex lookup", 71 | "{{#outer}}Goodbye {{#inner}}cruel {{../sibling}} {{../../omg}}{{/inner}}{{/outer}}", 72 | map[string]interface{}{"omg": "OMG!", "outer": []map[string]interface{}{{"sibling": "sad", "inner": []map[string]string{{"text": "goodbye"}}}}}, 73 | nil, nil, nil, 74 | "Goodbye cruel sad OMG!", 75 | }, 76 | { 77 | "inverted sections with unset value - Inverted section rendered when value isn't set.", 78 | "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}", 79 | map[string]interface{}{}, 80 | nil, nil, nil, 81 | "Right On!", 82 | }, 83 | { 84 | "inverted sections with false value - Inverted section rendered when value is false.", 85 | "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}", 86 | map[string]interface{}{"goodbyes": false}, 87 | nil, nil, nil, 88 | "Right On!", 89 | }, 90 | { 91 | "inverted section with empty set - Inverted section rendered when value is empty set.", 92 | "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}", 93 | map[string]interface{}{"goodbyes": []interface{}{}}, 94 | nil, nil, nil, 95 | "Right On!", 96 | }, 97 | { 98 | "block inverted sections", 99 | "{{#people}}{{name}}{{^}}{{none}}{{/people}}", 100 | map[string]interface{}{"none": "No people"}, 101 | nil, nil, nil, 102 | "No people", 103 | }, 104 | { 105 | "chained inverted sections (1)", 106 | "{{#people}}{{name}}{{else if none}}{{none}}{{/people}}", 107 | map[string]interface{}{"none": "No people"}, 108 | nil, nil, nil, 109 | "No people", 110 | }, 111 | { 112 | "chained inverted sections (2)", 113 | "{{#people}}{{name}}{{else if nothere}}fail{{else unless nothere}}{{none}}{{/people}}", 114 | map[string]interface{}{"none": "No people"}, 115 | nil, nil, nil, 116 | "No people", 117 | }, 118 | { 119 | "chained inverted sections (3)", 120 | "{{#people}}{{name}}{{else if none}}{{none}}{{else}}fail{{/people}}", 121 | map[string]interface{}{"none": "No people"}, 122 | nil, nil, nil, 123 | "No people", 124 | }, 125 | 126 | // @todo "{{#people}}{{name}}{{else if none}}{{none}}{{/if}}" should throw error 127 | 128 | { 129 | "block inverted sections with empty arrays", 130 | "{{#people}}{{name}}{{^}}{{none}}{{/people}}", 131 | map[string]interface{}{"none": "No people", "people": map[string]interface{}{}}, 132 | nil, nil, nil, 133 | "No people", 134 | }, 135 | { 136 | "block standalone else sections (1)", 137 | "{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n", 138 | map[string]interface{}{"none": "No people"}, 139 | nil, nil, nil, 140 | "No people\n", 141 | }, 142 | { 143 | "block standalone else sections (2)", 144 | "{{#none}}\n{{.}}\n{{^}}\n{{none}}\n{{/none}}\n", 145 | map[string]interface{}{"none": "No people"}, 146 | nil, nil, nil, 147 | "No people\n", 148 | }, 149 | { 150 | "block standalone else sections (3)", 151 | "{{#people}}\n{{name}}\n{{^}}\n{{none}}\n{{/people}}\n", 152 | map[string]interface{}{"none": "No people"}, 153 | nil, nil, nil, 154 | "No people\n", 155 | }, 156 | { 157 | "block standalone chained else sections (1)", 158 | "{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{/people}}\n", 159 | map[string]interface{}{"none": "No people"}, 160 | nil, nil, nil, 161 | "No people\n", 162 | }, 163 | { 164 | "block standalone chained else sections (2)", 165 | "{{#people}}\n{{name}}\n{{else if none}}\n{{none}}\n{{^}}\n{{/people}}\n", 166 | map[string]interface{}{"none": "No people"}, 167 | nil, nil, nil, 168 | "No people\n", 169 | }, 170 | { 171 | "should handle nesting", 172 | "{{#data}}\n{{#if true}}\n{{.}}\n{{/if}}\n{{/data}}\nOK.", 173 | map[string]interface{}{"data": []int{1, 3, 5}}, 174 | nil, nil, nil, 175 | "1\n3\n5\nOK.", 176 | }, 177 | // // @todo compat mode 178 | // { 179 | // "block with deep recursive lookup lookup", 180 | // "{{#outer}}Goodbye {{#inner}}cruel {{omg}}{{/inner}}{{/outer}}", 181 | // map[string]interface{}{"omg": "OMG!", "outer": []map[string]interface{}{{"inner": []map[string]string{{"text": "goodbye"}}}}}, 182 | // nil, 183 | // nil, 184 | // nil, 185 | // "Goodbye cruel OMG!", 186 | // }, 187 | // // @todo compat mode 188 | // { 189 | // "block with deep recursive pathed lookup", 190 | // "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}", 191 | // map[string]interface{}{"omg": map[string]string{"yes": "OMG!"}, "outer": []map[string]interface{}{{"inner": []map[string]string{{"yes": "no", "text": "goodbye"}}}}}, 192 | // nil, 193 | // nil, 194 | // nil, 195 | // "Goodbye cruel OMG!", 196 | // }, 197 | { 198 | "block with missed recursive lookup", 199 | "{{#outer}}Goodbye {{#inner}}cruel {{omg.yes}}{{/inner}}{{/outer}}", 200 | map[string]interface{}{"omg": map[string]string{"no": "OMG!"}, "outer": []map[string]interface{}{{"inner": []map[string]string{{"yes": "no", "text": "goodbye"}}}}}, 201 | nil, nil, nil, 202 | "Goodbye cruel ", 203 | }, 204 | } 205 | 206 | func TestBlocks(t *testing.T) { 207 | launchTests(t, blocksTests) 208 | } 209 | -------------------------------------------------------------------------------- /handlebars/builtins_test.go: -------------------------------------------------------------------------------- 1 | package handlebars 2 | 3 | import "testing" 4 | 5 | // 6 | // Those tests come from: 7 | // https://github.com/wycats/handlebars.js/blob/master/spec/builtin.js 8 | // 9 | var builtinsTests = []Test{ 10 | { 11 | "#if - if with boolean argument shows the contents when true", 12 | "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!", 13 | map[string]interface{}{"goodbye": true, "world": "world"}, 14 | nil, nil, nil, 15 | "GOODBYE cruel world!", 16 | }, 17 | { 18 | "#if - if with string argument shows the contents", 19 | "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!", 20 | map[string]interface{}{"goodbye": "dummy", "world": "world"}, 21 | nil, nil, nil, 22 | "GOODBYE cruel world!", 23 | }, 24 | { 25 | "#if - if with boolean argument does not show the contents when false", 26 | "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!", 27 | map[string]interface{}{"goodbye": false, "world": "world"}, 28 | nil, nil, nil, 29 | "cruel world!", 30 | }, 31 | { 32 | "#if - if with undefined does not show the contents", 33 | "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!", 34 | map[string]interface{}{"world": "world"}, 35 | nil, nil, nil, 36 | "cruel world!", 37 | }, 38 | { 39 | "#if - if with non-empty array shows the contents", 40 | "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!", 41 | map[string]interface{}{"goodbye": []string{"foo"}, "world": "world"}, 42 | nil, nil, nil, 43 | "GOODBYE cruel world!", 44 | }, 45 | { 46 | "#if - if with empty array does not show the contents", 47 | "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!", 48 | map[string]interface{}{"goodbye": []string{}, "world": "world"}, 49 | nil, nil, nil, 50 | "cruel world!", 51 | }, 52 | { 53 | "#if - if with zero does not show the contents", 54 | "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!", 55 | map[string]interface{}{"goodbye": 0, "world": "world"}, 56 | nil, nil, nil, 57 | "cruel world!", 58 | }, 59 | { 60 | "#if - if with zero and includeZero option shows the contents", 61 | "{{#if goodbye includeZero=true}}GOODBYE {{/if}}cruel {{world}}!", 62 | map[string]interface{}{"goodbye": 0, "world": "world"}, 63 | nil, nil, nil, 64 | "GOODBYE cruel world!", 65 | }, 66 | { 67 | "#if - if with function shows the contents when function returns true", 68 | "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!", 69 | map[string]interface{}{ 70 | "goodbye": func() bool { return true }, 71 | "world": "world", 72 | }, 73 | nil, nil, nil, 74 | "GOODBYE cruel world!", 75 | }, 76 | { 77 | "#if - if with function shows the contents when function returns string", 78 | "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!", 79 | map[string]interface{}{ 80 | "goodbye": func() string { return "world" }, 81 | "world": "world", 82 | }, 83 | nil, nil, nil, 84 | "GOODBYE cruel world!", 85 | }, 86 | { 87 | "#if - if with function does not show the contents when returns false", 88 | "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!", 89 | map[string]interface{}{ 90 | "goodbye": func() bool { return false }, 91 | "world": "world", 92 | }, 93 | nil, nil, nil, 94 | "cruel world!", 95 | }, 96 | { 97 | "#if - if with function does not show the contents when returns undefined", 98 | "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!", 99 | map[string]interface{}{ 100 | "goodbye": func() interface{} { return nil }, 101 | "world": "world", 102 | }, 103 | nil, nil, nil, 104 | "cruel world!", 105 | }, 106 | { 107 | "#with", 108 | "{{#with person}}{{first}} {{last}}{{/with}}", 109 | map[string]interface{}{"person": map[string]string{"first": "Alan", "last": "Johnson"}}, 110 | nil, nil, nil, 111 | "Alan Johnson", 112 | }, 113 | { 114 | "#with - with with function argument", 115 | "{{#with person}}{{first}} {{last}}{{/with}}", 116 | map[string]interface{}{ 117 | "person": func() map[string]string { return map[string]string{"first": "Alan", "last": "Johnson"} }, 118 | }, nil, nil, nil, 119 | "Alan Johnson", 120 | }, 121 | { 122 | "#with - with with else", 123 | "{{#with person}}Person is present{{else}}Person is not present{{/with}}", 124 | map[string]interface{}{}, 125 | nil, nil, nil, 126 | "Person is not present", 127 | }, 128 | 129 | { 130 | "#each - each with array argument iterates over the contents when not empty", 131 | "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!", 132 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"}, 133 | nil, nil, nil, 134 | "goodbye! Goodbye! GOODBYE! cruel world!", 135 | }, 136 | { 137 | "#each - each with array argument ignores the contents when empty", 138 | "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!", 139 | map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"}, 140 | nil, nil, nil, 141 | "cruel world!", 142 | }, 143 | { 144 | "#each - each without data (1)", 145 | "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!", 146 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"}, 147 | nil, nil, nil, 148 | "goodbye! Goodbye! GOODBYE! cruel world!", 149 | }, 150 | { 151 | "#each - each without data (2)", 152 | "{{#each .}}{{.}}{{/each}}", 153 | map[string]interface{}{"goodbyes": "cruel", "world": "world"}, 154 | nil, nil, nil, 155 | // note: a go hash is not ordered, so result may vary, this behaviour differs from the JS implementation 156 | []string{"cruelworld", "worldcruel"}, 157 | }, 158 | { 159 | "#each - each without context", 160 | "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!", 161 | nil, nil, nil, nil, 162 | "cruel !", 163 | }, 164 | 165 | // NOTE: we test with a map instead of an object 166 | { 167 | "#each - each with an object and @key (map)", 168 | "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!", 169 | map[string]interface{}{"goodbyes": map[interface{}]map[string]string{"#1": {"text": "goodbye"}, 2: {"text": "GOODBYE"}}, "world": "world"}, 170 | nil, nil, nil, 171 | []string{"<b>#1</b>. goodbye! 2. GOODBYE! cruel world!", "2. GOODBYE! <b>#1</b>. goodbye! cruel world!"}, 172 | }, 173 | // NOTE: An additional test with a struct, but without an html stuff for the key, because it is impossible 174 | { 175 | "#each - each with an object and @key (struct)", 176 | "{{#each goodbyes}}{{@key}}. {{text}}! {{/each}}cruel {{world}}!", 177 | map[string]interface{}{ 178 | "goodbyes": struct { 179 | Foo map[string]string 180 | Bar map[string]int 181 | }{map[string]string{"text": "baz"}, map[string]int{"text": 10}}, 182 | "world": "world", 183 | }, 184 | nil, nil, nil, 185 | []string{"Foo. baz! Bar. 10! cruel world!", "Bar. 10! Foo. baz! cruel world!"}, 186 | }, 187 | { 188 | "#each - each with @index", 189 | "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!", 190 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"}, 191 | nil, nil, nil, 192 | "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!", 193 | }, 194 | { 195 | "#each - each with nested @index", 196 | "{{#each goodbyes}}{{@index}}. {{text}}! {{#each ../goodbyes}}{{@index}} {{/each}}After {{@index}} {{/each}}{{@index}}cruel {{world}}!", 197 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"}, 198 | nil, nil, nil, 199 | "0. goodbye! 0 1 2 After 0 1. Goodbye! 0 1 2 After 1 2. GOODBYE! 0 1 2 After 2 cruel world!", 200 | }, 201 | { 202 | "#each - each with block params", 203 | "{{#each goodbyes as |value index|}}{{index}}. {{value.text}}! {{#each ../goodbyes as |childValue childIndex|}} {{index}} {{childIndex}}{{/each}} After {{index}} {{/each}}{{index}}cruel {{world}}!", 204 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}}, "world": "world"}, 205 | nil, nil, nil, 206 | "0. goodbye! 0 0 0 1 After 0 1. Goodbye! 1 0 1 1 After 1 cruel world!", 207 | }, 208 | // @note: That test differs from JS impl because maps and structs are not ordered in go 209 | { 210 | "#each - each object with @index", 211 | "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!", 212 | map[string]interface{}{"goodbyes": map[string]map[string]string{"a": {"text": "goodbye"}, "b": {"text": "Goodbye"}}, "world": "world"}, 213 | nil, nil, nil, 214 | []string{"0. goodbye! 1. Goodbye! cruel world!", "0. Goodbye! 1. goodbye! cruel world!"}, 215 | }, 216 | { 217 | "#each - each with nested @first", 218 | "{{#each goodbyes}}({{#if @first}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @first}}{{text}}!{{/if}}{{/each}}{{#if @first}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!", 219 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"}, 220 | nil, nil, nil, 221 | "(goodbye! goodbye! goodbye!) (goodbye!) (goodbye!) cruel world!", 222 | }, 223 | // @note: That test differs from JS impl because maps and structs are not ordered in go 224 | { 225 | "#each - each object with @first", 226 | "{{#each goodbyes}}{{#if @first}}{{text}}! {{/if}}{{/each}}cruel {{world}}!", 227 | map[string]interface{}{"goodbyes": map[string]map[string]string{"foo": {"text": "goodbye"}, "bar": {"text": "Goodbye"}}, "world": "world"}, 228 | nil, nil, nil, 229 | []string{"goodbye! cruel world!", "Goodbye! cruel world!"}, 230 | }, 231 | { 232 | "#each - each with @last", 233 | "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!", 234 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"}, 235 | nil, nil, nil, 236 | "GOODBYE! cruel world!", 237 | }, 238 | // @note: That test differs from JS impl because maps and structs are not ordered in go 239 | { 240 | "#each - each object with @last", 241 | "{{#each goodbyes}}{{#if @last}}{{text}}! {{/if}}{{/each}}cruel {{world}}!", 242 | map[string]interface{}{"goodbyes": map[string]map[string]string{"foo": {"text": "goodbye"}, "bar": {"text": "Goodbye"}}, "world": "world"}, 243 | nil, nil, nil, 244 | []string{"goodbye! cruel world!", "Goodbye! cruel world!"}, 245 | }, 246 | { 247 | "#each - each with nested @last", 248 | "{{#each goodbyes}}({{#if @last}}{{text}}! {{/if}}{{#each ../goodbyes}}{{#if @last}}{{text}}!{{/if}}{{/each}}{{#if @last}} {{text}}!{{/if}}) {{/each}}cruel {{world}}!", 249 | map[string]interface{}{"goodbyes": []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}}, "world": "world"}, 250 | nil, nil, nil, 251 | "(GOODBYE!) (GOODBYE!) (GOODBYE! GOODBYE! GOODBYE!) cruel world!", 252 | }, 253 | 254 | { 255 | "#each - each with function argument (1)", 256 | "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!", 257 | map[string]interface{}{"goodbyes": func() []map[string]string { 258 | return []map[string]string{{"text": "goodbye"}, {"text": "Goodbye"}, {"text": "GOODBYE"}} 259 | }, "world": "world"}, 260 | nil, nil, nil, 261 | "goodbye! Goodbye! GOODBYE! cruel world!", 262 | }, 263 | { 264 | "#each - each with function argument (2)", 265 | "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!", 266 | map[string]interface{}{"goodbyes": []map[string]string{}, "world": "world"}, 267 | nil, nil, nil, 268 | "cruel world!", 269 | }, 270 | { 271 | "#each - data passed to helpers", 272 | "{{#each letters}}{{this}}{{detectDataInsideEach}}{{/each}}", 273 | map[string][]string{"letters": {"a", "b", "c"}}, 274 | map[string]interface{}{"exclaim": "!"}, 275 | map[string]interface{}{"detectDataInsideEach": detectDataHelper}, 276 | nil, 277 | "a!b!c!", 278 | }, 279 | 280 | // @todo "each on implicit context" should throw error 281 | 282 | // SKIP: #log - "should call logger at default level" 283 | // SKIP: #log - "should call logger at data level" 284 | // SKIP: #log - "should output to info" 285 | // SKIP: #log - "should log at data level" 286 | // SKIP: #log - "should handle missing logger" 287 | 288 | // @note Test added 289 | // @todo Check log output 290 | { 291 | "#log", 292 | "{{log blah}}", 293 | map[string]string{"blah": "whee"}, 294 | nil, nil, nil, 295 | "", 296 | }, 297 | 298 | // @note Test added 299 | { 300 | "#lookup - should lookup array element", 301 | "{{#each goodbyes}}{{lookup ../data @index}}{{/each}}", 302 | map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}}, 303 | nil, nil, nil, 304 | "foobar", 305 | }, 306 | { 307 | "#lookup - should lookup map element", 308 | "{{#each goodbyes}}{{lookup ../data .}}{{/each}}", 309 | map[string]interface{}{"goodbyes": []string{"foo", "bar"}, "data": map[string]string{"foo": "baz", "bar": "bat"}}, 310 | nil, nil, nil, 311 | "bazbat", 312 | }, 313 | { 314 | "#lookup - should lookup struct field", 315 | "{{#each goodbyes}}{{lookup ../data .}}{{/each}}", 316 | map[string]interface{}{"goodbyes": []string{"Foo", "Bar"}, "data": struct { 317 | Foo string 318 | Bar string 319 | }{"baz", "bat"}}, 320 | nil, nil, nil, 321 | "bazbat", 322 | }, 323 | { 324 | "#lookup - should lookup arbitrary content", 325 | "{{#each goodbyes}}{{lookup ../data .}}{{/each}}", 326 | map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}}, 327 | nil, nil, nil, 328 | "foobar", 329 | }, 330 | { 331 | "#lookup - should not fail on undefined value", 332 | "{{#each goodbyes}}{{lookup ../bar .}}{{/each}}", 333 | map[string]interface{}{"goodbyes": []int{0, 1}, "data": []string{"foo", "bar"}}, 334 | nil, nil, nil, 335 | "", 336 | }, 337 | } 338 | 339 | func TestBuiltins(t *testing.T) { 340 | launchTests(t, builtinsTests) 341 | } 342 | -------------------------------------------------------------------------------- /handlebars/data_test.go: -------------------------------------------------------------------------------- 1 | package handlebars 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aymerick/raymond" 7 | ) 8 | 9 | // 10 | // Those tests come from: 11 | // https://github.com/wycats/handlebars.js/blob/master/spec/data.js 12 | // 13 | var dataTests = []Test{ 14 | { 15 | "passing in data to a compiled function that expects data - works with helpers", 16 | "{{hello}}", 17 | map[string]string{"noun": "cat"}, 18 | map[string]interface{}{"adjective": "happy"}, 19 | map[string]interface{}{"hello": func(options *raymond.Options) string { 20 | return options.DataStr("adjective") + " " + options.ValueStr("noun") 21 | }}, 22 | nil, 23 | "happy cat", 24 | }, 25 | { 26 | "data can be looked up via @foo", 27 | "{{@hello}}", 28 | nil, 29 | map[string]interface{}{"hello": "hello"}, 30 | nil, nil, 31 | "hello", 32 | }, 33 | { 34 | "deep @foo triggers automatic top-level data", 35 | `{{#let world="world"}}{{#if foo}}{{#if foo}}Hello {{@world}}{{/if}}{{/if}}{{/let}}`, 36 | map[string]bool{"foo": true}, 37 | map[string]interface{}{"hello": "hello"}, 38 | map[string]interface{}{"let": func(options *raymond.Options) string { 39 | frame := options.NewDataFrame() 40 | 41 | for k, v := range options.Hash() { 42 | frame.Set(k, v) 43 | } 44 | 45 | return options.FnData(frame) 46 | }}, 47 | nil, 48 | "Hello world", 49 | }, 50 | { 51 | "parameter data can be looked up via @foo", 52 | `{{hello @world}}`, 53 | nil, 54 | map[string]interface{}{"world": "world"}, 55 | map[string]interface{}{"hello": func(context string) string { 56 | return "Hello " + context 57 | }}, 58 | nil, 59 | "Hello world", 60 | }, 61 | { 62 | "hash values can be looked up via @foo", 63 | `{{hello noun=@world}}`, 64 | nil, 65 | map[string]interface{}{"world": "world"}, 66 | map[string]interface{}{"hello": func(options *raymond.Options) string { 67 | return "Hello " + options.HashStr("noun") 68 | }}, 69 | nil, 70 | "Hello world", 71 | }, 72 | { 73 | "nested parameter data can be looked up via @foo.bar", 74 | `{{hello @world.bar}}`, 75 | nil, 76 | map[string]interface{}{"world": map[string]string{"bar": "world"}}, 77 | map[string]interface{}{"hello": func(context string) string { 78 | return "Hello " + context 79 | }}, 80 | nil, 81 | "Hello world", 82 | }, 83 | { 84 | "nested parameter data does not fail with @world.bar", 85 | `{{hello @world.bar}}`, 86 | nil, 87 | map[string]interface{}{"foo": map[string]string{"bar": "world"}}, 88 | map[string]interface{}{"hello": func(context string) string { 89 | return "Hello " + context 90 | }}, 91 | nil, 92 | // @todo Test differs with JS implementation: we don't output `undefined` 93 | "Hello ", 94 | }, 95 | 96 | // @todo "parameter data throws when using complex scope references", 97 | 98 | { 99 | "data can be functions", 100 | `{{@hello}}`, 101 | nil, 102 | map[string]interface{}{"hello": func() string { return "hello" }}, 103 | nil, nil, 104 | "hello", 105 | }, 106 | { 107 | "data can be functions with params", 108 | `{{@hello "hello"}}`, 109 | nil, 110 | map[string]interface{}{"hello": func(context string) string { return context }}, 111 | nil, nil, 112 | "hello", 113 | }, 114 | 115 | { 116 | "data is inherited downstream", 117 | `{{#let foo=1 bar=2}}{{#let foo=bar.baz}}{{@bar}}{{@foo}}{{/let}}{{@foo}}{{/let}}`, 118 | map[string]map[string]string{"bar": {"baz": "hello world"}}, 119 | nil, 120 | map[string]interface{}{"let": func(options *raymond.Options) string { 121 | frame := options.NewDataFrame() 122 | 123 | for k, v := range options.Hash() { 124 | frame.Set(k, v) 125 | } 126 | 127 | return options.FnData(frame) 128 | }}, 129 | nil, 130 | "2hello world1", 131 | }, 132 | { 133 | "passing in data to a compiled function that expects data - works with helpers in partials", 134 | `{{>myPartial}}`, 135 | map[string]string{"noun": "cat"}, 136 | map[string]interface{}{"adjective": "happy"}, 137 | map[string]interface{}{"hello": func(options *raymond.Options) string { 138 | return options.DataStr("adjective") + " " + options.ValueStr("noun") 139 | }}, 140 | map[string]string{ 141 | "myPartial": "{{hello}}", 142 | }, 143 | "happy cat", 144 | }, 145 | { 146 | "passing in data to a compiled function that expects data - works with helpers and parameters", 147 | `{{hello world}}`, 148 | map[string]interface{}{"exclaim": true, "world": "world"}, 149 | map[string]interface{}{"adjective": "happy"}, 150 | map[string]interface{}{"hello": func(context string, options *raymond.Options) string { 151 | str := "error" 152 | if b, ok := options.Value("exclaim").(bool); ok { 153 | if b { 154 | str = "!" 155 | } else { 156 | str = "" 157 | } 158 | } 159 | 160 | return options.DataStr("adjective") + " " + context + str 161 | }}, 162 | nil, 163 | "happy world!", 164 | }, 165 | { 166 | "passing in data to a compiled function that expects data - works with block helpers", 167 | `{{#hello}}{{world}}{{/hello}}`, 168 | map[string]bool{"exclaim": true}, 169 | map[string]interface{}{"adjective": "happy"}, 170 | map[string]interface{}{ 171 | "hello": func(options *raymond.Options) string { 172 | return options.Fn() 173 | }, 174 | "world": func(options *raymond.Options) string { 175 | str := "error" 176 | if b, ok := options.Value("exclaim").(bool); ok { 177 | if b { 178 | str = "!" 179 | } else { 180 | str = "" 181 | } 182 | } 183 | 184 | return options.DataStr("adjective") + " world" + str 185 | }, 186 | }, 187 | nil, 188 | "happy world!", 189 | }, 190 | { 191 | "passing in data to a compiled function that expects data - works with block helpers that use ..", 192 | `{{#hello}}{{world ../zomg}}{{/hello}}`, 193 | map[string]interface{}{"exclaim": true, "zomg": "world"}, 194 | map[string]interface{}{"adjective": "happy"}, 195 | map[string]interface{}{ 196 | "hello": func(options *raymond.Options) string { 197 | return options.FnWith(map[string]string{"exclaim": "?"}) 198 | }, 199 | "world": func(context string, options *raymond.Options) string { 200 | return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim") 201 | }, 202 | }, 203 | nil, 204 | "happy world?", 205 | }, 206 | { 207 | "passing in data to a compiled function that expects data - data is passed to with block helpers where children use ..", 208 | `{{#hello}}{{world ../zomg}}{{/hello}}`, 209 | map[string]interface{}{"exclaim": true, "zomg": "world"}, 210 | map[string]interface{}{"adjective": "happy", "accessData": "#win"}, 211 | map[string]interface{}{ 212 | "hello": func(options *raymond.Options) string { 213 | return options.DataStr("accessData") + " " + options.FnWith(map[string]string{"exclaim": "?"}) 214 | }, 215 | "world": func(context string, options *raymond.Options) string { 216 | return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim") 217 | }, 218 | }, 219 | nil, 220 | "#win happy world?", 221 | }, 222 | { 223 | "you can override inherited data when invoking a helper", 224 | `{{#hello}}{{world zomg}}{{/hello}}`, 225 | map[string]interface{}{"exclaim": true, "zomg": "planet"}, 226 | map[string]interface{}{"adjective": "happy"}, 227 | map[string]interface{}{ 228 | "hello": func(options *raymond.Options) string { 229 | ctx := map[string]string{"exclaim": "?", "zomg": "world"} 230 | data := options.NewDataFrame() 231 | data.Set("adjective", "sad") 232 | 233 | return options.FnCtxData(ctx, data) 234 | }, 235 | "world": func(context string, options *raymond.Options) string { 236 | return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim") 237 | }, 238 | }, 239 | nil, 240 | "sad world?", 241 | }, 242 | { 243 | "you can override inherited data when invoking a helper with depth", 244 | `{{#hello}}{{world ../zomg}}{{/hello}}`, 245 | map[string]interface{}{"exclaim": true, "zomg": "world"}, 246 | map[string]interface{}{"adjective": "happy"}, 247 | map[string]interface{}{ 248 | "hello": func(options *raymond.Options) string { 249 | ctx := map[string]string{"exclaim": "?"} 250 | data := options.NewDataFrame() 251 | data.Set("adjective", "sad") 252 | 253 | return options.FnCtxData(ctx, data) 254 | }, 255 | "world": func(context string, options *raymond.Options) string { 256 | return options.DataStr("adjective") + " " + context + options.ValueStr("exclaim") 257 | }, 258 | }, 259 | nil, 260 | "sad world?", 261 | }, 262 | { 263 | "@root - the root context can be looked up via @root", 264 | `{{@root.foo}}`, 265 | map[string]interface{}{"foo": "hello"}, 266 | nil, nil, nil, 267 | "hello", 268 | }, 269 | { 270 | "@root - passed root values take priority", 271 | `{{@root.foo}}`, 272 | nil, 273 | map[string]interface{}{"root": map[string]string{"foo": "hello"}}, 274 | nil, nil, 275 | "hello", 276 | }, 277 | { 278 | "nesting - the root context can be looked up via @root", 279 | `{{#helper}}{{#helper}}{{@./depth}} {{@../depth}} {{@../../depth}}{{/helper}}{{/helper}}`, 280 | map[string]interface{}{"foo": "hello"}, 281 | map[string]interface{}{"depth": 0}, 282 | map[string]interface{}{ 283 | "helper": func(options *raymond.Options) string { 284 | data := options.NewDataFrame() 285 | 286 | if depth, ok := options.Data("depth").(int); ok { 287 | data.Set("depth", depth+1) 288 | } 289 | 290 | return options.FnData(data) 291 | }, 292 | }, 293 | nil, 294 | "2 1 0", 295 | }, 296 | } 297 | 298 | func TestData(t *testing.T) { 299 | launchTests(t, dataTests) 300 | } 301 | -------------------------------------------------------------------------------- /handlebars/doc.go: -------------------------------------------------------------------------------- 1 | // Package handlebars contains all the tests that come from handlebars.js project. 2 | package handlebars 3 | -------------------------------------------------------------------------------- /handlebars/partials_test.go: -------------------------------------------------------------------------------- 1 | package handlebars 2 | 3 | import "testing" 4 | 5 | // 6 | // Those tests come from: 7 | // https://github.com/wycats/handlebars.js/blob/master/spec/partials.js 8 | // 9 | var partialsTests = []Test{ 10 | { 11 | "basic partials", 12 | "Dudes: {{#dudes}}{{> dude}}{{/dudes}}", 13 | map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}}, 14 | nil, nil, 15 | map[string]string{"dude": "{{name}} ({{url}}) "}, 16 | "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", 17 | }, 18 | { 19 | "dynamic partials", 20 | "Dudes: {{#dudes}}{{> (partial)}}{{/dudes}}", 21 | map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}}, 22 | nil, 23 | map[string]interface{}{"partial": func() string { 24 | return "dude" 25 | }}, 26 | map[string]string{"dude": "{{name}} ({{url}}) "}, 27 | "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", 28 | }, 29 | 30 | // @todo "failing dynamic partials" 31 | 32 | { 33 | "partials with context", 34 | "Dudes: {{>dude dudes}}", 35 | map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}}, 36 | nil, nil, 37 | map[string]string{"dude": "{{#this}}{{name}} ({{url}}) {{/this}}"}, 38 | "Dudes: Yehuda (http://yehuda) Alan (http://alan) ", 39 | }, 40 | { 41 | "partials with undefined context", 42 | "Dudes: {{>dude dudes}}", 43 | map[string]interface{}{}, 44 | nil, nil, 45 | map[string]string{"dude": "{{foo}} Empty"}, 46 | "Dudes: Empty", 47 | }, 48 | 49 | // @todo "partials with duplicate parameters" 50 | 51 | { 52 | "partials with parameters", 53 | "Dudes: {{#dudes}}{{> dude others=..}}{{/dudes}}", 54 | map[string]interface{}{"foo": "bar", "dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}}, 55 | nil, nil, 56 | map[string]string{"dude": "{{others.foo}}{{name}} ({{url}}) "}, 57 | "Dudes: barYehuda (http://yehuda) barAlan (http://alan) ", 58 | }, 59 | { 60 | "partial in a partial", 61 | "Dudes: {{#dudes}}{{>dude}}{{/dudes}}", 62 | map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}}, 63 | nil, nil, 64 | map[string]string{"dude": "{{name}} {{> url}} ", "url": `{{url}}`}, 65 | `Dudes: Yehuda http://yehuda Alan http://alan `, 66 | }, 67 | 68 | // @todo "rendering undefined partial throws an exception" 69 | 70 | // @todo "registering undefined partial throws an exception" 71 | 72 | // SKIP: "rendering template partial in vm mode throws an exception" 73 | // SKIP: "rendering function partial in vm mode" 74 | 75 | { 76 | "GH-14: a partial preceding a selector", 77 | "Dudes: {{>dude}} {{anotherDude}}", 78 | map[string]string{"name": "Jeepers", "anotherDude": "Creepers"}, 79 | nil, nil, 80 | map[string]string{"dude": "{{name}}"}, 81 | "Dudes: Jeepers Creepers", 82 | }, 83 | { 84 | "Partials with slash paths", 85 | "Dudes: {{> shared/dude}}", 86 | map[string]string{"name": "Jeepers", "anotherDude": "Creepers"}, 87 | nil, nil, 88 | map[string]string{"shared/dude": "{{name}}"}, 89 | "Dudes: Jeepers", 90 | }, 91 | { 92 | "Partials with slash and point paths", 93 | "Dudes: {{> shared/dude.thing}}", 94 | map[string]string{"name": "Jeepers", "anotherDude": "Creepers"}, 95 | nil, nil, 96 | map[string]string{"shared/dude.thing": "{{name}}"}, 97 | "Dudes: Jeepers", 98 | }, 99 | 100 | // @todo "Global Partials" 101 | 102 | // @todo "Multiple partial registration" 103 | 104 | { 105 | "Partials with integer path", 106 | "Dudes: {{> 404}}", 107 | map[string]string{"name": "Jeepers", "anotherDude": "Creepers"}, 108 | nil, nil, 109 | map[string]string{"404": "{{name}}"}, // @note Difference with JS test: partial name is a string 110 | "Dudes: Jeepers", 111 | }, 112 | // @note This is not supported by our implementation. But really... who cares ? 113 | // { 114 | // "Partials with complex path", 115 | // "Dudes: {{> 404/asdf?.bar}}", 116 | // map[string]string{"name": "Jeepers", "anotherDude": "Creepers"}, 117 | // nil, nil, 118 | // map[string]string{"404/asdf?.bar": "{{name}}"}, 119 | // "Dudes: Jeepers", 120 | // }, 121 | { 122 | "Partials with escaped", 123 | "Dudes: {{> [+404/asdf?.bar]}}", 124 | map[string]string{"name": "Jeepers", "anotherDude": "Creepers"}, 125 | nil, nil, 126 | map[string]string{"+404/asdf?.bar": "{{name}}"}, 127 | "Dudes: Jeepers", 128 | }, 129 | { 130 | "Partials with string", 131 | "Dudes: {{> '+404/asdf?.bar'}}", 132 | map[string]string{"name": "Jeepers", "anotherDude": "Creepers"}, 133 | nil, nil, 134 | map[string]string{"+404/asdf?.bar": "{{name}}"}, 135 | "Dudes: Jeepers", 136 | }, 137 | { 138 | "should handle empty partial", 139 | "Dudes: {{#dudes}}{{> dude}}{{/dudes}}", 140 | map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}}, 141 | nil, nil, 142 | map[string]string{"dude": ""}, 143 | "Dudes: ", 144 | }, 145 | 146 | // @todo "throw on missing partial" 147 | 148 | // SKIP: "should pass compiler flags" 149 | 150 | { 151 | "standalone partials (1) - indented partials", 152 | "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}", 153 | map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}}, 154 | nil, nil, 155 | map[string]string{"dude": "{{name}}\n"}, 156 | "Dudes:\n Yehuda\n Alan\n", 157 | }, 158 | { 159 | "standalone partials (2) - nested indented partials", 160 | "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}", 161 | map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}}, 162 | nil, nil, 163 | map[string]string{"dude": "{{name}}\n {{> url}}", "url": "{{url}}!\n"}, 164 | "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n", 165 | }, 166 | 167 | // // @todo preventIndent option 168 | // { 169 | // "standalone partials (3) - prevent nested indented partials", 170 | // "Dudes:\n{{#dudes}}\n {{>dude}}\n{{/dudes}}", 171 | // map[string]interface{}{"dudes": []map[string]string{{"name": "Yehuda", "url": "http://yehuda"}, {"name": "Alan", "url": "http://alan"}}}, 172 | // nil, nil, 173 | // map[string]string{"dude": "{{name}}\n {{> url}}", "url": "{{url}}!\n"}, 174 | // "Dudes:\n Yehuda\n http://yehuda!\n Alan\n http://alan!\n", 175 | // }, 176 | 177 | // @todo "compat mode" 178 | } 179 | 180 | func TestPartials(t *testing.T) { 181 | launchTests(t, partialsTests) 182 | } 183 | -------------------------------------------------------------------------------- /handlebars/subexpressions_test.go: -------------------------------------------------------------------------------- 1 | package handlebars 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/aymerick/raymond" 7 | ) 8 | 9 | // 10 | // Those tests come from: 11 | // https://github.com/wycats/handlebars.js/blob/master/spec/subexpression.js 12 | // 13 | var subexpressionsTests = []Test{ 14 | { 15 | "arg-less helper", 16 | "{{foo (bar)}}!", 17 | map[string]interface{}{}, 18 | nil, 19 | map[string]interface{}{ 20 | "foo": func(val string) string { 21 | return val + val 22 | }, 23 | "bar": func() string { 24 | return "LOL" 25 | }, 26 | }, 27 | nil, 28 | "LOLLOL!", 29 | }, 30 | { 31 | "helper w args", 32 | "{{blog (equal a b)}}", 33 | map[string]interface{}{"bar": "LOL"}, 34 | nil, 35 | map[string]interface{}{ 36 | "blog": blogHelper, 37 | "equal": equalHelper, 38 | }, 39 | nil, 40 | "val is true", 41 | }, 42 | { 43 | "mixed paths and helpers", 44 | "{{blog baz.bat (equal a b) baz.bar}}", 45 | map[string]interface{}{"bar": "LOL", "baz": map[string]string{"bat": "foo!", "bar": "bar!"}}, 46 | nil, 47 | map[string]interface{}{ 48 | "blog": func(p, p2, p3 string) string { 49 | return "val is " + p + ", " + p2 + " and " + p3 50 | }, 51 | "equal": equalHelper, 52 | }, 53 | nil, 54 | "val is foo!, true and bar!", 55 | }, 56 | { 57 | "supports much nesting", 58 | "{{blog (equal (equal true true) true)}}", 59 | map[string]interface{}{"bar": "LOL"}, 60 | nil, 61 | map[string]interface{}{ 62 | "blog": blogHelper, 63 | "equal": equalHelper, 64 | }, 65 | nil, 66 | "val is true", 67 | }, 68 | 69 | { 70 | "GH-800 : Complex subexpressions (1)", 71 | "{{dash 'abc' (concat a b)}}", 72 | map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}}, 73 | nil, 74 | map[string]interface{}{"dash": dashHelper, "concat": concatHelper}, 75 | nil, 76 | "abc-ab", 77 | }, 78 | { 79 | "GH-800 : Complex subexpressions (2)", 80 | "{{dash d (concat a b)}}", 81 | map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}}, 82 | nil, 83 | map[string]interface{}{"dash": dashHelper, "concat": concatHelper}, 84 | nil, 85 | "d-ab", 86 | }, 87 | { 88 | "GH-800 : Complex subexpressions (3)", 89 | "{{dash c.c (concat a b)}}", 90 | map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}}, 91 | nil, 92 | map[string]interface{}{"dash": dashHelper, "concat": concatHelper}, 93 | nil, 94 | "c-ab", 95 | }, 96 | { 97 | "GH-800 : Complex subexpressions (4)", 98 | "{{dash (concat a b) c.c}}", 99 | map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}}, 100 | nil, 101 | map[string]interface{}{"dash": dashHelper, "concat": concatHelper}, 102 | nil, 103 | "ab-c", 104 | }, 105 | { 106 | "GH-800 : Complex subexpressions (5)", 107 | "{{dash (concat a e.e) c.c}}", 108 | map[string]interface{}{"a": "a", "b": "b", "c": map[string]string{"c": "c"}, "d": "d", "e": map[string]string{"e": "e"}}, 109 | nil, 110 | map[string]interface{}{"dash": dashHelper, "concat": concatHelper}, 111 | nil, 112 | "ae-c", 113 | }, 114 | 115 | { 116 | // note: test not relevant 117 | "provides each nested helper invocation its own options hash", 118 | "{{equal (equal true true) true}}", 119 | map[string]interface{}{}, 120 | nil, 121 | map[string]interface{}{ 122 | "equal": equalHelper, 123 | }, 124 | nil, 125 | "true", 126 | }, 127 | { 128 | "with hashes", 129 | "{{blog (equal (equal true true) true fun='yes')}}", 130 | map[string]interface{}{"bar": "LOL"}, 131 | nil, 132 | map[string]interface{}{ 133 | "blog": blogHelper, 134 | "equal": equalHelper, 135 | }, 136 | nil, 137 | "val is true", 138 | }, 139 | { 140 | "as hashes", 141 | "{{blog fun=(equal (blog fun=1) 'val is 1')}}", 142 | map[string]interface{}{}, 143 | nil, 144 | map[string]interface{}{ 145 | "blog": func(options *raymond.Options) string { 146 | return "val is " + options.HashStr("fun") 147 | }, 148 | "equal": equalHelper, 149 | }, 150 | nil, 151 | "val is true", 152 | }, 153 | { 154 | "multiple subexpressions in a hash", 155 | `{{input aria-label=(t "Name") placeholder=(t "Example User")}}`, 156 | map[string]interface{}{}, 157 | nil, 158 | map[string]interface{}{ 159 | "input": func(options *raymond.Options) raymond.SafeString { 160 | return raymond.SafeString(``) 161 | }, 162 | "t": func(param string) raymond.SafeString { 163 | return raymond.SafeString(param) 164 | }, 165 | }, 166 | nil, 167 | ``, 168 | }, 169 | { 170 | "multiple subexpressions in a hash with context", 171 | `{{input aria-label=(t item.field) placeholder=(t item.placeholder)}}`, 172 | map[string]map[string]string{"item": {"field": "Name", "placeholder": "Example User"}}, 173 | nil, 174 | map[string]interface{}{ 175 | "input": func(options *raymond.Options) raymond.SafeString { 176 | return raymond.SafeString(``) 177 | }, 178 | "t": func(param string) raymond.SafeString { 179 | return raymond.SafeString(param) 180 | }, 181 | }, 182 | nil, 183 | ``, 184 | }, 185 | 186 | // @todo "in string params mode" 187 | 188 | // @todo "as hashes in string params mode" 189 | 190 | { 191 | "subexpression functions on the context", 192 | "{{foo (bar)}}!", 193 | map[string]interface{}{"bar": func() string { return "LOL" }}, 194 | nil, 195 | map[string]interface{}{ 196 | "foo": func(val string) string { 197 | return val + val 198 | }, 199 | }, 200 | nil, 201 | "LOLLOL!", 202 | }, 203 | 204 | // @todo "subexpressions can't just be property lookups" should raise error 205 | } 206 | 207 | func TestSubexpressions(t *testing.T) { 208 | launchTests(t, subexpressionsTests) 209 | } 210 | -------------------------------------------------------------------------------- /handlebars/whitespace_test.go: -------------------------------------------------------------------------------- 1 | package handlebars 2 | 3 | import "testing" 4 | 5 | // 6 | // Those tests come from: 7 | // https://github.com/wycats/handlebars.js/blob/master/spec/whitespace-control.js 8 | // 9 | var whitespaceControlTests = []Test{ 10 | { 11 | "should strip whitespace around mustache calls (1)", 12 | " {{~foo~}} ", 13 | map[string]string{"foo": "bar<"}, 14 | nil, nil, nil, 15 | "bar<", 16 | }, 17 | { 18 | "should strip whitespace around mustache calls (2)", 19 | " {{~foo}} ", 20 | map[string]string{"foo": "bar<"}, 21 | nil, nil, nil, 22 | "bar< ", 23 | }, 24 | { 25 | "should strip whitespace around mustache calls (3)", 26 | " {{foo~}} ", 27 | map[string]string{"foo": "bar<"}, 28 | nil, nil, nil, 29 | " bar<", 30 | }, 31 | { 32 | "should strip whitespace around mustache calls (4)", 33 | " {{~&foo~}} ", 34 | map[string]string{"foo": "bar<"}, 35 | nil, nil, nil, 36 | "bar<", 37 | }, 38 | { 39 | "should strip whitespace around mustache calls (5)", 40 | " {{~{foo}~}} ", 41 | map[string]string{"foo": "bar<"}, 42 | nil, nil, nil, 43 | "bar<", 44 | }, 45 | { 46 | "should strip whitespace around mustache calls (6)", 47 | "1\n{{foo~}} \n\n 23\n{{bar}}4", 48 | nil, nil, nil, nil, 49 | "1\n23\n4", 50 | }, 51 | 52 | { 53 | "blocks - should strip whitespace around simple block calls (1)", 54 | " {{~#if foo~}} bar {{~/if~}} ", 55 | map[string]string{"foo": "bar<"}, 56 | nil, nil, nil, 57 | "bar", 58 | }, 59 | { 60 | "blocks - should strip whitespace around simple block calls (2)", 61 | " {{#if foo~}} bar {{/if~}} ", 62 | map[string]string{"foo": "bar<"}, 63 | nil, nil, nil, 64 | " bar ", 65 | }, 66 | { 67 | "blocks - should strip whitespace around simple block calls (3)", 68 | " {{~#if foo}} bar {{~/if}} ", 69 | map[string]string{"foo": "bar<"}, 70 | nil, nil, nil, 71 | " bar ", 72 | }, 73 | { 74 | "blocks - should strip whitespace around simple block calls (4)", 75 | " {{#if foo}} bar {{/if}} ", 76 | map[string]string{"foo": "bar<"}, 77 | nil, nil, nil, 78 | " bar ", 79 | }, 80 | { 81 | "blocks - should strip whitespace around simple block calls (5)", 82 | " \n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\n ", 83 | map[string]string{"foo": "bar<"}, 84 | nil, nil, nil, 85 | "bar", 86 | }, 87 | { 88 | "blocks - should strip whitespace around simple block calls (6)", 89 | " a\n\n{{~#if foo~}} \n\nbar \n\n{{~/if~}}\n\na ", 90 | map[string]string{"foo": "bar<"}, 91 | nil, nil, nil, 92 | " abara ", 93 | }, 94 | 95 | { 96 | "should strip whitespace around inverse block calls (1)", 97 | " {{~^if foo~}} bar {{~/if~}} ", 98 | nil, nil, nil, nil, 99 | "bar", 100 | }, 101 | { 102 | "should strip whitespace around inverse block calls (2)", 103 | " {{^if foo~}} bar {{/if~}} ", 104 | nil, nil, nil, nil, 105 | " bar ", 106 | }, 107 | { 108 | "should strip whitespace around inverse block calls (3)", 109 | " {{~^if foo}} bar {{~/if}} ", 110 | nil, nil, nil, nil, 111 | " bar ", 112 | }, 113 | { 114 | "should strip whitespace around inverse block calls (4)", 115 | " {{^if foo}} bar {{/if}} ", 116 | nil, nil, nil, nil, 117 | " bar ", 118 | }, 119 | { 120 | "should strip whitespace around inverse block calls (5)", 121 | " \n\n{{~^if foo~}} \n\nbar \n\n{{~/if~}}\n\n ", 122 | nil, nil, nil, nil, 123 | "bar", 124 | }, 125 | 126 | { 127 | "should strip whitespace around complex block calls (1)", 128 | "{{#if foo~}} bar {{~^~}} baz {{~/if}}", 129 | map[string]string{"foo": "bar<"}, 130 | nil, nil, nil, 131 | "bar", 132 | }, 133 | { 134 | "should strip whitespace around complex block calls (2)", 135 | "{{#if foo~}} bar {{^~}} baz {{/if}}", 136 | map[string]string{"foo": "bar<"}, 137 | nil, nil, nil, 138 | "bar ", 139 | }, 140 | { 141 | "should strip whitespace around complex block calls (3)", 142 | "{{#if foo}} bar {{~^~}} baz {{~/if}}", 143 | map[string]string{"foo": "bar<"}, 144 | nil, nil, nil, 145 | " bar", 146 | }, 147 | { 148 | "should strip whitespace around complex block calls (4)", 149 | "{{#if foo}} bar {{^~}} baz {{/if}}", 150 | map[string]string{"foo": "bar<"}, 151 | nil, nil, nil, 152 | " bar ", 153 | }, 154 | { 155 | "should strip whitespace around complex block calls (5)", 156 | "{{#if foo~}} bar {{~else~}} baz {{~/if}}", 157 | map[string]string{"foo": "bar<"}, 158 | nil, nil, nil, 159 | "bar", 160 | }, 161 | { 162 | "should strip whitespace around complex block calls (6)", 163 | "\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n", 164 | map[string]string{"foo": "bar<"}, 165 | nil, nil, nil, 166 | "bar", 167 | }, 168 | { 169 | "should strip whitespace around complex block calls (7)", 170 | "\n\n{{~#if foo~}} \n\n{{{foo}}} \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n", 171 | map[string]string{"foo": "bar<"}, 172 | nil, nil, nil, 173 | "bar<", 174 | }, 175 | { 176 | "should strip whitespace around complex block calls (8)", 177 | "{{#if foo~}} bar {{~^~}} baz {{~/if}}", 178 | nil, nil, nil, nil, 179 | "baz", 180 | }, 181 | { 182 | "should strip whitespace around complex block calls (9)", 183 | "{{#if foo}} bar {{~^~}} baz {{/if}}", 184 | nil, nil, nil, nil, 185 | "baz ", 186 | }, 187 | { 188 | "should strip whitespace around complex block calls (10)", 189 | "{{#if foo~}} bar {{~^}} baz {{~/if}}", 190 | nil, nil, nil, nil, 191 | " baz", 192 | }, 193 | { 194 | "should strip whitespace around complex block calls (11)", 195 | "{{#if foo~}} bar {{~^}} baz {{/if}}", 196 | nil, nil, nil, nil, 197 | " baz ", 198 | }, 199 | { 200 | "should strip whitespace around complex block calls (12)", 201 | "{{#if foo~}} bar {{~else~}} baz {{~/if}}", 202 | nil, nil, nil, nil, 203 | "baz", 204 | }, 205 | { 206 | "should strip whitespace around complex block calls (13)", 207 | "\n\n{{~#if foo~}} \n\nbar \n\n{{~^~}} \n\nbaz \n\n{{~/if~}}\n\n", 208 | nil, nil, nil, nil, 209 | "baz", 210 | }, 211 | 212 | { 213 | "should strip whitespace around partials (1)", 214 | "foo {{~> dude~}} ", 215 | nil, nil, nil, 216 | map[string]string{"dude": "bar"}, 217 | "foobar", 218 | }, 219 | { 220 | "should strip whitespace around partials (2)", 221 | "foo {{> dude~}} ", 222 | nil, nil, nil, 223 | map[string]string{"dude": "bar"}, 224 | "foo bar", 225 | }, 226 | { 227 | "should strip whitespace around partials (3)", 228 | "foo {{> dude}} ", 229 | nil, nil, nil, 230 | map[string]string{"dude": "bar"}, 231 | "foo bar ", 232 | }, 233 | { 234 | "should strip whitespace around partials (4)", 235 | "foo\n {{~> dude}} ", 236 | nil, nil, nil, 237 | map[string]string{"dude": "bar"}, 238 | "foobar", 239 | }, 240 | { 241 | "should strip whitespace around partials (5)", 242 | "foo\n {{> dude}} ", 243 | nil, nil, nil, 244 | map[string]string{"dude": "bar"}, 245 | "foo\n bar", 246 | }, 247 | 248 | { 249 | "should only strip whitespace once", 250 | " {{~foo~}} {{foo}} {{foo}} ", 251 | map[string]string{"foo": "bar"}, 252 | nil, nil, nil, 253 | "barbar bar ", 254 | }, 255 | } 256 | 257 | func TestWhitespaceControl(t *testing.T) { 258 | launchTests(t, whitespaceControlTests) 259 | } 260 | -------------------------------------------------------------------------------- /helper.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "reflect" 7 | "sync" 8 | ) 9 | 10 | // Options represents the options argument provided to helpers and context functions. 11 | type Options struct { 12 | // evaluation visitor 13 | eval *evalVisitor 14 | 15 | // params 16 | params []interface{} 17 | hash map[string]interface{} 18 | } 19 | 20 | // helpers stores all globally registered helpers 21 | var helpers = make(map[string]reflect.Value) 22 | 23 | // protects global helpers 24 | var helpersMutex sync.RWMutex 25 | 26 | func init() { 27 | // register builtin helpers 28 | RegisterHelper("if", ifHelper) 29 | RegisterHelper("unless", unlessHelper) 30 | RegisterHelper("with", withHelper) 31 | RegisterHelper("each", eachHelper) 32 | RegisterHelper("log", logHelper) 33 | RegisterHelper("lookup", lookupHelper) 34 | RegisterHelper("equal", equalHelper) 35 | } 36 | 37 | // RegisterHelper registers a global helper. That helper will be available to all templates. 38 | func RegisterHelper(name string, helper interface{}) { 39 | helpersMutex.Lock() 40 | defer helpersMutex.Unlock() 41 | 42 | if helpers[name] != zero { 43 | panic(fmt.Errorf("Helper already registered: %s", name)) 44 | } 45 | 46 | val := reflect.ValueOf(helper) 47 | ensureValidHelper(name, val) 48 | 49 | helpers[name] = val 50 | } 51 | 52 | // RegisterHelpers registers several global helpers. Those helpers will be available to all templates. 53 | func RegisterHelpers(helpers map[string]interface{}) { 54 | for name, helper := range helpers { 55 | RegisterHelper(name, helper) 56 | } 57 | } 58 | 59 | // RemoveHelper unregisters a global helper 60 | func RemoveHelper(name string) { 61 | helpersMutex.Lock() 62 | defer helpersMutex.Unlock() 63 | 64 | delete(helpers, name) 65 | } 66 | 67 | // RemoveAllHelpers unregisters all global helpers 68 | func RemoveAllHelpers() { 69 | helpersMutex.Lock() 70 | defer helpersMutex.Unlock() 71 | 72 | helpers = make(map[string]reflect.Value) 73 | } 74 | 75 | // ensureValidHelper panics if given helper is not valid 76 | func ensureValidHelper(name string, funcValue reflect.Value) { 77 | if funcValue.Kind() != reflect.Func { 78 | panic(fmt.Errorf("Helper must be a function: %s", name)) 79 | } 80 | 81 | funcType := funcValue.Type() 82 | 83 | if funcType.NumOut() != 1 { 84 | panic(fmt.Errorf("Helper function must return a string or a SafeString: %s", name)) 85 | } 86 | 87 | // @todo Check if first returned value is a string, SafeString or interface{} ? 88 | } 89 | 90 | // findHelper finds a globally registered helper 91 | func findHelper(name string) reflect.Value { 92 | helpersMutex.RLock() 93 | defer helpersMutex.RUnlock() 94 | 95 | return helpers[name] 96 | } 97 | 98 | // newOptions instanciates a new Options 99 | func newOptions(eval *evalVisitor, params []interface{}, hash map[string]interface{}) *Options { 100 | return &Options{ 101 | eval: eval, 102 | params: params, 103 | hash: hash, 104 | } 105 | } 106 | 107 | // newEmptyOptions instanciates a new empty Options 108 | func newEmptyOptions(eval *evalVisitor) *Options { 109 | return &Options{ 110 | eval: eval, 111 | hash: make(map[string]interface{}), 112 | } 113 | } 114 | 115 | // 116 | // Context Values 117 | // 118 | 119 | // Value returns field value from current context. 120 | func (options *Options) Value(name string) interface{} { 121 | value := options.eval.evalField(options.eval.curCtx(), name, false) 122 | if !value.IsValid() { 123 | return nil 124 | } 125 | 126 | return value.Interface() 127 | } 128 | 129 | // ValueStr returns string representation of field value from current context. 130 | func (options *Options) ValueStr(name string) string { 131 | return Str(options.Value(name)) 132 | } 133 | 134 | // Ctx returns current evaluation context. 135 | func (options *Options) Ctx() interface{} { 136 | return options.eval.curCtx().Interface() 137 | } 138 | 139 | // 140 | // Hash Arguments 141 | // 142 | 143 | // HashProp returns hash property. 144 | func (options *Options) HashProp(name string) interface{} { 145 | return options.hash[name] 146 | } 147 | 148 | // HashStr returns string representation of hash property. 149 | func (options *Options) HashStr(name string) string { 150 | return Str(options.hash[name]) 151 | } 152 | 153 | // Hash returns entire hash. 154 | func (options *Options) Hash() map[string]interface{} { 155 | return options.hash 156 | } 157 | 158 | // 159 | // Parameters 160 | // 161 | 162 | // Param returns parameter at given position. 163 | func (options *Options) Param(pos int) interface{} { 164 | if len(options.params) > pos { 165 | return options.params[pos] 166 | } 167 | 168 | return nil 169 | } 170 | 171 | // ParamStr returns string representation of parameter at given position. 172 | func (options *Options) ParamStr(pos int) string { 173 | return Str(options.Param(pos)) 174 | } 175 | 176 | // Params returns all parameters. 177 | func (options *Options) Params() []interface{} { 178 | return options.params 179 | } 180 | 181 | // 182 | // Private data 183 | // 184 | 185 | // Data returns private data value. 186 | func (options *Options) Data(name string) interface{} { 187 | return options.eval.dataFrame.Get(name) 188 | } 189 | 190 | // DataStr returns string representation of private data value. 191 | func (options *Options) DataStr(name string) string { 192 | return Str(options.eval.dataFrame.Get(name)) 193 | } 194 | 195 | // DataFrame returns current private data frame. 196 | func (options *Options) DataFrame() *DataFrame { 197 | return options.eval.dataFrame 198 | } 199 | 200 | // NewDataFrame instanciates a new data frame that is a copy of current evaluation data frame. 201 | // 202 | // Parent of returned data frame is set to current evaluation data frame. 203 | func (options *Options) NewDataFrame() *DataFrame { 204 | return options.eval.dataFrame.Copy() 205 | } 206 | 207 | // newIterDataFrame instanciates a new data frame and set iteration specific vars 208 | func (options *Options) newIterDataFrame(length int, i int, key interface{}) *DataFrame { 209 | return options.eval.dataFrame.newIterDataFrame(length, i, key) 210 | } 211 | 212 | // 213 | // Evaluation 214 | // 215 | 216 | // evalBlock evaluates block with given context, private data and iteration key 217 | func (options *Options) evalBlock(ctx interface{}, data *DataFrame, key interface{}) string { 218 | result := "" 219 | 220 | if block := options.eval.curBlock(); (block != nil) && (block.Program != nil) { 221 | result = options.eval.evalProgram(block.Program, ctx, data, key) 222 | } 223 | 224 | return result 225 | } 226 | 227 | // Fn evaluates block with current evaluation context. 228 | func (options *Options) Fn() string { 229 | return options.evalBlock(nil, nil, nil) 230 | } 231 | 232 | // FnCtxData evaluates block with given context and private data frame. 233 | func (options *Options) FnCtxData(ctx interface{}, data *DataFrame) string { 234 | return options.evalBlock(ctx, data, nil) 235 | } 236 | 237 | // FnWith evaluates block with given context. 238 | func (options *Options) FnWith(ctx interface{}) string { 239 | return options.evalBlock(ctx, nil, nil) 240 | } 241 | 242 | // FnData evaluates block with given private data frame. 243 | func (options *Options) FnData(data *DataFrame) string { 244 | return options.evalBlock(nil, data, nil) 245 | } 246 | 247 | // Inverse evaluates "else block". 248 | func (options *Options) Inverse() string { 249 | result := "" 250 | if block := options.eval.curBlock(); (block != nil) && (block.Inverse != nil) { 251 | result, _ = block.Inverse.Accept(options.eval).(string) 252 | } 253 | 254 | return result 255 | } 256 | 257 | // Eval evaluates field for given context. 258 | func (options *Options) Eval(ctx interface{}, field string) interface{} { 259 | if ctx == nil { 260 | return nil 261 | } 262 | 263 | if field == "" { 264 | return nil 265 | } 266 | 267 | val := options.eval.evalField(reflect.ValueOf(ctx), field, false) 268 | if !val.IsValid() { 269 | return nil 270 | } 271 | 272 | return val.Interface() 273 | } 274 | 275 | // 276 | // Misc 277 | // 278 | 279 | // isIncludableZero returns true if 'includeZero' option is set and first param is the number 0 280 | func (options *Options) isIncludableZero() bool { 281 | b, ok := options.HashProp("includeZero").(bool) 282 | if ok && b { 283 | nb, ok := options.Param(0).(int) 284 | if ok && nb == 0 { 285 | return true 286 | } 287 | } 288 | 289 | return false 290 | } 291 | 292 | // 293 | // Builtin helpers 294 | // 295 | 296 | // #if block helper 297 | func ifHelper(conditional interface{}, options *Options) interface{} { 298 | if options.isIncludableZero() || IsTrue(conditional) { 299 | return options.Fn() 300 | } 301 | 302 | return options.Inverse() 303 | } 304 | 305 | // #unless block helper 306 | func unlessHelper(conditional interface{}, options *Options) interface{} { 307 | if options.isIncludableZero() || IsTrue(conditional) { 308 | return options.Inverse() 309 | } 310 | 311 | return options.Fn() 312 | } 313 | 314 | // #with block helper 315 | func withHelper(context interface{}, options *Options) interface{} { 316 | if IsTrue(context) { 317 | return options.FnWith(context) 318 | } 319 | 320 | return options.Inverse() 321 | } 322 | 323 | // #each block helper 324 | func eachHelper(context interface{}, options *Options) interface{} { 325 | if !IsTrue(context) { 326 | return options.Inverse() 327 | } 328 | 329 | result := "" 330 | 331 | val := reflect.ValueOf(context) 332 | switch val.Kind() { 333 | case reflect.Array, reflect.Slice: 334 | for i := 0; i < val.Len(); i++ { 335 | // computes private data 336 | data := options.newIterDataFrame(val.Len(), i, nil) 337 | 338 | // evaluates block 339 | result += options.evalBlock(val.Index(i).Interface(), data, i) 340 | } 341 | case reflect.Map: 342 | // note: a go hash is not ordered, so result may vary, this behaviour differs from the JS implementation 343 | keys := val.MapKeys() 344 | for i := 0; i < len(keys); i++ { 345 | key := keys[i].Interface() 346 | ctx := val.MapIndex(keys[i]).Interface() 347 | 348 | // computes private data 349 | data := options.newIterDataFrame(len(keys), i, key) 350 | 351 | // evaluates block 352 | result += options.evalBlock(ctx, data, key) 353 | } 354 | case reflect.Struct: 355 | var exportedFields []int 356 | 357 | // collect exported fields only 358 | for i := 0; i < val.NumField(); i++ { 359 | if tField := val.Type().Field(i); tField.PkgPath == "" { 360 | exportedFields = append(exportedFields, i) 361 | } 362 | } 363 | 364 | for i, fieldIndex := range exportedFields { 365 | key := val.Type().Field(fieldIndex).Name 366 | ctx := val.Field(fieldIndex).Interface() 367 | 368 | // computes private data 369 | data := options.newIterDataFrame(len(exportedFields), i, key) 370 | 371 | // evaluates block 372 | result += options.evalBlock(ctx, data, key) 373 | } 374 | } 375 | 376 | return result 377 | } 378 | 379 | // #log helper 380 | func logHelper(message string) interface{} { 381 | log.Print(message) 382 | return "" 383 | } 384 | 385 | // #lookup helper 386 | func lookupHelper(obj interface{}, field string, options *Options) interface{} { 387 | return Str(options.Eval(obj, field)) 388 | } 389 | 390 | // #equal helper 391 | // Ref: https://github.com/aymerick/raymond/issues/7 392 | func equalHelper(a interface{}, b interface{}, options *Options) interface{} { 393 | if Str(a) == Str(b) { 394 | return options.Fn() 395 | } 396 | 397 | return "" 398 | } 399 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import "testing" 4 | 5 | const ( 6 | VERBOSE = false 7 | ) 8 | 9 | // 10 | // Helpers 11 | // 12 | 13 | func barHelper(options *Options) string { return "bar" } 14 | 15 | func echoHelper(str string, nb int) string { 16 | result := "" 17 | for i := 0; i < nb; i++ { 18 | result += str 19 | } 20 | 21 | return result 22 | } 23 | 24 | func boolHelper(b bool) string { 25 | if b { 26 | return "yes it is" 27 | } 28 | 29 | return "absolutely not" 30 | } 31 | 32 | func gnakHelper(nb int) string { 33 | result := "" 34 | for i := 0; i < nb; i++ { 35 | result += "GnAK!" 36 | } 37 | 38 | return result 39 | } 40 | 41 | // 42 | // Tests 43 | // 44 | 45 | var helperTests = []Test{ 46 | { 47 | "simple helper", 48 | `{{foo}}`, 49 | nil, nil, 50 | map[string]interface{}{"foo": barHelper}, 51 | nil, 52 | `bar`, 53 | }, 54 | { 55 | "helper with literal string param", 56 | `{{echo "foo" 1}}`, 57 | nil, nil, 58 | map[string]interface{}{"echo": echoHelper}, 59 | nil, 60 | `foo`, 61 | }, 62 | { 63 | "helper with identifier param", 64 | `{{echo foo 1}}`, 65 | map[string]interface{}{"foo": "bar"}, 66 | nil, 67 | map[string]interface{}{"echo": echoHelper}, 68 | nil, 69 | `bar`, 70 | }, 71 | { 72 | "helper with literal boolean param", 73 | `{{bool true}}`, 74 | nil, nil, 75 | map[string]interface{}{"bool": boolHelper}, 76 | nil, 77 | `yes it is`, 78 | }, 79 | { 80 | "helper with literal boolean param", 81 | `{{bool false}}`, 82 | nil, nil, 83 | map[string]interface{}{"bool": boolHelper}, 84 | nil, 85 | `absolutely not`, 86 | }, 87 | { 88 | "helper with literal boolean param", 89 | `{{gnak 5}}`, 90 | nil, nil, 91 | map[string]interface{}{"gnak": gnakHelper}, 92 | nil, 93 | `GnAK!GnAK!GnAK!GnAK!GnAK!`, 94 | }, 95 | { 96 | "helper with several parameters", 97 | `{{echo "GnAK!" 3}}`, 98 | nil, nil, 99 | map[string]interface{}{"echo": echoHelper}, 100 | nil, 101 | `GnAK!GnAK!GnAK!`, 102 | }, 103 | { 104 | "#if helper with true literal", 105 | `{{#if true}}YES MAN{{/if}}`, 106 | nil, nil, nil, nil, 107 | `YES MAN`, 108 | }, 109 | { 110 | "#if helper with false literal", 111 | `{{#if false}}YES MAN{{/if}}`, 112 | nil, nil, nil, nil, 113 | ``, 114 | }, 115 | { 116 | "#if helper with truthy identifier", 117 | `{{#if ok}}YES MAN{{/if}}`, 118 | map[string]interface{}{"ok": true}, 119 | nil, nil, nil, 120 | `YES MAN`, 121 | }, 122 | { 123 | "#if helper with falsy identifier", 124 | `{{#if ok}}YES MAN{{/if}}`, 125 | map[string]interface{}{"ok": false}, 126 | nil, nil, nil, 127 | ``, 128 | }, 129 | { 130 | "#unless helper with true literal", 131 | `{{#unless true}}YES MAN{{/unless}}`, 132 | nil, nil, nil, nil, 133 | ``, 134 | }, 135 | { 136 | "#unless helper with false literal", 137 | `{{#unless false}}YES MAN{{/unless}}`, 138 | nil, nil, nil, nil, 139 | `YES MAN`, 140 | }, 141 | { 142 | "#unless helper with truthy identifier", 143 | `{{#unless ok}}YES MAN{{/unless}}`, 144 | map[string]interface{}{"ok": true}, 145 | nil, nil, nil, 146 | ``, 147 | }, 148 | { 149 | "#unless helper with falsy identifier", 150 | `{{#unless ok}}YES MAN{{/unless}}`, 151 | map[string]interface{}{"ok": false}, 152 | nil, nil, nil, 153 | `YES MAN`, 154 | }, 155 | { 156 | "#equal helper with same string var", 157 | `{{#equal foo "bar"}}YES MAN{{/equal}}`, 158 | map[string]interface{}{"foo": "bar"}, 159 | nil, nil, nil, 160 | `YES MAN`, 161 | }, 162 | { 163 | "#equal helper with different string var", 164 | `{{#equal foo "baz"}}YES MAN{{/equal}}`, 165 | map[string]interface{}{"foo": "bar"}, 166 | nil, nil, nil, 167 | ``, 168 | }, 169 | { 170 | "#equal helper with same string vars", 171 | `{{#equal foo bar}}YES MAN{{/equal}}`, 172 | map[string]interface{}{"foo": "baz", "bar": "baz"}, 173 | nil, nil, nil, 174 | `YES MAN`, 175 | }, 176 | { 177 | "#equal helper with different string vars", 178 | `{{#equal foo bar}}YES MAN{{/equal}}`, 179 | map[string]interface{}{"foo": "baz", "bar": "tag"}, 180 | nil, nil, nil, 181 | ``, 182 | }, 183 | { 184 | "#equal helper with same integer var", 185 | `{{#equal foo 1}}YES MAN{{/equal}}`, 186 | map[string]interface{}{"foo": 1}, 187 | nil, nil, nil, 188 | `YES MAN`, 189 | }, 190 | { 191 | "#equal helper with different integer var", 192 | `{{#equal foo 0}}YES MAN{{/equal}}`, 193 | map[string]interface{}{"foo": 1}, 194 | nil, nil, nil, 195 | ``, 196 | }, 197 | { 198 | "#equal helper inside HTML tag", 199 | ``, 200 | map[string]interface{}{"value": "test"}, 201 | nil, nil, nil, 202 | ``, 203 | }, 204 | { 205 | "#equal full example", 206 | `{{#equal foo "bar"}}foo is bar{{/equal}} 207 | {{#equal foo baz}}foo is the same as baz{{/equal}} 208 | {{#equal nb 0}}nothing{{/equal}} 209 | {{#equal nb 1}}there is one{{/equal}} 210 | {{#equal nb "1"}}everything is stringified before comparison{{/equal}}`, 211 | map[string]interface{}{ 212 | "foo": "bar", 213 | "baz": "bar", 214 | "nb": 1, 215 | }, 216 | nil, nil, nil, 217 | `foo is bar 218 | foo is the same as baz 219 | 220 | there is one 221 | everything is stringified before comparison`, 222 | }, 223 | } 224 | 225 | // 226 | // Let's go 227 | // 228 | 229 | func TestHelper(t *testing.T) { 230 | t.Parallel() 231 | 232 | launchTests(t, helperTests) 233 | } 234 | 235 | func TestRemoveHelper(t *testing.T) { 236 | RegisterHelper("testremovehelper", func() string { return "" }) 237 | if _, ok := helpers["testremovehelper"]; !ok { 238 | t.Error("Failed to register global helper") 239 | } 240 | 241 | RemoveHelper("testremovehelper") 242 | if _, ok := helpers["testremovehelper"]; ok { 243 | t.Error("Failed to remove global helper") 244 | } 245 | } 246 | 247 | // 248 | // Fixes: https://github.com/aymerick/raymond/issues/2 249 | // 250 | 251 | type Author struct { 252 | FirstName string 253 | LastName string 254 | } 255 | 256 | func TestHelperCtx(t *testing.T) { 257 | RegisterHelper("template", func(name string, options *Options) SafeString { 258 | context := options.Ctx() 259 | 260 | template := name + " - {{ firstName }} {{ lastName }}" 261 | result, _ := Render(template, context) 262 | 263 | return SafeString(result) 264 | }) 265 | 266 | template := `By {{ template "namefile" }}` 267 | context := Author{"Alan", "Johnson"} 268 | 269 | result, _ := Render(template, context) 270 | if result != "By namefile - Alan Johnson" { 271 | t.Errorf("Failed to render template in helper: %q", result) 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /lexer/lexer.go: -------------------------------------------------------------------------------- 1 | // Package lexer provides a handlebars tokenizer. 2 | package lexer 3 | 4 | import ( 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | "unicode" 9 | "unicode/utf8" 10 | ) 11 | 12 | // References: 13 | // - https://github.com/wycats/handlebars.js/blob/master/src/handlebars.l 14 | // - https://github.com/golang/go/blob/master/src/text/template/parse/lex.go 15 | 16 | const ( 17 | // Mustaches detection 18 | escapedEscapedOpenMustache = "\\\\{{" 19 | escapedOpenMustache = "\\{{" 20 | openMustache = "{{" 21 | closeMustache = "}}" 22 | closeStripMustache = "~}}" 23 | closeUnescapedStripMustache = "}~}}" 24 | ) 25 | 26 | const eof = -1 27 | 28 | // lexFunc represents a function that returns the next lexer function. 29 | type lexFunc func(*Lexer) lexFunc 30 | 31 | // Lexer is a lexical analyzer. 32 | type Lexer struct { 33 | input string // input to scan 34 | name string // lexer name, used for testing purpose 35 | tokens chan Token // channel of scanned tokens 36 | nextFunc lexFunc // the next function to execute 37 | 38 | pos int // current byte position in input string 39 | line int // current line position in input string 40 | width int // size of last rune scanned from input string 41 | start int // start position of the token we are scanning 42 | 43 | // the shameful contextual properties needed because `nextFunc` is not enough 44 | closeComment *regexp.Regexp // regexp to scan close of current comment 45 | rawBlock bool // are we parsing a raw block content ? 46 | } 47 | 48 | var ( 49 | lookheadChars = `[\s` + regexp.QuoteMeta("=~}/)|") + `]` 50 | literalLookheadChars = `[\s` + regexp.QuoteMeta("~})") + `]` 51 | 52 | // characters not allowed in an identifier 53 | unallowedIDChars = " \n\t!\"#%&'()*+,./;<=>@[\\]^`{|}~" 54 | 55 | // regular expressions 56 | rID = regexp.MustCompile(`^[^` + regexp.QuoteMeta(unallowedIDChars) + `]+`) 57 | rDotID = regexp.MustCompile(`^\.` + lookheadChars) 58 | rTrue = regexp.MustCompile(`^true` + literalLookheadChars) 59 | rFalse = regexp.MustCompile(`^false` + literalLookheadChars) 60 | rOpenRaw = regexp.MustCompile(`^\{\{\{\{`) 61 | rCloseRaw = regexp.MustCompile(`^\}\}\}\}`) 62 | rOpenEndRaw = regexp.MustCompile(`^\{\{\{\{/`) 63 | rOpenEndRawLookAhead = regexp.MustCompile(`\{\{\{\{/`) 64 | rOpenUnescaped = regexp.MustCompile(`^\{\{~?\{`) 65 | rCloseUnescaped = regexp.MustCompile(`^\}~?\}\}`) 66 | rOpenBlock = regexp.MustCompile(`^\{\{~?#`) 67 | rOpenEndBlock = regexp.MustCompile(`^\{\{~?/`) 68 | rOpenPartial = regexp.MustCompile(`^\{\{~?>`) 69 | // {{^}} or {{else}} 70 | rInverse = regexp.MustCompile(`^(\{\{~?\^\s*~?\}\}|\{\{~?\s*else\s*~?\}\})`) 71 | rOpenInverse = regexp.MustCompile(`^\{\{~?\^`) 72 | rOpenInverseChain = regexp.MustCompile(`^\{\{~?\s*else`) 73 | // {{ or {{& 74 | rOpen = regexp.MustCompile(`^\{\{~?&?`) 75 | rClose = regexp.MustCompile(`^~?\}\}`) 76 | rOpenBlockParams = regexp.MustCompile(`^as\s+\|`) 77 | // {{!-- ... --}} 78 | rOpenCommentDash = regexp.MustCompile(`^\{\{~?!--\s*`) 79 | rCloseCommentDash = regexp.MustCompile(`^\s*--~?\}\}`) 80 | // {{! ... }} 81 | rOpenComment = regexp.MustCompile(`^\{\{~?!\s*`) 82 | rCloseComment = regexp.MustCompile(`^\s*~?\}\}`) 83 | ) 84 | 85 | // Scan scans given input. 86 | // 87 | // Tokens can then be fetched sequentially thanks to NextToken() function on returned lexer. 88 | func Scan(input string) *Lexer { 89 | return scanWithName(input, "") 90 | } 91 | 92 | // scanWithName scans given input, with a name used for testing 93 | // 94 | // Tokens can then be fetched sequentially thanks to NextToken() function on returned lexer. 95 | func scanWithName(input string, name string) *Lexer { 96 | result := &Lexer{ 97 | input: input, 98 | name: name, 99 | tokens: make(chan Token), 100 | line: 1, 101 | } 102 | 103 | go result.run() 104 | 105 | return result 106 | } 107 | 108 | // Collect scans and collect all tokens. 109 | // 110 | // This should be used for debugging purpose only. You should use Scan() and lexer.NextToken() functions instead. 111 | func Collect(input string) []Token { 112 | var result []Token 113 | 114 | l := Scan(input) 115 | for { 116 | token := l.NextToken() 117 | result = append(result, token) 118 | 119 | if token.Kind == TokenEOF || token.Kind == TokenError { 120 | break 121 | } 122 | } 123 | 124 | return result 125 | } 126 | 127 | // NextToken returns the next scanned token. 128 | func (l *Lexer) NextToken() Token { 129 | result := <-l.tokens 130 | 131 | return result 132 | } 133 | 134 | // run starts lexical analysis 135 | func (l *Lexer) run() { 136 | for l.nextFunc = lexContent; l.nextFunc != nil; { 137 | l.nextFunc = l.nextFunc(l) 138 | } 139 | } 140 | 141 | // next returns next character from input, or eof of there is nothing left to scan 142 | func (l *Lexer) next() rune { 143 | if l.pos >= len(l.input) { 144 | l.width = 0 145 | return eof 146 | } 147 | 148 | r, w := utf8.DecodeRuneInString(l.input[l.pos:]) 149 | l.width = w 150 | l.pos += l.width 151 | 152 | return r 153 | } 154 | 155 | func (l *Lexer) produce(kind TokenKind, val string) { 156 | l.tokens <- Token{kind, val, l.start, l.line} 157 | 158 | // scanning a new token 159 | l.start = l.pos 160 | 161 | // update line number 162 | l.line += strings.Count(val, "\n") 163 | } 164 | 165 | // emit emits a new scanned token 166 | func (l *Lexer) emit(kind TokenKind) { 167 | l.produce(kind, l.input[l.start:l.pos]) 168 | } 169 | 170 | // emitContent emits scanned content 171 | func (l *Lexer) emitContent() { 172 | if l.pos > l.start { 173 | l.emit(TokenContent) 174 | } 175 | } 176 | 177 | // emitString emits a scanned string 178 | func (l *Lexer) emitString(delimiter rune) { 179 | str := l.input[l.start:l.pos] 180 | 181 | // replace escaped delimiters 182 | str = strings.Replace(str, "\\"+string(delimiter), string(delimiter), -1) 183 | 184 | l.produce(TokenString, str) 185 | } 186 | 187 | // peek returns but does not consume the next character in the input 188 | func (l *Lexer) peek() rune { 189 | r := l.next() 190 | l.backup() 191 | return r 192 | } 193 | 194 | // backup steps back one character 195 | // 196 | // WARNING: Can only be called once per call of next 197 | func (l *Lexer) backup() { 198 | l.pos -= l.width 199 | } 200 | 201 | // ignoreskips all characters that have been scanned up to current position 202 | func (l *Lexer) ignore() { 203 | l.start = l.pos 204 | } 205 | 206 | // accept scans the next character if it is included in given string 207 | func (l *Lexer) accept(valid string) bool { 208 | if strings.IndexRune(valid, l.next()) >= 0 { 209 | return true 210 | } 211 | 212 | l.backup() 213 | 214 | return false 215 | } 216 | 217 | // acceptRun scans all following characters that are part of given string 218 | func (l *Lexer) acceptRun(valid string) { 219 | for strings.IndexRune(valid, l.next()) >= 0 { 220 | } 221 | 222 | l.backup() 223 | } 224 | 225 | // errorf emits an error token 226 | func (l *Lexer) errorf(format string, args ...interface{}) lexFunc { 227 | l.tokens <- Token{TokenError, fmt.Sprintf(format, args...), l.start, l.line} 228 | return nil 229 | } 230 | 231 | // isString returns true if content at current scanning position starts with given string 232 | func (l *Lexer) isString(str string) bool { 233 | return strings.HasPrefix(l.input[l.pos:], str) 234 | } 235 | 236 | // findRegexp returns the first string from current scanning position that matches given regular expression 237 | func (l *Lexer) findRegexp(r *regexp.Regexp) string { 238 | return r.FindString(l.input[l.pos:]) 239 | } 240 | 241 | // indexRegexp returns the index of the first string from current scanning position that matches given regular expression 242 | // 243 | // It returns -1 if not found 244 | func (l *Lexer) indexRegexp(r *regexp.Regexp) int { 245 | loc := r.FindStringIndex(l.input[l.pos:]) 246 | if loc == nil { 247 | return -1 248 | } 249 | return loc[0] 250 | } 251 | 252 | // lexContent scans content (ie: not between mustaches) 253 | func lexContent(l *Lexer) lexFunc { 254 | var next lexFunc 255 | 256 | if l.rawBlock { 257 | if i := l.indexRegexp(rOpenEndRawLookAhead); i != -1 { 258 | // {{{{/ 259 | l.rawBlock = false 260 | l.pos += i 261 | 262 | next = lexOpenMustache 263 | } else { 264 | return l.errorf("Unclosed raw block") 265 | } 266 | } else if l.isString(escapedEscapedOpenMustache) { 267 | // \\{{ 268 | 269 | // emit content with only one escaped escape 270 | l.next() 271 | l.emitContent() 272 | 273 | // ignore second escaped escape 274 | l.next() 275 | l.ignore() 276 | 277 | next = lexContent 278 | } else if l.isString(escapedOpenMustache) { 279 | // \{{ 280 | next = lexEscapedOpenMustache 281 | } else if str := l.findRegexp(rOpenCommentDash); str != "" { 282 | // {{!-- 283 | l.closeComment = rCloseCommentDash 284 | 285 | next = lexComment 286 | } else if str := l.findRegexp(rOpenComment); str != "" { 287 | // {{! 288 | l.closeComment = rCloseComment 289 | 290 | next = lexComment 291 | } else if l.isString(openMustache) { 292 | // {{ 293 | next = lexOpenMustache 294 | } 295 | 296 | if next != nil { 297 | // emit scanned content 298 | l.emitContent() 299 | 300 | // scan next token 301 | return next 302 | } 303 | 304 | // scan next rune 305 | if l.next() == eof { 306 | // emit scanned content 307 | l.emitContent() 308 | 309 | // this is over 310 | l.emit(TokenEOF) 311 | return nil 312 | } 313 | 314 | // continue content scanning 315 | return lexContent 316 | } 317 | 318 | // lexEscapedOpenMustache scans \{{ 319 | func lexEscapedOpenMustache(l *Lexer) lexFunc { 320 | // ignore escape character 321 | l.next() 322 | l.ignore() 323 | 324 | // scan mustaches 325 | for l.peek() == '{' { 326 | l.next() 327 | } 328 | 329 | return lexContent 330 | } 331 | 332 | // lexOpenMustache scans {{ 333 | func lexOpenMustache(l *Lexer) lexFunc { 334 | var str string 335 | var tok TokenKind 336 | 337 | nextFunc := lexExpression 338 | 339 | if str = l.findRegexp(rOpenEndRaw); str != "" { 340 | tok = TokenOpenEndRawBlock 341 | } else if str = l.findRegexp(rOpenRaw); str != "" { 342 | tok = TokenOpenRawBlock 343 | l.rawBlock = true 344 | } else if str = l.findRegexp(rOpenUnescaped); str != "" { 345 | tok = TokenOpenUnescaped 346 | } else if str = l.findRegexp(rOpenBlock); str != "" { 347 | tok = TokenOpenBlock 348 | } else if str = l.findRegexp(rOpenEndBlock); str != "" { 349 | tok = TokenOpenEndBlock 350 | } else if str = l.findRegexp(rOpenPartial); str != "" { 351 | tok = TokenOpenPartial 352 | } else if str = l.findRegexp(rInverse); str != "" { 353 | tok = TokenInverse 354 | nextFunc = lexContent 355 | } else if str = l.findRegexp(rOpenInverse); str != "" { 356 | tok = TokenOpenInverse 357 | } else if str = l.findRegexp(rOpenInverseChain); str != "" { 358 | tok = TokenOpenInverseChain 359 | } else if str = l.findRegexp(rOpen); str != "" { 360 | tok = TokenOpen 361 | } else { 362 | // this is rotten 363 | panic("Current pos MUST be an opening mustache") 364 | } 365 | 366 | l.pos += len(str) 367 | l.emit(tok) 368 | 369 | return nextFunc 370 | } 371 | 372 | // lexCloseMustache scans }} or ~}} 373 | func lexCloseMustache(l *Lexer) lexFunc { 374 | var str string 375 | var tok TokenKind 376 | 377 | if str = l.findRegexp(rCloseRaw); str != "" { 378 | // }}}} 379 | tok = TokenCloseRawBlock 380 | } else if str = l.findRegexp(rCloseUnescaped); str != "" { 381 | // }}} 382 | tok = TokenCloseUnescaped 383 | } else if str = l.findRegexp(rClose); str != "" { 384 | // }} 385 | tok = TokenClose 386 | } else { 387 | // this is rotten 388 | panic("Current pos MUST be a closing mustache") 389 | } 390 | 391 | l.pos += len(str) 392 | l.emit(tok) 393 | 394 | return lexContent 395 | } 396 | 397 | // lexExpression scans inside mustaches 398 | func lexExpression(l *Lexer) lexFunc { 399 | // search close mustache delimiter 400 | if l.isString(closeMustache) || l.isString(closeStripMustache) || l.isString(closeUnescapedStripMustache) { 401 | return lexCloseMustache 402 | } 403 | 404 | // search some patterns before advancing scanning position 405 | 406 | // "as |" 407 | if str := l.findRegexp(rOpenBlockParams); str != "" { 408 | l.pos += len(str) 409 | l.emit(TokenOpenBlockParams) 410 | return lexExpression 411 | } 412 | 413 | // .. 414 | if l.isString("..") { 415 | l.pos += len("..") 416 | l.emit(TokenID) 417 | return lexExpression 418 | } 419 | 420 | // . 421 | if str := l.findRegexp(rDotID); str != "" { 422 | l.pos += len(".") 423 | l.emit(TokenID) 424 | return lexExpression 425 | } 426 | 427 | // true 428 | if str := l.findRegexp(rTrue); str != "" { 429 | l.pos += len("true") 430 | l.emit(TokenBoolean) 431 | return lexExpression 432 | } 433 | 434 | // false 435 | if str := l.findRegexp(rFalse); str != "" { 436 | l.pos += len("false") 437 | l.emit(TokenBoolean) 438 | return lexExpression 439 | } 440 | 441 | // let's scan next character 442 | switch r := l.next(); { 443 | case r == eof: 444 | return l.errorf("Unclosed expression") 445 | case isIgnorable(r): 446 | return lexIgnorable 447 | case r == '(': 448 | l.emit(TokenOpenSexpr) 449 | case r == ')': 450 | l.emit(TokenCloseSexpr) 451 | case r == '=': 452 | l.emit(TokenEquals) 453 | case r == '@': 454 | l.emit(TokenData) 455 | case r == '"' || r == '\'': 456 | l.backup() 457 | return lexString 458 | case r == '/' || r == '.': 459 | l.emit(TokenSep) 460 | case r == '|': 461 | l.emit(TokenCloseBlockParams) 462 | case r == '+' || r == '-' || (r >= '0' && r <= '9'): 463 | l.backup() 464 | return lexNumber 465 | case r == '[': 466 | return lexPathLiteral 467 | case strings.IndexRune(unallowedIDChars, r) < 0: 468 | l.backup() 469 | return lexIdentifier 470 | default: 471 | return l.errorf("Unexpected character in expression: '%c'", r) 472 | } 473 | 474 | return lexExpression 475 | } 476 | 477 | // lexComment scans {{!-- or {{! 478 | func lexComment(l *Lexer) lexFunc { 479 | if str := l.findRegexp(l.closeComment); str != "" { 480 | l.pos += len(str) 481 | l.emit(TokenComment) 482 | 483 | return lexContent 484 | } 485 | 486 | if r := l.next(); r == eof { 487 | return l.errorf("Unclosed comment") 488 | } 489 | 490 | return lexComment 491 | } 492 | 493 | // lexIgnorable scans all following ignorable characters 494 | func lexIgnorable(l *Lexer) lexFunc { 495 | for isIgnorable(l.peek()) { 496 | l.next() 497 | } 498 | l.ignore() 499 | 500 | return lexExpression 501 | } 502 | 503 | // lexString scans a string 504 | func lexString(l *Lexer) lexFunc { 505 | // get string delimiter 506 | delim := l.next() 507 | var prev rune 508 | 509 | // ignore delimiter 510 | l.ignore() 511 | 512 | for { 513 | r := l.next() 514 | if r == eof || r == '\n' { 515 | return l.errorf("Unterminated string") 516 | } 517 | 518 | if (r == delim) && (prev != '\\') { 519 | break 520 | } 521 | 522 | prev = r 523 | } 524 | 525 | // remove end delimiter 526 | l.backup() 527 | 528 | // emit string 529 | l.emitString(delim) 530 | 531 | // skip end delimiter 532 | l.next() 533 | l.ignore() 534 | 535 | return lexExpression 536 | } 537 | 538 | // lexNumber scans a number: decimal, octal, hex, float, or imaginary. This 539 | // isn't a perfect number scanner - for instance it accepts "." and "0x0.2" 540 | // and "089" - but when it's wrong the input is invalid and the parser (via 541 | // strconv) will notice. 542 | // 543 | // NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go 544 | func lexNumber(l *Lexer) lexFunc { 545 | if !l.scanNumber() { 546 | return l.errorf("bad number syntax: %q", l.input[l.start:l.pos]) 547 | } 548 | if sign := l.peek(); sign == '+' || sign == '-' { 549 | // Complex: 1+2i. No spaces, must end in 'i'. 550 | if !l.scanNumber() || l.input[l.pos-1] != 'i' { 551 | return l.errorf("bad number syntax: %q", l.input[l.start:l.pos]) 552 | } 553 | l.emit(TokenNumber) 554 | } else { 555 | l.emit(TokenNumber) 556 | } 557 | return lexExpression 558 | } 559 | 560 | // scanNumber scans a number 561 | // 562 | // NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go 563 | func (l *Lexer) scanNumber() bool { 564 | // Optional leading sign. 565 | l.accept("+-") 566 | 567 | // Is it hex? 568 | digits := "0123456789" 569 | 570 | if l.accept("0") && l.accept("xX") { 571 | digits = "0123456789abcdefABCDEF" 572 | } 573 | 574 | l.acceptRun(digits) 575 | 576 | if l.accept(".") { 577 | l.acceptRun(digits) 578 | } 579 | 580 | if l.accept("eE") { 581 | l.accept("+-") 582 | l.acceptRun("0123456789") 583 | } 584 | 585 | // Is it imaginary? 586 | l.accept("i") 587 | 588 | // Next thing mustn't be alphanumeric. 589 | if isAlphaNumeric(l.peek()) { 590 | l.next() 591 | return false 592 | } 593 | 594 | return true 595 | } 596 | 597 | // lexIdentifier scans an ID 598 | func lexIdentifier(l *Lexer) lexFunc { 599 | str := l.findRegexp(rID) 600 | if len(str) == 0 { 601 | // this is rotten 602 | panic("Identifier expected") 603 | } 604 | 605 | l.pos += len(str) 606 | l.emit(TokenID) 607 | 608 | return lexExpression 609 | } 610 | 611 | // lexPathLiteral scans an [ID] 612 | func lexPathLiteral(l *Lexer) lexFunc { 613 | for { 614 | r := l.next() 615 | if r == eof || r == '\n' { 616 | return l.errorf("Unterminated path literal") 617 | } 618 | 619 | if r == ']' { 620 | break 621 | } 622 | } 623 | 624 | l.emit(TokenID) 625 | 626 | return lexExpression 627 | } 628 | 629 | // isIgnorable returns true if given character is ignorable (ie. whitespace of line feed) 630 | func isIgnorable(r rune) bool { 631 | return r == ' ' || r == '\t' || r == '\n' 632 | } 633 | 634 | // isAlphaNumeric reports whether r is an alphabetic, digit, or underscore. 635 | // 636 | // NOTE borrowed from https://github.com/golang/go/tree/master/src/text/template/parse/lex.go 637 | func isAlphaNumeric(r rune) bool { 638 | return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) 639 | } 640 | -------------------------------------------------------------------------------- /lexer/lexer_test.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type lexTest struct { 9 | name string 10 | input string 11 | tokens []Token 12 | } 13 | 14 | // helpers 15 | func tokContent(val string) Token { return Token{TokenContent, val, 0, 1} } 16 | func tokID(val string) Token { return Token{TokenID, val, 0, 1} } 17 | func tokSep(val string) Token { return Token{TokenSep, val, 0, 1} } 18 | func tokString(val string) Token { return Token{TokenString, val, 0, 1} } 19 | func tokNumber(val string) Token { return Token{TokenNumber, val, 0, 1} } 20 | func tokInverse(val string) Token { return Token{TokenInverse, val, 0, 1} } 21 | func tokBool(val string) Token { return Token{TokenBoolean, val, 0, 1} } 22 | func tokError(val string) Token { return Token{TokenError, val, 0, 1} } 23 | func tokComment(val string) Token { return Token{TokenComment, val, 0, 1} } 24 | 25 | var tokEOF = Token{TokenEOF, "", 0, 1} 26 | var tokEquals = Token{TokenEquals, "=", 0, 1} 27 | var tokData = Token{TokenData, "@", 0, 1} 28 | var tokOpen = Token{TokenOpen, "{{", 0, 1} 29 | var tokOpenAmp = Token{TokenOpen, "{{&", 0, 1} 30 | var tokOpenPartial = Token{TokenOpenPartial, "{{>", 0, 1} 31 | var tokClose = Token{TokenClose, "}}", 0, 1} 32 | var tokOpenStrip = Token{TokenOpen, "{{~", 0, 1} 33 | var tokCloseStrip = Token{TokenClose, "~}}", 0, 1} 34 | var tokOpenUnescaped = Token{TokenOpenUnescaped, "{{{", 0, 1} 35 | var tokCloseUnescaped = Token{TokenCloseUnescaped, "}}}", 0, 1} 36 | var tokOpenUnescapedStrip = Token{TokenOpenUnescaped, "{{~{", 0, 1} 37 | var tokCloseUnescapedStrip = Token{TokenCloseUnescaped, "}~}}", 0, 1} 38 | var tokOpenBlock = Token{TokenOpenBlock, "{{#", 0, 1} 39 | var tokOpenEndBlock = Token{TokenOpenEndBlock, "{{/", 0, 1} 40 | var tokOpenInverse = Token{TokenOpenInverse, "{{^", 0, 1} 41 | var tokOpenInverseChain = Token{TokenOpenInverseChain, "{{else", 0, 1} 42 | var tokOpenSexpr = Token{TokenOpenSexpr, "(", 0, 1} 43 | var tokCloseSexpr = Token{TokenCloseSexpr, ")", 0, 1} 44 | var tokOpenBlockParams = Token{TokenOpenBlockParams, "as |", 0, 1} 45 | var tokCloseBlockParams = Token{TokenCloseBlockParams, "|", 0, 1} 46 | var tokOpenRawBlock = Token{TokenOpenRawBlock, "{{{{", 0, 1} 47 | var tokCloseRawBlock = Token{TokenCloseRawBlock, "}}}}", 0, 1} 48 | var tokOpenEndRawBlock = Token{TokenOpenEndRawBlock, "{{{{/", 0, 1} 49 | 50 | var lexTests = []lexTest{ 51 | {"empty", "", []Token{tokEOF}}, 52 | {"spaces", " \t\n", []Token{tokContent(" \t\n"), tokEOF}}, 53 | {"content", `now is the time`, []Token{tokContent(`now is the time`), tokEOF}}, 54 | 55 | { 56 | `does not tokenizes identifier starting with true as boolean`, 57 | `{{ foo truebar }}`, 58 | []Token{tokOpen, tokID("foo"), tokID("truebar"), tokClose, tokEOF}, 59 | }, 60 | { 61 | `does not tokenizes identifier starting with false as boolean`, 62 | `{{ foo falsebar }}`, 63 | []Token{tokOpen, tokID("foo"), tokID("falsebar"), tokClose, tokEOF}, 64 | }, 65 | { 66 | `tokenizes raw block`, 67 | `{{{{foo}}}} {{{{/foo}}}}`, 68 | []Token{tokOpenRawBlock, tokID("foo"), tokCloseRawBlock, tokContent(" "), tokOpenEndRawBlock, tokID("foo"), tokCloseRawBlock, tokEOF}, 69 | }, 70 | { 71 | `tokenizes raw block with mustaches in content`, 72 | `{{{{foo}}}}{{bar}}{{{{/foo}}}}`, 73 | []Token{tokOpenRawBlock, tokID("foo"), tokCloseRawBlock, tokContent("{{bar}}"), tokOpenEndRawBlock, tokID("foo"), tokCloseRawBlock, tokEOF}, 74 | }, 75 | { 76 | `tokenizes @../foo`, 77 | `{{@../foo}}`, 78 | []Token{tokOpen, tokData, tokID(".."), tokSep("/"), tokID("foo"), tokClose, tokEOF}, 79 | }, 80 | { 81 | `tokenizes escaped mustaches`, 82 | "\\{{bar}}", 83 | []Token{tokContent("{{bar}}"), tokEOF}, 84 | }, 85 | { 86 | `tokenizes strip mustaches`, 87 | `{{~ foo ~}}`, 88 | []Token{tokOpenStrip, tokID("foo"), tokCloseStrip, tokEOF}, 89 | }, 90 | { 91 | `tokenizes unescaped strip mustaches`, 92 | `{{~{ foo }~}}`, 93 | []Token{tokOpenUnescapedStrip, tokID("foo"), tokCloseUnescapedStrip, tokEOF}, 94 | }, 95 | 96 | // 97 | // Next tests come from: 98 | // https://github.com/wycats/handlebars.js/blob/master/spec/tokenizer.js 99 | // 100 | { 101 | `tokenizes a simple mustache as "OPEN ID CLOSE"`, 102 | `{{foo}}`, 103 | []Token{tokOpen, tokID("foo"), tokClose, tokEOF}, 104 | }, 105 | { 106 | `supports unescaping with &`, 107 | `{{&bar}}`, 108 | []Token{tokOpenAmp, tokID("bar"), tokClose, tokEOF}, 109 | }, 110 | { 111 | `supports unescaping with {{{`, 112 | `{{{bar}}}`, 113 | []Token{tokOpenUnescaped, tokID("bar"), tokCloseUnescaped, tokEOF}, 114 | }, 115 | { 116 | `supports escaping delimiters`, 117 | "{{foo}} \\{{bar}} {{baz}}", 118 | []Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokOpen, tokID("baz"), tokClose, tokEOF}, 119 | }, 120 | { 121 | `supports escaping multiple delimiters`, 122 | "{{foo}} \\{{bar}} \\{{baz}}", 123 | []Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokContent("{{baz}}"), tokEOF}, 124 | }, 125 | { 126 | `supports escaping a triple stash`, 127 | "{{foo}} \\{{{bar}}} {{baz}}", 128 | []Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{{bar}}} "), tokOpen, tokID("baz"), tokClose, tokEOF}, 129 | }, 130 | { 131 | `supports escaping escape character`, 132 | "{{foo}} \\\\{{bar}} {{baz}}", 133 | []Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" "), tokOpen, tokID("baz"), tokClose, tokEOF}, 134 | }, 135 | { 136 | `supports escaping multiple escape characters`, 137 | "{{foo}} \\\\{{bar}} \\\\{{baz}}", 138 | []Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" \\"), tokOpen, tokID("baz"), tokClose, tokEOF}, 139 | }, 140 | { 141 | `supports escaped mustaches after escaped escape characters`, 142 | "{{foo}} \\\\{{bar}} \\{{baz}}", 143 | // NOTE: JS implementation returns: 144 | // ['OPEN', 'ID', 'CLOSE', 'CONTENT', 'OPEN', 'ID', 'CLOSE', 'CONTENT', 'CONTENT', 'CONTENT'], 145 | // WTF is the last CONTENT ? 146 | []Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpen, tokID("bar"), tokClose, tokContent(" "), tokContent("{{baz}}"), tokEOF}, 147 | }, 148 | { 149 | `supports escaped escape characters after escaped mustaches`, 150 | "{{foo}} \\{{bar}} \\\\{{baz}}", 151 | // NOTE: JS implementation returns: 152 | // []Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} "), tokContent("\\"), tokOpen, tokID("baz"), tokClose, tokEOF}, 153 | []Token{tokOpen, tokID("foo"), tokClose, tokContent(" "), tokContent("{{bar}} \\"), tokOpen, tokID("baz"), tokClose, tokEOF}, 154 | }, 155 | { 156 | `supports escaped escape character on a triple stash`, 157 | "{{foo}} \\\\{{{bar}}} {{baz}}", 158 | []Token{tokOpen, tokID("foo"), tokClose, tokContent(" \\"), tokOpenUnescaped, tokID("bar"), tokCloseUnescaped, tokContent(" "), tokOpen, tokID("baz"), tokClose, tokEOF}, 159 | }, 160 | { 161 | `tokenizes a simple path`, 162 | `{{foo/bar}}`, 163 | []Token{tokOpen, tokID("foo"), tokSep("/"), tokID("bar"), tokClose, tokEOF}, 164 | }, 165 | { 166 | `allows dot notation (1)`, 167 | `{{foo.bar}}`, 168 | []Token{tokOpen, tokID("foo"), tokSep("."), tokID("bar"), tokClose, tokEOF}, 169 | }, 170 | { 171 | `allows dot notation (2)`, 172 | `{{foo.bar.baz}}`, 173 | []Token{tokOpen, tokID("foo"), tokSep("."), tokID("bar"), tokSep("."), tokID("baz"), tokClose, tokEOF}, 174 | }, 175 | { 176 | `allows path literals with []`, 177 | `{{foo.[bar]}}`, 178 | []Token{tokOpen, tokID("foo"), tokSep("."), tokID("[bar]"), tokClose, tokEOF}, 179 | }, 180 | { 181 | `allows multiple path literals on a line with []`, 182 | `{{foo.[bar]}}{{foo.[baz]}}`, 183 | []Token{tokOpen, tokID("foo"), tokSep("."), tokID("[bar]"), tokClose, tokOpen, tokID("foo"), tokSep("."), tokID("[baz]"), tokClose, tokEOF}, 184 | }, 185 | { 186 | `tokenizes {{.}} as OPEN ID CLOSE`, 187 | `{{.}}`, 188 | []Token{tokOpen, tokID("."), tokClose, tokEOF}, 189 | }, 190 | { 191 | `tokenizes a path as "OPEN (ID SEP)* ID CLOSE"`, 192 | `{{../foo/bar}}`, 193 | []Token{tokOpen, tokID(".."), tokSep("/"), tokID("foo"), tokSep("/"), tokID("bar"), tokClose, tokEOF}, 194 | }, 195 | { 196 | `tokenizes a path with .. as a parent path`, 197 | `{{../foo.bar}}`, 198 | []Token{tokOpen, tokID(".."), tokSep("/"), tokID("foo"), tokSep("."), tokID("bar"), tokClose, tokEOF}, 199 | }, 200 | { 201 | `tokenizes a path with this/foo as OPEN ID SEP ID CLOSE`, 202 | `{{this/foo}}`, 203 | []Token{tokOpen, tokID("this"), tokSep("/"), tokID("foo"), tokClose, tokEOF}, 204 | }, 205 | { 206 | `tokenizes a simple mustache with spaces as "OPEN ID CLOSE"`, 207 | `{{ foo }}`, 208 | []Token{tokOpen, tokID("foo"), tokClose, tokEOF}, 209 | }, 210 | { 211 | `tokenizes a simple mustache with line breaks as "OPEN ID ID CLOSE"`, 212 | "{{ foo \n bar }}", 213 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokClose, tokEOF}, 214 | }, 215 | { 216 | `tokenizes raw content as "CONTENT"`, 217 | `foo {{ bar }} baz`, 218 | []Token{tokContent("foo "), tokOpen, tokID("bar"), tokClose, tokContent(" baz"), tokEOF}, 219 | }, 220 | { 221 | `tokenizes a partial as "OPEN_PARTIAL ID CLOSE"`, 222 | `{{> foo}}`, 223 | []Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF}, 224 | }, 225 | { 226 | `tokenizes a partial with context as "OPEN_PARTIAL ID ID CLOSE"`, 227 | `{{> foo bar }}`, 228 | []Token{tokOpenPartial, tokID("foo"), tokID("bar"), tokClose, tokEOF}, 229 | }, 230 | { 231 | `tokenizes a partial without spaces as "OPEN_PARTIAL ID CLOSE"`, 232 | `{{>foo}}`, 233 | []Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF}, 234 | }, 235 | { 236 | `tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"`, 237 | `{{>foo }}`, 238 | []Token{tokOpenPartial, tokID("foo"), tokClose, tokEOF}, 239 | }, 240 | { 241 | `tokenizes a partial space at the }); as "OPEN_PARTIAL ID CLOSE"`, 242 | `{{>foo/bar.baz }}`, 243 | []Token{tokOpenPartial, tokID("foo"), tokSep("/"), tokID("bar"), tokSep("."), tokID("baz"), tokClose, tokEOF}, 244 | }, 245 | { 246 | `tokenizes a comment as "COMMENT"`, 247 | `foo {{! this is a comment }} bar {{ baz }}`, 248 | []Token{tokContent("foo "), tokComment("{{! this is a comment }}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF}, 249 | }, 250 | { 251 | `tokenizes a block comment as "COMMENT"`, 252 | `foo {{!-- this is a {{comment}} --}} bar {{ baz }}`, 253 | []Token{tokContent("foo "), tokComment("{{!-- this is a {{comment}} --}}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF}, 254 | }, 255 | { 256 | `tokenizes a block comment with whitespace as "COMMENT"`, 257 | "foo {{!-- this is a\n{{comment}}\n--}} bar {{ baz }}", 258 | []Token{tokContent("foo "), tokComment("{{!-- this is a\n{{comment}}\n--}}"), tokContent(" bar "), tokOpen, tokID("baz"), tokClose, tokEOF}, 259 | }, 260 | { 261 | `tokenizes open and closing blocks as OPEN_BLOCK, ID, CLOSE ..., OPEN_ENDBLOCK ID CLOSE`, 262 | `{{#foo}}content{{/foo}}`, 263 | []Token{tokOpenBlock, tokID("foo"), tokClose, tokContent("content"), tokOpenEndBlock, tokID("foo"), tokClose, tokEOF}, 264 | }, 265 | { 266 | `tokenizes inverse sections as "INVERSE"`, 267 | `{{^}}`, 268 | []Token{tokInverse("{{^}}"), tokEOF}, 269 | }, 270 | { 271 | `tokenizes inverse sections as "INVERSE" with alternate format`, 272 | `{{else}}`, 273 | []Token{tokInverse("{{else}}"), tokEOF}, 274 | }, 275 | { 276 | `tokenizes inverse sections as "INVERSE" with spaces`, 277 | `{{ else }}`, 278 | []Token{tokInverse("{{ else }}"), tokEOF}, 279 | }, 280 | { 281 | `tokenizes inverse sections with ID as "OPEN_INVERSE ID CLOSE"`, 282 | `{{^foo}}`, 283 | []Token{tokOpenInverse, tokID("foo"), tokClose, tokEOF}, 284 | }, 285 | { 286 | `tokenizes inverse sections with ID and spaces as "OPEN_INVERSE ID CLOSE"`, 287 | `{{^ foo }}`, 288 | []Token{tokOpenInverse, tokID("foo"), tokClose, tokEOF}, 289 | }, 290 | { 291 | `tokenizes mustaches with params as "OPEN ID ID ID CLOSE"`, 292 | `{{ foo bar baz }}`, 293 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokClose, tokEOF}, 294 | }, 295 | { 296 | `tokenizes mustaches with String params as "OPEN ID ID STRING CLOSE"`, 297 | `{{ foo bar "baz" }}`, 298 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz"), tokClose, tokEOF}, 299 | }, 300 | { 301 | `tokenizes mustaches with String params using single quotes as "OPEN ID ID STRING CLOSE"`, 302 | `{{ foo bar 'baz' }}`, 303 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz"), tokClose, tokEOF}, 304 | }, 305 | { 306 | `tokenizes String params with spaces inside as "STRING"`, 307 | `{{ foo bar "baz bat" }}`, 308 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokString("baz bat"), tokClose, tokEOF}, 309 | }, 310 | { 311 | `tokenizes String params with escapes quotes as STRING`, 312 | `{{ foo "bar\"baz" }}`, 313 | []Token{tokOpen, tokID("foo"), tokString(`bar"baz`), tokClose, tokEOF}, 314 | }, 315 | { 316 | `tokenizes String params using single quotes with escapes quotes as STRING`, 317 | `{{ foo 'bar\'baz' }}`, 318 | []Token{tokOpen, tokID("foo"), tokString(`bar'baz`), tokClose, tokEOF}, 319 | }, 320 | { 321 | `tokenizes numbers`, 322 | `{{ foo 1 }}`, 323 | []Token{tokOpen, tokID("foo"), tokNumber("1"), tokClose, tokEOF}, 324 | }, 325 | { 326 | `tokenizes floats`, 327 | `{{ foo 1.1 }}`, 328 | []Token{tokOpen, tokID("foo"), tokNumber("1.1"), tokClose, tokEOF}, 329 | }, 330 | { 331 | `tokenizes negative numbers`, 332 | `{{ foo -1 }}`, 333 | []Token{tokOpen, tokID("foo"), tokNumber("-1"), tokClose, tokEOF}, 334 | }, 335 | { 336 | `tokenizes negative floats`, 337 | `{{ foo -1.1 }}`, 338 | []Token{tokOpen, tokID("foo"), tokNumber("-1.1"), tokClose, tokEOF}, 339 | }, 340 | { 341 | `tokenizes boolean true`, 342 | `{{ foo true }}`, 343 | []Token{tokOpen, tokID("foo"), tokBool("true"), tokClose, tokEOF}, 344 | }, 345 | { 346 | `tokenizes boolean false`, 347 | `{{ foo false }}`, 348 | []Token{tokOpen, tokID("foo"), tokBool("false"), tokClose, tokEOF}, 349 | }, 350 | // SKIP: 'tokenizes undefined and null' 351 | { 352 | `tokenizes hash arguments (1)`, 353 | `{{ foo bar=baz }}`, 354 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokEquals, tokID("baz"), tokClose, tokEOF}, 355 | }, 356 | { 357 | `tokenizes hash arguments (2)`, 358 | `{{ foo bar baz=bat }}`, 359 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokID("bat"), tokClose, tokEOF}, 360 | }, 361 | { 362 | `tokenizes hash arguments (3)`, 363 | `{{ foo bar baz=1 }}`, 364 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokNumber("1"), tokClose, tokEOF}, 365 | }, 366 | { 367 | `tokenizes hash arguments (4)`, 368 | `{{ foo bar baz=true }}`, 369 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokBool("true"), tokClose, tokEOF}, 370 | }, 371 | { 372 | `tokenizes hash arguments (5)`, 373 | `{{ foo bar baz=false }}`, 374 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokBool("false"), tokClose, tokEOF}, 375 | }, 376 | { 377 | `tokenizes hash arguments (6)`, 378 | "{{ foo bar\n baz=bat }}", 379 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokID("bat"), tokClose, tokEOF}, 380 | }, 381 | { 382 | `tokenizes hash arguments (7)`, 383 | `{{ foo bar baz="bat" }}`, 384 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokString("bat"), tokClose, tokEOF}, 385 | }, 386 | { 387 | `tokenizes hash arguments (8)`, 388 | `{{ foo bar baz="bat" bam=wot }}`, 389 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokID("baz"), tokEquals, tokString("bat"), tokID("bam"), tokEquals, tokID("wot"), tokClose, tokEOF}, 390 | }, 391 | { 392 | `tokenizes hash arguments (9)`, 393 | `{{foo omg bar=baz bat="bam"}}`, 394 | []Token{tokOpen, tokID("foo"), tokID("omg"), tokID("bar"), tokEquals, tokID("baz"), tokID("bat"), tokEquals, tokString("bam"), tokClose, tokEOF}, 395 | }, 396 | { 397 | `tokenizes special @ identifiers (1)`, 398 | `{{ @foo }}`, 399 | []Token{tokOpen, tokData, tokID("foo"), tokClose, tokEOF}, 400 | }, 401 | { 402 | `tokenizes special @ identifiers (2)`, 403 | `{{ foo @bar }}`, 404 | []Token{tokOpen, tokID("foo"), tokData, tokID("bar"), tokClose, tokEOF}, 405 | }, 406 | { 407 | `tokenizes special @ identifiers (3)`, 408 | `{{ foo bar=@baz }}`, 409 | []Token{tokOpen, tokID("foo"), tokID("bar"), tokEquals, tokData, tokID("baz"), tokClose, tokEOF}, 410 | }, 411 | { 412 | `does not time out in a mustache with a single } followed by EOF`, 413 | `{{foo}`, 414 | []Token{tokOpen, tokID("foo"), tokError("Unexpected character in expression: '}'")}, 415 | }, 416 | { 417 | `does not time out in a mustache when invalid ID characters are used`, 418 | `{{foo & }}`, 419 | []Token{tokOpen, tokID("foo"), tokError("Unexpected character in expression: '&'")}, 420 | }, 421 | { 422 | `tokenizes subexpressions (1)`, 423 | `{{foo (bar)}}`, 424 | []Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokCloseSexpr, tokClose, tokEOF}, 425 | }, 426 | { 427 | `tokenizes subexpressions (2)`, 428 | `{{foo (a-x b-y)}}`, 429 | []Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("a-x"), tokID("b-y"), tokCloseSexpr, tokClose, tokEOF}, 430 | }, 431 | { 432 | `tokenizes nested subexpressions`, 433 | `{{foo (bar (lol rofl)) (baz)}}`, 434 | []Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokOpenSexpr, tokID("lol"), tokID("rofl"), tokCloseSexpr, tokCloseSexpr, tokOpenSexpr, tokID("baz"), tokCloseSexpr, tokClose, tokEOF}, 435 | }, 436 | { 437 | `tokenizes nested subexpressions: literals`, 438 | `{{foo (bar (lol true) false) (baz 1) (blah 'b') (blorg "c")}}`, 439 | []Token{tokOpen, tokID("foo"), tokOpenSexpr, tokID("bar"), tokOpenSexpr, tokID("lol"), tokBool("true"), tokCloseSexpr, tokBool("false"), tokCloseSexpr, tokOpenSexpr, tokID("baz"), tokNumber("1"), tokCloseSexpr, tokOpenSexpr, tokID("blah"), tokString("b"), tokCloseSexpr, tokOpenSexpr, tokID("blorg"), tokString("c"), tokCloseSexpr, tokClose, tokEOF}, 440 | }, 441 | { 442 | `tokenizes block params (1)`, 443 | `{{#foo as |bar|}}`, 444 | []Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokCloseBlockParams, tokClose, tokEOF}, 445 | }, 446 | { 447 | `tokenizes block params (2)`, 448 | `{{#foo as |bar baz|}}`, 449 | []Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF}, 450 | }, 451 | { 452 | `tokenizes block params (3)`, 453 | `{{#foo as | bar baz |}}`, 454 | []Token{tokOpenBlock, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF}, 455 | }, 456 | { 457 | `tokenizes block params (4)`, 458 | `{{#foo as as | bar baz |}}`, 459 | []Token{tokOpenBlock, tokID("foo"), tokID("as"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF}, 460 | }, 461 | { 462 | `tokenizes block params (5)`, 463 | `{{else foo as |bar baz|}}`, 464 | []Token{tokOpenInverseChain, tokID("foo"), tokOpenBlockParams, tokID("bar"), tokID("baz"), tokCloseBlockParams, tokClose, tokEOF}, 465 | }, 466 | } 467 | 468 | func collect(t *lexTest) []Token { 469 | var result []Token 470 | 471 | l := scanWithName(t.input, t.name) 472 | for { 473 | token := l.NextToken() 474 | result = append(result, token) 475 | 476 | if token.Kind == TokenEOF || token.Kind == TokenError { 477 | break 478 | } 479 | } 480 | 481 | return result 482 | } 483 | 484 | func equal(i1, i2 []Token, checkPos bool) bool { 485 | if len(i1) != len(i2) { 486 | return false 487 | } 488 | 489 | for k := range i1 { 490 | if i1[k].Kind != i2[k].Kind { 491 | return false 492 | } 493 | 494 | if checkPos && i1[k].Pos != i2[k].Pos { 495 | return false 496 | } 497 | 498 | if i1[k].Val != i2[k].Val { 499 | return false 500 | } 501 | } 502 | 503 | return true 504 | } 505 | 506 | func TestLexer(t *testing.T) { 507 | t.Parallel() 508 | 509 | for _, test := range lexTests { 510 | tokens := collect(&test) 511 | if !equal(tokens, test.tokens, false) { 512 | t.Errorf("Test '%s' failed\ninput:\n\t'%s'\nexpected\n\t%v\ngot\n\t%+v\n", test.name, test.input, test.tokens, tokens) 513 | } 514 | } 515 | } 516 | 517 | // @todo Test errors: 518 | // `{{{{raw foo` 519 | 520 | // package example 521 | func Example() { 522 | source := "You know {{nothing}} John Snow" 523 | 524 | output := "" 525 | 526 | lex := Scan(source) 527 | for { 528 | // consume next token 529 | token := lex.NextToken() 530 | 531 | output += fmt.Sprintf(" %s", token) 532 | 533 | // stops when all tokens have been consumed, or on error 534 | if token.Kind == TokenEOF || token.Kind == TokenError { 535 | break 536 | } 537 | } 538 | 539 | fmt.Print(output) 540 | // Output: Content{"You know "} Open{"{{"} ID{"nothing"} Close{"}}"} Content{" John Snow"} EOF 541 | } 542 | -------------------------------------------------------------------------------- /lexer/token.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import "fmt" 4 | 5 | const ( 6 | // TokenError represents an error 7 | TokenError TokenKind = iota 8 | 9 | // TokenEOF represents an End Of File 10 | TokenEOF 11 | 12 | // 13 | // Mustache delimiters 14 | // 15 | 16 | // TokenOpen is the OPEN token 17 | TokenOpen 18 | 19 | // TokenClose is the CLOSE token 20 | TokenClose 21 | 22 | // TokenOpenRawBlock is the OPEN_RAW_BLOCK token 23 | TokenOpenRawBlock 24 | 25 | // TokenCloseRawBlock is the CLOSE_RAW_BLOCK token 26 | TokenCloseRawBlock 27 | 28 | // TokenOpenEndRawBlock is the END_RAW_BLOCK token 29 | TokenOpenEndRawBlock 30 | 31 | // TokenOpenUnescaped is the OPEN_UNESCAPED token 32 | TokenOpenUnescaped 33 | 34 | // TokenCloseUnescaped is the CLOSE_UNESCAPED token 35 | TokenCloseUnescaped 36 | 37 | // TokenOpenBlock is the OPEN_BLOCK token 38 | TokenOpenBlock 39 | 40 | // TokenOpenEndBlock is the OPEN_ENDBLOCK token 41 | TokenOpenEndBlock 42 | 43 | // TokenInverse is the INVERSE token 44 | TokenInverse 45 | 46 | // TokenOpenInverse is the OPEN_INVERSE token 47 | TokenOpenInverse 48 | 49 | // TokenOpenInverseChain is the OPEN_INVERSE_CHAIN token 50 | TokenOpenInverseChain 51 | 52 | // TokenOpenPartial is the OPEN_PARTIAL token 53 | TokenOpenPartial 54 | 55 | // TokenComment is the COMMENT token 56 | TokenComment 57 | 58 | // 59 | // Inside mustaches 60 | // 61 | 62 | // TokenOpenSexpr is the OPEN_SEXPR token 63 | TokenOpenSexpr 64 | 65 | // TokenCloseSexpr is the CLOSE_SEXPR token 66 | TokenCloseSexpr 67 | 68 | // TokenEquals is the EQUALS token 69 | TokenEquals 70 | 71 | // TokenData is the DATA token 72 | TokenData 73 | 74 | // TokenSep is the SEP token 75 | TokenSep 76 | 77 | // TokenOpenBlockParams is the OPEN_BLOCK_PARAMS token 78 | TokenOpenBlockParams 79 | 80 | // TokenCloseBlockParams is the CLOSE_BLOCK_PARAMS token 81 | TokenCloseBlockParams 82 | 83 | // 84 | // Tokens with content 85 | // 86 | 87 | // TokenContent is the CONTENT token 88 | TokenContent 89 | 90 | // TokenID is the ID token 91 | TokenID 92 | 93 | // TokenString is the STRING token 94 | TokenString 95 | 96 | // TokenNumber is the NUMBER token 97 | TokenNumber 98 | 99 | // TokenBoolean is the BOOLEAN token 100 | TokenBoolean 101 | ) 102 | 103 | const ( 104 | // Option to generate token position in its string representation 105 | dumpTokenPos = false 106 | 107 | // Option to generate values for all token kinds for their string representations 108 | dumpAllTokensVal = true 109 | ) 110 | 111 | // TokenKind represents a Token type. 112 | type TokenKind int 113 | 114 | // Token represents a scanned token. 115 | type Token struct { 116 | Kind TokenKind // Token kind 117 | Val string // Token value 118 | 119 | Pos int // Byte position in input string 120 | Line int // Line number in input string 121 | } 122 | 123 | // tokenName permits to display token name given token type 124 | var tokenName = map[TokenKind]string{ 125 | TokenError: "Error", 126 | TokenEOF: "EOF", 127 | TokenContent: "Content", 128 | TokenComment: "Comment", 129 | TokenOpen: "Open", 130 | TokenClose: "Close", 131 | TokenOpenUnescaped: "OpenUnescaped", 132 | TokenCloseUnescaped: "CloseUnescaped", 133 | TokenOpenBlock: "OpenBlock", 134 | TokenOpenEndBlock: "OpenEndBlock", 135 | TokenOpenRawBlock: "OpenRawBlock", 136 | TokenCloseRawBlock: "CloseRawBlock", 137 | TokenOpenEndRawBlock: "OpenEndRawBlock", 138 | TokenOpenBlockParams: "OpenBlockParams", 139 | TokenCloseBlockParams: "CloseBlockParams", 140 | TokenInverse: "Inverse", 141 | TokenOpenInverse: "OpenInverse", 142 | TokenOpenInverseChain: "OpenInverseChain", 143 | TokenOpenPartial: "OpenPartial", 144 | TokenOpenSexpr: "OpenSexpr", 145 | TokenCloseSexpr: "CloseSexpr", 146 | TokenID: "ID", 147 | TokenEquals: "Equals", 148 | TokenString: "String", 149 | TokenNumber: "Number", 150 | TokenBoolean: "Boolean", 151 | TokenData: "Data", 152 | TokenSep: "Sep", 153 | } 154 | 155 | // String returns the token kind string representation for debugging. 156 | func (k TokenKind) String() string { 157 | s := tokenName[k] 158 | if s == "" { 159 | return fmt.Sprintf("Token-%d", int(k)) 160 | } 161 | return s 162 | } 163 | 164 | // String returns the token string representation for debugging. 165 | func (t Token) String() string { 166 | result := "" 167 | 168 | if dumpTokenPos { 169 | result += fmt.Sprintf("%d:", t.Pos) 170 | } 171 | 172 | result += fmt.Sprintf("%s", t.Kind) 173 | 174 | if (dumpAllTokensVal || (t.Kind >= TokenContent)) && len(t.Val) > 0 { 175 | if len(t.Val) > 100 { 176 | result += fmt.Sprintf("{%.20q...}", t.Val) 177 | } else { 178 | result += fmt.Sprintf("{%q}", t.Val) 179 | } 180 | } 181 | 182 | return result 183 | } 184 | -------------------------------------------------------------------------------- /mustache_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "io/ioutil" 5 | "path" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | 10 | "gopkg.in/yaml.v2" 11 | ) 12 | 13 | // 14 | // Note, as the JS implementation, the divergences from mustache spec: 15 | // - we don't support alternative delimeters 16 | // - the mustache lambda spec differs 17 | // 18 | 19 | type mustacheTest struct { 20 | Name string 21 | Desc string 22 | Data interface{} 23 | Template string 24 | Expected string 25 | Partials map[string]string 26 | } 27 | 28 | type mustacheTestFile struct { 29 | Overview string 30 | Tests []mustacheTest 31 | } 32 | 33 | var ( 34 | rAltDelim = regexp.MustCompile(regexp.QuoteMeta("{{=")) 35 | ) 36 | 37 | var ( 38 | musTestLambdaInterMult = 0 39 | ) 40 | 41 | func TestMustache(t *testing.T) { 42 | skipFiles := map[string]bool{ 43 | // mustache lambdas differ from handlebars lambdas 44 | "~lambdas.yml": true, 45 | } 46 | 47 | for _, fileName := range mustacheTestFiles() { 48 | if skipFiles[fileName] { 49 | // fmt.Printf("Skipped file: %s\n", fileName) 50 | continue 51 | } 52 | 53 | launchTests(t, testsFromMustacheFile(fileName)) 54 | } 55 | } 56 | 57 | func testsFromMustacheFile(fileName string) []Test { 58 | result := []Test{} 59 | 60 | fileData, err := ioutil.ReadFile(path.Join("mustache", "specs", fileName)) 61 | if err != nil { 62 | panic(err) 63 | } 64 | 65 | var testFile mustacheTestFile 66 | if err := yaml.Unmarshal(fileData, &testFile); err != nil { 67 | panic(err) 68 | } 69 | 70 | for _, mustacheTest := range testFile.Tests { 71 | if mustBeSkipped(mustacheTest, fileName) { 72 | // fmt.Printf("Skipped test: %s\n", mustacheTest.Name) 73 | continue 74 | } 75 | 76 | test := Test{ 77 | name: mustacheTest.Name, 78 | input: mustacheTest.Template, 79 | data: mustacheTest.Data, 80 | partials: mustacheTest.Partials, 81 | output: mustacheTest.Expected, 82 | } 83 | 84 | result = append(result, test) 85 | } 86 | 87 | return result 88 | } 89 | 90 | // returns true if test must be skipped 91 | func mustBeSkipped(test mustacheTest, fileName string) bool { 92 | // handlebars does not support alternative delimiters 93 | return haveAltDelimiter(test) || 94 | // the JS implementation skips those tests 95 | fileName == "partials.yml" && (test.Name == "Failed Lookup" || test.Name == "Standalone Indentation") 96 | } 97 | 98 | // returns true if test have alternative delimeter in template or in partials 99 | func haveAltDelimiter(test mustacheTest) bool { 100 | // check template 101 | if rAltDelim.MatchString(test.Template) { 102 | return true 103 | } 104 | 105 | // check partials 106 | for _, partial := range test.Partials { 107 | if rAltDelim.MatchString(partial) { 108 | return true 109 | } 110 | } 111 | 112 | return false 113 | } 114 | 115 | func mustacheTestFiles() []string { 116 | var result []string 117 | 118 | files, err := ioutil.ReadDir(path.Join("mustache", "specs")) 119 | if err != nil { 120 | panic(err) 121 | } 122 | 123 | for _, file := range files { 124 | fileName := file.Name() 125 | 126 | if !file.IsDir() && strings.HasSuffix(fileName, ".yml") { 127 | result = append(result, fileName) 128 | } 129 | } 130 | 131 | return result 132 | } 133 | 134 | // 135 | // Following tests come fron ~lambdas.yml 136 | // 137 | 138 | var mustacheLambdasTests = []Test{ 139 | { 140 | "Interpolation", 141 | "Hello, {{lambda}}!", 142 | map[string]interface{}{"lambda": func() string { return "world" }}, 143 | nil, nil, nil, 144 | "Hello, world!", 145 | }, 146 | 147 | // // SKIP: lambda return value is not parsed 148 | // { 149 | // "Interpolation - Expansion", 150 | // "Hello, {{lambda}}!", 151 | // map[string]interface{}{"lambda": func() string { return "{{planet}}" }}, 152 | // nil, nil, nil, 153 | // "Hello, world!", 154 | // }, 155 | 156 | // SKIP "Interpolation - Alternate Delimiters" 157 | 158 | { 159 | "Interpolation - Multiple Calls", 160 | "{{lambda}} == {{{lambda}}} == {{lambda}}", 161 | map[string]interface{}{"lambda": func() string { 162 | musTestLambdaInterMult++ 163 | return Str(musTestLambdaInterMult) 164 | }}, 165 | nil, nil, nil, 166 | "1 == 2 == 3", 167 | }, 168 | 169 | { 170 | "Escaping", 171 | "<{{lambda}}{{{lambda}}}", 172 | map[string]interface{}{"lambda": func() string { return ">" }}, 173 | nil, nil, nil, 174 | "<>>", 175 | }, 176 | 177 | // // SKIP: "Lambdas used for sections should receive the raw section string." 178 | // { 179 | // "Section", 180 | // "<{{#lambda}}{{x}}{{/lambda}}>", 181 | // map[string]interface{}{"lambda": func(param string) string { 182 | // if param == "{{x}}" { 183 | // return "yes" 184 | // } 185 | 186 | // return "false" 187 | // }, "x": "Error!"}, 188 | // nil, nil, nil, 189 | // "", 190 | // }, 191 | 192 | // // SKIP: lambda return value is not parsed 193 | // { 194 | // "Section - Expansion", 195 | // "<{{#lambda}}-{{/lambda}}>", 196 | // map[string]interface{}{"lambda": func(param string) string { 197 | // return param + "{{planet}}" + param 198 | // }, "planet": "Earth"}, 199 | // nil, nil, nil, 200 | // "<-Earth->", 201 | // }, 202 | 203 | // SKIP: "Section - Alternate Delimiters" 204 | 205 | { 206 | "Section - Multiple Calls", 207 | "{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}", 208 | map[string]interface{}{"lambda": func(options *Options) string { 209 | return "__" + options.Fn() + "__" 210 | }}, 211 | nil, nil, nil, 212 | "__FILE__ != __LINE__", 213 | }, 214 | 215 | // // SKIP: "Lambdas used for inverted sections should be considered truthy." 216 | // { 217 | // "Inverted Section", 218 | // "<{{^lambda}}{{static}}{{/lambda}}>", 219 | // map[string]interface{}{ 220 | // "lambda": func() interface{} { 221 | // return false 222 | // }, 223 | // "static": "static", 224 | // }, 225 | // nil, nil, nil, 226 | // "<>", 227 | // }, 228 | } 229 | 230 | func TestMustacheLambdas(t *testing.T) { 231 | t.Parallel() 232 | 233 | launchTests(t, mustacheLambdasTests) 234 | } 235 | -------------------------------------------------------------------------------- /parser/parser_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/aymerick/raymond/ast" 9 | "github.com/aymerick/raymond/lexer" 10 | ) 11 | 12 | type parserTest struct { 13 | name string 14 | input string 15 | output string 16 | } 17 | 18 | var parserTests = []parserTest{ 19 | // 20 | // Next tests come from: 21 | // https://github.com/wycats/handlebars.js/blob/master/spec/parser.js 22 | // 23 | {"parses simple mustaches (1)", `{{123}}`, "{{ NUMBER{123} [] }}\n"}, 24 | {"parses simple mustaches (2)", `{{"foo"}}`, "{{ \"foo\" [] }}\n"}, 25 | {"parses simple mustaches (3)", `{{false}}`, "{{ BOOLEAN{false} [] }}\n"}, 26 | {"parses simple mustaches (4)", `{{true}}`, "{{ BOOLEAN{true} [] }}\n"}, 27 | {"parses simple mustaches (5)", `{{foo}}`, "{{ PATH:foo [] }}\n"}, 28 | {"parses simple mustaches (6)", `{{foo?}}`, "{{ PATH:foo? [] }}\n"}, 29 | {"parses simple mustaches (7)", `{{foo_}}`, "{{ PATH:foo_ [] }}\n"}, 30 | {"parses simple mustaches (8)", `{{foo-}}`, "{{ PATH:foo- [] }}\n"}, 31 | {"parses simple mustaches (9)", `{{foo:}}`, "{{ PATH:foo: [] }}\n"}, 32 | 33 | {"parses simple mustaches with data", `{{@foo}}`, "{{ @PATH:foo [] }}\n"}, 34 | {"parses simple mustaches with data paths", `{{@../foo}}`, "{{ @PATH:foo [] }}\n"}, 35 | {"parses mustaches with paths", `{{foo/bar}}`, "{{ PATH:foo/bar [] }}\n"}, 36 | {"parses mustaches with this/foo", `{{this/foo}}`, "{{ PATH:foo [] }}\n"}, 37 | {"parses mustaches with - in a path", `{{foo-bar}}`, "{{ PATH:foo-bar [] }}\n"}, 38 | {"parses mustaches with parameters", `{{foo bar}}`, "{{ PATH:foo [PATH:bar] }}\n"}, 39 | {"parses mustaches with string parameters", `{{foo bar "baz" }}`, "{{ PATH:foo [PATH:bar, \"baz\"] }}\n"}, 40 | {"parses mustaches with NUMBER parameters", `{{foo 1}}`, "{{ PATH:foo [NUMBER{1}] }}\n"}, 41 | {"parses mustaches with BOOLEAN parameters (1)", `{{foo true}}`, "{{ PATH:foo [BOOLEAN{true}] }}\n"}, 42 | {"parses mustaches with BOOLEAN parameters (2)", `{{foo false}}`, "{{ PATH:foo [BOOLEAN{false}] }}\n"}, 43 | {"parses mustaches with DATA parameters", `{{foo @bar}}`, "{{ PATH:foo [@PATH:bar] }}\n"}, 44 | 45 | {"parses mustaches with hash arguments (01)", `{{foo bar=baz}}`, "{{ PATH:foo [] HASH{bar=PATH:baz} }}\n"}, 46 | {"parses mustaches with hash arguments (02)", `{{foo bar=1}}`, "{{ PATH:foo [] HASH{bar=NUMBER{1}} }}\n"}, 47 | {"parses mustaches with hash arguments (03)", `{{foo bar=true}}`, "{{ PATH:foo [] HASH{bar=BOOLEAN{true}} }}\n"}, 48 | {"parses mustaches with hash arguments (04)", `{{foo bar=false}}`, "{{ PATH:foo [] HASH{bar=BOOLEAN{false}} }}\n"}, 49 | {"parses mustaches with hash arguments (05)", `{{foo bar=@baz}}`, "{{ PATH:foo [] HASH{bar=@PATH:baz} }}\n"}, 50 | {"parses mustaches with hash arguments (06)", `{{foo bar=baz bat=bam}}`, "{{ PATH:foo [] HASH{bar=PATH:baz, bat=PATH:bam} }}\n"}, 51 | {"parses mustaches with hash arguments (07)", `{{foo bar=baz bat="bam"}}`, "{{ PATH:foo [] HASH{bar=PATH:baz, bat=\"bam\"} }}\n"}, 52 | {"parses mustaches with hash arguments (08)", `{{foo bat='bam'}}`, "{{ PATH:foo [] HASH{bat=\"bam\"} }}\n"}, 53 | {"parses mustaches with hash arguments (09)", `{{foo omg bar=baz bat="bam"}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\"} }}\n"}, 54 | {"parses mustaches with hash arguments (10)", `{{foo omg bar=baz bat="bam" baz=1}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=NUMBER{1}} }}\n"}, 55 | {"parses mustaches with hash arguments (11)", `{{foo omg bar=baz bat="bam" baz=true}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=BOOLEAN{true}} }}\n"}, 56 | {"parses mustaches with hash arguments (12)", `{{foo omg bar=baz bat="bam" baz=false}}`, "{{ PATH:foo [PATH:omg] HASH{bar=PATH:baz, bat=\"bam\", baz=BOOLEAN{false}} }}\n"}, 57 | 58 | {"parses contents followed by a mustache", `foo bar {{baz}}`, "CONTENT[ 'foo bar ' ]\n{{ PATH:baz [] }}\n"}, 59 | 60 | {"parses a partial (1)", `{{> foo }}`, "{{> PARTIAL:foo }}\n"}, 61 | {"parses a partial (2)", `{{> "foo" }}`, "{{> PARTIAL:foo }}\n"}, 62 | {"parses a partial (3)", `{{> 1 }}`, "{{> PARTIAL:1 }}\n"}, 63 | {"parses a partial with context", `{{> foo bar}}`, "{{> PARTIAL:foo PATH:bar }}\n"}, 64 | {"parses a partial with hash", `{{> foo bar=bat}}`, "{{> PARTIAL:foo HASH{bar=PATH:bat} }}\n"}, 65 | {"parses a partial with context and hash", `{{> foo bar bat=baz}}`, "{{> PARTIAL:foo PATH:bar HASH{bat=PATH:baz} }}\n"}, 66 | {"parses a partial with a complex name", `{{> shared/partial?.bar}}`, "{{> PARTIAL:shared/partial?.bar }}\n"}, 67 | 68 | {"parses a comment", `{{! this is a comment }}`, "{{! ' this is a comment ' }}\n"}, 69 | {"parses a multi-line comment", "{{!\nthis is a multi-line comment\n}}", "{{! '\nthis is a multi-line comment\n' }}\n"}, 70 | 71 | {"parses an inverse section", `{{#foo}} bar {{^}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"}, 72 | {"parses an inverse (else-style) section", `{{#foo}} bar {{else}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n CONTENT[ ' baz ' ]\n"}, 73 | {"parses multiple inverse sections", `{{#foo}} bar {{else if bar}}{{else}} baz {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n BLOCK:\n PATH:if [PATH:bar]\n PROGRAM:\n {{^}}\n CONTENT[ ' baz ' ]\n"}, 74 | {"parses empty blocks", `{{#foo}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n"}, 75 | {"parses empty blocks with empty inverse section", `{{#foo}}{{^}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"}, 76 | {"parses empty blocks with empty inverse (else-style) section", `{{#foo}}{{else}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n"}, 77 | {"parses non-empty blocks with empty inverse section", `{{#foo}} bar {{^}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"}, 78 | {"parses non-empty blocks with empty inverse (else-style) section", `{{#foo}} bar {{else}}{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n CONTENT[ ' bar ' ]\n {{^}}\n"}, 79 | {"parses empty blocks with non-empty inverse section", `{{#foo}}{{^}} bar {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"}, 80 | {"parses empty blocks with non-empty inverse (else-style) section", `{{#foo}}{{else}} bar {{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n CONTENT[ ' bar ' ]\n"}, 81 | {"parses a standalone inverse section", `{{^foo}}bar{{/foo}}`, "BLOCK:\n PATH:foo []\n {{^}}\n CONTENT[ 'bar' ]\n"}, 82 | {"parses block with block params", `{{#foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"}, 83 | {"parses inverse block with block params", `{{^foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n {{^}}\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"}, 84 | {"parses chained inverse block with block params", `{{#foo}}{{else foo as |bar baz|}}content{{/foo}}`, "BLOCK:\n PATH:foo []\n PROGRAM:\n {{^}}\n BLOCK:\n PATH:foo []\n PROGRAM:\n BLOCK PARAMS: [ bar baz ]\n CONTENT[ 'content' ]\n"}, 85 | } 86 | 87 | func TestParser(t *testing.T) { 88 | t.Parallel() 89 | 90 | for _, test := range parserTests { 91 | output := "" 92 | 93 | node, err := Parse(test.input) 94 | if err == nil { 95 | output = ast.Print(node) 96 | } 97 | 98 | if (err != nil) || (test.output != output) { 99 | t.Errorf("Test '%s' failed\ninput:\n\t'%s'\nexpected\n\t%q\ngot\n\t%q\nerror:\n\t%s", test.name, test.input, test.output, output, err) 100 | } 101 | } 102 | } 103 | 104 | var parserErrorTests = []parserTest{ 105 | {"lexer error", `{{! unclosed comment`, "Lexer error"}, 106 | {"syntax error", `foo{{^}}`, "Syntax error"}, 107 | 108 | {"open raw block must be closed", `{{{{raw foo}} bar {{{{/raw}}}}`, "Expecting CloseRawBlock"}, 109 | {"end raw block must be closed", `{{{{raw foo}}}} bar {{{{/raw}}`, "Expecting CloseRawBlock"}, 110 | 111 | {"raw block names must match (1)", `{{{{1}}}}{{foo}}{{{{/raw}}}}`, "1 doesn't match raw"}, 112 | {"raw block names must match (2)", `{{{{raw}}}}{{foo}}{{{{/1}}}}`, "raw doesn't match 1"}, 113 | {"raw block names must match (3)", `{{{{goodbyes}}}}test{{{{/hellos}}}}`, "goodbyes doesn't match hellos"}, 114 | 115 | {"open block must be closed", `{{#foo bar}}}{{/foo}}`, "Expecting Close"}, 116 | {"end block must be closed", `{{#foo bar}}{{/foo}}}`, "Expecting Close"}, 117 | {"an open block must have a end block", `{{#foo}}test`, "Expecting OpenEndBlock"}, 118 | 119 | {"block names must match (1)", `{{#1 bar}}{{/foo}}`, "1 doesn't match foo"}, 120 | {"block names must match (2)", `{{#foo bar}}{{/1}}`, "foo doesn't match 1"}, 121 | {"block names must match (3)", `{{#foo}}test{{/bar}}`, "foo doesn't match bar"}, 122 | 123 | {"an mustache must terminate with a close mustache", `{{foo}}}`, "Expecting Close"}, 124 | {"an unescaped mustache must terminate with a close unescaped mustache", `{{{foo}}`, "Expecting CloseUnescaped"}, 125 | 126 | {"an partial must terminate with a close mustache", `{{> foo}}}`, "Expecting Close"}, 127 | {"a subexpression must terminate with a close subexpression", `{{foo (false}}`, "Expecting CloseSexpr"}, 128 | 129 | {"raises on missing hash value (1)", `{{foo bar=}}`, "Parse error on line 1"}, 130 | {"raises on missing hash value (2)", `{{foo bar=baz bim=}}`, "Parse error on line 1"}, 131 | 132 | {"block param must have at least one param", `{{#foo as ||}}content{{/foo}}`, "Expecting ID"}, 133 | {"open block params must be closed", `{{#foo as |}}content{{/foo}}`, "Expecting ID"}, 134 | 135 | {"a path must start with an ID", `{{#/}}content{{/foo}}`, "Expecting ID"}, 136 | {"a path must end with an ID", `{{foo/bar/}}`, "Expecting ID"}, 137 | 138 | // 139 | // Next tests come from: 140 | // https://github.com/wycats/handlebars.js/blob/master/spec/parser.js 141 | // 142 | {"throws on old inverse section", `{{else foo}}bar{{/foo}}`, ""}, 143 | 144 | {"raises if there's a parser error (1)", `foo{{^}}bar`, "Parse error on line 1"}, 145 | {"raises if there's a parser error (2)", `{{foo}`, "Parse error on line 1"}, 146 | {"raises if there's a parser error (3)", `{{foo &}}`, "Parse error on line 1"}, 147 | {"raises if there's a parser error (4)", `{{#goodbyes}}{{/hellos}}`, "Parse error on line 1"}, 148 | {"raises if there's a parser error (5)", `{{#goodbyes}}{{/hellos}}`, "goodbyes doesn't match hellos"}, 149 | 150 | {"should handle invalid paths (1)", `{{foo/../bar}}`, `Invalid path: foo/..`}, 151 | {"should handle invalid paths (2)", `{{foo/./bar}}`, `Invalid path: foo/.`}, 152 | {"should handle invalid paths (3)", `{{foo/this/bar}}`, `Invalid path: foo/this`}, 153 | 154 | {"knows how to report the correct line number in errors (1)", "hello\nmy\n{{foo}", "Parse error on line 3"}, 155 | {"knows how to report the correct line number in errors (2)", "hello\n\nmy\n\n{{foo}", "Parse error on line 5"}, 156 | 157 | {"knows how to report the correct line number in errors when the first character is a newline", "\n\nhello\n\nmy\n\n{{foo}", "Parse error on line 7"}, 158 | } 159 | 160 | func TestParserErrors(t *testing.T) { 161 | t.Parallel() 162 | 163 | for _, test := range parserErrorTests { 164 | node, err := Parse(test.input) 165 | if err == nil { 166 | output := ast.Print(node) 167 | tokens := lexer.Collect(test.input) 168 | 169 | t.Errorf("Test '%s' failed - Error expected\ninput:\n\t'%s'\ngot\n\t%q\ntokens:\n\t%q", test.name, test.input, output, tokens) 170 | } else if test.output != "" { 171 | matched, errMatch := regexp.MatchString(regexp.QuoteMeta(test.output), fmt.Sprint(err)) 172 | if errMatch != nil { 173 | panic("Failed to match regexp") 174 | } 175 | 176 | if !matched { 177 | t.Errorf("Test '%s' failed - Incorrect error returned\ninput:\n\t'%s'\nexpected\n\t%q\ngot\n\t%q", test.name, test.input, test.output, err) 178 | } 179 | } 180 | } 181 | } 182 | 183 | // package example 184 | func Example() { 185 | source := "You know {{nothing}} John Snow" 186 | 187 | // parse template 188 | program, err := Parse(source) 189 | if err != nil { 190 | panic(err) 191 | } 192 | 193 | // print AST 194 | output := ast.Print(program) 195 | 196 | fmt.Print(output) 197 | // CONTENT[ 'You know ' ] 198 | // {{ PATH:nothing [] }} 199 | // CONTENT[ ' John Snow' ] 200 | } 201 | -------------------------------------------------------------------------------- /parser/whitespace.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/aymerick/raymond/ast" 7 | ) 8 | 9 | // whitespaceVisitor walks through the AST to perform whitespace control 10 | // 11 | // The logic was shamelessly borrowed from: 12 | // https://github.com/wycats/handlebars.js/blob/master/lib/handlebars/compiler/whitespace-control.js 13 | type whitespaceVisitor struct { 14 | isRootSeen bool 15 | } 16 | 17 | var ( 18 | rTrimLeft = regexp.MustCompile(`^[ \t]*\r?\n?`) 19 | rTrimLeftMultiple = regexp.MustCompile(`^\s+`) 20 | 21 | rTrimRight = regexp.MustCompile(`[ \t]+$`) 22 | rTrimRightMultiple = regexp.MustCompile(`\s+$`) 23 | 24 | rPrevWhitespace = regexp.MustCompile(`\r?\n\s*?$`) 25 | rPrevWhitespaceStart = regexp.MustCompile(`(^|\r?\n)\s*?$`) 26 | 27 | rNextWhitespace = regexp.MustCompile(`^\s*?\r?\n`) 28 | rNextWhitespaceEnd = regexp.MustCompile(`^\s*?(\r?\n|$)`) 29 | 30 | rPartialIndent = regexp.MustCompile(`([ \t]+$)`) 31 | ) 32 | 33 | // newWhitespaceVisitor instanciates a new whitespaceVisitor 34 | func newWhitespaceVisitor() *whitespaceVisitor { 35 | return &whitespaceVisitor{} 36 | } 37 | 38 | // processWhitespaces performs whitespace control on given AST 39 | // 40 | // WARNING: It must be called only once on AST. 41 | func processWhitespaces(node ast.Node) { 42 | node.Accept(newWhitespaceVisitor()) 43 | } 44 | 45 | func omitRightFirst(body []ast.Node, multiple bool) { 46 | omitRight(body, -1, multiple) 47 | } 48 | 49 | func omitRight(body []ast.Node, i int, multiple bool) { 50 | if i+1 >= len(body) { 51 | return 52 | } 53 | 54 | current := body[i+1] 55 | 56 | node, ok := current.(*ast.ContentStatement) 57 | if !ok { 58 | return 59 | } 60 | 61 | if !multiple && node.RightStripped { 62 | return 63 | } 64 | 65 | original := node.Value 66 | 67 | r := rTrimLeft 68 | if multiple { 69 | r = rTrimLeftMultiple 70 | } 71 | 72 | node.Value = r.ReplaceAllString(node.Value, "") 73 | 74 | node.RightStripped = (original != node.Value) 75 | } 76 | 77 | func omitLeftLast(body []ast.Node, multiple bool) { 78 | omitLeft(body, len(body), multiple) 79 | } 80 | 81 | func omitLeft(body []ast.Node, i int, multiple bool) bool { 82 | if i-1 < 0 { 83 | return false 84 | } 85 | 86 | current := body[i-1] 87 | 88 | node, ok := current.(*ast.ContentStatement) 89 | if !ok { 90 | return false 91 | } 92 | 93 | if !multiple && node.LeftStripped { 94 | return false 95 | } 96 | 97 | original := node.Value 98 | 99 | r := rTrimRight 100 | if multiple { 101 | r = rTrimRightMultiple 102 | } 103 | 104 | node.Value = r.ReplaceAllString(node.Value, "") 105 | 106 | node.LeftStripped = (original != node.Value) 107 | 108 | return node.LeftStripped 109 | } 110 | 111 | func isPrevWhitespace(body []ast.Node) bool { 112 | return isPrevWhitespaceProgram(body, len(body), false) 113 | } 114 | 115 | func isPrevWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool { 116 | if i < 1 { 117 | return isRoot 118 | } 119 | 120 | prev := body[i-1] 121 | 122 | if node, ok := prev.(*ast.ContentStatement); ok { 123 | if (node.Value == "") && node.RightStripped { 124 | // already stripped, so it may be an empty string not catched by regexp 125 | return true 126 | } 127 | 128 | r := rPrevWhitespaceStart 129 | if (i > 1) || !isRoot { 130 | r = rPrevWhitespace 131 | } 132 | 133 | return r.MatchString(node.Value) 134 | } 135 | 136 | return false 137 | } 138 | 139 | func isNextWhitespace(body []ast.Node) bool { 140 | return isNextWhitespaceProgram(body, -1, false) 141 | } 142 | 143 | func isNextWhitespaceProgram(body []ast.Node, i int, isRoot bool) bool { 144 | if i+1 >= len(body) { 145 | return isRoot 146 | } 147 | 148 | next := body[i+1] 149 | 150 | if node, ok := next.(*ast.ContentStatement); ok { 151 | if (node.Value == "") && node.LeftStripped { 152 | // already stripped, so it may be an empty string not catched by regexp 153 | return true 154 | } 155 | 156 | r := rNextWhitespaceEnd 157 | if (i+2 > len(body)) || !isRoot { 158 | r = rNextWhitespace 159 | } 160 | 161 | return r.MatchString(node.Value) 162 | } 163 | 164 | return false 165 | } 166 | 167 | // 168 | // Visitor interface 169 | // 170 | 171 | func (v *whitespaceVisitor) VisitProgram(program *ast.Program) interface{} { 172 | isRoot := !v.isRootSeen 173 | v.isRootSeen = true 174 | 175 | body := program.Body 176 | for i, current := range body { 177 | strip, _ := current.Accept(v).(*ast.Strip) 178 | if strip == nil { 179 | continue 180 | } 181 | 182 | _isPrevWhitespace := isPrevWhitespaceProgram(body, i, isRoot) 183 | _isNextWhitespace := isNextWhitespaceProgram(body, i, isRoot) 184 | 185 | openStandalone := strip.OpenStandalone && _isPrevWhitespace 186 | closeStandalone := strip.CloseStandalone && _isNextWhitespace 187 | inlineStandalone := strip.InlineStandalone && _isPrevWhitespace && _isNextWhitespace 188 | 189 | if strip.Close { 190 | omitRight(body, i, true) 191 | } 192 | 193 | if strip.Open && (i > 0) { 194 | omitLeft(body, i, true) 195 | } 196 | 197 | if inlineStandalone { 198 | omitRight(body, i, false) 199 | 200 | if omitLeft(body, i, false) { 201 | // If we are on a standalone node, save the indent info for partials 202 | if partial, ok := current.(*ast.PartialStatement); ok { 203 | // Pull out the whitespace from the final line 204 | if i > 0 { 205 | if prevContent, ok := body[i-1].(*ast.ContentStatement); ok { 206 | partial.Indent = rPartialIndent.FindString(prevContent.Original) 207 | } 208 | } 209 | } 210 | } 211 | } 212 | 213 | if b, ok := current.(*ast.BlockStatement); ok { 214 | if openStandalone { 215 | prog := b.Program 216 | if prog == nil { 217 | prog = b.Inverse 218 | } 219 | 220 | omitRightFirst(prog.Body, false) 221 | 222 | // Strip out the previous content node if it's whitespace only 223 | omitLeft(body, i, false) 224 | } 225 | 226 | if closeStandalone { 227 | prog := b.Inverse 228 | if prog == nil { 229 | prog = b.Program 230 | } 231 | 232 | // Always strip the next node 233 | omitRight(body, i, false) 234 | 235 | omitLeftLast(prog.Body, false) 236 | } 237 | 238 | } 239 | } 240 | 241 | return nil 242 | } 243 | 244 | func (v *whitespaceVisitor) VisitBlock(block *ast.BlockStatement) interface{} { 245 | if block.Program != nil { 246 | block.Program.Accept(v) 247 | } 248 | 249 | if block.Inverse != nil { 250 | block.Inverse.Accept(v) 251 | } 252 | 253 | program := block.Program 254 | inverse := block.Inverse 255 | 256 | if program == nil { 257 | program = inverse 258 | inverse = nil 259 | } 260 | 261 | firstInverse := inverse 262 | lastInverse := inverse 263 | 264 | if (inverse != nil) && inverse.Chained { 265 | b, _ := inverse.Body[0].(*ast.BlockStatement) 266 | firstInverse = b.Program 267 | 268 | for lastInverse.Chained { 269 | b, _ := lastInverse.Body[len(lastInverse.Body)-1].(*ast.BlockStatement) 270 | lastInverse = b.Program 271 | } 272 | } 273 | 274 | closeProg := firstInverse 275 | if closeProg == nil { 276 | closeProg = program 277 | } 278 | 279 | strip := &ast.Strip{ 280 | Open: (block.OpenStrip != nil) && block.OpenStrip.Open, 281 | Close: (block.CloseStrip != nil) && block.CloseStrip.Close, 282 | 283 | OpenStandalone: isNextWhitespace(program.Body), 284 | CloseStandalone: isPrevWhitespace(closeProg.Body), 285 | } 286 | 287 | if (block.OpenStrip != nil) && block.OpenStrip.Close { 288 | omitRightFirst(program.Body, true) 289 | } 290 | 291 | if inverse != nil { 292 | if block.InverseStrip != nil { 293 | inverseStrip := block.InverseStrip 294 | 295 | if inverseStrip.Open { 296 | omitLeftLast(program.Body, true) 297 | } 298 | 299 | if inverseStrip.Close { 300 | omitRightFirst(firstInverse.Body, true) 301 | } 302 | } 303 | 304 | if (block.CloseStrip != nil) && block.CloseStrip.Open { 305 | omitLeftLast(lastInverse.Body, true) 306 | } 307 | 308 | // Find standalone else statements 309 | if isPrevWhitespace(program.Body) && isNextWhitespace(firstInverse.Body) { 310 | omitLeftLast(program.Body, false) 311 | 312 | omitRightFirst(firstInverse.Body, false) 313 | } 314 | } else if (block.CloseStrip != nil) && block.CloseStrip.Open { 315 | omitLeftLast(program.Body, true) 316 | } 317 | 318 | return strip 319 | } 320 | 321 | func (v *whitespaceVisitor) VisitMustache(mustache *ast.MustacheStatement) interface{} { 322 | return mustache.Strip 323 | } 324 | 325 | func _inlineStandalone(strip *ast.Strip) interface{} { 326 | return &ast.Strip{ 327 | Open: strip.Open, 328 | Close: strip.Close, 329 | InlineStandalone: true, 330 | } 331 | } 332 | 333 | func (v *whitespaceVisitor) VisitPartial(node *ast.PartialStatement) interface{} { 334 | strip := node.Strip 335 | if strip == nil { 336 | strip = &ast.Strip{} 337 | } 338 | 339 | return _inlineStandalone(strip) 340 | } 341 | 342 | func (v *whitespaceVisitor) VisitComment(node *ast.CommentStatement) interface{} { 343 | strip := node.Strip 344 | if strip == nil { 345 | strip = &ast.Strip{} 346 | } 347 | 348 | return _inlineStandalone(strip) 349 | } 350 | 351 | // NOOP 352 | func (v *whitespaceVisitor) VisitContent(node *ast.ContentStatement) interface{} { return nil } 353 | func (v *whitespaceVisitor) VisitExpression(node *ast.Expression) interface{} { return nil } 354 | func (v *whitespaceVisitor) VisitSubExpression(node *ast.SubExpression) interface{} { return nil } 355 | func (v *whitespaceVisitor) VisitPath(node *ast.PathExpression) interface{} { return nil } 356 | func (v *whitespaceVisitor) VisitString(node *ast.StringLiteral) interface{} { return nil } 357 | func (v *whitespaceVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} { return nil } 358 | func (v *whitespaceVisitor) VisitNumber(node *ast.NumberLiteral) interface{} { return nil } 359 | func (v *whitespaceVisitor) VisitHash(node *ast.Hash) interface{} { return nil } 360 | func (v *whitespaceVisitor) VisitHashPair(node *ast.HashPair) interface{} { return nil } 361 | -------------------------------------------------------------------------------- /partial.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // partial represents a partial template 9 | type partial struct { 10 | name string 11 | source string 12 | tpl *Template 13 | } 14 | 15 | // partials stores all global partials 16 | var partials map[string]*partial 17 | 18 | // protects global partials 19 | var partialsMutex sync.RWMutex 20 | 21 | func init() { 22 | partials = make(map[string]*partial) 23 | } 24 | 25 | // newPartial instanciates a new partial 26 | func newPartial(name string, source string, tpl *Template) *partial { 27 | return &partial{ 28 | name: name, 29 | source: source, 30 | tpl: tpl, 31 | } 32 | } 33 | 34 | // RegisterPartial registers a global partial. That partial will be available to all templates. 35 | func RegisterPartial(name string, source string) { 36 | partialsMutex.Lock() 37 | defer partialsMutex.Unlock() 38 | 39 | if partials[name] != nil { 40 | panic(fmt.Errorf("Partial already registered: %s", name)) 41 | } 42 | 43 | partials[name] = newPartial(name, source, nil) 44 | } 45 | 46 | // RegisterPartials registers several global partials. Those partials will be available to all templates. 47 | func RegisterPartials(partials map[string]string) { 48 | for name, p := range partials { 49 | RegisterPartial(name, p) 50 | } 51 | } 52 | 53 | // RegisterPartialTemplate registers a global partial with given parsed template. That partial will be available to all templates. 54 | func RegisterPartialTemplate(name string, tpl *Template) { 55 | partialsMutex.Lock() 56 | defer partialsMutex.Unlock() 57 | 58 | if partials[name] != nil { 59 | panic(fmt.Errorf("Partial already registered: %s", name)) 60 | } 61 | 62 | partials[name] = newPartial(name, "", tpl) 63 | } 64 | 65 | // RemovePartial removes the partial registered under the given name. The partial will not be available globally anymore. This does not affect partials registered on a specific template. 66 | func RemovePartial(name string) { 67 | partialsMutex.Lock() 68 | defer partialsMutex.Unlock() 69 | 70 | delete(partials, name) 71 | } 72 | 73 | // RemoveAllPartials removes all globally registered partials. This does not affect partials registered on a specific template. 74 | func RemoveAllPartials() { 75 | partialsMutex.Lock() 76 | defer partialsMutex.Unlock() 77 | 78 | partials = make(map[string]*partial) 79 | } 80 | 81 | // findPartial finds a registered global partial 82 | func findPartial(name string) *partial { 83 | partialsMutex.RLock() 84 | defer partialsMutex.RUnlock() 85 | 86 | return partials[name] 87 | } 88 | 89 | // template returns parsed partial template 90 | func (p *partial) template() (*Template, error) { 91 | if p.tpl == nil { 92 | var err error 93 | 94 | p.tpl, err = Parse(p.source) 95 | if err != nil { 96 | return nil, err 97 | } 98 | } 99 | 100 | return p.tpl, nil 101 | } 102 | -------------------------------------------------------------------------------- /raymond.go: -------------------------------------------------------------------------------- 1 | // Package raymond provides handlebars evaluation 2 | package raymond 3 | 4 | // Render parses a template and evaluates it with given context 5 | // 6 | // Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead. 7 | func Render(source string, ctx interface{}) (string, error) { 8 | // parse template 9 | tpl, err := Parse(source) 10 | if err != nil { 11 | return "", err 12 | } 13 | 14 | // renders template 15 | str, err := tpl.Exec(ctx) 16 | if err != nil { 17 | return "", err 18 | } 19 | 20 | return str, nil 21 | } 22 | 23 | // MustRender parses a template and evaluates it with given context. It panics on error. 24 | // 25 | // Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead. 26 | func MustRender(source string, ctx interface{}) string { 27 | return MustParse(source).MustExec(ctx) 28 | } 29 | -------------------------------------------------------------------------------- /raymond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aymerick/raymond/b565731e1464263de0bda75f2e45d97b54b60110/raymond.png -------------------------------------------------------------------------------- /raymond_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import "fmt" 4 | 5 | func Example() { 6 | source := "

{{title}}

{{body.content}}

" 7 | 8 | ctx := map[string]interface{}{ 9 | "title": "foo", 10 | "body": map[string]string{"content": "bar"}, 11 | } 12 | 13 | // parse template 14 | tpl := MustParse(source) 15 | 16 | // evaluate template with context 17 | output := tpl.MustExec(ctx) 18 | 19 | // alternatively, for one shots: 20 | // output := MustRender(source, ctx) 21 | 22 | fmt.Print(output) 23 | // Output:

foo

bar

24 | } 25 | 26 | func Example_struct() { 27 | source := `
28 |

By {{fullName author}}

29 |
{{body}}
30 | 31 |

Comments

32 | 33 | {{#each comments}} 34 |

By {{fullName author}}

35 |
{{content}}
36 | {{/each}} 37 |
` 38 | 39 | type Person struct { 40 | FirstName string 41 | LastName string 42 | } 43 | 44 | type Comment struct { 45 | Author Person 46 | Body string `handlebars:"content"` 47 | } 48 | 49 | type Post struct { 50 | Author Person 51 | Body string 52 | Comments []Comment 53 | } 54 | 55 | ctx := Post{ 56 | Person{"Jean", "Valjean"}, 57 | "Life is difficult", 58 | []Comment{ 59 | Comment{ 60 | Person{"Marcel", "Beliveau"}, 61 | "LOL!", 62 | }, 63 | }, 64 | } 65 | 66 | RegisterHelper("fullName", func(person Person) string { 67 | return person.FirstName + " " + person.LastName 68 | }) 69 | 70 | output := MustRender(source, ctx) 71 | 72 | fmt.Print(output) 73 | // Output:
74 | //

By Jean Valjean

75 | //
Life is difficult
76 | // 77 | //

Comments

78 | // 79 | //

By Marcel Beliveau

80 | //
LOL!
81 | //
82 | } 83 | 84 | func ExampleRender() { 85 | tpl := "

{{title}}

{{body.content}}

" 86 | 87 | ctx := map[string]interface{}{ 88 | "title": "foo", 89 | "body": map[string]string{"content": "bar"}, 90 | } 91 | 92 | // render template with context 93 | output, err := Render(tpl, ctx) 94 | if err != nil { 95 | panic(err) 96 | } 97 | 98 | fmt.Print(output) 99 | // Output:

foo

bar

100 | } 101 | 102 | func ExampleMustRender() { 103 | tpl := "

{{title}}

{{body.content}}

" 104 | 105 | ctx := map[string]interface{}{ 106 | "title": "foo", 107 | "body": map[string]string{"content": "bar"}, 108 | } 109 | 110 | // render template with context 111 | output := MustRender(tpl, ctx) 112 | 113 | fmt.Print(output) 114 | // Output:

foo

bar

115 | } 116 | -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | ) 8 | 9 | // SafeString represents a string that must not be escaped. 10 | // 11 | // A SafeString can be returned by helpers to disable escaping. 12 | type SafeString string 13 | 14 | // isSafeString returns true if argument is a SafeString 15 | func isSafeString(value interface{}) bool { 16 | if _, ok := value.(SafeString); ok { 17 | return true 18 | } 19 | return false 20 | } 21 | 22 | // Str returns string representation of any basic type value. 23 | func Str(value interface{}) string { 24 | return strValue(reflect.ValueOf(value)) 25 | } 26 | 27 | // strValue returns string representation of a reflect.Value 28 | func strValue(value reflect.Value) string { 29 | result := "" 30 | 31 | ival, ok := printableValue(value) 32 | if !ok { 33 | panic(fmt.Errorf("Can't print value: %q", value)) 34 | } 35 | 36 | val := reflect.ValueOf(ival) 37 | 38 | switch val.Kind() { 39 | case reflect.Array, reflect.Slice: 40 | for i := 0; i < val.Len(); i++ { 41 | result += strValue(val.Index(i)) 42 | } 43 | case reflect.Bool: 44 | result = "false" 45 | if val.Bool() { 46 | result = "true" 47 | } 48 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 49 | result = fmt.Sprintf("%d", ival) 50 | case reflect.Float32, reflect.Float64: 51 | result = strconv.FormatFloat(val.Float(), 'f', -1, 64) 52 | case reflect.Invalid: 53 | result = "" 54 | default: 55 | result = fmt.Sprintf("%s", ival) 56 | } 57 | 58 | return result 59 | } 60 | 61 | // printableValue returns the, possibly indirected, interface value inside v that 62 | // is best for a call to formatted printer. 63 | // 64 | // NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go 65 | func printableValue(v reflect.Value) (interface{}, bool) { 66 | if v.Kind() == reflect.Ptr { 67 | v, _ = indirect(v) // fmt.Fprint handles nil. 68 | } 69 | if !v.IsValid() { 70 | return "", true 71 | } 72 | 73 | if !v.Type().Implements(errorType) && !v.Type().Implements(fmtStringerType) { 74 | if v.CanAddr() && (reflect.PtrTo(v.Type()).Implements(errorType) || reflect.PtrTo(v.Type()).Implements(fmtStringerType)) { 75 | v = v.Addr() 76 | } else { 77 | switch v.Kind() { 78 | case reflect.Chan, reflect.Func: 79 | return nil, false 80 | } 81 | } 82 | } 83 | return v.Interface(), true 84 | } 85 | -------------------------------------------------------------------------------- /string_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type strTest struct { 9 | name string 10 | input interface{} 11 | output string 12 | } 13 | 14 | var strTests = []strTest{ 15 | {"String", "foo", "foo"}, 16 | {"Boolean true", true, "true"}, 17 | {"Boolean false", false, "false"}, 18 | {"Integer", 25, "25"}, 19 | {"Float", 25.75, "25.75"}, 20 | {"Nil", nil, ""}, 21 | {"[]string", []string{"foo", "bar"}, "foobar"}, 22 | {"[]interface{} (strings)", []interface{}{"foo", "bar"}, "foobar"}, 23 | {"[]Boolean", []bool{true, false}, "truefalse"}, 24 | } 25 | 26 | func TestStr(t *testing.T) { 27 | t.Parallel() 28 | 29 | for _, test := range strTests { 30 | if res := Str(test.input); res != test.output { 31 | t.Errorf("Failed to stringify: %s\nexpected:\n\t'%s'got:\n\t%q", test.name, test.output, res) 32 | } 33 | } 34 | } 35 | 36 | func ExampleStr() { 37 | output := Str(3) + " foos are " + Str(true) + " and " + Str(-1.25) + " bars are " + Str(false) + "\n" 38 | output += "But you know '" + Str(nil) + "' John Snow\n" 39 | output += "map: " + Str(map[string]string{"foo": "bar"}) + "\n" 40 | output += "array: " + Str([]interface{}{true, 10, "foo", 5, "bar"}) 41 | 42 | fmt.Println(output) 43 | // Output: 3 foos are true and -1.25 bars are false 44 | // But you know '' John Snow 45 | // map: map[foo:bar] 46 | // array: true10foo5bar 47 | } 48 | 49 | func ExampleSafeString() { 50 | RegisterHelper("em", func() SafeString { 51 | return SafeString("FOO BAR") 52 | }) 53 | 54 | tpl := MustParse("{{em}}") 55 | 56 | result := tpl.MustExec(nil) 57 | fmt.Print(result) 58 | // Output: FOO BAR 59 | } 60 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "reflect" 7 | "runtime" 8 | "sync" 9 | 10 | "github.com/aymerick/raymond/ast" 11 | "github.com/aymerick/raymond/parser" 12 | ) 13 | 14 | // Template represents a handlebars template. 15 | type Template struct { 16 | source string 17 | program *ast.Program 18 | helpers map[string]reflect.Value 19 | partials map[string]*partial 20 | mutex sync.RWMutex // protects helpers and partials 21 | } 22 | 23 | // newTemplate instanciate a new template without parsing it 24 | func newTemplate(source string) *Template { 25 | return &Template{ 26 | source: source, 27 | helpers: make(map[string]reflect.Value), 28 | partials: make(map[string]*partial), 29 | } 30 | } 31 | 32 | // Parse instanciates a template by parsing given source. 33 | func Parse(source string) (*Template, error) { 34 | tpl := newTemplate(source) 35 | 36 | // parse template 37 | if err := tpl.parse(); err != nil { 38 | return nil, err 39 | } 40 | 41 | return tpl, nil 42 | } 43 | 44 | // MustParse instanciates a template by parsing given source. It panics on error. 45 | func MustParse(source string) *Template { 46 | result, err := Parse(source) 47 | if err != nil { 48 | panic(err) 49 | } 50 | return result 51 | } 52 | 53 | // ParseFile reads given file and returns parsed template. 54 | func ParseFile(filePath string) (*Template, error) { 55 | b, err := ioutil.ReadFile(filePath) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return Parse(string(b)) 61 | } 62 | 63 | // parse parses the template 64 | // 65 | // It can be called several times, the parsing will be done only once. 66 | func (tpl *Template) parse() error { 67 | if tpl.program == nil { 68 | var err error 69 | 70 | tpl.program, err = parser.Parse(tpl.source) 71 | if err != nil { 72 | return err 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | 79 | // Clone returns a copy of that template. 80 | func (tpl *Template) Clone() *Template { 81 | result := newTemplate(tpl.source) 82 | 83 | result.program = tpl.program 84 | 85 | tpl.mutex.RLock() 86 | defer tpl.mutex.RUnlock() 87 | 88 | for name, helper := range tpl.helpers { 89 | result.RegisterHelper(name, helper.Interface()) 90 | } 91 | 92 | for name, partial := range tpl.partials { 93 | result.addPartial(name, partial.source, partial.tpl) 94 | } 95 | 96 | return result 97 | } 98 | 99 | func (tpl *Template) findHelper(name string) reflect.Value { 100 | tpl.mutex.RLock() 101 | defer tpl.mutex.RUnlock() 102 | 103 | return tpl.helpers[name] 104 | } 105 | 106 | // RegisterHelper registers a helper for that template. 107 | func (tpl *Template) RegisterHelper(name string, helper interface{}) { 108 | tpl.mutex.Lock() 109 | defer tpl.mutex.Unlock() 110 | 111 | if tpl.helpers[name] != zero { 112 | panic(fmt.Sprintf("Helper %s already registered", name)) 113 | } 114 | 115 | val := reflect.ValueOf(helper) 116 | ensureValidHelper(name, val) 117 | 118 | tpl.helpers[name] = val 119 | } 120 | 121 | // RegisterHelpers registers several helpers for that template. 122 | func (tpl *Template) RegisterHelpers(helpers map[string]interface{}) { 123 | for name, helper := range helpers { 124 | tpl.RegisterHelper(name, helper) 125 | } 126 | } 127 | 128 | func (tpl *Template) addPartial(name string, source string, template *Template) { 129 | tpl.mutex.Lock() 130 | defer tpl.mutex.Unlock() 131 | 132 | if tpl.partials[name] != nil { 133 | panic(fmt.Sprintf("Partial %s already registered", name)) 134 | } 135 | 136 | tpl.partials[name] = newPartial(name, source, template) 137 | } 138 | 139 | func (tpl *Template) findPartial(name string) *partial { 140 | tpl.mutex.RLock() 141 | defer tpl.mutex.RUnlock() 142 | 143 | return tpl.partials[name] 144 | } 145 | 146 | // RegisterPartial registers a partial for that template. 147 | func (tpl *Template) RegisterPartial(name string, source string) { 148 | tpl.addPartial(name, source, nil) 149 | } 150 | 151 | // RegisterPartials registers several partials for that template. 152 | func (tpl *Template) RegisterPartials(partials map[string]string) { 153 | for name, partial := range partials { 154 | tpl.RegisterPartial(name, partial) 155 | } 156 | } 157 | 158 | // RegisterPartialFile reads given file and registers its content as a partial with given name. 159 | func (tpl *Template) RegisterPartialFile(filePath string, name string) error { 160 | b, err := ioutil.ReadFile(filePath) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | tpl.RegisterPartial(name, string(b)) 166 | 167 | return nil 168 | } 169 | 170 | // RegisterPartialFiles reads several files and registers them as partials, the filename base is used as the partial name. 171 | func (tpl *Template) RegisterPartialFiles(filePaths ...string) error { 172 | if len(filePaths) == 0 { 173 | return nil 174 | } 175 | 176 | for _, filePath := range filePaths { 177 | name := fileBase(filePath) 178 | 179 | if err := tpl.RegisterPartialFile(filePath, name); err != nil { 180 | return err 181 | } 182 | } 183 | 184 | return nil 185 | } 186 | 187 | // RegisterPartialTemplate registers an already parsed partial for that template. 188 | func (tpl *Template) RegisterPartialTemplate(name string, template *Template) { 189 | tpl.addPartial(name, "", template) 190 | } 191 | 192 | // Exec evaluates template with given context. 193 | func (tpl *Template) Exec(ctx interface{}) (result string, err error) { 194 | return tpl.ExecWith(ctx, nil) 195 | } 196 | 197 | // MustExec evaluates template with given context. It panics on error. 198 | func (tpl *Template) MustExec(ctx interface{}) string { 199 | result, err := tpl.Exec(ctx) 200 | if err != nil { 201 | panic(err) 202 | } 203 | return result 204 | } 205 | 206 | // ExecWith evaluates template with given context and private data frame. 207 | func (tpl *Template) ExecWith(ctx interface{}, privData *DataFrame) (result string, err error) { 208 | defer errRecover(&err) 209 | 210 | // parses template if necessary 211 | err = tpl.parse() 212 | if err != nil { 213 | return 214 | } 215 | 216 | // setup visitor 217 | v := newEvalVisitor(tpl, ctx, privData) 218 | 219 | // visit AST 220 | result, _ = tpl.program.Accept(v).(string) 221 | 222 | // named return values 223 | return 224 | } 225 | 226 | // errRecover recovers evaluation panic 227 | func errRecover(errp *error) { 228 | e := recover() 229 | if e != nil { 230 | switch err := e.(type) { 231 | case runtime.Error: 232 | panic(e) 233 | case error: 234 | *errp = err 235 | default: 236 | panic(e) 237 | } 238 | } 239 | } 240 | 241 | // PrintAST returns string representation of parsed template. 242 | func (tpl *Template) PrintAST() string { 243 | if err := tpl.parse(); err != nil { 244 | return fmt.Sprintf("PARSER ERROR: %s", err) 245 | } 246 | 247 | return ast.Print(tpl.program) 248 | } 249 | -------------------------------------------------------------------------------- /template_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | var sourceBasic = `
9 |

{{title}}

10 |
11 | {{body}} 12 |
13 |
` 14 | 15 | var basicAST = `CONTENT[ '
16 |

' ] 17 | {{ PATH:title [] }} 18 | CONTENT[ '

19 |
20 | ' ] 21 | {{ PATH:body [] }} 22 | CONTENT[ ' 23 |
24 |
' ] 25 | ` 26 | 27 | func TestNewTemplate(t *testing.T) { 28 | t.Parallel() 29 | 30 | tpl := newTemplate(sourceBasic) 31 | if tpl.source != sourceBasic { 32 | t.Errorf("Failed to instantiate template") 33 | } 34 | } 35 | 36 | func TestParse(t *testing.T) { 37 | t.Parallel() 38 | 39 | tpl, err := Parse(sourceBasic) 40 | if err != nil || (tpl.source != sourceBasic) { 41 | t.Errorf("Failed to parse template") 42 | } 43 | 44 | if str := tpl.PrintAST(); str != basicAST { 45 | t.Errorf("Template parsing incorrect: %s", str) 46 | } 47 | } 48 | 49 | func TestClone(t *testing.T) { 50 | t.Parallel() 51 | 52 | sourcePartial := `I am a {{wat}} partial` 53 | sourcePartial2 := `Partial for the {{wat}}` 54 | 55 | tpl := MustParse(sourceBasic) 56 | tpl.RegisterPartial("p", sourcePartial) 57 | 58 | if (len(tpl.partials) != 1) || (tpl.partials["p"] == nil) { 59 | t.Errorf("What?") 60 | } 61 | 62 | cloned := tpl.Clone() 63 | 64 | if (len(cloned.partials) != 1) || (cloned.partials["p"] == nil) { 65 | t.Errorf("Template partials must be cloned") 66 | } 67 | 68 | cloned.RegisterPartial("p2", sourcePartial2) 69 | 70 | if (len(cloned.partials) != 2) || (cloned.partials["p"] == nil) || (cloned.partials["p2"] == nil) { 71 | t.Errorf("Failed to register a partial on cloned template") 72 | } 73 | 74 | if (len(tpl.partials) != 1) || (tpl.partials["p"] == nil) { 75 | t.Errorf("Modification of a cloned template MUST NOT affect original template") 76 | } 77 | } 78 | 79 | func ExampleTemplate_Exec() { 80 | source := "

{{title}}

{{body.content}}

" 81 | 82 | ctx := map[string]interface{}{ 83 | "title": "foo", 84 | "body": map[string]string{"content": "bar"}, 85 | } 86 | 87 | // parse template 88 | tpl := MustParse(source) 89 | 90 | // evaluate template with context 91 | output, err := tpl.Exec(ctx) 92 | if err != nil { 93 | panic(err) 94 | } 95 | 96 | fmt.Print(output) 97 | // Output:

foo

bar

98 | } 99 | 100 | func ExampleTemplate_MustExec() { 101 | source := "

{{title}}

{{body.content}}

" 102 | 103 | ctx := map[string]interface{}{ 104 | "title": "foo", 105 | "body": map[string]string{"content": "bar"}, 106 | } 107 | 108 | // parse template 109 | tpl := MustParse(source) 110 | 111 | // evaluate template with context 112 | output := tpl.MustExec(ctx) 113 | 114 | fmt.Print(output) 115 | // Output:

foo

bar

116 | } 117 | 118 | func ExampleTemplate_ExecWith() { 119 | source := "

{{title}}

{{#body}}{{content}} and {{@baz.bat}}{{/body}}

" 120 | 121 | ctx := map[string]interface{}{ 122 | "title": "foo", 123 | "body": map[string]string{"content": "bar"}, 124 | } 125 | 126 | // parse template 127 | tpl := MustParse(source) 128 | 129 | // computes private data frame 130 | frame := NewDataFrame() 131 | frame.Set("baz", map[string]string{"bat": "unicorns"}) 132 | 133 | // evaluate template 134 | output, err := tpl.ExecWith(ctx, frame) 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | fmt.Print(output) 140 | // Output:

foo

bar and unicorns

141 | } 142 | 143 | func ExampleTemplate_PrintAST() { 144 | source := "

{{title}}

{{#body}}{{content}} and {{@baz.bat}}{{/body}}

" 145 | 146 | // parse template 147 | tpl := MustParse(source) 148 | 149 | // print AST 150 | output := tpl.PrintAST() 151 | 152 | fmt.Print(output) 153 | // Output: CONTENT[ '

' ] 154 | // {{ PATH:title [] }} 155 | // CONTENT[ '

' ] 156 | // BLOCK: 157 | // PATH:body [] 158 | // PROGRAM: 159 | // {{ PATH:content [] 160 | // }} 161 | // CONTENT[ ' and ' ] 162 | // {{ @PATH:baz/bat [] 163 | // }} 164 | // CONTENT[ '

' ] 165 | // 166 | } 167 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "path" 5 | "reflect" 6 | ) 7 | 8 | // indirect returns the item at the end of indirection, and a bool to indicate if it's nil. 9 | // We indirect through pointers and empty interfaces (only) because 10 | // non-empty interfaces have methods we might need. 11 | // 12 | // NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go 13 | func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { 14 | for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() { 15 | if v.IsNil() { 16 | return v, true 17 | } 18 | if v.Kind() == reflect.Interface && v.NumMethod() > 0 { 19 | break 20 | } 21 | } 22 | return v, false 23 | } 24 | 25 | // IsTrue returns true if obj is a truthy value. 26 | func IsTrue(obj interface{}) bool { 27 | thruth, ok := isTrueValue(reflect.ValueOf(obj)) 28 | if !ok { 29 | return false 30 | } 31 | return thruth 32 | } 33 | 34 | // isTrueValue reports whether the value is 'true', in the sense of not the zero of its type, 35 | // and whether the value has a meaningful truth value 36 | // 37 | // NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go 38 | func isTrueValue(val reflect.Value) (truth, ok bool) { 39 | if !val.IsValid() { 40 | // Something like var x interface{}, never set. It's a form of nil. 41 | return false, true 42 | } 43 | switch val.Kind() { 44 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 45 | truth = val.Len() > 0 46 | case reflect.Bool: 47 | truth = val.Bool() 48 | case reflect.Complex64, reflect.Complex128: 49 | truth = val.Complex() != 0 50 | case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface: 51 | truth = !val.IsNil() 52 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 53 | truth = val.Int() != 0 54 | case reflect.Float32, reflect.Float64: 55 | truth = val.Float() != 0 56 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 57 | truth = val.Uint() != 0 58 | case reflect.Struct: 59 | truth = true // Struct values are always true. 60 | default: 61 | return 62 | } 63 | return truth, true 64 | } 65 | 66 | // canBeNil reports whether an untyped nil can be assigned to the type. See reflect.Zero. 67 | // 68 | // NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go 69 | func canBeNil(typ reflect.Type) bool { 70 | switch typ.Kind() { 71 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 72 | return true 73 | } 74 | return false 75 | } 76 | 77 | // fileBase returns base file name 78 | // 79 | // example: /foo/bar/baz.png => baz 80 | func fileBase(filePath string) string { 81 | fileName := path.Base(filePath) 82 | fileExt := path.Ext(filePath) 83 | 84 | return fileName[:len(fileName)-len(fileExt)] 85 | } 86 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import "fmt" 4 | 5 | func ExampleIsTrue() { 6 | output := "Empty array: " + Str(IsTrue([0]string{})) + "\n" 7 | output += "Non empty array: " + Str(IsTrue([1]string{"foo"})) + "\n" 8 | 9 | output += "Empty slice: " + Str(IsTrue([]string{})) + "\n" 10 | output += "Non empty slice: " + Str(IsTrue([]string{"foo"})) + "\n" 11 | 12 | output += "Empty map: " + Str(IsTrue(map[string]string{})) + "\n" 13 | output += "Non empty map: " + Str(IsTrue(map[string]string{"foo": "bar"})) + "\n" 14 | 15 | output += "Empty string: " + Str(IsTrue("")) + "\n" 16 | output += "Non empty string: " + Str(IsTrue("foo")) + "\n" 17 | 18 | output += "true bool: " + Str(IsTrue(true)) + "\n" 19 | output += "false bool: " + Str(IsTrue(false)) + "\n" 20 | 21 | output += "0 integer: " + Str(IsTrue(0)) + "\n" 22 | output += "positive integer: " + Str(IsTrue(10)) + "\n" 23 | output += "negative integer: " + Str(IsTrue(-10)) + "\n" 24 | 25 | output += "0 float: " + Str(IsTrue(0.0)) + "\n" 26 | output += "positive float: " + Str(IsTrue(10.0)) + "\n" 27 | output += "negative integer: " + Str(IsTrue(-10.0)) + "\n" 28 | 29 | output += "struct: " + Str(IsTrue(struct{}{})) + "\n" 30 | output += "nil: " + Str(IsTrue(nil)) + "\n" 31 | 32 | fmt.Println(output) 33 | // Output: Empty array: false 34 | // Non empty array: true 35 | // Empty slice: false 36 | // Non empty slice: true 37 | // Empty map: false 38 | // Non empty map: true 39 | // Empty string: false 40 | // Non empty string: true 41 | // true bool: true 42 | // false bool: false 43 | // 0 integer: false 44 | // positive integer: true 45 | // negative integer: true 46 | // 0 float: false 47 | // positive float: true 48 | // negative integer: true 49 | // struct: true 50 | // nil: false 51 | } 52 | --------------------------------------------------------------------------------