├── .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 |
72 | {{#each items}}
73 | {{#if current}}
74 | - {{name}}
75 | {{^}}
76 | - {{name}}
77 | {{/if}}
78 | {{/each}}
79 |
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 |
177 | - City: {{info.location}}
178 | - Rug: {{info.[r.u.g]}}
179 | - Activity: {{info.activity}}
180 |
181 | {{#each other-names}}
182 |
{{alias-name}}
183 | {{/each}}
184 |
`
185 |
186 | expected := `
187 |
Lebowski
188 |
189 | - City: Venice
190 | - Rug: Tied The Room Together
191 | - Activity: Bowling
192 |
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: <b>",
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 |
--------------------------------------------------------------------------------