├── VERSION ├── raymond.png ├── .gitignore ├── handlebars ├── doc.go ├── base_test.go ├── subexpressions_test.go ├── partials_test.go ├── whitespace_test.go ├── blocks_test.go └── data_test.go ├── .travis.yml ├── go.mod ├── escape_test.go ├── LICENSE ├── raymond.go ├── escape.go ├── string_test.go ├── CHANGELOG.md ├── utils_test.go ├── go.sum ├── BENCHMARKS.md ├── mustache └── specs │ ├── comments.json │ ├── partials.json │ ├── comments.yml │ ├── delimiters.json │ ├── partials.yml │ ├── delimiters.yml │ ├── ~lambdas.json │ ├── inverted.json │ ├── ~lambdas.yml │ ├── inverted.yml │ ├── interpolation.json │ ├── sections.json │ ├── interpolation.yml │ └── sections.yml ├── data_frame.go ├── string.go ├── raymond_test.go ├── utils.go ├── partial.go ├── context_test.go ├── template_test.go ├── lexer └── token.go ├── context.go ├── base_test.go ├── mustache_test.go ├── ast └── print.go ├── json_visitor_test.go ├── eval_test.go ├── template.go ├── benchmark_test.go └── parser └── whitespace.go /VERSION: -------------------------------------------------------------------------------- 1 | 2.0.2 2 | -------------------------------------------------------------------------------- /raymond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailgun/raymond/HEAD/raymond.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.swp 3 | .vscode/ 4 | __pycache__ 5 | *.pyc 6 | gubernator.egg-info/ 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /handlebars/doc.go: -------------------------------------------------------------------------------- 1 | // Package handlebars contains all the tests that come from handlebars.js project. 2 | package handlebars 3 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mailgun/raymond/v2 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/sirupsen/logrus v1.8.1 // indirect 7 | github.com/stretchr/testify v1.7.0 // indirect 8 | gopkg.in/yaml.v2 v2.4.0 9 | ) 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /raymond.go: -------------------------------------------------------------------------------- 1 | // Package raymond provides handlebars evaluation 2 | package raymond 3 | 4 | import "github.com/sirupsen/logrus" 5 | 6 | var log *logrus.Entry 7 | 8 | func init() { 9 | log = logrus.NewEntry(logrus.StandardLogger()) 10 | } 11 | 12 | // SetLogger allows the user to set a customer logger adding the ability to add custom fields to 13 | // the log entries. 14 | func SetLogger(entry *logrus.Entry) { 15 | log = entry 16 | } 17 | 18 | // Render parses a template and evaluates it with given context 19 | // 20 | // Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead. 21 | func Render(source string, ctx interface{}) (string, error) { 22 | // parse template 23 | tpl, err := Parse(source) 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | // renders template 29 | str, err := tpl.Exec(ctx) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | return str, nil 35 | } 36 | 37 | // MustRender parses a template and evaluates it with given context. It panics on error. 38 | // 39 | // Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead. 40 | func MustRender(source string, ctx interface{}) string { 41 | return MustParse(source).MustExec(ctx) 42 | } 43 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 7 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 8 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 9 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 10 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 11 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 12 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= 14 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 18 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 20 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mustache/specs/comments.json: -------------------------------------------------------------------------------- 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Comment tags represent content that should never appear in the resulting\noutput.\n\nThe tag's content may contain any substring (including newlines) EXCEPT the\nclosing delimiter.\n\nComment tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Inline","data":{},"expected":"1234567890","template":"12345{{! Comment Block! }}67890","desc":"Comment blocks should be removed from the template."},{"name":"Multiline","data":{},"expected":"1234567890\n","template":"12345{{!\n This is a\n multi-line comment...\n}}67890\n","desc":"Multiline comments should be permitted."},{"name":"Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{! Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{! Indented Comment Block! }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{! Standalone Comment }}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"!","template":" {{! I'm Still Standalone }}\n!","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"!\n","template":"!\n {{! I'm Still Standalone }}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{!\nSomething's going on here...\n}}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Multiline Standalone","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{!\n Something's going on here...\n }}\nEnd.\n","desc":"All standalone comment lines should be removed."},{"name":"Indented Inline","data":{},"expected":" 12 \n","template":" 12 {{! 34 }}\n","desc":"Inline comments should not strip whitespace"},{"name":"Surrounding Whitespace","data":{},"expected":"12345 67890","template":"12345 {{! Comment Block! }} 67890","desc":"Comment removal should preserve surrounding whitespace."}]} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mustache/specs/partials.json: -------------------------------------------------------------------------------- 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Partial tags are used to expand an external template into the current\ntemplate.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the partial to inject. Set Delimiter tags MUST NOT\naffect the parsing of a partial. The partial MUST be rendered against the\ncontext stack local to the tag. If the named partial cannot be found, the\nempty string SHOULD be used instead, as in interpolations.\n\nPartial tags SHOULD be treated as standalone when appropriate. If this tag\nis used standalone, any whitespace preceding the tag should treated as\nindentation, and prepended to each line of the partial before rendering.\n","tests":[{"name":"Basic Behavior","data":{},"expected":"\"from partial\"","template":"\"{{>text}}\"","desc":"The greater-than operator should expand to the named partial.","partials":{"text":"from partial"}},{"name":"Failed Lookup","data":{},"expected":"\"\"","template":"\"{{>text}}\"","desc":"The empty string should be used when the named partial is not found.","partials":{}},{"name":"Context","data":{"text":"content"},"expected":"\"*content*\"","template":"\"{{>partial}}\"","desc":"The greater-than operator should operate within the current context.","partials":{"partial":"*{{text}}*"}},{"name":"Recursion","data":{"content":"X","nodes":[{"content":"Y","nodes":[]}]},"expected":"X>","template":"{{>node}}","desc":"The greater-than operator should properly recurse.","partials":{"node":"{{content}}<{{#nodes}}{{>node}}{{/nodes}}>"}},{"name":"Surrounding Whitespace","data":{},"expected":"| \t|\t |","template":"| {{>partial}} |","desc":"The greater-than operator should not alter surrounding whitespace.","partials":{"partial":"\t|\t"}},{"name":"Inline Indentation","data":{"data":"|"},"expected":" | >\n>\n","template":" {{data}} {{> partial}}\n","desc":"Whitespace should be left untouched.","partials":{"partial":">\n>"}},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n>|","template":"|\r\n{{>partial}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags.","partials":{"partial":">"}},{"name":"Standalone Without Previous Line","data":{},"expected":" >\n >>","template":" {{>partial}}\n>","desc":"Standalone tags should not require a newline to precede them.","partials":{"partial":">\n>"}},{"name":"Standalone Without Newline","data":{},"expected":">\n >\n >","template":">\n {{>partial}}","desc":"Standalone tags should not require a newline to follow them.","partials":{"partial":">\n>"}},{"name":"Standalone Indentation","data":{"content":"<\n->"},"expected":"\\\n |\n <\n->\n |\n/\n","template":"\\\n {{>partial}}\n/\n","desc":"Each line of the partial should be indented before rendering.","partials":{"partial":"|\n{{{content}}}\n|\n"}},{"name":"Padding Whitespace","data":{"boolean":true},"expected":"|[]|","template":"|{{> partial }}|","desc":"Superfluous in-tag whitespace should be ignored.","partials":{"partial":"[]"}}]} -------------------------------------------------------------------------------- /mustache/specs/comments.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Comment tags represent content that should never appear in the resulting 3 | output. 4 | 5 | The tag's content may contain any substring (including newlines) EXCEPT the 6 | closing delimiter. 7 | 8 | Comment tags SHOULD be treated as standalone when appropriate. 9 | tests: 10 | - name: Inline 11 | desc: Comment blocks should be removed from the template. 12 | data: { } 13 | template: '12345{{! Comment Block! }}67890' 14 | expected: '1234567890' 15 | 16 | - name: Multiline 17 | desc: Multiline comments should be permitted. 18 | data: { } 19 | template: | 20 | 12345{{! 21 | This is a 22 | multi-line comment... 23 | }}67890 24 | expected: | 25 | 1234567890 26 | 27 | - name: Standalone 28 | desc: All standalone comment lines should be removed. 29 | data: { } 30 | template: | 31 | Begin. 32 | {{! Comment Block! }} 33 | End. 34 | expected: | 35 | Begin. 36 | End. 37 | 38 | - name: Indented Standalone 39 | desc: All standalone comment lines should be removed. 40 | data: { } 41 | template: | 42 | Begin. 43 | {{! Indented Comment Block! }} 44 | End. 45 | expected: | 46 | Begin. 47 | End. 48 | 49 | - name: Standalone Line Endings 50 | desc: '"\r\n" should be considered a newline for standalone tags.' 51 | data: { } 52 | template: "|\r\n{{! Standalone Comment }}\r\n|" 53 | expected: "|\r\n|" 54 | 55 | - name: Standalone Without Previous Line 56 | desc: Standalone tags should not require a newline to precede them. 57 | data: { } 58 | template: " {{! I'm Still Standalone }}\n!" 59 | expected: "!" 60 | 61 | - name: Standalone Without Newline 62 | desc: Standalone tags should not require a newline to follow them. 63 | data: { } 64 | template: "!\n {{! I'm Still Standalone }}" 65 | expected: "!\n" 66 | 67 | - name: Multiline Standalone 68 | desc: All standalone comment lines should be removed. 69 | data: { } 70 | template: | 71 | Begin. 72 | {{! 73 | Something's going on here... 74 | }} 75 | End. 76 | expected: | 77 | Begin. 78 | End. 79 | 80 | - name: Indented Multiline Standalone 81 | desc: All standalone comment lines should be removed. 82 | data: { } 83 | template: | 84 | Begin. 85 | {{! 86 | Something's going on here... 87 | }} 88 | End. 89 | expected: | 90 | Begin. 91 | End. 92 | 93 | - name: Indented Inline 94 | desc: Inline comments should not strip whitespace 95 | data: { } 96 | template: " 12 {{! 34 }}\n" 97 | expected: " 12 \n" 98 | 99 | - name: Surrounding Whitespace 100 | desc: Comment removal should preserve surrounding whitespace. 101 | data: { } 102 | template: '12345 {{! Comment Block! }} 67890' 103 | expected: '12345 67890' 104 | -------------------------------------------------------------------------------- /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/mailgun/raymond/v2" 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 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | func TestHandlebarsContext(t *testing.T) { 11 | suite.Run(t, new(HandlebarsContextSuite)) 12 | } 13 | 14 | type HandlebarsContextSuite struct { 15 | suite.Suite 16 | c *handlebarsContext 17 | } 18 | 19 | func (s *HandlebarsContextSuite) SetupTest() { 20 | s.c = newHandlebarsContext() 21 | } 22 | 23 | func (s *HandlebarsContextSuite) TestHandlebarsContextAddMemberContext() { 24 | assert.Equal(s.T(), 0, len(s.c.GetCurrentContext()), "Len expected to be zero.") 25 | s.c.AddMemberContext("foo", "bar") 26 | assert.Equal(s.T(), 1, len(s.c.GetCurrentContext()), "Len expected to be one.") 27 | s.c.AddMemberContext("baz", "bar") 28 | assert.Equal(s.T(), 2, len(s.c.GetCurrentContext()), "Len expected to be two.") 29 | s.c.AddMemberContext("bean", "bar") 30 | assert.Equal(s.T(), 3, len(s.c.GetCurrentContext()), "Len expected to be three.") 31 | assert.Equal(s.T(), "foo.baz.bean", s.c.GetCurrentContextString(), "Should be all three scopes.") 32 | assert.Equal(s.T(), 2, len(s.c.GetParentContext(1)), "Len expected to be two.") 33 | assert.Equal(s.T(), "foo.baz", s.c.GetParentContextString(1), "Should be two scopes.") 34 | assert.Equal(s.T(), "", s.c.GetParentContextString(3), "Should be empty string.") 35 | assert.Equal(s.T(), "foo.baz.bean", s.c.GetParentContextString(4), "Parent context exceeded use default ancestor.") 36 | assert.Equal(s.T(), 3, len(s.c.GetCurrentContext()), "Len expected to be three.") 37 | s.c.MoveUpContext() 38 | assert.Equal(s.T(), 2, len(s.c.GetCurrentContext()), "Len expected to be two.") 39 | assert.Equal(s.T(), "foo.baz", s.c.GetCurrentContextString(), "Should be two scopes.") 40 | } 41 | 42 | func (s *HandlebarsContextSuite) TestHandlebarsContextMappedContextAllTheSameMapping() { 43 | assert.Equal(s.T(), 0, len(s.c.GetCurrentContext()), "Len expected to be zero.") 44 | s.c.AddMemberContext("foo", "bar") 45 | s.c.AddMemberContext("baz", "bar") 46 | s.c.AddMemberContext("bean", "bar") 47 | assert.Equal(s.T(), "foo.blah.baz.bing.bean.bong", s.c.GetMappedContextString([]string{"bar", "blah", "bar", "bing", "bar", "bong"}, 0), "Should be all three scopes.") 48 | } 49 | 50 | func (s *HandlebarsContextSuite) TestHandlebarsContextMappedContextLongNamesSameMapping() { 51 | assert.Equal(s.T(), 0, len(s.c.GetCurrentContext()), "Len expected to be zero.") 52 | s.c.AddMemberContext("foo.foo.foo", "bar") 53 | s.c.AddMemberContext("baz.baz.baz", "bar") 54 | s.c.AddMemberContext("bean.bean.bean", "bar") 55 | assert.Equal(s.T(), "foo.foo.foo.baz.baz.baz.bean.bean.bean", s.c.GetMappedContextString([]string{"bar", "bar", "bar"}, 0), "Should be all three scopes.") 56 | } 57 | 58 | func (s *HandlebarsContextSuite) TestHandlebarsContextMappedContextLongNamesSameMappingNoMapping() { 59 | assert.Equal(s.T(), 0, len(s.c.GetCurrentContext()), "Len expected to be zero.") 60 | s.c.AddMemberContext("foo.foo.foo", "bar") 61 | s.c.AddMemberContext("baz.baz.baz", "") 62 | s.c.AddMemberContext("bean.bean.bean", "bar") 63 | assert.Equal(s.T(), "foo.foo.foo.baz.baz.baz.bleep.bean.bean.bean.bop", s.c.GetMappedContextString([]string{"bar", "bleep", "bar", "bop"}, 0), "Should be all three scopes.") 64 | } 65 | -------------------------------------------------------------------------------- /mustache/specs/delimiters.json: -------------------------------------------------------------------------------- 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Set Delimiter tags are used to change the tag delimiters for all content\nfollowing the tag in the current compilation unit.\n\nThe tag's content MUST be any two non-whitespace sequences (separated by\nwhitespace) EXCEPT an equals sign ('=') followed by the current closing\ndelimiter.\n\nSet Delimiter tags SHOULD be treated as standalone when appropriate.\n","tests":[{"name":"Pair Behavior","data":{"text":"Hey!"},"expected":"(Hey!)","template":"{{=<% %>=}}(<%text%>)","desc":"The equals sign (used on both sides) should permit delimiter changes."},{"name":"Special Characters","data":{"text":"It worked!"},"expected":"(It worked!)","template":"({{=[ ]=}}[text])","desc":"Characters with special meaning regexen should be valid delimiters."},{"name":"Sections","data":{"section":true,"data":"I got interpolated."},"expected":"[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n","template":"[\n{{#section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|#section|\n {{data}}\n |data|\n|/section|\n]\n","desc":"Delimiters set outside sections should persist."},{"name":"Inverted Sections","data":{"section":false,"data":"I got interpolated."},"expected":"[\n I got interpolated.\n |data|\n\n {{data}}\n I got interpolated.\n]\n","template":"[\n{{^section}}\n {{data}}\n |data|\n{{/section}}\n\n{{= | | =}}\n|^section|\n {{data}}\n |data|\n|/section|\n]\n","desc":"Delimiters set outside inverted sections should persist."},{"name":"Partial Inheritence","data":{"value":"yes"},"expected":"[ .yes. ]\n[ .yes. ]\n","template":"[ {{>include}} ]\n{{= | | =}}\n[ |>include| ]\n","desc":"Delimiters set in a parent template should not affect a partial.","partials":{"include":".{{value}}."}},{"name":"Post-Partial Behavior","data":{"value":"yes"},"expected":"[ .yes. .yes. ]\n[ .yes. .|value|. ]\n","template":"[ {{>include}} ]\n[ .{{value}}. .|value|. ]\n","desc":"Delimiters set in a partial should not affect the parent template.","partials":{"include":".{{value}}. {{= | | =}} .|value|."}},{"name":"Surrounding Whitespace","data":{},"expected":"| |","template":"| {{=@ @=}} |","desc":"Surrounding whitespace should be left untouched."},{"name":"Outlying Whitespace (Inline)","data":{},"expected":" | \n","template":" | {{=@ @=}}\n","desc":"Whitespace should be left untouched."},{"name":"Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n{{=@ @=}}\nEnd.\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Tag","data":{},"expected":"Begin.\nEnd.\n","template":"Begin.\n {{=@ @=}}\nEnd.\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{},"expected":"|\r\n|","template":"|\r\n{{= @ @ =}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{},"expected":"=","template":" {{=@ @=}}\n=","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{},"expected":"=\n","template":"=\n {{=@ @=}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Pair with Padding","data":{},"expected":"||","template":"|{{= @ @ =}}|","desc":"Superfluous in-tag whitespace should be ignored."}]} -------------------------------------------------------------------------------- /mustache/specs/partials.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Partial tags are used to expand an external template into the current 3 | template. 4 | 5 | The tag's content MUST be a non-whitespace character sequence NOT containing 6 | the current closing delimiter. 7 | 8 | This tag's content names the partial to inject. Set Delimiter tags MUST NOT 9 | affect the parsing of a partial. The partial MUST be rendered against the 10 | context stack local to the tag. If the named partial cannot be found, the 11 | empty string SHOULD be used instead, as in interpolations. 12 | 13 | Partial tags SHOULD be treated as standalone when appropriate. If this tag 14 | is used standalone, any whitespace preceding the tag should treated as 15 | indentation, and prepended to each line of the partial before rendering. 16 | tests: 17 | - name: Basic Behavior 18 | desc: The greater-than operator should expand to the named partial. 19 | data: { } 20 | template: '"{{>text}}"' 21 | partials: { text: 'from partial' } 22 | expected: '"from partial"' 23 | 24 | - name: Failed Lookup 25 | desc: The empty string should be used when the named partial is not found. 26 | data: { } 27 | template: '"{{>text}}"' 28 | partials: { } 29 | expected: '""' 30 | 31 | - name: Context 32 | desc: The greater-than operator should operate within the current context. 33 | data: { text: 'content' } 34 | template: '"{{>partial}}"' 35 | partials: { partial: '*{{text}}*' } 36 | expected: '"*content*"' 37 | 38 | - name: Recursion 39 | desc: The greater-than operator should properly recurse. 40 | data: { content: "X", nodes: [ { content: "Y", nodes: [] } ] } 41 | template: '{{>node}}' 42 | partials: { node: '{{content}}<{{#nodes}}{{>node}}{{/nodes}}>' } 43 | expected: 'X>' 44 | 45 | # Whitespace Sensitivity 46 | 47 | - name: Surrounding Whitespace 48 | desc: The greater-than operator should not alter surrounding whitespace. 49 | data: { } 50 | template: '| {{>partial}} |' 51 | partials: { partial: "\t|\t" } 52 | expected: "| \t|\t |" 53 | 54 | - name: Inline Indentation 55 | desc: Whitespace should be left untouched. 56 | data: { data: '|' } 57 | template: " {{data}} {{> partial}}\n" 58 | partials: { partial: ">\n>" } 59 | expected: " | >\n>\n" 60 | 61 | - name: Standalone Line Endings 62 | desc: '"\r\n" should be considered a newline for standalone tags.' 63 | data: { } 64 | template: "|\r\n{{>partial}}\r\n|" 65 | partials: { partial: ">" } 66 | expected: "|\r\n>|" 67 | 68 | - name: Standalone Without Previous Line 69 | desc: Standalone tags should not require a newline to precede them. 70 | data: { } 71 | template: " {{>partial}}\n>" 72 | partials: { partial: ">\n>"} 73 | expected: " >\n >>" 74 | 75 | - name: Standalone Without Newline 76 | desc: Standalone tags should not require a newline to follow them. 77 | data: { } 78 | template: ">\n {{>partial}}" 79 | partials: { partial: ">\n>" } 80 | expected: ">\n >\n >" 81 | 82 | - name: Standalone Indentation 83 | desc: Each line of the partial should be indented before rendering. 84 | data: { content: "<\n->" } 85 | template: | 86 | \ 87 | {{>partial}} 88 | / 89 | partials: 90 | partial: | 91 | | 92 | {{{content}}} 93 | | 94 | expected: | 95 | \ 96 | | 97 | < 98 | -> 99 | | 100 | / 101 | 102 | # Whitespace Insensitivity 103 | 104 | - name: Padding Whitespace 105 | desc: Superfluous in-tag whitespace should be ignored. 106 | data: { boolean: true } 107 | template: "|{{> partial }}|" 108 | partials: { partial: "[]" } 109 | expected: '|[]|' 110 | -------------------------------------------------------------------------------- /template_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | const 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 | -------------------------------------------------------------------------------- /mustache/specs/delimiters.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Set Delimiter tags are used to change the tag delimiters for all content 3 | following the tag in the current compilation unit. 4 | 5 | The tag's content MUST be any two non-whitespace sequences (separated by 6 | whitespace) EXCEPT an equals sign ('=') followed by the current closing 7 | delimiter. 8 | 9 | Set Delimiter tags SHOULD be treated as standalone when appropriate. 10 | tests: 11 | - name: Pair Behavior 12 | desc: The equals sign (used on both sides) should permit delimiter changes. 13 | data: { text: 'Hey!' } 14 | template: '{{=<% %>=}}(<%text%>)' 15 | expected: '(Hey!)' 16 | 17 | - name: Special Characters 18 | desc: Characters with special meaning regexen should be valid delimiters. 19 | data: { text: 'It worked!' } 20 | template: '({{=[ ]=}}[text])' 21 | expected: '(It worked!)' 22 | 23 | - name: Sections 24 | desc: Delimiters set outside sections should persist. 25 | data: { section: true, data: 'I got interpolated.' } 26 | template: | 27 | [ 28 | {{#section}} 29 | {{data}} 30 | |data| 31 | {{/section}} 32 | 33 | {{= | | =}} 34 | |#section| 35 | {{data}} 36 | |data| 37 | |/section| 38 | ] 39 | expected: | 40 | [ 41 | I got interpolated. 42 | |data| 43 | 44 | {{data}} 45 | I got interpolated. 46 | ] 47 | 48 | - name: Inverted Sections 49 | desc: Delimiters set outside inverted sections should persist. 50 | data: { section: false, data: 'I got interpolated.' } 51 | template: | 52 | [ 53 | {{^section}} 54 | {{data}} 55 | |data| 56 | {{/section}} 57 | 58 | {{= | | =}} 59 | |^section| 60 | {{data}} 61 | |data| 62 | |/section| 63 | ] 64 | expected: | 65 | [ 66 | I got interpolated. 67 | |data| 68 | 69 | {{data}} 70 | I got interpolated. 71 | ] 72 | 73 | - name: Partial Inheritence 74 | desc: Delimiters set in a parent template should not affect a partial. 75 | data: { value: 'yes' } 76 | partials: 77 | include: '.{{value}}.' 78 | template: | 79 | [ {{>include}} ] 80 | {{= | | =}} 81 | [ |>include| ] 82 | expected: | 83 | [ .yes. ] 84 | [ .yes. ] 85 | 86 | - name: Post-Partial Behavior 87 | desc: Delimiters set in a partial should not affect the parent template. 88 | data: { value: 'yes' } 89 | partials: 90 | include: '.{{value}}. {{= | | =}} .|value|.' 91 | template: | 92 | [ {{>include}} ] 93 | [ .{{value}}. .|value|. ] 94 | expected: | 95 | [ .yes. .yes. ] 96 | [ .yes. .|value|. ] 97 | 98 | # Whitespace Sensitivity 99 | 100 | - name: Surrounding Whitespace 101 | desc: Surrounding whitespace should be left untouched. 102 | data: { } 103 | template: '| {{=@ @=}} |' 104 | expected: '| |' 105 | 106 | - name: Outlying Whitespace (Inline) 107 | desc: Whitespace should be left untouched. 108 | data: { } 109 | template: " | {{=@ @=}}\n" 110 | expected: " | \n" 111 | 112 | - name: Standalone Tag 113 | desc: Standalone lines should be removed from the template. 114 | data: { } 115 | template: | 116 | Begin. 117 | {{=@ @=}} 118 | End. 119 | expected: | 120 | Begin. 121 | End. 122 | 123 | - name: Indented Standalone Tag 124 | desc: Indented standalone lines should be removed from the template. 125 | data: { } 126 | template: | 127 | Begin. 128 | {{=@ @=}} 129 | End. 130 | expected: | 131 | Begin. 132 | End. 133 | 134 | - name: Standalone Line Endings 135 | desc: '"\r\n" should be considered a newline for standalone tags.' 136 | data: { } 137 | template: "|\r\n{{= @ @ =}}\r\n|" 138 | expected: "|\r\n|" 139 | 140 | - name: Standalone Without Previous Line 141 | desc: Standalone tags should not require a newline to precede them. 142 | data: { } 143 | template: " {{=@ @=}}\n=" 144 | expected: "=" 145 | 146 | - name: Standalone Without Newline 147 | desc: Standalone tags should not require a newline to follow them. 148 | data: { } 149 | template: "=\n {{=@ @=}}" 150 | expected: "=\n" 151 | 152 | # Whitespace Insensitivity 153 | 154 | - name: Pair with Padding 155 | desc: Superfluous in-tag whitespace should be ignored. 156 | data: { } 157 | template: '|{{= @ @ =}}|' 158 | expected: '||' 159 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type contextMember struct { 8 | path string 9 | asMapping string 10 | } 11 | 12 | type handlebarsContext struct { 13 | contextMembers []contextMember 14 | } 15 | 16 | func newHandlebarsContext() *handlebarsContext { 17 | var cm []contextMember 18 | return &handlebarsContext{contextMembers: cm} 19 | } 20 | 21 | func (h *handlebarsContext) AddMemberContext(path, asMapping string) { 22 | cmp := contextMember{path: path, asMapping: asMapping} 23 | h.contextMembers = append(h.contextMembers, cmp) 24 | } 25 | 26 | func (h *handlebarsContext) GetCurrentContext() []string { 27 | return h.GetParentContext(0) 28 | } 29 | 30 | func (h *handlebarsContext) GetCurrentContextString() string { 31 | return h.GetParentContextString(0) 32 | } 33 | 34 | func (h *handlebarsContext) GetParentContext(num_ancestors int) []string { 35 | if len(h.contextMembers) == 0 { 36 | return []string{} 37 | } 38 | return strings.Split(h.GetParentContextString(num_ancestors), ".") 39 | } 40 | 41 | func (h *handlebarsContext) GetParentContextString(num_ancestors int) string { 42 | if len(h.contextMembers) == 0 { 43 | return "" 44 | } 45 | if num_ancestors > len(h.contextMembers) { 46 | num_ancestors = 0 47 | } 48 | var res string 49 | for _, val := range h.contextMembers[:len(h.contextMembers)-num_ancestors] { 50 | if len(res) == 0 { 51 | res = val.path 52 | } else { 53 | res = res + "." + val.path 54 | } 55 | } 56 | return res 57 | } 58 | 59 | func (h *handlebarsContext) MoveUpContext() { 60 | if len(h.contextMembers) > 0 { 61 | h.contextMembers = h.contextMembers[:len(h.contextMembers)-1] 62 | } 63 | } 64 | 65 | func (h *handlebarsContext) HaveAsContexts(num_ancestors int) bool { 66 | if num_ancestors > len(h.contextMembers) { 67 | num_ancestors = 0 68 | } 69 | for val := range h.contextMembers[:len(h.contextMembers)-num_ancestors] { 70 | if h.contextMembers[val].asMapping != "" { 71 | return true 72 | } 73 | } 74 | return false 75 | } 76 | 77 | func (h *handlebarsContext) GetMappedContext(path []string, num_ancestors int) []string { 78 | if len(path) == 0 { 79 | return []string{} 80 | } 81 | return strings.Split(h.GetMappedContextString(path, num_ancestors), ".") 82 | } 83 | 84 | func (h *handlebarsContext) GetMappedContextString(path []string, num_ancestors int) string { 85 | if len(h.contextMembers) == 0 { 86 | return strings.Join(path, ".") 87 | } 88 | if num_ancestors > len(h.contextMembers) { 89 | num_ancestors = 0 90 | } 91 | if !h.HaveAsContexts(num_ancestors) { 92 | var res string 93 | if path[0] == "" { 94 | res = h.GetParentContextString(num_ancestors) 95 | } else { 96 | res = h.GetParentContextString(num_ancestors) + "." + strings.Join(path, ".") 97 | } 98 | return strings.Trim(res, ".") 99 | } 100 | var res string 101 | copiedMembers := make([]contextMember, 0) 102 | if num_ancestors > 0 { 103 | copiedMembers = append(copiedMembers, h.contextMembers[:len(h.contextMembers)-num_ancestors]...) 104 | } else { 105 | copiedMembers = append(copiedMembers, h.contextMembers...) 106 | } 107 | for p := len(path) - 1; p >= 0; p-- { 108 | var val contextMember 109 | var found string 110 | if len(copiedMembers) == 0 { 111 | found = path[p] 112 | } else { 113 | val = copiedMembers[len(copiedMembers)-1] 114 | if val.asMapping == path[p] { 115 | found = val.path 116 | if len(copiedMembers) > 1 { 117 | val2 := copiedMembers[len(copiedMembers)-2] 118 | tmp := strings.Split(val.path, ".") 119 | if tmp[0] == val2.asMapping { 120 | found = strings.Join(tmp[1:], ".") 121 | } 122 | } 123 | copiedMembers = copiedMembers[:len(copiedMembers)-1] 124 | } else { 125 | if len(val.asMapping) == 0 { 126 | found = val.path + "." + path[p] 127 | copiedMembers = copiedMembers[:len(copiedMembers)-1] 128 | } else { 129 | if len(copiedMembers) == 0 { 130 | ss := strings.Split(val.asMapping, ".") 131 | if ss[0] == path[p] { 132 | found = val.path 133 | } else { 134 | } 135 | } else { 136 | if len(copiedMembers) > 1 { 137 | cv := copiedMembers[len(copiedMembers)-2] 138 | mappedPath := strings.Split(cv.path, ".") 139 | if len(mappedPath) > 1 { 140 | tmp := strings.Join(mappedPath[1:], ".") 141 | if tmp == val.asMapping { 142 | found = val.path 143 | copiedMembers = copiedMembers[:len(copiedMembers)-1] 144 | } else { 145 | found = path[p] 146 | } 147 | } else { 148 | found = path[p] 149 | } 150 | } else { 151 | found = path[p] 152 | } 153 | } 154 | } 155 | } 156 | } 157 | res = found + "." + res 158 | } 159 | if len(copiedMembers) > 0 { 160 | for p := len(copiedMembers) - 1; p >= 0; p-- { 161 | res = copiedMembers[p].path + "." + res 162 | } 163 | } 164 | return strings.Trim(res, ".") 165 | } 166 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mustache/specs/~lambdas.json: -------------------------------------------------------------------------------- 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Lambdas are a special-cased data type for use in interpolations and\nsections.\n\nWhen used as the data value for an Interpolation tag, the lambda MUST be\ntreatable as an arity 0 function, and invoked as such. The returned value\nMUST be rendered against the default delimiters, then interpolated in place\nof the lambda.\n\nWhen used as the data value for a Section tag, the lambda MUST be treatable\nas an arity 1 function, and invoked as such (passing a String containing the\nunprocessed section contents). The returned value MUST be rendered against\nthe current delimiters, then interpolated in place of the section.\n","tests":[{"name":"Interpolation","data":{"lambda":{"php":"return \"world\";","clojure":"(fn [] \"world\")","__tag__":"code","perl":"sub { \"world\" }","python":"lambda: \"world\"","ruby":"proc { \"world\" }","js":"function() { return \"world\" }"}},"expected":"Hello, world!","template":"Hello, {{lambda}}!","desc":"A lambda's return value should be interpolated."},{"name":"Interpolation - Expansion","data":{"planet":"world","lambda":{"php":"return \"{{planet}}\";","clojure":"(fn [] \"{{planet}}\")","__tag__":"code","perl":"sub { \"{{planet}}\" }","python":"lambda: \"{{planet}}\"","ruby":"proc { \"{{planet}}\" }","js":"function() { return \"{{planet}}\" }"}},"expected":"Hello, world!","template":"Hello, {{lambda}}!","desc":"A lambda's return value should be parsed."},{"name":"Interpolation - Alternate Delimiters","data":{"planet":"world","lambda":{"php":"return \"|planet| => {{planet}}\";","clojure":"(fn [] \"|planet| => {{planet}}\")","__tag__":"code","perl":"sub { \"|planet| => {{planet}}\" }","python":"lambda: \"|planet| => {{planet}}\"","ruby":"proc { \"|planet| => {{planet}}\" }","js":"function() { return \"|planet| => {{planet}}\" }"}},"expected":"Hello, (|planet| => world)!","template":"{{= | | =}}\nHello, (|&lambda|)!","desc":"A lambda's return value should parse with the default delimiters."},{"name":"Interpolation - Multiple Calls","data":{"lambda":{"php":"global $calls; return ++$calls;","clojure":"(def g (atom 0)) (fn [] (swap! g inc))","__tag__":"code","perl":"sub { no strict; $calls += 1 }","python":"lambda: globals().update(calls=globals().get(\"calls\",0)+1) or calls","ruby":"proc { $calls ||= 0; $calls += 1 }","js":"function() { return (g=(function(){return this})()).calls=(g.calls||0)+1 }"}},"expected":"1 == 2 == 3","template":"{{lambda}} == {{{lambda}}} == {{lambda}}","desc":"Interpolated lambdas should not be cached."},{"name":"Escaping","data":{"lambda":{"php":"return \">\";","clojure":"(fn [] \">\")","__tag__":"code","perl":"sub { \">\" }","python":"lambda: \">\"","ruby":"proc { \">\" }","js":"function() { return \">\" }"}},"expected":"<>>","template":"<{{lambda}}{{{lambda}}}","desc":"Lambda results should be appropriately escaped."},{"name":"Section","data":{"x":"Error!","lambda":{"php":"return ($text == \"{{x}}\") ? \"yes\" : \"no\";","clojure":"(fn [text] (if (= text \"{{x}}\") \"yes\" \"no\"))","__tag__":"code","perl":"sub { $_[0] eq \"{{x}}\" ? \"yes\" : \"no\" }","python":"lambda text: text == \"{{x}}\" and \"yes\" or \"no\"","ruby":"proc { |text| text == \"{{x}}\" ? \"yes\" : \"no\" }","js":"function(txt) { return (txt == \"{{x}}\" ? \"yes\" : \"no\") }"}},"expected":"","template":"<{{#lambda}}{{x}}{{/lambda}}>","desc":"Lambdas used for sections should receive the raw section string."},{"name":"Section - Expansion","data":{"planet":"Earth","lambda":{"php":"return $text . \"{{planet}}\" . $text;","clojure":"(fn [text] (str text \"{{planet}}\" text))","__tag__":"code","perl":"sub { $_[0] . \"{{planet}}\" . $_[0] }","python":"lambda text: \"%s{{planet}}%s\" % (text, text)","ruby":"proc { |text| \"#{text}{{planet}}#{text}\" }","js":"function(txt) { return txt + \"{{planet}}\" + txt }"}},"expected":"<-Earth->","template":"<{{#lambda}}-{{/lambda}}>","desc":"Lambdas used for sections should have their results parsed."},{"name":"Section - Alternate Delimiters","data":{"planet":"Earth","lambda":{"php":"return $text . \"{{planet}} => |planet|\" . $text;","clojure":"(fn [text] (str text \"{{planet}} => |planet|\" text))","__tag__":"code","perl":"sub { $_[0] . \"{{planet}} => |planet|\" . $_[0] }","python":"lambda text: \"%s{{planet}} => |planet|%s\" % (text, text)","ruby":"proc { |text| \"#{text}{{planet}} => |planet|#{text}\" }","js":"function(txt) { return txt + \"{{planet}} => |planet|\" + txt }"}},"expected":"<-{{planet}} => Earth->","template":"{{= | | =}}<|#lambda|-|/lambda|>","desc":"Lambdas used for sections should parse with the current delimiters."},{"name":"Section - Multiple Calls","data":{"lambda":{"php":"return \"__\" . $text . \"__\";","clojure":"(fn [text] (str \"__\" text \"__\"))","__tag__":"code","perl":"sub { \"__\" . $_[0] . \"__\" }","python":"lambda text: \"__%s__\" % (text)","ruby":"proc { |text| \"__#{text}__\" }","js":"function(txt) { return \"__\" + txt + \"__\" }"}},"expected":"__FILE__ != __LINE__","template":"{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}","desc":"Lambdas used for sections should not be cached."},{"name":"Inverted Section","data":{"static":"static","lambda":{"php":"return false;","clojure":"(fn [text] false)","__tag__":"code","perl":"sub { 0 }","python":"lambda text: 0","ruby":"proc { |text| false }","js":"function(txt) { return false }"}},"expected":"<>","template":"<{{^lambda}}{{static}}{{/lambda}}>","desc":"Lambdas used for inverted sections should be considered truthy."}]} -------------------------------------------------------------------------------- /mustache/specs/inverted.json: -------------------------------------------------------------------------------- 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Inverted Section tags and End Section tags are used in combination to wrap a\nsection of the template.\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Inverted Section tag MUST be\nfollowed by an End Section tag with the same content within the same\nsection.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nThis section MUST NOT be rendered unless the data list is empty.\n\nInverted Section and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Falsey","data":{"boolean":false},"expected":"\"This should be rendered.\"","template":"\"{{^boolean}}This should be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents rendered."},{"name":"Truthy","data":{"boolean":true},"expected":"\"\"","template":"\"{{^boolean}}This should not be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"\"","template":"\"{{^context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should behave like truthy values."},{"name":"List","data":{"list":[{"n":1},{"n":2},{"n":3}]},"expected":"\"\"","template":"\"{{^list}}{{n}}{{/list}}\"","desc":"Lists should behave like truthy values."},{"name":"Empty List","data":{"list":[]},"expected":"\"Yay lists!\"","template":"\"{{^list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":false},"expected":"* first\n* second\n* third\n","template":"{{^bool}}\n* first\n{{/bool}}\n* {{two}}\n{{^bool}}\n* third\n{{/bool}}\n","desc":"Multiple inverted sections per template should be permitted."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A B C D E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should have their contents rendered."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A E |","template":"| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[Cannot find key 'missing'!]","template":"[{{^missing}}Cannot find key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"\" == \"\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names should be valid for Inverted Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"Not Here\" == \"Not Here\"","template":"\"{{^a.b.c}}Not Here{{/a.b.c}}\" == \"Not Here\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":false},"expected":" | \t|\t | \n","template":" | {{^boolean}}\t|\t{{/boolean}} | \n","desc":"Inverted sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":false},"expected":" | \n | \n","template":" | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Inverted should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":false},"expected":" NO\n WAY\n","template":" {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{^boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Standalone Indented Lines","data":{"boolean":false},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{^boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Standalone indented lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":false},"expected":"|\r\n|","template":"|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":false},"expected":"^\n/","template":" {{^boolean}}\n^{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":false},"expected":"^\n/\n","template":"^{{^boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":false},"expected":"|=|","template":"|{{^ boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} -------------------------------------------------------------------------------- /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 | // These files are pulled in from https://github.com/mustache/spec/tree/83b0721610a4e11832e83df19c73ace3289972b9 61 | fileData, err := ioutil.ReadFile(path.Join("mustache", "specs", fileName)) 62 | if err != nil { 63 | panic(err) 64 | } 65 | 66 | var testFile mustacheTestFile 67 | if err := yaml.Unmarshal(fileData, &testFile); err != nil { 68 | panic(err) 69 | } 70 | 71 | for _, mustacheTest := range testFile.Tests { 72 | if mustBeSkipped(mustacheTest, fileName) { 73 | // fmt.Printf("Skipped test: %s\n", mustacheTest.Name) 74 | continue 75 | } 76 | 77 | test := Test{ 78 | name: mustacheTest.Name, 79 | input: mustacheTest.Template, 80 | data: mustacheTest.Data, 81 | partials: mustacheTest.Partials, 82 | output: mustacheTest.Expected, 83 | } 84 | 85 | result = append(result, test) 86 | } 87 | 88 | return result 89 | } 90 | 91 | // returns true if test must be skipped 92 | func mustBeSkipped(test mustacheTest, fileName string) bool { 93 | // handlebars does not support alternative delimiters 94 | return haveAltDelimiter(test) || 95 | // the JS implementation skips those tests 96 | fileName == "partials.yml" && (test.Name == "Failed Lookup" || test.Name == "Standalone Indentation") 97 | } 98 | 99 | // returns true if test have alternative delimeter in template or in partials 100 | func haveAltDelimiter(test mustacheTest) bool { 101 | // check template 102 | if rAltDelim.MatchString(test.Template) { 103 | return true 104 | } 105 | 106 | // check partials 107 | for _, partial := range test.Partials { 108 | if rAltDelim.MatchString(partial) { 109 | return true 110 | } 111 | } 112 | 113 | return false 114 | } 115 | 116 | func mustacheTestFiles() []string { 117 | var result []string 118 | 119 | files, err := ioutil.ReadDir(path.Join("mustache", "specs")) 120 | if err != nil { 121 | panic(err) 122 | } 123 | 124 | for _, file := range files { 125 | fileName := file.Name() 126 | 127 | if !file.IsDir() && strings.HasSuffix(fileName, ".yml") { 128 | result = append(result, fileName) 129 | } 130 | } 131 | 132 | return result 133 | } 134 | 135 | // 136 | // Following tests come fron ~lambdas.yml 137 | // 138 | 139 | var mustacheLambdasTests = []Test{ 140 | { 141 | "Interpolation", 142 | "Hello, {{lambda}}!", 143 | map[string]interface{}{"lambda": func() string { return "world" }}, 144 | nil, nil, nil, 145 | "Hello, world!", 146 | }, 147 | 148 | // // SKIP: lambda return value is not parsed 149 | // { 150 | // "Interpolation - Expansion", 151 | // "Hello, {{lambda}}!", 152 | // map[string]interface{}{"lambda": func() string { return "{{planet}}" }}, 153 | // nil, nil, nil, 154 | // "Hello, world!", 155 | // }, 156 | 157 | // SKIP "Interpolation - Alternate Delimiters" 158 | 159 | { 160 | "Interpolation - Multiple Calls", 161 | "{{lambda}} == {{{lambda}}} == {{lambda}}", 162 | map[string]interface{}{"lambda": func() string { 163 | musTestLambdaInterMult++ 164 | return Str(musTestLambdaInterMult) 165 | }}, 166 | nil, nil, nil, 167 | "1 == 2 == 3", 168 | }, 169 | 170 | { 171 | "Escaping", 172 | "<{{lambda}}{{{lambda}}}", 173 | map[string]interface{}{"lambda": func() string { return ">" }}, 174 | nil, nil, nil, 175 | "<>>", 176 | }, 177 | 178 | // // SKIP: "Lambdas used for sections should receive the raw section string." 179 | // { 180 | // "Section", 181 | // "<{{#lambda}}{{x}}{{/lambda}}>", 182 | // map[string]interface{}{"lambda": func(param string) string { 183 | // if param == "{{x}}" { 184 | // return "yes" 185 | // } 186 | 187 | // return "false" 188 | // }, "x": "Error!"}, 189 | // nil, nil, nil, 190 | // "", 191 | // }, 192 | 193 | // // SKIP: lambda return value is not parsed 194 | // { 195 | // "Section - Expansion", 196 | // "<{{#lambda}}-{{/lambda}}>", 197 | // map[string]interface{}{"lambda": func(param string) string { 198 | // return param + "{{planet}}" + param 199 | // }, "planet": "Earth"}, 200 | // nil, nil, nil, 201 | // "<-Earth->", 202 | // }, 203 | 204 | // SKIP: "Section - Alternate Delimiters" 205 | 206 | { 207 | "Section - Multiple Calls", 208 | "{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}", 209 | map[string]interface{}{"lambda": func(options *Options) string { 210 | return "__" + options.Fn() + "__" 211 | }}, 212 | nil, nil, nil, 213 | "__FILE__ != __LINE__", 214 | }, 215 | 216 | // // SKIP: "Lambdas used for inverted sections should be considered truthy." 217 | // { 218 | // "Inverted Section", 219 | // "<{{^lambda}}{{static}}{{/lambda}}>", 220 | // map[string]interface{}{ 221 | // "lambda": func() interface{} { 222 | // return false 223 | // }, 224 | // "static": "static", 225 | // }, 226 | // nil, nil, nil, 227 | // "<>", 228 | // }, 229 | } 230 | 231 | func TestMustacheLambdas(t *testing.T) { 232 | t.Parallel() 233 | 234 | launchTests(t, mustacheLambdasTests) 235 | } 236 | -------------------------------------------------------------------------------- /handlebars/subexpressions_test.go: -------------------------------------------------------------------------------- 1 | package handlebars 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mailgun/raymond/v2" 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /json_visitor_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "github.com/stretchr/testify/require" 6 | "testing" 7 | ) 8 | 9 | func TestJSONVisitor(t *testing.T) { 10 | for _, tt := range []struct { 11 | name string 12 | source string 13 | want map[string]interface{} 14 | }{{ 15 | name: "basic", 16 | source: sourceBasic, 17 | want: map[string]interface{}{ 18 | "title": "test_title", 19 | "body": "test_body", 20 | }, 21 | }, { 22 | name: "nested vars", 23 | source: `
24 |

{{title.name}}

25 |
26 | {{body.content}} 27 |
28 |
`, 29 | want: map[string]interface{}{ 30 | "title": map[string]interface{}{"name": "test_name"}, 31 | "body": map[string]interface{}{"content": "test_content"}, 32 | }, 33 | }, { 34 | name: "block params", 35 | source: `{{#foo as |bar|}} 36 | {{bar.baz}} 37 | {{/foo}}`, 38 | want: map[string]interface{}{ 39 | "bar": map[string]interface{}{"baz": "test_baz"}, 40 | }, 41 | }, { 42 | name: "with block", 43 | source: `{{#with people.[0].[0]}} 44 | {{name}} 45 | {{/with}}`, 46 | want: map[string]interface{}{"people": newList(newList(map[string]interface{}{"name": "test_name"}))}, 47 | }, { 48 | name: "if block", 49 | source: `{{#if people.name}} {{people.name}}{{/if}}`, 50 | want: map[string]interface{}{ 51 | "people": map[string]interface{}{"name": "test_name"}, 52 | }, 53 | }, { 54 | name: "if block with incomplete foo path and complete foo path", 55 | source: `{{#if floo}} {{floo.blar.blaz}} {{/if}}`, 56 | want: map[string]interface{}{"floo": map[string]interface{}{"blar": map[string]interface{}{"blaz": "test_blaz"}}}, 57 | }, { 58 | name: "accesses multiple elements of a map in multiple paths", 59 | source: `{{bar.baz}} {{name.first}}{{name.last}}`, 60 | want: map[string]interface{}{ 61 | "bar": map[string]interface{}{"baz": "test_baz"}, 62 | "name": map[string]interface{}{"first": "test_first", "last": "test_last"}}, 63 | }, { 64 | name: "large template", 65 | source: largeTemplate, 66 | want: map[string]interface{}{"bar": "test_bar", "foo": "test_foo", "name": "test_name", "phone": "test_phone"}, 67 | }, { 68 | name: "multi with", 69 | source: "{{#with foo}}{{#with bar}}{{baz}}{{/with}}{{/with}}", 70 | want: map[string]interface{}{"foo": map[string]interface{}{"bar": map[string]interface{}{"baz": "test_baz"}}}, 71 | }, { 72 | name: "multi as", 73 | source: "{{#with foo as |bee|}}{{#with bee.bar as |bazinga|}}{{bazinga.baz}}{{/with}}{{/with}}", 74 | want: map[string]interface{}{"foo": map[string]interface{}{"bar": map[string]interface{}{"baz": "test_baz"}}}, 75 | }, { 76 | name: "each this", 77 | source: "{{#each user.services}}{{this.service}}{{this.date}}{{/each}}", 78 | want: map[string]interface{}{"user": map[string]interface{}{"services": newList(map[string]interface{}{"service": "test_service", "date": "test_date"})}}, 79 | }, { 80 | name: "multi multi with", 81 | source: "{{#with fizz}}{{#with foo}}{{#with bar}}{{baz}}{{bop}}{{/with}}{{/with}}{{/with}}", 82 | want: map[string]interface{}{"fizz": map[string]interface{}{"foo": map[string]interface{}{"bar": map[string]interface{}{"baz": "test_baz", "bop": "test_bop"}}}}, 83 | }, { 84 | name: "multi with same names", 85 | source: "{{#with foo}}{{#with foo}}{{baz}}{{/with}}{{/with}}", 86 | want: map[string]interface{}{"foo": map[string]interface{}{"foo": map[string]interface{}{"baz": "test_baz"}}}, 87 | }, { 88 | name: "up a level", 89 | source: "{{#with foo}}{{#with foo}}{{../baz}}{{/with}}{{/with}}", 90 | want: map[string]interface{}{"foo": map[string]interface{}{"baz": "test_baz"}}, 91 | }, { 92 | name: "each lookup", 93 | source: "{{#each people}} {{.}} lives in {{lookup ../cities @index}}{{/each}}", 94 | want: map[string]interface{}{"people": newList("test_people"), "cities": newList("test_cities")}, 95 | }, { 96 | name: "each lookup complex", 97 | source: "{{#each people}} {{./fioo/biar/biaz}} lives in {{lookup ../cities @index}}{{/each}}", 98 | want: map[string]interface{}{"people": newList(map[string]interface{}{"fioo": map[string]interface{}{"biar": map[string]interface{}{"biaz": "test_biaz"}}}), "cities": newList("test_cities")}, 99 | }, { 100 | name: "each", 101 | source: "{{#with foo}}{{#each foo}}{{baz}}{{/each}}{{/with}}", 102 | want: map[string]interface{}{"foo": map[string]interface{}{"foo": newList(map[string]interface{}{"baz": "test_baz"})}}, 103 | }, { 104 | name: "multiple paths in a non-block helper block", 105 | source: `{{#foo bar baz}} {{name.first name.last}} {{/foo}}`, 106 | want: map[string]interface{}{ 107 | "bar": "test_bar", 108 | "baz": "test_baz", 109 | "name": map[string]interface{}{ 110 | "first": "test_first", 111 | "last": "test_last"}}, 112 | }} { 113 | t.Run(tt.name, func(t *testing.T) { 114 | tpl, err := Parse(tt.source) 115 | require.NoError(t, err) 116 | require.Equal(t, tpl.source, tt.source) 117 | 118 | //fmt.Println(tpl.PrintAST()) 119 | 120 | vars, err := tpl.ExtractTemplateVars() 121 | require.NoError(t, err) 122 | assert.Equal(t, tt.want, vars) 123 | }) 124 | } 125 | } 126 | 127 | var largeTemplate = ` 128 | {{#if name}} 129 |
Hello {{name}}! 130 | {{else}} 131 |
Hello there!
132 | {{/if}} 133 | 134 | {{#ifGt foo bar}} 135 |

foo is greater than bar
136 | {{/ifGt}} 137 | 138 | {{#ifGt foo 10}} 139 |

foo is greater than 10
140 | {{else}} 141 |

foo is not greater than 10
142 | {{/ifGt}} 143 | 144 | 145 | {{#ifLt foo bar}} 146 |

foo is less than bar


147 | {{/ifLt}} 148 | 149 | {{#ifLt foo 10}} 150 |
foo is less than 10
151 | {{else}} 152 |
foo is not less than 10
153 | {{/ifLt}} 154 | 155 | {{#ifEq foo bar}} 156 |

foo is equal to bar


157 | {{/ifEq}} 158 | 159 | {{#ifEq foo 10}} 160 |
foo is equal to 10
161 | {{else}} 162 |
foo is not equal to 10
163 | {{/ifEq}} 164 | 165 | {{#ifMatchesRegexStr "^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$" phone}} 166 |
phone var is a phone number
167 | {{else}} 168 |
phone var is not a phone number
169 | {{/ifMatchesRegexStr}} 170 | ` 171 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /eval_test.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var evalTests = []Test{ 8 | { 9 | "only content", 10 | "this is content", 11 | nil, nil, nil, nil, 12 | "this is content", 13 | }, 14 | { 15 | "checks path in parent contexts", 16 | "{{#a}}{{one}}{{#b}}{{one}}{{two}}{{one}}{{/b}}{{/a}}", 17 | map[string]interface{}{"a": map[string]int{"one": 1}, "b": map[string]int{"two": 2}}, 18 | nil, nil, nil, 19 | "1121", 20 | }, 21 | { 22 | "block params", 23 | "{{#foo as |bar|}}{{bar}}{{/foo}}{{bar}}", 24 | map[string]string{"foo": "baz", "bar": "bat"}, 25 | nil, nil, nil, 26 | "bazbat", 27 | }, 28 | { 29 | "block params on array", 30 | "{{#foo as |bar i|}}{{i}}.{{bar}} {{/foo}}", 31 | map[string][]string{"foo": {"baz", "bar", "bat"}}, 32 | nil, nil, nil, 33 | "0.baz 1.bar 2.bat ", 34 | }, 35 | { 36 | "nested block params", 37 | "{{#foos as |foo iFoo|}}{{#wats as |wat iWat|}}{{iFoo}}.{{iWat}}.{{foo}}-{{wat}} {{/wats}}{{/foos}}", 38 | map[string][]string{"foos": {"baz", "bar"}, "wats": {"the", "phoque"}}, 39 | nil, nil, nil, 40 | "0.0.baz-the 0.1.baz-phoque 1.0.bar-the 1.1.bar-phoque ", 41 | }, 42 | { 43 | "block params with path reference", 44 | "{{#foo as |bar|}}{{bar.baz}}{{/foo}}", 45 | map[string]map[string]string{"foo": {"baz": "bat"}}, 46 | nil, nil, nil, 47 | "bat", 48 | }, 49 | { 50 | "falsy block evaluation", 51 | "{{#foo}}bar{{/foo}} baz", 52 | map[string]interface{}{"foo": false}, 53 | nil, nil, nil, 54 | " baz", 55 | }, 56 | { 57 | "block helper returns a SafeString", 58 | "{{title}} - {{#bold}}{{body}}{{/bold}}", 59 | map[string]string{ 60 | "title": "My new blog post", 61 | "body": "I have so many things to say!", 62 | }, 63 | nil, 64 | map[string]interface{}{"bold": func(options *Options) SafeString { 65 | return SafeString(`
` + options.Fn() + "
") 66 | }}, 67 | nil, 68 | `My new blog post -
I have so many things to say!
`, 69 | }, 70 | { 71 | "chained blocks", 72 | "{{#if a}}A{{else if b}}B{{else}}C{{/if}}", 73 | map[string]interface{}{"b": false}, 74 | nil, nil, nil, 75 | "C", 76 | }, 77 | 78 | // @todo Test with a "../../path" (depth 2 path) while context is only depth 1 79 | } 80 | 81 | func TestEval(t *testing.T) { 82 | t.Parallel() 83 | 84 | launchTests(t, evalTests) 85 | } 86 | 87 | var evalErrors = []Test{ 88 | { 89 | "functions with wrong number of arguments", 90 | `{{foo "bar"}}`, 91 | map[string]interface{}{"foo": func(a string, b string) string { return "foo" }}, 92 | nil, nil, nil, 93 | "Helper 'foo' called with wrong number of arguments, needed 2 but got 1", 94 | }, 95 | { 96 | "functions with wrong number of returned values (1)", 97 | "{{foo}}", 98 | map[string]interface{}{"foo": func() {}}, 99 | nil, nil, nil, 100 | "Helper function must return a string or a SafeString", 101 | }, 102 | { 103 | "functions with wrong number of returned values (2)", 104 | "{{foo}}", 105 | map[string]interface{}{"foo": func() (string, bool, string) { return "foo", true, "bar" }}, 106 | nil, nil, nil, 107 | "Helper function must return a string or a SafeString", 108 | }, 109 | } 110 | 111 | func TestEvalErrors(t *testing.T) { 112 | launchErrorTests(t, evalErrors) 113 | } 114 | 115 | func TestEvalStruct(t *testing.T) { 116 | t.Parallel() 117 | 118 | source := `
119 |

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

120 |
{{Body}}
121 | 122 |

Comments

123 | 124 | {{#each comments}} 125 |

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

126 |
{{body}}
127 | {{/each}} 128 |
` 129 | 130 | expected := `
131 |

By Jean Valjean

132 |
Life is difficult
133 | 134 |

Comments

135 | 136 |

By Marcel Beliveau

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

{{real-name}}

178 |
    179 |
  • City: {{info.location}}
  • 180 |
  • Rug: {{info.[r.u.g]}}
  • 181 |
  • Activity: {{info.activity}}
  • 182 |
183 | {{#each other-names}} 184 |

{{alias-name}}

185 | {{/each}} 186 |
` 187 | 188 | expected := `
189 |

Lebowski

190 |
    191 |
  • City: Venice
  • 192 |
  • Rug: Tied The Room Together
  • 193 |
  • Activity: Bowling
  • 194 |
195 |

his dudeness

196 |

el duderino

197 |
` 198 | 199 | type Alias struct { 200 | Name string `handlebars:"alias-name"` 201 | } 202 | 203 | type CharacterInfo struct { 204 | City string `handlebars:"location"` 205 | Rug string `handlebars:"r.u.g"` 206 | Activity string `handlebars:"not-activity"` 207 | } 208 | 209 | type Character struct { 210 | RealName string `handlebars:"real-name"` 211 | Info CharacterInfo 212 | Aliases []Alias `handlebars:"other-names"` 213 | } 214 | 215 | ctx := Character{ 216 | "Lebowski", 217 | CharacterInfo{"Venice", "Tied The Room Together", "Bowling"}, 218 | []Alias{ 219 | {"his dudeness"}, 220 | {"el duderino"}, 221 | }, 222 | } 223 | 224 | output := MustRender(source, ctx) 225 | if output != expected { 226 | t.Errorf("Failed to evaluate with struct tag context") 227 | } 228 | } 229 | 230 | type TestFoo struct { 231 | } 232 | 233 | func (t *TestFoo) Subject() string { 234 | return "foo" 235 | } 236 | 237 | func TestEvalMethod(t *testing.T) { 238 | t.Parallel() 239 | 240 | source := `Subject is {{subject}}! YES I SAID {{Subject}}!` 241 | expected := `Subject is foo! YES I SAID foo!` 242 | 243 | ctx := &TestFoo{} 244 | 245 | output := MustRender(source, ctx) 246 | if output != expected { 247 | t.Errorf("Failed to evaluate struct method: %s", output) 248 | } 249 | } 250 | 251 | type TestBar struct { 252 | } 253 | 254 | func (t *TestBar) Subject() interface{} { 255 | return testBar 256 | } 257 | 258 | func testBar() string { 259 | return "bar" 260 | } 261 | 262 | func TestEvalMethodReturningFunc(t *testing.T) { 263 | t.Parallel() 264 | 265 | source := `Subject is {{subject}}! YES I SAID {{Subject}}!` 266 | expected := `Subject is bar! YES I SAID bar!` 267 | 268 | ctx := &TestBar{} 269 | 270 | output := MustRender(source, ctx) 271 | if output != expected { 272 | t.Errorf("Failed to evaluate struct method: %s", output) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mustache/specs/~lambdas.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Lambdas are a special-cased data type for use in interpolations and 3 | sections. 4 | 5 | When used as the data value for an Interpolation tag, the lambda MUST be 6 | treatable as an arity 0 function, and invoked as such. The returned value 7 | MUST be rendered against the default delimiters, then interpolated in place 8 | of the lambda. 9 | 10 | When used as the data value for a Section tag, the lambda MUST be treatable 11 | as an arity 1 function, and invoked as such (passing a String containing the 12 | unprocessed section contents). The returned value MUST be rendered against 13 | the current delimiters, then interpolated in place of the section. 14 | tests: 15 | - name: Interpolation 16 | desc: A lambda's return value should be interpolated. 17 | data: 18 | lambda: !code 19 | ruby: 'proc { "world" }' 20 | perl: 'sub { "world" }' 21 | js: 'function() { return "world" }' 22 | php: 'return "world";' 23 | python: 'lambda: "world"' 24 | clojure: '(fn [] "world")' 25 | lisp: '(lambda () "world")' 26 | template: "Hello, {{lambda}}!" 27 | expected: "Hello, world!" 28 | 29 | - name: Interpolation - Expansion 30 | desc: A lambda's return value should be parsed. 31 | data: 32 | planet: "world" 33 | lambda: !code 34 | ruby: 'proc { "{{planet}}" }' 35 | perl: 'sub { "{{planet}}" }' 36 | js: 'function() { return "{{planet}}" }' 37 | php: 'return "{{planet}}";' 38 | python: 'lambda: "{{planet}}"' 39 | clojure: '(fn [] "{{planet}}")' 40 | lisp: '(lambda () "{{planet}}")' 41 | template: "Hello, {{lambda}}!" 42 | expected: "Hello, world!" 43 | 44 | - name: Interpolation - Alternate Delimiters 45 | desc: A lambda's return value should parse with the default delimiters. 46 | data: 47 | planet: "world" 48 | lambda: !code 49 | ruby: 'proc { "|planet| => {{planet}}" }' 50 | perl: 'sub { "|planet| => {{planet}}" }' 51 | js: 'function() { return "|planet| => {{planet}}" }' 52 | php: 'return "|planet| => {{planet}}";' 53 | python: 'lambda: "|planet| => {{planet}}"' 54 | clojure: '(fn [] "|planet| => {{planet}}")' 55 | lisp: '(lambda () "|planet| => {{planet}}")' 56 | template: "{{= | | =}}\nHello, (|&lambda|)!" 57 | expected: "Hello, (|planet| => world)!" 58 | 59 | - name: Interpolation - Multiple Calls 60 | desc: Interpolated lambdas should not be cached. 61 | data: 62 | lambda: !code 63 | ruby: 'proc { $calls ||= 0; $calls += 1 }' 64 | perl: 'sub { no strict; $calls += 1 }' 65 | js: 'function() { return (g=(function(){return this})()).calls=(g.calls||0)+1 }' 66 | php: 'global $calls; return ++$calls;' 67 | python: 'lambda: globals().update(calls=globals().get("calls",0)+1) or calls' 68 | clojure: '(def g (atom 0)) (fn [] (swap! g inc))' 69 | lisp: '(let ((g 0)) (lambda () (incf g)))' 70 | template: '{{lambda}} == {{{lambda}}} == {{lambda}}' 71 | expected: '1 == 2 == 3' 72 | 73 | - name: Escaping 74 | desc: Lambda results should be appropriately escaped. 75 | data: 76 | lambda: !code 77 | ruby: 'proc { ">" }' 78 | perl: 'sub { ">" }' 79 | js: 'function() { return ">" }' 80 | php: 'return ">";' 81 | python: 'lambda: ">"' 82 | clojure: '(fn [] ">")' 83 | lisp: '(lambda () ">")' 84 | template: "<{{lambda}}{{{lambda}}}" 85 | expected: "<>>" 86 | 87 | - name: Section 88 | desc: Lambdas used for sections should receive the raw section string. 89 | data: 90 | x: 'Error!' 91 | lambda: !code 92 | ruby: 'proc { |text| text == "{{x}}" ? "yes" : "no" }' 93 | perl: 'sub { $_[0] eq "{{x}}" ? "yes" : "no" }' 94 | js: 'function(txt) { return (txt == "{{x}}" ? "yes" : "no") }' 95 | php: 'return ($text == "{{x}}") ? "yes" : "no";' 96 | python: 'lambda text: text == "{{x}}" and "yes" or "no"' 97 | clojure: '(fn [text] (if (= text "{{x}}") "yes" "no"))' 98 | lisp: '(lambda (text) (if (string= text "{{x}}") "yes" "no"))' 99 | template: "<{{#lambda}}{{x}}{{/lambda}}>" 100 | expected: "" 101 | 102 | - name: Section - Expansion 103 | desc: Lambdas used for sections should have their results parsed. 104 | data: 105 | planet: "Earth" 106 | lambda: !code 107 | ruby: 'proc { |text| "#{text}{{planet}}#{text}" }' 108 | perl: 'sub { $_[0] . "{{planet}}" . $_[0] }' 109 | js: 'function(txt) { return txt + "{{planet}}" + txt }' 110 | php: 'return $text . "{{planet}}" . $text;' 111 | python: 'lambda text: "%s{{planet}}%s" % (text, text)' 112 | clojure: '(fn [text] (str text "{{planet}}" text))' 113 | lisp: '(lambda (text) (format nil "~a{{planet}}~a" text text))' 114 | template: "<{{#lambda}}-{{/lambda}}>" 115 | expected: "<-Earth->" 116 | 117 | - name: Section - Alternate Delimiters 118 | desc: Lambdas used for sections should parse with the current delimiters. 119 | data: 120 | planet: "Earth" 121 | lambda: !code 122 | ruby: 'proc { |text| "#{text}{{planet}} => |planet|#{text}" }' 123 | perl: 'sub { $_[0] . "{{planet}} => |planet|" . $_[0] }' 124 | js: 'function(txt) { return txt + "{{planet}} => |planet|" + txt }' 125 | php: 'return $text . "{{planet}} => |planet|" . $text;' 126 | python: 'lambda text: "%s{{planet}} => |planet|%s" % (text, text)' 127 | clojure: '(fn [text] (str text "{{planet}} => |planet|" text))' 128 | lisp: '(lambda (text) (format nil "~a{{planet}} => |planet|~a" text text))' 129 | template: "{{= | | =}}<|#lambda|-|/lambda|>" 130 | expected: "<-{{planet}} => Earth->" 131 | 132 | - name: Section - Multiple Calls 133 | desc: Lambdas used for sections should not be cached. 134 | data: 135 | lambda: !code 136 | ruby: 'proc { |text| "__#{text}__" }' 137 | perl: 'sub { "__" . $_[0] . "__" }' 138 | js: 'function(txt) { return "__" + txt + "__" }' 139 | php: 'return "__" . $text . "__";' 140 | python: 'lambda text: "__%s__" % (text)' 141 | clojure: '(fn [text] (str "__" text "__"))' 142 | lisp: '(lambda (text) (format nil "__~a__" text))' 143 | template: '{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}' 144 | expected: '__FILE__ != __LINE__' 145 | 146 | - name: Inverted Section 147 | desc: Lambdas used for inverted sections should be considered truthy. 148 | data: 149 | static: 'static' 150 | lambda: !code 151 | ruby: 'proc { |text| false }' 152 | perl: 'sub { 0 }' 153 | js: 'function(txt) { return false }' 154 | php: 'return false;' 155 | python: 'lambda text: 0' 156 | clojure: '(fn [text] false)' 157 | lisp: '(lambda (text) (declare (ignore text)) nil)' 158 | template: "<{{^lambda}}{{static}}{{/lambda}}>" 159 | expected: "<>" 160 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package raymond 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "reflect" 7 | "runtime" 8 | "sync" 9 | 10 | "github.com/mailgun/raymond/v2/ast" 11 | "github.com/mailgun/raymond/v2/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 | func (tpl *Template) ExtractTemplateVars() (result map[string]interface{}, err error) { 227 | defer errRecover(&err) 228 | 229 | // parses template if necessary 230 | err = tpl.parse() 231 | if err != nil { 232 | return 233 | } 234 | 235 | // setup visitor 236 | v := newJSONVisitor() 237 | 238 | // visit AST 239 | result, _ = tpl.program.Accept(v).(map[string]interface{}) 240 | 241 | // named return values 242 | return 243 | } 244 | 245 | // errRecover recovers evaluation panic 246 | func errRecover(errp *error) { 247 | e := recover() 248 | if e != nil { 249 | switch err := e.(type) { 250 | case runtime.Error: 251 | panic(e) 252 | case error: 253 | *errp = err 254 | default: 255 | panic(e) 256 | } 257 | } 258 | } 259 | 260 | // PrintAST returns string representation of parsed template. 261 | func (tpl *Template) PrintAST() string { 262 | if err := tpl.parse(); err != nil { 263 | return fmt.Sprintf("PARSER ERROR: %s", err) 264 | } 265 | 266 | return ast.Print(tpl.program) 267 | } 268 | -------------------------------------------------------------------------------- /mustache/specs/inverted.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Inverted Section tags and End Section tags are used in combination to wrap a 3 | section of the template. 4 | 5 | These tags' content MUST be a non-whitespace character sequence NOT 6 | containing the current closing delimiter; each Inverted Section tag MUST be 7 | followed by an End Section tag with the same content within the same 8 | section. 9 | 10 | This tag's content names the data to replace the tag. Name resolution is as 11 | follows: 12 | 1) Split the name on periods; the first part is the name to resolve, any 13 | remaining parts should be retained. 14 | 2) Walk the context stack from top to bottom, finding the first context 15 | that is a) a hash containing the name as a key OR b) an object responding 16 | to a method with the given name. 17 | 3) If the context is a hash, the data is the value associated with the 18 | name. 19 | 4) If the context is an object and the method with the given name has an 20 | arity of 1, the method SHOULD be called with a String containing the 21 | unprocessed contents of the sections; the data is the value returned. 22 | 5) Otherwise, the data is the value returned by calling the method with 23 | the given name. 24 | 6) If any name parts were retained in step 1, each should be resolved 25 | against a context stack containing only the result from the former 26 | resolution. If any part fails resolution, the result should be considered 27 | falsey, and should interpolate as the empty string. 28 | If the data is not of a list type, it is coerced into a list as follows: if 29 | the data is truthy (e.g. `!!data == true`), use a single-element list 30 | containing the data, otherwise use an empty list. 31 | 32 | This section MUST NOT be rendered unless the data list is empty. 33 | 34 | Inverted Section and End Section tags SHOULD be treated as standalone when 35 | appropriate. 36 | tests: 37 | - name: Falsey 38 | desc: Falsey sections should have their contents rendered. 39 | data: { boolean: false } 40 | template: '"{{^boolean}}This should be rendered.{{/boolean}}"' 41 | expected: '"This should be rendered."' 42 | 43 | - name: Truthy 44 | desc: Truthy sections should have their contents omitted. 45 | data: { boolean: true } 46 | template: '"{{^boolean}}This should not be rendered.{{/boolean}}"' 47 | expected: '""' 48 | 49 | - name: Context 50 | desc: Objects and hashes should behave like truthy values. 51 | data: { context: { name: 'Joe' } } 52 | template: '"{{^context}}Hi {{name}}.{{/context}}"' 53 | expected: '""' 54 | 55 | - name: List 56 | desc: Lists should behave like truthy values. 57 | data: { list: [ { n: 1 }, { n: 2 }, { n: 3 } ] } 58 | template: '"{{^list}}{{n}}{{/list}}"' 59 | expected: '""' 60 | 61 | - name: Empty List 62 | desc: Empty lists should behave like falsey values. 63 | data: { list: [ ] } 64 | template: '"{{^list}}Yay lists!{{/list}}"' 65 | expected: '"Yay lists!"' 66 | 67 | - name: Doubled 68 | desc: Multiple inverted sections per template should be permitted. 69 | data: { bool: false, two: 'second' } 70 | template: | 71 | {{^bool}} 72 | * first 73 | {{/bool}} 74 | * {{two}} 75 | {{^bool}} 76 | * third 77 | {{/bool}} 78 | expected: | 79 | * first 80 | * second 81 | * third 82 | 83 | - name: Nested (Falsey) 84 | desc: Nested falsey sections should have their contents rendered. 85 | data: { bool: false } 86 | template: "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |" 87 | expected: "| A B C D E |" 88 | 89 | - name: Nested (Truthy) 90 | desc: Nested truthy sections should be omitted. 91 | data: { bool: true } 92 | template: "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |" 93 | expected: "| A E |" 94 | 95 | - name: Context Misses 96 | desc: Failed context lookups should be considered falsey. 97 | data: { } 98 | template: "[{{^missing}}Cannot find key 'missing'!{{/missing}}]" 99 | expected: "[Cannot find key 'missing'!]" 100 | 101 | # Dotted Names 102 | 103 | - name: Dotted Names - Truthy 104 | desc: Dotted names should be valid for Inverted Section tags. 105 | data: { a: { b: { c: true } } } 106 | template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == ""' 107 | expected: '"" == ""' 108 | 109 | - name: Dotted Names - Falsey 110 | desc: Dotted names should be valid for Inverted Section tags. 111 | data: { a: { b: { c: false } } } 112 | template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == "Not Here"' 113 | expected: '"Not Here" == "Not Here"' 114 | 115 | - name: Dotted Names - Broken Chains 116 | desc: Dotted names that cannot be resolved should be considered falsey. 117 | data: { a: { } } 118 | template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == "Not Here"' 119 | expected: '"Not Here" == "Not Here"' 120 | 121 | # Whitespace Sensitivity 122 | 123 | - name: Surrounding Whitespace 124 | desc: Inverted sections should not alter surrounding whitespace. 125 | data: { boolean: false } 126 | template: " | {{^boolean}}\t|\t{{/boolean}} | \n" 127 | expected: " | \t|\t | \n" 128 | 129 | - name: Internal Whitespace 130 | desc: Inverted should not alter internal whitespace. 131 | data: { boolean: false } 132 | template: " | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n" 133 | expected: " | \n | \n" 134 | 135 | - name: Indented Inline Sections 136 | desc: Single-line sections should not alter surrounding whitespace. 137 | data: { boolean: false } 138 | template: " {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n" 139 | expected: " NO\n WAY\n" 140 | 141 | - name: Standalone Lines 142 | desc: Standalone lines should be removed from the template. 143 | data: { boolean: false } 144 | template: | 145 | | This Is 146 | {{^boolean}} 147 | | 148 | {{/boolean}} 149 | | A Line 150 | expected: | 151 | | This Is 152 | | 153 | | A Line 154 | 155 | - name: Standalone Indented Lines 156 | desc: Standalone indented lines should be removed from the template. 157 | data: { boolean: false } 158 | template: | 159 | | This Is 160 | {{^boolean}} 161 | | 162 | {{/boolean}} 163 | | A Line 164 | expected: | 165 | | This Is 166 | | 167 | | A Line 168 | 169 | - name: Standalone Line Endings 170 | desc: '"\r\n" should be considered a newline for standalone tags.' 171 | data: { boolean: false } 172 | template: "|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|" 173 | expected: "|\r\n|" 174 | 175 | - name: Standalone Without Previous Line 176 | desc: Standalone tags should not require a newline to precede them. 177 | data: { boolean: false } 178 | template: " {{^boolean}}\n^{{/boolean}}\n/" 179 | expected: "^\n/" 180 | 181 | - name: Standalone Without Newline 182 | desc: Standalone tags should not require a newline to follow them. 183 | data: { boolean: false } 184 | template: "^{{^boolean}}\n/\n {{/boolean}}" 185 | expected: "^\n/\n" 186 | 187 | # Whitespace Insensitivity 188 | 189 | - name: Padding 190 | desc: Superfluous in-tag whitespace should be ignored. 191 | data: { boolean: false } 192 | template: '|{{^ boolean }}={{/ boolean }}|' 193 | expected: '|=|' 194 | -------------------------------------------------------------------------------- /mustache/specs/interpolation.json: -------------------------------------------------------------------------------- 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Interpolation tags are used to integrate dynamic content into the template.\n\nThe tag's content MUST be a non-whitespace character sequence NOT containing\nthe current closing delimiter.\n\nThis tag's content names the data to replace the tag. A single period (`.`)\nindicates that the item currently sitting atop the context stack should be\nused; otherwise, name resolution is as follows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object, the data is the value returned by the\n method with the given name.\n 5) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nData should be coerced into a string (and escaped, if appropriate) before\ninterpolation.\n\nThe Interpolation tags MUST NOT be treated as standalone.\n","tests":[{"name":"No Interpolation","data":{},"expected":"Hello from {Mustache}!\n","template":"Hello from {Mustache}!\n","desc":"Mustache-free templates should render as-is."},{"name":"Basic Interpolation","data":{"subject":"world"},"expected":"Hello, world!\n","template":"Hello, {{subject}}!\n","desc":"Unadorned tags should interpolate content into the template."},{"name":"HTML Escaping","data":{"forbidden":"& \" < >"},"expected":"These characters should be HTML escaped: & " < >\n","template":"These characters should be HTML escaped: {{forbidden}}\n","desc":"Basic interpolation should be HTML escaped."},{"name":"Triple Mustache","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{{forbidden}}}\n","desc":"Triple mustaches should interpolate without HTML escaping."},{"name":"Ampersand","data":{"forbidden":"& \" < >"},"expected":"These characters should not be HTML escaped: & \" < >\n","template":"These characters should not be HTML escaped: {{&forbidden}}\n","desc":"Ampersand should interpolate without HTML escaping."},{"name":"Basic Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Triple Mustache Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{{mph}}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Ampersand Integer Interpolation","data":{"mph":85},"expected":"\"85 miles an hour!\"","template":"\"{{&mph}} miles an hour!\"","desc":"Integers should interpolate seamlessly."},{"name":"Basic Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Triple Mustache Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{{power}}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Ampersand Decimal Interpolation","data":{"power":1.21},"expected":"\"1.21 jiggawatts!\"","template":"\"{{&power}} jiggawatts!\"","desc":"Decimals should interpolate seamlessly with proper significance."},{"name":"Basic Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Triple Mustache Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{{cannot}}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Ampersand Context Miss Interpolation","data":{},"expected":"I () be seen!","template":"I ({{&cannot}}) be seen!","desc":"Failed context lookups should default to empty strings."},{"name":"Dotted Names - Basic Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{person.name}}\" == \"{{#person}}{{name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Triple Mustache Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{{person.name}}}\" == \"{{#person}}{{{name}}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Ampersand Interpolation","data":{"person":{"name":"Joe"}},"expected":"\"Joe\" == \"Joe\"","template":"\"{{&person.name}}\" == \"{{#person}}{{&name}}{{/person}}\"","desc":"Dotted names should be considered a form of shorthand for sections."},{"name":"Dotted Names - Arbitrary Depth","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{a.b.c.d.e.name}}\" == \"Phil\"","desc":"Dotted names should be functional to any level of nesting."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{a.b.c}}\" == \"\"","desc":"Any falsey value prior to the last part of the name should yield ''."},{"name":"Dotted Names - Broken Chain Resolution","data":{"a":{"b":{}},"c":{"name":"Jim"}},"expected":"\"\" == \"\"","template":"\"{{a.b.c.name}}\" == \"\"","desc":"Each part of a dotted name should resolve only against its parent."},{"name":"Dotted Names - Initial Resolution","data":{"a":{"b":{"c":{"d":{"e":{"name":"Phil"}}}}},"b":{"c":{"d":{"e":{"name":"Wrong"}}}}},"expected":"\"Phil\" == \"Phil\"","template":"\"{{#a}}{{b.c.d.e.name}}{{/a}}\" == \"Phil\"","desc":"The first part of a dotted name should resolve as any other name."},{"name":"Interpolation - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{{string}}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Surrounding Whitespace","data":{"string":"---"},"expected":"| --- |","template":"| {{&string}} |","desc":"Interpolation should not alter surrounding whitespace."},{"name":"Interpolation - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Triple Mustache - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{{string}}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Ampersand - Standalone","data":{"string":"---"},"expected":" ---\n","template":" {{&string}}\n","desc":"Standalone interpolation should not alter surrounding whitespace."},{"name":"Interpolation With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{ string }}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Triple Mustache With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{{ string }}}|","desc":"Superfluous in-tag whitespace should be ignored."},{"name":"Ampersand With Padding","data":{"string":"---"},"expected":"|---|","template":"|{{& string }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} -------------------------------------------------------------------------------- /mustache/specs/sections.json: -------------------------------------------------------------------------------- 1 | {"__ATTN__":"Do not edit this file; changes belong in the appropriate YAML file.","overview":"Section tags and End Section tags are used in combination to wrap a section\nof the template for iteration\n\nThese tags' content MUST be a non-whitespace character sequence NOT\ncontaining the current closing delimiter; each Section tag MUST be followed\nby an End Section tag with the same content within the same section.\n\nThis tag's content names the data to replace the tag. Name resolution is as\nfollows:\n 1) Split the name on periods; the first part is the name to resolve, any\n remaining parts should be retained.\n 2) Walk the context stack from top to bottom, finding the first context\n that is a) a hash containing the name as a key OR b) an object responding\n to a method with the given name.\n 3) If the context is a hash, the data is the value associated with the\n name.\n 4) If the context is an object and the method with the given name has an\n arity of 1, the method SHOULD be called with a String containing the\n unprocessed contents of the sections; the data is the value returned.\n 5) Otherwise, the data is the value returned by calling the method with\n the given name.\n 6) If any name parts were retained in step 1, each should be resolved\n against a context stack containing only the result from the former\n resolution. If any part fails resolution, the result should be considered\n falsey, and should interpolate as the empty string.\nIf the data is not of a list type, it is coerced into a list as follows: if\nthe data is truthy (e.g. `!!data == true`), use a single-element list\ncontaining the data, otherwise use an empty list.\n\nFor each element in the data list, the element MUST be pushed onto the\ncontext stack, the section MUST be rendered, and the element MUST be popped\noff the context stack.\n\nSection and End Section tags SHOULD be treated as standalone when\nappropriate.\n","tests":[{"name":"Truthy","data":{"boolean":true},"expected":"\"This should be rendered.\"","template":"\"{{#boolean}}This should be rendered.{{/boolean}}\"","desc":"Truthy sections should have their contents rendered."},{"name":"Falsey","data":{"boolean":false},"expected":"\"\"","template":"\"{{#boolean}}This should not be rendered.{{/boolean}}\"","desc":"Falsey sections should have their contents omitted."},{"name":"Context","data":{"context":{"name":"Joe"}},"expected":"\"Hi Joe.\"","template":"\"{{#context}}Hi {{name}}.{{/context}}\"","desc":"Objects and hashes should be pushed onto the context stack."},{"name":"Deeply Nested Contexts","data":{"a":{"one":1},"b":{"two":2},"c":{"three":3},"d":{"four":4},"e":{"five":5}},"expected":"1\n121\n12321\n1234321\n123454321\n1234321\n12321\n121\n1\n","template":"{{#a}}\n{{one}}\n{{#b}}\n{{one}}{{two}}{{one}}\n{{#c}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{#d}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{#e}}\n{{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}\n{{/e}}\n{{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}\n{{/d}}\n{{one}}{{two}}{{three}}{{two}}{{one}}\n{{/c}}\n{{one}}{{two}}{{one}}\n{{/b}}\n{{one}}\n{{/a}}\n","desc":"All elements on the context stack should be accessible."},{"name":"List","data":{"list":[{"item":1},{"item":2},{"item":3}]},"expected":"\"123\"","template":"\"{{#list}}{{item}}{{/list}}\"","desc":"Lists should be iterated; list items should visit the context stack."},{"name":"Empty List","data":{"list":[]},"expected":"\"\"","template":"\"{{#list}}Yay lists!{{/list}}\"","desc":"Empty lists should behave like falsey values."},{"name":"Doubled","data":{"two":"second","bool":true},"expected":"* first\n* second\n* third\n","template":"{{#bool}}\n* first\n{{/bool}}\n* {{two}}\n{{#bool}}\n* third\n{{/bool}}\n","desc":"Multiple sections per template should be permitted."},{"name":"Nested (Truthy)","data":{"bool":true},"expected":"| A B C D E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested truthy sections should have their contents rendered."},{"name":"Nested (Falsey)","data":{"bool":false},"expected":"| A E |","template":"| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |","desc":"Nested falsey sections should be omitted."},{"name":"Context Misses","data":{},"expected":"[]","template":"[{{#missing}}Found key 'missing'!{{/missing}}]","desc":"Failed context lookups should be considered falsey."},{"name":"Implicit Iterator - String","data":{"list":["a","b","c","d","e"]},"expected":"\"(a)(b)(c)(d)(e)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should directly interpolate strings."},{"name":"Implicit Iterator - Integer","data":{"list":[1,2,3,4,5]},"expected":"\"(1)(2)(3)(4)(5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast integers to strings and interpolate."},{"name":"Implicit Iterator - Decimal","data":{"list":[1.1,2.2,3.3,4.4,5.5]},"expected":"\"(1.1)(2.2)(3.3)(4.4)(5.5)\"","template":"\"{{#list}}({{.}}){{/list}}\"","desc":"Implicit iterators should cast decimals to strings and interpolate."},{"name":"Implicit Iterator - Array","desc":"Implicit iterators should allow iterating over nested arrays.","data":{"list":[[1,2,3],["a","b","c"]]},"template":"\"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}\"","expected":"\"(123)(abc)\""},{"name":"Dotted Names - Truthy","data":{"a":{"b":{"c":true}}},"expected":"\"Here\" == \"Here\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"Here\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Falsey","data":{"a":{"b":{"c":false}}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names should be valid for Section tags."},{"name":"Dotted Names - Broken Chains","data":{"a":{}},"expected":"\"\" == \"\"","template":"\"{{#a.b.c}}Here{{/a.b.c}}\" == \"\"","desc":"Dotted names that cannot be resolved should be considered falsey."},{"name":"Surrounding Whitespace","data":{"boolean":true},"expected":" | \t|\t | \n","template":" | {{#boolean}}\t|\t{{/boolean}} | \n","desc":"Sections should not alter surrounding whitespace."},{"name":"Internal Whitespace","data":{"boolean":true},"expected":" | \n | \n","template":" | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n","desc":"Sections should not alter internal whitespace."},{"name":"Indented Inline Sections","data":{"boolean":true},"expected":" YES\n GOOD\n","template":" {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n","desc":"Single-line sections should not alter surrounding whitespace."},{"name":"Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n{{#boolean}}\n|\n{{/boolean}}\n| A Line\n","desc":"Standalone lines should be removed from the template."},{"name":"Indented Standalone Lines","data":{"boolean":true},"expected":"| This Is\n|\n| A Line\n","template":"| This Is\n {{#boolean}}\n|\n {{/boolean}}\n| A Line\n","desc":"Indented standalone lines should be removed from the template."},{"name":"Standalone Line Endings","data":{"boolean":true},"expected":"|\r\n|","template":"|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|","desc":"\"\\r\\n\" should be considered a newline for standalone tags."},{"name":"Standalone Without Previous Line","data":{"boolean":true},"expected":"#\n/","template":" {{#boolean}}\n#{{/boolean}}\n/","desc":"Standalone tags should not require a newline to precede them."},{"name":"Standalone Without Newline","data":{"boolean":true},"expected":"#\n/\n","template":"#{{#boolean}}\n/\n {{/boolean}}","desc":"Standalone tags should not require a newline to follow them."},{"name":"Padding","data":{"boolean":true},"expected":"|=|","template":"|{{# boolean }}={{/ boolean }}|","desc":"Superfluous in-tag whitespace should be ignored."}]} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mustache/specs/interpolation.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Interpolation tags are used to integrate dynamic content into the template. 3 | 4 | The tag's content MUST be a non-whitespace character sequence NOT containing 5 | the current closing delimiter. 6 | 7 | This tag's content names the data to replace the tag. A single period (`.`) 8 | indicates that the item currently sitting atop the context stack should be 9 | used; otherwise, name resolution is as follows: 10 | 1) Split the name on periods; the first part is the name to resolve, any 11 | remaining parts should be retained. 12 | 2) Walk the context stack from top to bottom, finding the first context 13 | that is a) a hash containing the name as a key OR b) an object responding 14 | to a method with the given name. 15 | 3) If the context is a hash, the data is the value associated with the 16 | name. 17 | 4) If the context is an object, the data is the value returned by the 18 | method with the given name. 19 | 5) If any name parts were retained in step 1, each should be resolved 20 | against a context stack containing only the result from the former 21 | resolution. If any part fails resolution, the result should be considered 22 | falsey, and should interpolate as the empty string. 23 | Data should be coerced into a string (and escaped, if appropriate) before 24 | interpolation. 25 | 26 | The Interpolation tags MUST NOT be treated as standalone. 27 | tests: 28 | - name: No Interpolation 29 | desc: Mustache-free templates should render as-is. 30 | data: { } 31 | template: | 32 | Hello from {Mustache}! 33 | expected: | 34 | Hello from {Mustache}! 35 | 36 | - name: Basic Interpolation 37 | desc: Unadorned tags should interpolate content into the template. 38 | data: { subject: "world" } 39 | template: | 40 | Hello, {{subject}}! 41 | expected: | 42 | Hello, world! 43 | 44 | - name: HTML Escaping 45 | desc: Basic interpolation should be HTML escaped. 46 | data: { forbidden: '& " < >' } 47 | template: | 48 | These characters should be HTML escaped: {{forbidden}} 49 | expected: | 50 | These characters should be HTML escaped: & " < > 51 | 52 | - name: Triple Mustache 53 | desc: Triple mustaches should interpolate without HTML escaping. 54 | data: { forbidden: '& " < >' } 55 | template: | 56 | These characters should not be HTML escaped: {{{forbidden}}} 57 | expected: | 58 | These characters should not be HTML escaped: & " < > 59 | 60 | - name: Ampersand 61 | desc: Ampersand should interpolate without HTML escaping. 62 | data: { forbidden: '& " < >' } 63 | template: | 64 | These characters should not be HTML escaped: {{&forbidden}} 65 | expected: | 66 | These characters should not be HTML escaped: & " < > 67 | 68 | - name: Basic Integer Interpolation 69 | desc: Integers should interpolate seamlessly. 70 | data: { mph: 85 } 71 | template: '"{{mph}} miles an hour!"' 72 | expected: '"85 miles an hour!"' 73 | 74 | - name: Triple Mustache Integer Interpolation 75 | desc: Integers should interpolate seamlessly. 76 | data: { mph: 85 } 77 | template: '"{{{mph}}} miles an hour!"' 78 | expected: '"85 miles an hour!"' 79 | 80 | - name: Ampersand Integer Interpolation 81 | desc: Integers should interpolate seamlessly. 82 | data: { mph: 85 } 83 | template: '"{{&mph}} miles an hour!"' 84 | expected: '"85 miles an hour!"' 85 | 86 | - name: Basic Decimal Interpolation 87 | desc: Decimals should interpolate seamlessly with proper significance. 88 | data: { power: 1.210 } 89 | template: '"{{power}} jiggawatts!"' 90 | expected: '"1.21 jiggawatts!"' 91 | 92 | - name: Triple Mustache Decimal Interpolation 93 | desc: Decimals should interpolate seamlessly with proper significance. 94 | data: { power: 1.210 } 95 | template: '"{{{power}}} jiggawatts!"' 96 | expected: '"1.21 jiggawatts!"' 97 | 98 | - name: Ampersand Decimal Interpolation 99 | desc: Decimals should interpolate seamlessly with proper significance. 100 | data: { power: 1.210 } 101 | template: '"{{&power}} jiggawatts!"' 102 | expected: '"1.21 jiggawatts!"' 103 | 104 | # Context Misses 105 | 106 | - name: Basic Context Miss Interpolation 107 | desc: Failed context lookups should default to empty strings. 108 | data: { } 109 | template: "I ({{cannot}}) be seen!" 110 | expected: "I () be seen!" 111 | 112 | - name: Triple Mustache Context Miss Interpolation 113 | desc: Failed context lookups should default to empty strings. 114 | data: { } 115 | template: "I ({{{cannot}}}) be seen!" 116 | expected: "I () be seen!" 117 | 118 | - name: Ampersand Context Miss Interpolation 119 | desc: Failed context lookups should default to empty strings. 120 | data: { } 121 | template: "I ({{&cannot}}) be seen!" 122 | expected: "I () be seen!" 123 | 124 | # Dotted Names 125 | 126 | - name: Dotted Names - Basic Interpolation 127 | desc: Dotted names should be considered a form of shorthand for sections. 128 | data: { person: { name: 'Joe' } } 129 | template: '"{{person.name}}" == "{{#person}}{{name}}{{/person}}"' 130 | expected: '"Joe" == "Joe"' 131 | 132 | - name: Dotted Names - Triple Mustache Interpolation 133 | desc: Dotted names should be considered a form of shorthand for sections. 134 | data: { person: { name: 'Joe' } } 135 | template: '"{{{person.name}}}" == "{{#person}}{{{name}}}{{/person}}"' 136 | expected: '"Joe" == "Joe"' 137 | 138 | - name: Dotted Names - Ampersand Interpolation 139 | desc: Dotted names should be considered a form of shorthand for sections. 140 | data: { person: { name: 'Joe' } } 141 | template: '"{{&person.name}}" == "{{#person}}{{&name}}{{/person}}"' 142 | expected: '"Joe" == "Joe"' 143 | 144 | - name: Dotted Names - Arbitrary Depth 145 | desc: Dotted names should be functional to any level of nesting. 146 | data: 147 | a: { b: { c: { d: { e: { name: 'Phil' } } } } } 148 | template: '"{{a.b.c.d.e.name}}" == "Phil"' 149 | expected: '"Phil" == "Phil"' 150 | 151 | - name: Dotted Names - Broken Chains 152 | desc: Any falsey value prior to the last part of the name should yield ''. 153 | data: 154 | a: { } 155 | template: '"{{a.b.c}}" == ""' 156 | expected: '"" == ""' 157 | 158 | - name: Dotted Names - Broken Chain Resolution 159 | desc: Each part of a dotted name should resolve only against its parent. 160 | data: 161 | a: { b: { } } 162 | c: { name: 'Jim' } 163 | template: '"{{a.b.c.name}}" == ""' 164 | expected: '"" == ""' 165 | 166 | - name: Dotted Names - Initial Resolution 167 | desc: The first part of a dotted name should resolve as any other name. 168 | data: 169 | a: { b: { c: { d: { e: { name: 'Phil' } } } } } 170 | b: { c: { d: { e: { name: 'Wrong' } } } } 171 | template: '"{{#a}}{{b.c.d.e.name}}{{/a}}" == "Phil"' 172 | expected: '"Phil" == "Phil"' 173 | 174 | - name: Dotted Names - Context Precedence 175 | desc: Dotted names should be resolved against former resolutions. 176 | data: 177 | a: { b: { } } 178 | b: { c: 'ERROR' } 179 | template: '{{#a}}{{b.c}}{{/a}}' 180 | expected: '' 181 | 182 | # Whitespace Sensitivity 183 | 184 | - name: Interpolation - Surrounding Whitespace 185 | desc: Interpolation should not alter surrounding whitespace. 186 | data: { string: '---' } 187 | template: '| {{string}} |' 188 | expected: '| --- |' 189 | 190 | - name: Triple Mustache - Surrounding Whitespace 191 | desc: Interpolation should not alter surrounding whitespace. 192 | data: { string: '---' } 193 | template: '| {{{string}}} |' 194 | expected: '| --- |' 195 | 196 | - name: Ampersand - Surrounding Whitespace 197 | desc: Interpolation should not alter surrounding whitespace. 198 | data: { string: '---' } 199 | template: '| {{&string}} |' 200 | expected: '| --- |' 201 | 202 | - name: Interpolation - Standalone 203 | desc: Standalone interpolation should not alter surrounding whitespace. 204 | data: { string: '---' } 205 | template: " {{string}}\n" 206 | expected: " ---\n" 207 | 208 | - name: Triple Mustache - Standalone 209 | desc: Standalone interpolation should not alter surrounding whitespace. 210 | data: { string: '---' } 211 | template: " {{{string}}}\n" 212 | expected: " ---\n" 213 | 214 | - name: Ampersand - Standalone 215 | desc: Standalone interpolation should not alter surrounding whitespace. 216 | data: { string: '---' } 217 | template: " {{&string}}\n" 218 | expected: " ---\n" 219 | 220 | # Whitespace Insensitivity 221 | 222 | - name: Interpolation With Padding 223 | desc: Superfluous in-tag whitespace should be ignored. 224 | data: { string: "---" } 225 | template: '|{{ string }}|' 226 | expected: '|---|' 227 | 228 | - name: Triple Mustache With Padding 229 | desc: Superfluous in-tag whitespace should be ignored. 230 | data: { string: "---" } 231 | template: '|{{{ string }}}|' 232 | expected: '|---|' 233 | 234 | - name: Ampersand With Padding 235 | desc: Superfluous in-tag whitespace should be ignored. 236 | data: { string: "---" } 237 | template: '|{{& string }}|' 238 | expected: '|---|' 239 | -------------------------------------------------------------------------------- /mustache/specs/sections.yml: -------------------------------------------------------------------------------- 1 | overview: | 2 | Section tags and End Section tags are used in combination to wrap a section 3 | of the template for iteration 4 | 5 | These tags' content MUST be a non-whitespace character sequence NOT 6 | containing the current closing delimiter; each Section tag MUST be followed 7 | by an End Section tag with the same content within the same section. 8 | 9 | This tag's content names the data to replace the tag. Name resolution is as 10 | follows: 11 | 1) Split the name on periods; the first part is the name to resolve, any 12 | remaining parts should be retained. 13 | 2) Walk the context stack from top to bottom, finding the first context 14 | that is a) a hash containing the name as a key OR b) an object responding 15 | to a method with the given name. 16 | 3) If the context is a hash, the data is the value associated with the 17 | name. 18 | 4) If the context is an object and the method with the given name has an 19 | arity of 1, the method SHOULD be called with a String containing the 20 | unprocessed contents of the sections; the data is the value returned. 21 | 5) Otherwise, the data is the value returned by calling the method with 22 | the given name. 23 | 6) If any name parts were retained in step 1, each should be resolved 24 | against a context stack containing only the result from the former 25 | resolution. If any part fails resolution, the result should be considered 26 | falsey, and should interpolate as the empty string. 27 | If the data is not of a list type, it is coerced into a list as follows: if 28 | the data is truthy (e.g. `!!data == true`), use a single-element list 29 | containing the data, otherwise use an empty list. 30 | 31 | For each element in the data list, the element MUST be pushed onto the 32 | context stack, the section MUST be rendered, and the element MUST be popped 33 | off the context stack. 34 | 35 | Section and End Section tags SHOULD be treated as standalone when 36 | appropriate. 37 | tests: 38 | - name: Truthy 39 | desc: Truthy sections should have their contents rendered. 40 | data: { boolean: true } 41 | template: '"{{#boolean}}This should be rendered.{{/boolean}}"' 42 | expected: '"This should be rendered."' 43 | 44 | - name: Falsey 45 | desc: Falsey sections should have their contents omitted. 46 | data: { boolean: false } 47 | template: '"{{#boolean}}This should not be rendered.{{/boolean}}"' 48 | expected: '""' 49 | 50 | - name: Context 51 | desc: Objects and hashes should be pushed onto the context stack. 52 | data: { context: { name: 'Joe' } } 53 | template: '"{{#context}}Hi {{name}}.{{/context}}"' 54 | expected: '"Hi Joe."' 55 | 56 | - name: Deeply Nested Contexts 57 | desc: All elements on the context stack should be accessible. 58 | data: 59 | a: { one: 1 } 60 | b: { two: 2 } 61 | c: { three: 3 } 62 | d: { four: 4 } 63 | e: { five: 5 } 64 | template: | 65 | {{#a}} 66 | {{one}} 67 | {{#b}} 68 | {{one}}{{two}}{{one}} 69 | {{#c}} 70 | {{one}}{{two}}{{three}}{{two}}{{one}} 71 | {{#d}} 72 | {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}} 73 | {{#e}} 74 | {{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}} 75 | {{/e}} 76 | {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}} 77 | {{/d}} 78 | {{one}}{{two}}{{three}}{{two}}{{one}} 79 | {{/c}} 80 | {{one}}{{two}}{{one}} 81 | {{/b}} 82 | {{one}} 83 | {{/a}} 84 | expected: | 85 | 1 86 | 121 87 | 12321 88 | 1234321 89 | 123454321 90 | 1234321 91 | 12321 92 | 121 93 | 1 94 | 95 | - name: List 96 | desc: Lists should be iterated; list items should visit the context stack. 97 | data: { list: [ { item: 1 }, { item: 2 }, { item: 3 } ] } 98 | template: '"{{#list}}{{item}}{{/list}}"' 99 | expected: '"123"' 100 | 101 | - name: Empty List 102 | desc: Empty lists should behave like falsey values. 103 | data: { list: [ ] } 104 | template: '"{{#list}}Yay lists!{{/list}}"' 105 | expected: '""' 106 | 107 | - name: Doubled 108 | desc: Multiple sections per template should be permitted. 109 | data: { bool: true, two: 'second' } 110 | template: | 111 | {{#bool}} 112 | * first 113 | {{/bool}} 114 | * {{two}} 115 | {{#bool}} 116 | * third 117 | {{/bool}} 118 | expected: | 119 | * first 120 | * second 121 | * third 122 | 123 | - name: Nested (Truthy) 124 | desc: Nested truthy sections should have their contents rendered. 125 | data: { bool: true } 126 | template: "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |" 127 | expected: "| A B C D E |" 128 | 129 | - name: Nested (Falsey) 130 | desc: Nested falsey sections should be omitted. 131 | data: { bool: false } 132 | template: "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |" 133 | expected: "| A E |" 134 | 135 | - name: Context Misses 136 | desc: Failed context lookups should be considered falsey. 137 | data: { } 138 | template: "[{{#missing}}Found key 'missing'!{{/missing}}]" 139 | expected: "[]" 140 | 141 | # Implicit Iterators 142 | 143 | - name: Implicit Iterator - String 144 | desc: Implicit iterators should directly interpolate strings. 145 | data: 146 | list: [ 'a', 'b', 'c', 'd', 'e' ] 147 | template: '"{{#list}}({{.}}){{/list}}"' 148 | expected: '"(a)(b)(c)(d)(e)"' 149 | 150 | - name: Implicit Iterator - Integer 151 | desc: Implicit iterators should cast integers to strings and interpolate. 152 | data: 153 | list: [ 1, 2, 3, 4, 5 ] 154 | template: '"{{#list}}({{.}}){{/list}}"' 155 | expected: '"(1)(2)(3)(4)(5)"' 156 | 157 | - name: Implicit Iterator - Decimal 158 | desc: Implicit iterators should cast decimals to strings and interpolate. 159 | data: 160 | list: [ 1.10, 2.20, 3.30, 4.40, 5.50 ] 161 | template: '"{{#list}}({{.}}){{/list}}"' 162 | expected: '"(1.1)(2.2)(3.3)(4.4)(5.5)"' 163 | 164 | - name: Implicit Iterator - Array 165 | desc: Implicit iterators should allow iterating over nested arrays. 166 | data: 167 | list: [ [1, 2, 3], ['a', 'b', 'c'] ] 168 | template: '"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}"' 169 | expected: '"(123)(abc)"' 170 | 171 | # Dotted Names 172 | 173 | - name: Dotted Names - Truthy 174 | desc: Dotted names should be valid for Section tags. 175 | data: { a: { b: { c: true } } } 176 | template: '"{{#a.b.c}}Here{{/a.b.c}}" == "Here"' 177 | expected: '"Here" == "Here"' 178 | 179 | - name: Dotted Names - Falsey 180 | desc: Dotted names should be valid for Section tags. 181 | data: { a: { b: { c: false } } } 182 | template: '"{{#a.b.c}}Here{{/a.b.c}}" == ""' 183 | expected: '"" == ""' 184 | 185 | - name: Dotted Names - Broken Chains 186 | desc: Dotted names that cannot be resolved should be considered falsey. 187 | data: { a: { } } 188 | template: '"{{#a.b.c}}Here{{/a.b.c}}" == ""' 189 | expected: '"" == ""' 190 | 191 | # Whitespace Sensitivity 192 | 193 | - name: Surrounding Whitespace 194 | desc: Sections should not alter surrounding whitespace. 195 | data: { boolean: true } 196 | template: " | {{#boolean}}\t|\t{{/boolean}} | \n" 197 | expected: " | \t|\t | \n" 198 | 199 | - name: Internal Whitespace 200 | desc: Sections should not alter internal whitespace. 201 | data: { boolean: true } 202 | template: " | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n" 203 | expected: " | \n | \n" 204 | 205 | - name: Indented Inline Sections 206 | desc: Single-line sections should not alter surrounding whitespace. 207 | data: { boolean: true } 208 | template: " {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n" 209 | expected: " YES\n GOOD\n" 210 | 211 | - name: Standalone Lines 212 | desc: Standalone lines should be removed from the template. 213 | data: { boolean: true } 214 | template: | 215 | | This Is 216 | {{#boolean}} 217 | | 218 | {{/boolean}} 219 | | A Line 220 | expected: | 221 | | This Is 222 | | 223 | | A Line 224 | 225 | - name: Indented Standalone Lines 226 | desc: Indented standalone lines should be removed from the template. 227 | data: { boolean: true } 228 | template: | 229 | | This Is 230 | {{#boolean}} 231 | | 232 | {{/boolean}} 233 | | A Line 234 | expected: | 235 | | This Is 236 | | 237 | | A Line 238 | 239 | - name: Standalone Line Endings 240 | desc: '"\r\n" should be considered a newline for standalone tags.' 241 | data: { boolean: true } 242 | template: "|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|" 243 | expected: "|\r\n|" 244 | 245 | - name: Standalone Without Previous Line 246 | desc: Standalone tags should not require a newline to precede them. 247 | data: { boolean: true } 248 | template: " {{#boolean}}\n#{{/boolean}}\n/" 249 | expected: "#\n/" 250 | 251 | - name: Standalone Without Newline 252 | desc: Standalone tags should not require a newline to follow them. 253 | data: { boolean: true } 254 | template: "#{{#boolean}}\n/\n {{/boolean}}" 255 | expected: "#\n/\n" 256 | 257 | # Whitespace Insensitivity 258 | 259 | - name: Padding 260 | desc: Superfluous in-tag whitespace should be ignored. 261 | data: { boolean: true } 262 | template: '|{{# boolean }}={{/ boolean }}|' 263 | expected: '|=|' 264 | -------------------------------------------------------------------------------- /handlebars/data_test.go: -------------------------------------------------------------------------------- 1 | package handlebars 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/mailgun/raymond/v2" 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 | -------------------------------------------------------------------------------- /parser/whitespace.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/mailgun/raymond/v2/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 | --------------------------------------------------------------------------------