├── .travis.yml ├── unless_helper.go ├── markdown_helper.go ├── .gitignore ├── unless_helper_test.go ├── markdown_helper_test.go ├── .codeclimate.yml ├── content_helper_test.go ├── block_params.go ├── equal_helper.go ├── block_params_test.go ├── content_helper.go ├── eval_test.go ├── context_test.go ├── LICENSE.txt ├── if_helper_test.go ├── template_test.go ├── velvet.go ├── helper_map_test.go ├── each_helper.go ├── if_helper.go ├── equal_helper_test.go ├── context.go ├── template.go ├── velvet_test.go ├── helper_map.go ├── helpers_test.go ├── helpers.go ├── each_helper_test.go ├── README.md └── eval.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | go: 6 | - 1.5 7 | - 1.6 8 | - 1.7 9 | - tip 10 | 11 | matrix: 12 | allow_failures: 13 | - go: 'tip' 14 | -------------------------------------------------------------------------------- /unless_helper.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import "html/template" 4 | 5 | func unlessHelper(conditional bool, help HelperContext) (template.HTML, error) { 6 | return ifHelper(!conditional, help) 7 | } 8 | -------------------------------------------------------------------------------- /markdown_helper.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import ( 4 | "html/template" 5 | 6 | "github.com/shurcooL/github_flavored_markdown" 7 | ) 8 | 9 | // Markdown converts the string into HTML using GitHub flavored markdown. 10 | func markdownHelper(body string) template.HTML { 11 | b := github_flavored_markdown.Markdown([]byte(body)) 12 | return template.HTML(b) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | doc 4 | tmp 5 | pkg 6 | *.gem 7 | *.pid 8 | coverage 9 | coverage.data 10 | build/* 11 | *.pbxuser 12 | *.mode1v3 13 | .svn 14 | profile 15 | .console_history 16 | .sass-cache/* 17 | .rake_tasks~ 18 | *.log.lck 19 | solr/ 20 | .jhw-cache/ 21 | jhw.* 22 | *.sublime* 23 | node_modules/ 24 | dist/ 25 | generated/ 26 | .vendor/ 27 | bin/* 28 | gin-bin 29 | -------------------------------------------------------------------------------- /unless_helper_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/velvet" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Unless_Helper(t *testing.T) { 11 | r := require.New(t) 12 | ctx := velvet.NewContext() 13 | input := `{{#unless false}}hi{{/unless}}` 14 | 15 | s, err := velvet.Render(input, ctx) 16 | r.NoError(err) 17 | r.Equal("hi", s) 18 | } 19 | -------------------------------------------------------------------------------- /markdown_helper_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/velvet" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_MarkdownHelper(t *testing.T) { 11 | r := require.New(t) 12 | input := `{{markdown m}}` 13 | ctx := velvet.NewContext() 14 | ctx.Set("m", "# H1") 15 | s, err := velvet.Render(input, ctx) 16 | r.NoError(err) 17 | r.Contains(s, "H1") 18 | } 19 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | golint: 4 | enabled: true 5 | checks: 6 | GoLint/Naming/MixedCaps: 7 | enabled: false 8 | govet: 9 | enabled: true 10 | gofmt: 11 | enabled: true 12 | fixme: 13 | enabled: true 14 | ratings: 15 | paths: 16 | - "**.go" 17 | exclude_paths: 18 | - "examples/**/*" 19 | - "grifts/**/*" 20 | - "**/*_test.go" 21 | - "*_test.go" 22 | - "**_test.go" 23 | - "middleware_test.go" 24 | -------------------------------------------------------------------------------- /content_helper_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/velvet" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_ContentForOf(t *testing.T) { 11 | r := require.New(t) 12 | input := ` 13 | {{#content_for "buttons"}}{{/content_for}} 14 | {{content_of "buttons"}} 15 | {{content_of "buttons"}} 16 | ` 17 | s, err := velvet.Render(input, velvet.NewContext()) 18 | r.NoError(err) 19 | r.Contains(s, "") 20 | r.Contains(s, "") 21 | } 22 | -------------------------------------------------------------------------------- /block_params.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | type blockParams struct { 4 | current []string 5 | stack [][]string 6 | } 7 | 8 | func newBlockParams() *blockParams { 9 | return &blockParams{ 10 | current: []string{}, 11 | stack: [][]string{}, 12 | } 13 | } 14 | 15 | func (bp *blockParams) push(params []string) { 16 | bp.current = params 17 | bp.stack = append(bp.stack, params) 18 | } 19 | 20 | func (bp *blockParams) pop() []string { 21 | l := len(bp.stack) 22 | if l == 0 { 23 | return bp.current 24 | } 25 | p := bp.stack[l-1] 26 | bp.stack = bp.stack[0:(l - 1)] 27 | l = len(bp.stack) 28 | if l == 0 { 29 | bp.current = []string{} 30 | } else { 31 | bp.current = bp.stack[l-1] 32 | } 33 | return p 34 | } 35 | -------------------------------------------------------------------------------- /equal_helper.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import "html/template" 4 | 5 | func equalHelper(a, b interface{}, help HelperContext) (template.HTML, error) { 6 | if a == b { 7 | s, err := help.Block() 8 | if err != nil { 9 | return "", err 10 | } 11 | return template.HTML(s), nil 12 | } 13 | s, err := help.ElseBlock() 14 | if err != nil { 15 | return "", err 16 | } 17 | return template.HTML(s), nil 18 | } 19 | 20 | func notEqualHelper(a, b interface{}, help HelperContext) (template.HTML, error) { 21 | if a != b { 22 | s, err := help.Block() 23 | if err != nil { 24 | return "", err 25 | } 26 | return template.HTML(s), nil 27 | } 28 | s, err := help.ElseBlock() 29 | if err != nil { 30 | return "", err 31 | } 32 | return template.HTML(s), nil 33 | } 34 | -------------------------------------------------------------------------------- /block_params_test.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_blockParams(t *testing.T) { 10 | r := require.New(t) 11 | bp := newBlockParams() 12 | r.Equal([]string{}, bp.current) 13 | r.Len(bp.stack, 0) 14 | 15 | bp.push([]string{"mark"}) 16 | r.Equal([]string{"mark"}, bp.current) 17 | r.Len(bp.stack, 1) 18 | 19 | bp.push([]string{"bates"}) 20 | r.Equal([]string{"bates"}, bp.current) 21 | r.Len(bp.stack, 2) 22 | r.Equal([][]string{ 23 | []string{"mark"}, 24 | []string{"bates"}, 25 | }, bp.stack) 26 | 27 | b := bp.pop() 28 | r.Equal([]string{"bates"}, b) 29 | r.Equal([]string{"mark"}, bp.current) 30 | r.Len(bp.stack, 1) 31 | 32 | b = bp.pop() 33 | r.Equal([]string{"mark"}, b) 34 | r.Len(bp.stack, 0) 35 | r.Equal([]string{}, bp.current) 36 | } 37 | -------------------------------------------------------------------------------- /content_helper.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import ( 4 | "html/template" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // ContentFor stores a block of templating code to be re-used later in the template. 10 | /* 11 | {{#content_for "buttons"}} 12 | 13 | {{/content_for}} 14 | */ 15 | func contentForHelper(name string, help HelperContext) (string, error) { 16 | body, err := help.Block() 17 | if err != nil { 18 | return "", errors.WithStack(err) 19 | } 20 | help.Context.Set(name, template.HTML(body)) 21 | return "", nil 22 | } 23 | 24 | // ContentOf retrieves a stored block for templating and renders it. 25 | /* 26 | {{content_of "buttons"}} 27 | */ 28 | func contentOfHelper(name string, help HelperContext) template.HTML { 29 | if s := help.Context.Get(name); s != nil { 30 | return s.(template.HTML) 31 | } 32 | return "" 33 | } 34 | -------------------------------------------------------------------------------- /eval_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/gobuffalo/velvet" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Eval_Map_Call_Key(t *testing.T) { 12 | r := require.New(t) 13 | ctx := velvet.NewContext() 14 | data := map[string]string{ 15 | "a": "A", 16 | "b": "B", 17 | } 18 | ctx.Set("letters", data) 19 | input := ` 20 | {{letters.a}}|{{letters.b}} 21 | ` 22 | 23 | s, err := velvet.Render(input, ctx) 24 | r.NoError(err) 25 | r.Equal("A|B", strings.TrimSpace(s)) 26 | } 27 | 28 | func Test_Eval_Calls_on_Pointers(t *testing.T) { 29 | r := require.New(t) 30 | type user struct { 31 | Name string 32 | } 33 | u := &user{Name: "Mark"} 34 | ctx := velvet.NewContext() 35 | ctx.Set("user", u) 36 | 37 | s, err := velvet.Render("{{user.Name}}", ctx) 38 | r.NoError(err) 39 | r.Equal("Mark", s) 40 | } 41 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/velvet" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Context_Set(t *testing.T) { 11 | r := require.New(t) 12 | c := velvet.NewContext() 13 | r.Nil(c.Get("foo")) 14 | c.Set("foo", "bar") 15 | r.NotNil(c.Get("foo")) 16 | } 17 | 18 | func Test_Context_Get(t *testing.T) { 19 | r := require.New(t) 20 | c := velvet.NewContext() 21 | r.Nil(c.Get("foo")) 22 | c.Set("foo", "bar") 23 | r.Equal("bar", c.Get("foo")) 24 | } 25 | 26 | func Test_NewSubContext_Set(t *testing.T) { 27 | r := require.New(t) 28 | 29 | c := velvet.NewContext() 30 | r.Nil(c.Get("foo")) 31 | 32 | sc := c.New() 33 | r.Nil(sc.Get("foo")) 34 | sc.Set("foo", "bar") 35 | r.Equal("bar", sc.Get("foo")) 36 | 37 | r.Nil(c.Get("foo")) 38 | } 39 | 40 | func Test_NewSubContext_Get(t *testing.T) { 41 | r := require.New(t) 42 | 43 | c := velvet.NewContext() 44 | c.Set("foo", "bar") 45 | 46 | sc := c.New() 47 | r.Equal("bar", sc.Get("foo")) 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2016 Mark Bates 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /if_helper_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/velvet" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_If_Helper(t *testing.T) { 11 | r := require.New(t) 12 | ctx := velvet.NewContext() 13 | input := `{{#if true}}hi{{/if}}` 14 | 15 | s, err := velvet.Render(input, ctx) 16 | r.NoError(err) 17 | r.Equal("hi", s) 18 | } 19 | 20 | func Test_If_Helper_false(t *testing.T) { 21 | r := require.New(t) 22 | ctx := velvet.NewContext() 23 | input := `{{#if false}}hi{{/if}}` 24 | 25 | s, err := velvet.Render(input, ctx) 26 | r.NoError(err) 27 | r.Equal("", s) 28 | } 29 | 30 | func Test_If_Helper_NoArgs(t *testing.T) { 31 | r := require.New(t) 32 | ctx := velvet.NewContext() 33 | input := `{{#if }}hi{{/if}}` 34 | 35 | _, err := velvet.Render(input, ctx) 36 | r.Error(err) 37 | } 38 | 39 | func Test_If_Helper_Else(t *testing.T) { 40 | r := require.New(t) 41 | ctx := velvet.NewContext() 42 | input := ` 43 | {{#if false}} 44 | hi 45 | {{ else }} 46 | bye 47 | {{/if}}` 48 | 49 | s, err := velvet.Render(input, ctx) 50 | r.NoError(err) 51 | r.Contains(s, "bye") 52 | } 53 | -------------------------------------------------------------------------------- /template_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gobuffalo/velvet" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Template_Helpers(t *testing.T) { 12 | r := require.New(t) 13 | 14 | input := `{{say "mark"}}` 15 | tpl, err := velvet.Parse(input) 16 | r.NoError(err) 17 | 18 | tpl.Helpers.Add("say", func(name string) string { 19 | return fmt.Sprintf("say: %s", name) 20 | }) 21 | 22 | ctx := velvet.NewContext() 23 | s, err := tpl.Exec(ctx) 24 | r.NoError(err) 25 | r.Equal("say: mark", s) 26 | 27 | input = `{{say "jane"}}` 28 | tpl, err = velvet.Parse(input) 29 | r.NoError(err) 30 | _, err = tpl.Exec(ctx) 31 | r.Error(err) 32 | } 33 | 34 | func Test_Template_Clone(t *testing.T) { 35 | r := require.New(t) 36 | 37 | say := func(name string) string { 38 | return fmt.Sprintf("speak: %s", name) 39 | } 40 | 41 | input := `{{speak "mark"}}` 42 | t1, err := velvet.Parse(input) 43 | r.NoError(err) 44 | 45 | t2 := t1.Clone() 46 | t2.Helpers.Add("speak", say) 47 | 48 | ctx := velvet.NewContext() 49 | 50 | _, err = t1.Exec(ctx) 51 | r.Error(err) 52 | 53 | s, err := t2.Exec(ctx) 54 | r.NoError(err) 55 | r.Equal("speak: mark", s) 56 | } 57 | -------------------------------------------------------------------------------- /velvet.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // BuffaloRenderer implements the render.TemplateEngine interface allowing velvet to be used as a template engine 10 | // for Buffalo 11 | func BuffaloRenderer(input string, data map[string]interface{}, helpers map[string]interface{}) (string, error) { 12 | t, err := Parse(input) 13 | if err != nil { 14 | return "", err 15 | } 16 | if helpers != nil { 17 | t.Helpers.AddMany(helpers) 18 | } 19 | return t.Exec(NewContextWith(data)) 20 | } 21 | 22 | var cache = map[string]*Template{} 23 | var moot = &sync.Mutex{} 24 | 25 | // Parse an input string and return a Template. 26 | func Parse(input string) (*Template, error) { 27 | moot.Lock() 28 | defer moot.Unlock() 29 | if t, ok := cache[input]; ok { 30 | return t, nil 31 | } 32 | t, err := NewTemplate(input) 33 | 34 | if err == nil { 35 | cache[input] = t 36 | } 37 | 38 | if err != nil { 39 | return t, errors.WithStack(err) 40 | } 41 | 42 | return t, nil 43 | } 44 | 45 | // Render a string using the given the context. 46 | func Render(input string, ctx *Context) (string, error) { 47 | t, err := Parse(input) 48 | if err != nil { 49 | return "", errors.WithStack(err) 50 | } 51 | return t.Exec(ctx) 52 | } 53 | -------------------------------------------------------------------------------- /helper_map_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/velvet" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_HelperMap_Add(t *testing.T) { 11 | r := require.New(t) 12 | hm, err := velvet.NewHelperMap() 13 | r.NoError(err) 14 | err = hm.Add("foo", func(help velvet.HelperContext) (string, error) { 15 | return "", nil 16 | }) 17 | r.NoError(err) 18 | r.NotNil(hm.Helpers()["foo"]) 19 | } 20 | 21 | func Test_HelperMap_Add_Invalid_NoReturn(t *testing.T) { 22 | r := require.New(t) 23 | 24 | hm, err := velvet.NewHelperMap() 25 | r.NoError(err) 26 | 27 | err = hm.Add("foo", func(help velvet.HelperContext) {}) 28 | r.Error(err) 29 | r.Contains(err.Error(), "must return at least one") 30 | r.Nil(hm.Helpers()["foo"]) 31 | } 32 | 33 | func Test_HelperMap_Add_Invalid_ReturnTypes(t *testing.T) { 34 | r := require.New(t) 35 | 36 | hm, err := velvet.NewHelperMap() 37 | r.NoError(err) 38 | 39 | err = hm.Add("foo", func(help velvet.HelperContext) (string, string) { 40 | return "", "" 41 | }) 42 | r.Error(err) 43 | r.Contains(err.Error(), "foo must return ([string|template.HTML], [error]), not (string, string)") 44 | r.Nil(hm.Helpers()["foo"]) 45 | 46 | err = hm.Add("foo", func(help velvet.HelperContext) int { return 1 }) 47 | r.Error(err) 48 | r.Contains(err.Error(), "foo must return ([string|template.HTML], [error]), not (int)") 49 | r.Nil(hm.Helpers()["foo"]) 50 | } 51 | -------------------------------------------------------------------------------- /each_helper.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "reflect" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func eachHelper(collection interface{}, help HelperContext) (template.HTML, error) { 12 | out := bytes.Buffer{} 13 | val := reflect.ValueOf(collection) 14 | if val.Kind() == reflect.Ptr { 15 | val = val.Elem() 16 | } 17 | if val.Kind() == reflect.Struct || val.Len() == 0 { 18 | s, err := help.ElseBlock() 19 | return template.HTML(s), err 20 | } 21 | switch val.Kind() { 22 | case reflect.Array, reflect.Slice: 23 | for i := 0; i < val.Len(); i++ { 24 | v := val.Index(i).Interface() 25 | ctx := help.Context.New() 26 | ctx.Set("@first", i == 0) 27 | ctx.Set("@last", i == val.Len()-1) 28 | ctx.Set("@index", i) 29 | ctx.Set("@value", v) 30 | s, err := help.BlockWith(ctx) 31 | if err != nil { 32 | return "", errors.WithStack(err) 33 | } 34 | out.WriteString(s) 35 | } 36 | case reflect.Map: 37 | keys := val.MapKeys() 38 | for i := 0; i < len(keys); i++ { 39 | key := keys[i].Interface() 40 | v := val.MapIndex(keys[i]).Interface() 41 | ctx := help.Context.New() 42 | ctx.Set("@first", i == 0) 43 | ctx.Set("@last", i == len(keys)-1) 44 | ctx.Set("@key", key) 45 | ctx.Set("@value", v) 46 | s, err := help.BlockWith(ctx) 47 | if err != nil { 48 | return "", errors.WithStack(err) 49 | } 50 | out.WriteString(s) 51 | } 52 | } 53 | return template.HTML(out.String()), nil 54 | } 55 | -------------------------------------------------------------------------------- /if_helper.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import ( 4 | "html/template" 5 | "reflect" 6 | ) 7 | 8 | func ifHelper(conditional interface{}, help HelperContext) (template.HTML, error) { 9 | if IsTrue(conditional) { 10 | s, err := help.Block() 11 | return template.HTML(s), err 12 | } 13 | s, err := help.ElseBlock() 14 | return template.HTML(s), err 15 | } 16 | 17 | // IsTrue returns true if obj is a truthy value. 18 | func IsTrue(obj interface{}) bool { 19 | thruth, ok := isTrueValue(reflect.ValueOf(obj)) 20 | if !ok { 21 | return false 22 | } 23 | return thruth 24 | } 25 | 26 | // isTrueValue reports whether the value is 'true', in the sense of not the zero of its type, 27 | // and whether the value has a meaningful truth value 28 | // 29 | // NOTE: borrowed from https://github.com/golang/go/tree/master/src/text/template/exec.go 30 | func isTrueValue(val reflect.Value) (truth, ok bool) { 31 | if !val.IsValid() { 32 | // Something like var x interface{}, never set. It's a form of nil. 33 | return false, true 34 | } 35 | switch val.Kind() { 36 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 37 | truth = val.Len() > 0 38 | case reflect.Bool: 39 | truth = val.Bool() 40 | case reflect.Complex64, reflect.Complex128: 41 | truth = val.Complex() != 0 42 | case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface: 43 | truth = !val.IsNil() 44 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 45 | truth = val.Int() != 0 46 | case reflect.Float32, reflect.Float64: 47 | truth = val.Float() != 0 48 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 49 | truth = val.Uint() != 0 50 | case reflect.Struct: 51 | truth = true // Struct values are always true. 52 | default: 53 | return 54 | } 55 | return truth, true 56 | } 57 | -------------------------------------------------------------------------------- /equal_helper_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/velvet" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_EqualHelper_True(t *testing.T) { 11 | r := require.New(t) 12 | input := ` 13 | {{#eq 1 1}} 14 | it was true 15 | {{else}} 16 | it was false 17 | {{/eq}} 18 | ` 19 | s, err := velvet.Render(input, velvet.NewContext()) 20 | r.NoError(err) 21 | r.Contains(s, "it was true") 22 | } 23 | 24 | func Test_EqualHelper_False(t *testing.T) { 25 | r := require.New(t) 26 | input := ` 27 | {{#eq 1 2}} 28 | it was true 29 | {{else}} 30 | it was false 31 | {{/eq}} 32 | ` 33 | s, err := velvet.Render(input, velvet.NewContext()) 34 | r.NoError(err) 35 | r.Contains(s, "it was false") 36 | } 37 | 38 | func Test_EqualHelper_DifferentTypes(t *testing.T) { 39 | r := require.New(t) 40 | input := ` 41 | {{#eq 1 "1"}} 42 | it was true 43 | {{else}} 44 | it was false 45 | {{/eq}} 46 | ` 47 | s, err := velvet.Render(input, velvet.NewContext()) 48 | r.NoError(err) 49 | r.Contains(s, "it was false") 50 | } 51 | 52 | func Test_NotEqualHelper_True(t *testing.T) { 53 | r := require.New(t) 54 | input := ` 55 | {{#neq 1 1}} 56 | it was true 57 | {{else}} 58 | it was false 59 | {{/neq}} 60 | ` 61 | s, err := velvet.Render(input, velvet.NewContext()) 62 | r.NoError(err) 63 | r.Contains(s, "it was false") 64 | } 65 | 66 | func Test_NotEqualHelper_False(t *testing.T) { 67 | r := require.New(t) 68 | input := ` 69 | {{#neq 1 2}} 70 | it was true 71 | {{else}} 72 | it was false 73 | {{/neq}} 74 | ` 75 | s, err := velvet.Render(input, velvet.NewContext()) 76 | r.NoError(err) 77 | r.Contains(s, "it was true") 78 | } 79 | 80 | func Test_NotEqualHelper_DifferentTypes(t *testing.T) { 81 | r := require.New(t) 82 | input := ` 83 | {{#neq 1 "1"}} 84 | it was true 85 | {{else}} 86 | it was false 87 | {{/neq}} 88 | ` 89 | s, err := velvet.Render(input, velvet.NewContext()) 90 | r.NoError(err) 91 | r.Contains(s, "it was true") 92 | } 93 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | // Context holds all of the data for the template that is being rendered. 4 | type Context struct { 5 | data map[string]interface{} 6 | options map[string]interface{} 7 | outer *Context 8 | } 9 | 10 | func (c *Context) export() map[string]interface{} { 11 | m := map[string]interface{}{} 12 | if c.outer != nil { 13 | for k, v := range c.outer.export() { 14 | m[k] = v 15 | } 16 | } 17 | for k, v := range c.data { 18 | m[k] = v 19 | } 20 | if c.options != nil { 21 | for k, v := range c.options { 22 | m[k] = v 23 | } 24 | } 25 | 26 | return m 27 | } 28 | 29 | // New context containing the current context. Values set on the new context 30 | // will not be set onto the original context, however, the original context's 31 | // values will be available to the new context. 32 | func (c *Context) New() *Context { 33 | cc := NewContext() 34 | cc.outer = c 35 | return cc 36 | } 37 | 38 | // Set a value onto the context 39 | func (c *Context) Set(key string, value interface{}) { 40 | c.data[key] = value 41 | } 42 | 43 | // Get a value from the context, or it's parent's context if one exists. 44 | func (c *Context) Get(key string) interface{} { 45 | if v, ok := c.data[key]; ok { 46 | return v 47 | } 48 | if c.outer != nil { 49 | return c.outer.Get(key) 50 | } 51 | return nil 52 | } 53 | 54 | // Has checks the existence of the key in the context. 55 | func (c *Context) Has(key string) bool { 56 | return c.Get(key) != nil 57 | } 58 | 59 | // Options are the values passed into a helper. 60 | func (c *Context) Options() map[string]interface{} { 61 | return c.options 62 | } 63 | 64 | // NewContext returns a fully formed context ready to go 65 | func NewContext() *Context { 66 | return &Context{ 67 | data: map[string]interface{}{}, 68 | options: map[string]interface{}{}, 69 | outer: nil, 70 | } 71 | } 72 | 73 | // NewContextWith returns a fully formed context using the data 74 | // provided. 75 | func NewContextWith(data map[string]interface{}) *Context { 76 | c := NewContext() 77 | c.data = data 78 | return c 79 | } 80 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import ( 4 | "github.com/aymerick/raymond/ast" 5 | "github.com/aymerick/raymond/parser" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // Template represents an input and helpers to be used 10 | // to evaluate and render the input. 11 | type Template struct { 12 | Input string 13 | Helpers HelperMap 14 | program *ast.Program 15 | } 16 | 17 | // NewTemplate from the input string. Adds all of the 18 | // global helper functions from "velvet.Helpers". 19 | func NewTemplate(input string) (*Template, error) { 20 | hm, err := NewHelperMap() 21 | if err != nil { 22 | return nil, errors.WithStack(err) 23 | } 24 | t := &Template{ 25 | Input: input, 26 | Helpers: hm, 27 | } 28 | err = t.Parse() 29 | if err != nil { 30 | return t, errors.WithStack(err) 31 | } 32 | return t, nil 33 | } 34 | 35 | // Parse the template this can be called many times 36 | // as a successful result is cached and is used on subsequent 37 | // uses. 38 | func (t *Template) Parse() error { 39 | if t.program != nil { 40 | return nil 41 | } 42 | program, err := parser.Parse(t.Input) 43 | if err != nil { 44 | return errors.WithStack(err) 45 | } 46 | t.program = program 47 | return nil 48 | } 49 | 50 | // Exec the template using the content and return the results 51 | func (t *Template) Exec(ctx *Context) (string, error) { 52 | err := t.Parse() 53 | if err != nil { 54 | return "", errors.WithStack(err) 55 | } 56 | v := newEvalVisitor(t, ctx) 57 | r := t.program.Accept(v) 58 | switch rp := r.(type) { 59 | case string: 60 | return rp, nil 61 | case error: 62 | return "", rp 63 | case nil: 64 | return "", nil 65 | default: 66 | return "", errors.WithStack(errors.Errorf("unsupport eval return format %T: %+v", r, r)) 67 | } 68 | } 69 | 70 | // Clone a template. This is useful for defining helpers on per "instance" of the template. 71 | func (t *Template) Clone() *Template { 72 | hm, _ := NewHelperMap() 73 | hm.AddMany(t.Helpers.Helpers()) 74 | t2 := &Template{ 75 | Helpers: hm, 76 | Input: t.Input, 77 | program: t.program, 78 | } 79 | return t2 80 | } 81 | -------------------------------------------------------------------------------- /velvet_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/velvet" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Render(t *testing.T) { 11 | r := require.New(t) 12 | 13 | ctx := velvet.NewContext() 14 | ctx.Set("name", "Tim") 15 | s, err := velvet.Render("{{name}}", ctx) 16 | r.NoError(err) 17 | r.Equal("Tim", s) 18 | } 19 | 20 | func Test_Render_with_Content(t *testing.T) { 21 | r := require.New(t) 22 | 23 | ctx := velvet.NewContext() 24 | ctx.Set("name", "Tim") 25 | s, err := velvet.Render("

{{name}}

", ctx) 26 | r.NoError(err) 27 | r.Equal("

Tim

", s) 28 | } 29 | 30 | func Test_Render_Unknown_Value(t *testing.T) { 31 | r := require.New(t) 32 | 33 | ctx := velvet.NewContext() 34 | _, err := velvet.Render("

{{name}}

", ctx) 35 | r.Error(err) 36 | r.Equal("could not find value for name [line 1:3]", err.Error()) 37 | } 38 | 39 | func Test_Render_with_String(t *testing.T) { 40 | r := require.New(t) 41 | 42 | ctx := velvet.NewContext() 43 | s, err := velvet.Render(`

{{"Tim"}}

`, ctx) 44 | r.NoError(err) 45 | r.Equal("

Tim

", s) 46 | } 47 | 48 | func Test_Render_with_Math(t *testing.T) { 49 | r := require.New(t) 50 | 51 | ctx := velvet.NewContext() 52 | _, err := velvet.Render(`

{{2 + 1}}

`, ctx) 53 | r.Error(err) 54 | } 55 | 56 | func Test_Render_with_Comments(t *testing.T) { 57 | r := require.New(t) 58 | ctx := velvet.NewContext() 59 | s, err := velvet.Render(`

`, ctx) 60 | r.NoError(err) 61 | r.Equal("

", s) 62 | } 63 | 64 | func Test_Render_with_Func(t *testing.T) { 65 | r := require.New(t) 66 | ctx := velvet.NewContext() 67 | ctx.Set("user", user{First: "Mark", Last: "Bates"}) 68 | s, err := velvet.Render("{{user.FullName}}", ctx) 69 | r.NoError(err) 70 | r.Equal("Mark Bates", s) 71 | } 72 | 73 | func Test_Render_Array(t *testing.T) { 74 | r := require.New(t) 75 | 76 | ctx := velvet.NewContext() 77 | ctx.Set("names", []string{"mark", "bates"}) 78 | s, err := velvet.Render("{{names}}", ctx) 79 | r.NoError(err) 80 | r.Equal("mark bates", s) 81 | } 82 | 83 | type user struct { 84 | First string 85 | Last string 86 | } 87 | 88 | func (u user) FullName() string { 89 | return u.First + " " + u.Last 90 | } 91 | -------------------------------------------------------------------------------- /helper_map.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // HelperMap holds onto helpers and validates they are properly formed. 12 | type HelperMap struct { 13 | moot *sync.Mutex 14 | helpers map[string]interface{} 15 | } 16 | 17 | // NewHelperMap containing all of the "default" helpers from "velvet.Helpers". 18 | func NewHelperMap() (HelperMap, error) { 19 | hm := HelperMap{ 20 | helpers: map[string]interface{}{}, 21 | moot: &sync.Mutex{}, 22 | } 23 | 24 | err := hm.AddMany(Helpers.Helpers()) 25 | if err != nil { 26 | return hm, errors.WithStack(err) 27 | } 28 | return hm, nil 29 | } 30 | 31 | // Add a new helper to the map. New Helpers will be validated to ensure they 32 | // meet the requirements for a helper: 33 | /* 34 | func(...) (string) {} 35 | func(...) (string, error) {} 36 | func(...) (template.HTML) {} 37 | func(...) (template.HTML, error) {} 38 | */ 39 | func (h *HelperMap) Add(key string, helper interface{}) error { 40 | h.moot.Lock() 41 | defer h.moot.Unlock() 42 | if h.helpers == nil { 43 | h.helpers = map[string]interface{}{} 44 | } 45 | err := h.validateHelper(key, helper) 46 | if err != nil { 47 | return errors.WithStack(err) 48 | } 49 | h.helpers[key] = helper 50 | return nil 51 | } 52 | 53 | // AddMany helpers at the same time. 54 | func (h *HelperMap) AddMany(helpers map[string]interface{}) error { 55 | for k, v := range helpers { 56 | err := h.Add(k, v) 57 | if err != nil { 58 | return errors.WithStack(err) 59 | } 60 | } 61 | return nil 62 | } 63 | 64 | // Helpers returns the underlying list of helpers from the map 65 | func (h HelperMap) Helpers() map[string]interface{} { 66 | return h.helpers 67 | } 68 | 69 | func (h *HelperMap) validateHelper(key string, helper interface{}) error { 70 | ht := reflect.ValueOf(helper).Type() 71 | 72 | if ht.NumOut() < 1 { 73 | return errors.WithStack(errors.Errorf("%s must return at least one value ([string|template.HTML], [error])", key)) 74 | } 75 | so := ht.Out(0).Kind().String() 76 | if ht.NumOut() > 1 { 77 | et := ht.Out(1) 78 | ev := reflect.ValueOf(et) 79 | ek := fmt.Sprintf("%s", ev.Interface()) 80 | if (so != "string" && so != "template.HTML") || (ek != "error") { 81 | return errors.WithStack(errors.Errorf("%s must return ([string|template.HTML], [error]), not (%s, %s)", key, so, et.Kind())) 82 | } 83 | } else { 84 | if so != "string" && so != "template.HTML" { 85 | return errors.WithStack(errors.Errorf("%s must return ([string|template.HTML], [error]), not (%s)", key, so)) 86 | } 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/gobuffalo/velvet" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_CustomGlobalHelper(t *testing.T) { 14 | r := require.New(t) 15 | err := velvet.Helpers.Add("say", func(name string) (string, error) { 16 | return fmt.Sprintf("say: %s", name), nil 17 | }) 18 | r.NoError(err) 19 | 20 | input := `{{say "mark"}}` 21 | ctx := velvet.NewContext() 22 | s, err := velvet.Render(input, ctx) 23 | r.NoError(err) 24 | r.Equal("say: mark", s) 25 | } 26 | 27 | func Test_CustomGlobalBlockHelper(t *testing.T) { 28 | r := require.New(t) 29 | velvet.Helpers.Add("say", func(name string, help velvet.HelperContext) (template.HTML, error) { 30 | ctx := help.Context 31 | ctx.Set("name", strings.ToUpper(name)) 32 | s, err := help.BlockWith(ctx) 33 | return template.HTML(s), err 34 | }) 35 | 36 | input := ` 37 | {{#say "mark"}} 38 |

{{name}}

39 | {{/say}} 40 | ` 41 | ctx := velvet.NewContext() 42 | s, err := velvet.Render(input, ctx) 43 | r.NoError(err) 44 | r.Contains(s, "

MARK

") 45 | } 46 | 47 | func Test_Helper_Hash_Options(t *testing.T) { 48 | r := require.New(t) 49 | velvet.Helpers.Add("say", func(help velvet.HelperContext) string { 50 | return help.Context.Get("name").(string) 51 | }) 52 | 53 | input := `{{say name="mark"}}` 54 | ctx := velvet.NewContext() 55 | s, err := velvet.Render(input, ctx) 56 | r.NoError(err) 57 | r.Equal("mark", s) 58 | } 59 | 60 | func Test_Helper_Hash_Options_Many(t *testing.T) { 61 | r := require.New(t) 62 | velvet.Helpers.Add("say", func(help velvet.HelperContext) string { 63 | return help.Context.Get("first").(string) + help.Context.Get("last").(string) 64 | }) 65 | 66 | input := `{{say first=first_name last=last_name}}` 67 | ctx := velvet.NewContext() 68 | ctx.Set("first_name", "Mark") 69 | ctx.Set("last_name", "Bates") 70 | s, err := velvet.Render(input, ctx) 71 | r.NoError(err) 72 | r.Equal("MarkBates", s) 73 | } 74 | 75 | func Test_Helper_Santize_Output(t *testing.T) { 76 | r := require.New(t) 77 | 78 | velvet.Helpers.Add("safe", func(help velvet.HelperContext) template.HTML { 79 | return template.HTML("

safe

") 80 | }) 81 | velvet.Helpers.Add("unsafe", func(help velvet.HelperContext) string { 82 | return "unsafe" 83 | }) 84 | 85 | input := `{{safe}}|{{unsafe}}` 86 | s, err := velvet.Render(input, velvet.NewContext()) 87 | r.NoError(err) 88 | r.Equal("

safe

|<b>unsafe</b>", s) 89 | } 90 | 91 | func Test_JSON_Helper(t *testing.T) { 92 | r := require.New(t) 93 | 94 | input := `{{json names}}` 95 | ctx := velvet.NewContext() 96 | ctx.Set("names", []string{"mark", "bates"}) 97 | s, err := velvet.Render(input, ctx) 98 | r.NoError(err) 99 | r.Equal(`["mark","bates"]`, s) 100 | } 101 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "html/template" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/markbates/inflect" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // Helpers contains all of the default helpers for velvet. 17 | // These will be available to all templates. You should add 18 | // any custom global helpers to this list. 19 | var Helpers = HelperMap{ 20 | moot: &sync.Mutex{}, 21 | } 22 | 23 | func init() { 24 | Helpers.Add("if", ifHelper) 25 | Helpers.Add("unless", unlessHelper) 26 | Helpers.Add("each", eachHelper) 27 | Helpers.Add("eq", equalHelper) 28 | Helpers.Add("equal", equalHelper) 29 | Helpers.Add("neq", notEqualHelper) 30 | Helpers.Add("notequal", notEqualHelper) 31 | Helpers.Add("json", toJSONHelper) 32 | Helpers.Add("js_escape", template.JSEscapeString) 33 | Helpers.Add("html_escape", template.HTMLEscapeString) 34 | Helpers.Add("upcase", strings.ToUpper) 35 | Helpers.Add("downcase", strings.ToLower) 36 | Helpers.Add("content_for", contentForHelper) 37 | Helpers.Add("content_of", contentOfHelper) 38 | Helpers.Add("markdown", markdownHelper) 39 | Helpers.Add("len", lenHelper) 40 | Helpers.Add("debug", debugHelper) 41 | Helpers.Add("inspect", inspectHelper) 42 | Helpers.AddMany(inflect.Helpers) 43 | } 44 | 45 | // HelperContext is an optional context that can be passed 46 | // as the last argument to helper functions. 47 | type HelperContext struct { 48 | Context *Context 49 | Args []interface{} 50 | evalVisitor *evalVisitor 51 | } 52 | 53 | // Block executes the block of template associated with 54 | // the helper, think the block inside of an "if" or "each" 55 | // statement. 56 | func (h HelperContext) Block() (string, error) { 57 | return h.BlockWith(h.Context) 58 | } 59 | 60 | // BlockWith executes the block of template associated with 61 | // the helper, think the block inside of an "if" or "each" 62 | // statement. It takes a new context with which to evaluate 63 | // the block. 64 | func (h HelperContext) BlockWith(ctx *Context) (string, error) { 65 | nev := newEvalVisitor(h.evalVisitor.template, ctx) 66 | nev.blockParams = h.evalVisitor.blockParams 67 | dd := nev.VisitProgram(h.evalVisitor.curBlock.Program) 68 | switch tp := dd.(type) { 69 | case string: 70 | return tp, nil 71 | case error: 72 | return "", errors.WithStack(tp) 73 | case nil: 74 | return "", nil 75 | default: 76 | return "", errors.WithStack(errors.Errorf("unknown return value %T %+v", dd, dd)) 77 | } 78 | } 79 | 80 | // ElseBlock executes the "inverse" block of template associated with 81 | // the helper, think the "else" block of an "if" or "each" 82 | // statement. 83 | func (h HelperContext) ElseBlock() (string, error) { 84 | return h.ElseBlockWith(h.Context) 85 | } 86 | 87 | // ElseBlockWith executes the "inverse" block of template associated with 88 | // the helper, think the "else" block of an "if" or "each" 89 | // statement. It takes a new context with which to evaluate 90 | // the block. 91 | func (h HelperContext) ElseBlockWith(ctx *Context) (string, error) { 92 | if h.evalVisitor.curBlock.Inverse == nil { 93 | return "", nil 94 | } 95 | nev := newEvalVisitor(h.evalVisitor.template, ctx) 96 | nev.blockParams = h.evalVisitor.blockParams 97 | dd := nev.VisitProgram(h.evalVisitor.curBlock.Inverse) 98 | switch tp := dd.(type) { 99 | case string: 100 | return tp, nil 101 | case error: 102 | return "", errors.WithStack(tp) 103 | case nil: 104 | return "", nil 105 | default: 106 | return "", errors.WithStack(errors.Errorf("unknown return value %T %+v", dd, dd)) 107 | } 108 | } 109 | 110 | // Helpers returns a HelperMap containing all of the known helpers 111 | func (h HelperContext) Helpers() *HelperMap { 112 | return &h.evalVisitor.template.Helpers 113 | } 114 | 115 | // Get is a convenience method that calls the underlying Context. 116 | func (h HelperContext) Get(key string) interface{} { 117 | return h.Context.Get(key) 118 | } 119 | 120 | // toJSONHelper converts an interface into a string. 121 | func toJSONHelper(v interface{}) (template.HTML, error) { 122 | b, err := json.Marshal(v) 123 | if err != nil { 124 | return "", errors.WithStack(err) 125 | } 126 | return template.HTML(b), nil 127 | } 128 | 129 | func lenHelper(v interface{}) string { 130 | rv := reflect.ValueOf(v) 131 | if rv.Kind() == reflect.Ptr { 132 | rv = rv.Elem() 133 | } 134 | return strconv.Itoa(rv.Len()) 135 | } 136 | 137 | // Debug by verbosely printing out using 'pre' tags. 138 | func debugHelper(v interface{}) template.HTML { 139 | return template.HTML(fmt.Sprintf("
%+v
", v)) 140 | } 141 | 142 | func inspectHelper(v interface{}) string { 143 | return fmt.Sprintf("%+v", v) 144 | } 145 | -------------------------------------------------------------------------------- /each_helper_test.go: -------------------------------------------------------------------------------- 1 | package velvet_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gobuffalo/velvet" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Each_Helper_NoArgs(t *testing.T) { 12 | r := require.New(t) 13 | ctx := velvet.NewContext() 14 | input := `{{#each }}{{@value}}{{/each}}` 15 | 16 | _, err := velvet.Render(input, ctx) 17 | r.Error(err) 18 | } 19 | 20 | func Test_Each_Helper(t *testing.T) { 21 | r := require.New(t) 22 | ctx := velvet.NewContext() 23 | ctx.Set("names", []string{"mark", "bates"}) 24 | input := `{{#each names }}

{{@value}}

{{/each}}` 25 | 26 | s, err := velvet.Render(input, ctx) 27 | r.NoError(err) 28 | r.Equal("

mark

bates

", s) 29 | } 30 | 31 | func Test_Each_Helper_Index(t *testing.T) { 32 | r := require.New(t) 33 | ctx := velvet.NewContext() 34 | ctx.Set("names", []string{"mark", "bates"}) 35 | input := `{{#each names }}

{{@index}}

{{/each}}` 36 | 37 | s, err := velvet.Render(input, ctx) 38 | r.NoError(err) 39 | r.Equal("

0

1

", s) 40 | } 41 | 42 | func Test_Each_Helper_As(t *testing.T) { 43 | r := require.New(t) 44 | ctx := velvet.NewContext() 45 | ctx.Set("names", []string{"mark", "bates"}) 46 | input := `{{#each names as |ind name| }}

{{ind}}-{{name}}

{{/each}}` 47 | 48 | s, err := velvet.Render(input, ctx) 49 | r.NoError(err) 50 | r.Equal("

0-mark

1-bates

", s) 51 | } 52 | 53 | func Test_Each_Helper_As_Nested(t *testing.T) { 54 | r := require.New(t) 55 | ctx := velvet.NewContext() 56 | users := []struct { 57 | Name string 58 | Initials []string 59 | }{ 60 | {Name: "Mark", Initials: []string{"M", "F", "B"}}, 61 | {Name: "Rachel", Initials: []string{"R", "A", "B"}}, 62 | } 63 | ctx.Set("users", users) 64 | input := ` 65 | {{#each users as |user|}} 66 |

{{user.Name}}

67 | {{#each user.Initials as |i|}} 68 | {{user.Name}}: {{i}} 69 | {{/each}} 70 | {{/each}} 71 | ` 72 | 73 | s, err := velvet.Render(input, ctx) 74 | r.NoError(err) 75 | r.Contains(s, "

Mark

") 76 | r.Contains(s, "Mark: M") 77 | r.Contains(s, "Mark: F") 78 | r.Contains(s, "Mark: B") 79 | r.Contains(s, "

Rachel

") 80 | r.Contains(s, "Rachel: R") 81 | r.Contains(s, "Rachel: A") 82 | r.Contains(s, "Rachel: B") 83 | } 84 | 85 | func Test_Each_Helper_SlicePtr(t *testing.T) { 86 | r := require.New(t) 87 | type user struct { 88 | Name string 89 | } 90 | type users []user 91 | 92 | us := &users{ 93 | {Name: "Mark"}, 94 | {Name: "Rachel"}, 95 | } 96 | 97 | ctx := velvet.NewContext() 98 | ctx.Set("users", us) 99 | 100 | input := ` 101 | {{#each users as |user|}} 102 | {{user.Name}} 103 | {{/each}} 104 | ` 105 | s, err := velvet.Render(input, ctx) 106 | r.NoError(err) 107 | r.Contains(s, "Mark") 108 | r.Contains(s, "Rachel") 109 | } 110 | 111 | func Test_Each_Helper_Map(t *testing.T) { 112 | r := require.New(t) 113 | ctx := velvet.NewContext() 114 | data := map[string]string{ 115 | "a": "A", 116 | "b": "B", 117 | } 118 | ctx.Set("letters", data) 119 | input := ` 120 | {{#each letters}} 121 | {{@key}}:{{@value}} 122 | {{/each}} 123 | ` 124 | 125 | s, err := velvet.Render(input, ctx) 126 | r.NoError(err) 127 | for k, v := range data { 128 | r.Contains(s, fmt.Sprintf("%s:%s", k, v)) 129 | } 130 | } 131 | 132 | func Test_Each_Helper_Map_As(t *testing.T) { 133 | r := require.New(t) 134 | ctx := velvet.NewContext() 135 | data := map[string]string{ 136 | "a": "A", 137 | "b": "B", 138 | } 139 | ctx.Set("letters", data) 140 | input := ` 141 | {{#each letters as |k v|}} 142 | {{k}}:{{v}} 143 | {{/each}} 144 | ` 145 | 146 | s, err := velvet.Render(input, ctx) 147 | r.NoError(err) 148 | for k, v := range data { 149 | r.Contains(s, fmt.Sprintf("%s:%s", k, v)) 150 | } 151 | } 152 | 153 | func Test_Each_Helper_Else(t *testing.T) { 154 | r := require.New(t) 155 | ctx := velvet.NewContext() 156 | data := map[string]string{} 157 | ctx.Set("letters", data) 158 | input := ` 159 | {{#each letters as |k v|}} 160 | {{k}}:{{v}} 161 | {{else}} 162 | no letters 163 | {{/each}} 164 | ` 165 | 166 | s, err := velvet.Render(input, ctx) 167 | r.NoError(err) 168 | r.Contains(s, "no letters") 169 | } 170 | 171 | func Test_Each_Helper_Else_Collection(t *testing.T) { 172 | r := require.New(t) 173 | ctx := velvet.NewContext() 174 | data := map[string][]string{} 175 | ctx.Set("collection", data) 176 | 177 | input := ` 178 | {{#each collection.emptykey as |k v|}} 179 | {{k}}:{{v}} 180 | {{else}} 181 | no letters 182 | {{/each}} 183 | ` 184 | 185 | s, err := velvet.Render(input, ctx) 186 | r.NoError(err) 187 | r.Contains(s, "no letters") 188 | } 189 | 190 | func Test_Each_Helper_Else_CollectionMap(t *testing.T) { 191 | r := require.New(t) 192 | ctx := velvet.NewContext() 193 | data := map[string]map[string]string{ 194 | "emptykey": map[string]string{}, 195 | } 196 | 197 | ctx.Set("collection", data) 198 | 199 | input := ` 200 | {{#each collection.emptykey.something as |k v|}} 201 | {{k}}:{{v}} 202 | {{else}} 203 | no letters 204 | {{/each}} 205 | ` 206 | 207 | s, err := velvet.Render(input, ctx) 208 | r.NoError(err) 209 | r.Contains(s, "no letters") 210 | } 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Velvet [![GoDoc](https://godoc.org/github.com/gobuffalo/velvet?status.svg)](https://godoc.org/github.com/gobuffalo/velvet) [![Build Status](https://travis-ci.org/gobuffalo/velvet.svg?branch=master)](https://travis-ci.org/gobuffalo/velvet) [![Code Climate](https://codeclimate.com/github/gobuffalo/velvet/badges/gpa.svg)](https://codeclimate.com/github/gobuffalo/velvet) 2 | 3 | Velvet is a templating package for Go. It bears a striking resemblance to "handlebars" based templates, there are a few small changes/tweaks, that make it slightly different. 4 | 5 | ## General Usage 6 | 7 | If you know handlebars, you basically know how to use Velvet. 8 | 9 | Let's assume you have a template (a string of some kind): 10 | 11 | ```handlebars 12 | 13 |

{{ name }}

14 | 19 | ``` 20 | 21 | Given that string, you can render the template like such: 22 | 23 | ```go 24 | ctx := velvet.NewContext() 25 | ctx.Set("name", "Mark") 26 | ctx.Set("names", []string{"John", "Paul", "George", "Ringo"}) 27 | s, err := velvet.Render(input, ctx) 28 | if err != nil { 29 | // handle errors 30 | } 31 | ``` 32 | Which would result in the following output: 33 | 34 | ```html 35 |

Mark

36 | 42 | ``` 43 | 44 | ## Helpers 45 | 46 | ### If Statements 47 | 48 | What to do? Should you render the content, or not? Using Velvet's built in `if`, `else`, and `unless` helpers, let you figure it out for yourself. 49 | 50 | ```handlebars 51 | {{#if true }} 52 | render this 53 | {{/if}} 54 | ``` 55 | 56 | #### Else Statements 57 | 58 | ```handlebars 59 | {{#if false }} 60 | won't render this 61 | {{ else }} 62 | render this 63 | {{/if}} 64 | ``` 65 | 66 | #### Unless Statements 67 | 68 | ```handlebars 69 | {{#unless true }} 70 | won't render this 71 | {{/unless}} 72 | ``` 73 | 74 | ### Each Statements 75 | 76 | Into everyone's life a little looping must happen. We can't avoid the need to write loops in applications, so Velvet helps you out by coming loaded with an `each` helper to iterate through `arrays`, `slices`, and `maps`. 77 | 78 | #### Arrays 79 | 80 | When looping through `arrays` or `slices`, the block being looped through will be access to the "global" context, as well as have four new variables available within that block: 81 | 82 | * `@first` [`bool`] - is this the first pass through the iteration? 83 | * `@last` [`bool`] - is this the last pass through the iteration? 84 | * `@index` [`int`] - the counter of where in the loop you are, starting with `0`. 85 | * `@value` - the current element in the array or slice that is being iterated over. 86 | 87 | ```handlebars 88 | 93 | ``` 94 | 95 | By using "block parameters" you can change the "key" of the element being accessed from `@value` to a key of your choosing. 96 | 97 | ```handlebars 98 | 103 | ``` 104 | 105 | To change both the key and the index name you can pass two "block parameters"; the first being the new name for the index and the second being the name for the element. 106 | 107 | ```handlebars 108 | 113 | ``` 114 | 115 | #### Maps 116 | 117 | Looping through `maps` using the `each` helper is also supported, and follows very similar guidelines to looping through `arrays`. 118 | 119 | * `@first` [`bool`] - is this the first pass through the iteration? 120 | * `@last` [`bool`] - is this the last pass through the iteration? 121 | * `@key` - the key of the pair being accessed. 122 | * `@value` - the value of the pair being accessed. 123 | 124 | ```handlebars 125 | 130 | ``` 131 | 132 | By using "block parameters" you can change the "key" of the element being accessed from `@value` to a key of your choosing. 133 | 134 | ```handlebars 135 | 140 | ``` 141 | 142 | To change both the key and the value name you can pass two "block parameters"; the first being the new name for the key and the second being the name for the value. 143 | 144 | ```handlebars 145 | 150 | ``` 151 | 152 | ### Other Builtin Helpers 153 | 154 | * `json` - returns a JSON marshaled string of the value passed to it. 155 | * `js_escape` - safely escapes a string to be used in a JavaScript bit of code. 156 | * `html_escape` - safely escapes a string to be used in an HTML bit of code. 157 | * `upcase` - upper cases the entire string passed to it. 158 | * `downcase` - lower cases the entire string passed to it. 159 | * `markdown` - converts markdown to HTML. 160 | * `len` - returns the length of an array or slice 161 | 162 | Velvet also imports all of the helpers found [https://github.com/markbates/inflect/blob/master/helpers.go](https://github.com/markbates/inflect/blob/master/helpers.go) 163 | 164 | ## Custom Helpers 165 | 166 | No templating package would be complete without allowing for you to build your own, custom, helper functions. 167 | 168 | ### Return Values 169 | 170 | The first thing to understand about building custom helper functions is their are a few "valid" return values: 171 | 172 | #### `string` 173 | 174 | Return just a `string`. The `string` will be HTML escaped, and deemed "not"-safe. 175 | 176 | ```go 177 | func() string { 178 | return "" 179 | } 180 | ``` 181 | 182 | #### `string, error` 183 | 184 | Return a `string` and an error. The `string` will be HTML escaped, and deemed "not"-safe. 185 | 186 | ```go 187 | func() (string, error) { 188 | return "", nil 189 | } 190 | ``` 191 | 192 | #### `template.HTML` 193 | 194 | [https://golang.org/pkg/html/template/#HTML](https://golang.org/pkg/html/https://golang.org/pkg/html/template/#HTMLlate/#HTML) 195 | 196 | Return a `template.HTML` string. The `template.HTML` will **not** be HTML escaped, and will be deemed safe. 197 | 198 | ```go 199 | func() template.HTML { 200 | return template.HTML("") 201 | } 202 | ``` 203 | 204 | 205 | #### `template.HTML, error` 206 | 207 | Return a `template.HTML` string and an error. The `template.HTML` will **not** be HTML escaped, and will be deemed safe. 208 | 209 | ```go 210 | func() ( template.HTML, error ) { 211 | return template.HTML(""), error 212 | } 213 | ``` 214 | 215 | ### Input Values 216 | 217 | Custom helper functions can take any type, and any number of arguments. There is an option last argument, [`velvet.HelperContext`](https://godoc.org/github.com/gobuffalo/velvet#HelperContext), that can be received. It's quite useful, and I would recommend taking it, as it provides you access to things like the context of the call, the block associated with the helper, etc... 218 | 219 | ### Registering Helpers 220 | 221 | Custom helpers can be registered in one of two different places; globally and per template. 222 | 223 | #### Global Helpers 224 | 225 | ```go 226 | err := velvet.Helpers.Add("greet", func(name string) string { 227 | return fmt.Sprintf("Hi %s!", name) 228 | }) 229 | if err != nil { 230 | // handle errors 231 | } 232 | ``` 233 | 234 | The `greet` function is now available to all templates that use Velvet. 235 | 236 | ```go 237 | s, err := velvet.Render(`

{{greet "mark"}}

`, velvet.NewContext()) 238 | if err != nil { 239 | // handle errors 240 | } 241 | fmt.Print(s) //

Hi mark!

242 | ``` 243 | 244 | #### Per Template Helpers 245 | 246 | ```go 247 | t, err := velvet.Parse(`

{{greet "mark"}}

`) 248 | if err != nil { 249 | // handle errors 250 | } 251 | t.Helpers.Add("greet", func(name string) string { 252 | return fmt.Sprintf("Hi %s!", name) 253 | }) 254 | if err != nil { 255 | // handle errors 256 | } 257 | ``` 258 | 259 | The `greet` function is now only available to the template it was added to. 260 | 261 | ```go 262 | s, err := t.Exec(velvet.NewContext()) 263 | if err != nil { 264 | // handle errors 265 | } 266 | fmt.Print(s) //

Hi mark!

267 | ``` 268 | 269 | ### Block Helpers 270 | 271 | Like the `if` and `each` helpers, block helpers take a "block" of text that can be evaluated and potentially rendered, manipulated, or whatever you would like. To write a block helper, you have to take the `velvet.HelperContext` as the last argument to your helper function. This will give you access to the block associated with that call. 272 | 273 | #### Example 274 | 275 | ```go 276 | velvet.Helpers.Add("upblock", func(help velvet.HelperContext) (template.HTML, error) { 277 | s, err := help.Block() 278 | if err != nil { 279 | return "", err 280 | } 281 | return strings.ToUpper(s), nil 282 | }) 283 | 284 | s, err := velvet.Render(`{{#upblock}}hi{{/upblock}}`, velvet.NewContext()) 285 | if err != nil { 286 | // handle errors 287 | } 288 | fmt.Print(s) // HI 289 | ``` 290 | 291 | -------------------------------------------------------------------------------- /eval.go: -------------------------------------------------------------------------------- 1 | package velvet 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/aymerick/raymond/ast" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // HTMLer generates HTML source 16 | type HTMLer interface { 17 | HTML() template.HTML 18 | } 19 | 20 | type interfacer interface { 21 | Interface() interface{} 22 | } 23 | 24 | var helperContextKind = "HelperContext" 25 | 26 | type evalVisitor struct { 27 | template *Template 28 | context *Context 29 | curBlock *ast.BlockStatement 30 | blockParams *blockParams 31 | } 32 | 33 | func newEvalVisitor(t *Template, c *Context) *evalVisitor { 34 | return &evalVisitor{ 35 | template: t, 36 | context: c, 37 | blockParams: newBlockParams(), 38 | } 39 | } 40 | 41 | func (ev *evalVisitor) VisitProgram(p *ast.Program) interface{} { 42 | // fmt.Println("VisitProgram") 43 | defer ev.blockParams.pop() 44 | out := &bytes.Buffer{} 45 | ev.blockParams.push(p.BlockParams) 46 | for _, b := range p.Body { 47 | ev.context = ev.context.New() 48 | var value interface{} 49 | value = b.Accept(ev) 50 | switch vp := value.(type) { 51 | case error: 52 | return vp 53 | case template.HTML: 54 | out.Write([]byte(vp)) 55 | case HTMLer: 56 | out.Write([]byte(vp.HTML())) 57 | case string: 58 | out.WriteString(template.HTMLEscapeString(vp)) 59 | case []string: 60 | out.WriteString(template.HTMLEscapeString(strings.Join(vp, " "))) 61 | case int: 62 | out.WriteString(strconv.Itoa(vp)) 63 | case fmt.Stringer: 64 | out.WriteString(template.HTMLEscapeString(vp.String())) 65 | case interfacer: 66 | out.WriteString(template.HTMLEscaper(vp.Interface())) 67 | case nil: 68 | default: 69 | return errors.WithStack(errors.Errorf("unsupport eval return format %T: %+v", value, value)) 70 | } 71 | 72 | } 73 | return out.String() 74 | } 75 | func (ev *evalVisitor) VisitMustache(m *ast.MustacheStatement) interface{} { 76 | // fmt.Println("VisitMustache") 77 | expr := m.Expression.Accept(ev) 78 | return expr 79 | } 80 | func (ev *evalVisitor) VisitBlock(node *ast.BlockStatement) interface{} { 81 | // fmt.Println("VisitBlock") 82 | defer func() { 83 | ev.curBlock = nil 84 | }() 85 | ev.curBlock = node 86 | expr := node.Expression.Accept(ev) 87 | return expr 88 | } 89 | 90 | func (ev *evalVisitor) VisitPartial(*ast.PartialStatement) interface{} { 91 | // fmt.Println("VisitPartial") 92 | return "" 93 | } 94 | 95 | func (ev *evalVisitor) VisitContent(c *ast.ContentStatement) interface{} { 96 | // fmt.Println("VisitContent") 97 | return template.HTML(c.Original) 98 | } 99 | 100 | func (ev *evalVisitor) VisitComment(*ast.CommentStatement) interface{} { 101 | return "" 102 | } 103 | 104 | func (ev *evalVisitor) VisitExpression(e *ast.Expression) interface{} { 105 | // fmt.Println("VisitExpression") 106 | if e.Hash != nil { 107 | e.Hash.Accept(ev) 108 | } 109 | h := ev.helperName(e.HelperName()) 110 | if h != "" { 111 | if helper, ok := ev.template.Helpers.Helpers()[h]; ok { 112 | return ev.evalHelper(e, helper) 113 | } 114 | if ev.context.Has(h) { 115 | x := ev.context.Get(h) 116 | if x != nil && h == "partial" { 117 | return ev.evalHelper(e, x) 118 | } 119 | return x 120 | } 121 | return errors.WithStack(errors.Errorf("could not find value for %s [line %d:%d]", h, e.Line, e.Pos)) 122 | } 123 | parts := strings.Split(e.Canonical(), ".") 124 | if len(parts) > 1 && ev.context.Has(parts[0]) { 125 | rv := reflect.ValueOf(ev.context.Get(parts[0])) 126 | if rv.Kind() == reflect.Ptr { 127 | rv = rv.Elem() 128 | } 129 | m := rv.MethodByName(parts[1]) 130 | if m.IsValid() { 131 | return ev.evalHelper(e, m.Interface()) 132 | } 133 | } 134 | if fp := e.FieldPath(); fp != nil { 135 | return ev.VisitPath(fp) 136 | } 137 | if e.Path != nil { 138 | return e.Path.Accept(ev) 139 | } 140 | return nil 141 | } 142 | 143 | func (ev *evalVisitor) VisitSubExpression(*ast.SubExpression) interface{} { 144 | // fmt.Println("VisitSubExpression") 145 | return nil 146 | } 147 | 148 | func (ev *evalVisitor) VisitPath(node *ast.PathExpression) interface{} { 149 | // fmt.Println("VisitPath") 150 | // fmt.Printf("### node -> %+v\n", node) 151 | // fmt.Printf("### node -> %T\n", node) 152 | // fmt.Printf("### node.IsDataRoot() -> %+v\n", node.IsDataRoot()) 153 | // fmt.Printf("### node.Loc() -> %+v\n", node.Location()) 154 | // fmt.Printf("### node.String() -> %+v\n", node.String()) 155 | // fmt.Printf("### node.Type() -> %+v\n", node.Type()) 156 | // fmt.Printf("### node.Data -> %+v\n", node.Data) 157 | // fmt.Printf("### node.Depth -> %+v\n", node.Depth) 158 | // fmt.Printf("### node.Original -> %+v\n", node.Original) 159 | // fmt.Printf("### node.Parts -> %+v\n", node.Parts) 160 | // fmt.Printf("### node.Scoped -> %+v\n", node.Scoped) 161 | var v interface{} 162 | var h string 163 | if node.Data || len(node.Parts) == 0 { 164 | h = ev.helperName(node.Original) 165 | } else { 166 | h = ev.helperName(node.Parts[0]) 167 | } 168 | if ev.context.Get(h) != nil { 169 | v = ev.context.Get(h) 170 | } 171 | if v == nil { 172 | return "" 173 | // return errors.WithStack(errors.Errorf("could not find value for %s [line %d:%d]", h, node.Line, node.Pos)) 174 | } 175 | 176 | for i := 1; i < len(node.Parts); i++ { 177 | rv := reflect.ValueOf(v) 178 | if rv.Kind() == reflect.Ptr { 179 | rv = rv.Elem() 180 | } 181 | p := node.Parts[i] 182 | m := rv.MethodByName(p) 183 | if m.IsValid() { 184 | 185 | args := []reflect.Value{} 186 | rt := m.Type() 187 | if rt.NumIn() > 0 { 188 | last := rt.In(rt.NumIn() - 1) 189 | if last.Name() == helperContextKind { 190 | hargs := HelperContext{ 191 | Context: ev.context, 192 | Args: []interface{}{}, 193 | evalVisitor: ev, 194 | } 195 | args = append(args, reflect.ValueOf(hargs)) 196 | } else if last.Kind() == reflect.Map { 197 | args = append(args, reflect.ValueOf(ev.context.Options())) 198 | } 199 | if len(args) > rt.NumIn() { 200 | err := errors.Errorf("Incorrect number of arguments being passed to %s (%d for %d)", p, len(args), rt.NumIn()) 201 | return errors.WithStack(err) 202 | } 203 | } 204 | vv := m.Call(args) 205 | 206 | if len(vv) >= 1 { 207 | v = vv[0].Interface() 208 | } 209 | continue 210 | } 211 | switch rv.Kind() { 212 | case reflect.Map: 213 | pv := reflect.ValueOf(p) 214 | keys := rv.MapKeys() 215 | for i := 0; i < len(keys); i++ { 216 | k := keys[i] 217 | if k.Interface() == pv.Interface() { 218 | return rv.MapIndex(k).Interface() 219 | } 220 | } 221 | return errors.WithStack(errors.Errorf("could not find value for %s [line %d:%d]", node.Original, node.Line, node.Pos)) 222 | default: 223 | f := rv.FieldByName(p) 224 | v = f.Interface() 225 | } 226 | } 227 | return v 228 | } 229 | 230 | func (ev *evalVisitor) VisitString(node *ast.StringLiteral) interface{} { 231 | // fmt.Println("VisitString") 232 | return node.Value 233 | } 234 | 235 | func (ev *evalVisitor) VisitBoolean(node *ast.BooleanLiteral) interface{} { 236 | // fmt.Println("VisitBoolean") 237 | return node.Value 238 | } 239 | 240 | func (ev *evalVisitor) VisitNumber(node *ast.NumberLiteral) interface{} { 241 | // fmt.Println("VisitNumber") 242 | return node.Number() 243 | } 244 | 245 | func (ev *evalVisitor) VisitHash(node *ast.Hash) interface{} { 246 | // fmt.Println("VisitHash") 247 | ctx := ev.context.New() 248 | for _, h := range node.Pairs { 249 | val := h.Accept(ev).(map[string]interface{}) 250 | for k, v := range val { 251 | ctx.Set(k, v) 252 | ctx.Options()[k] = v 253 | } 254 | } 255 | ev.context = ctx 256 | return nil 257 | } 258 | 259 | func (ev *evalVisitor) VisitHashPair(node *ast.HashPair) interface{} { 260 | // fmt.Println("VisitHashPair") 261 | return map[string]interface{}{ 262 | node.Key: node.Val.Accept(ev), 263 | } 264 | } 265 | 266 | func (ev *evalVisitor) evalHelper(node *ast.Expression, helper interface{}) (ret interface{}) { 267 | // fmt.Println("evalHelper") 268 | defer func() { 269 | if r := recover(); r != nil { 270 | switch rp := r.(type) { 271 | case error: 272 | ret = errors.WithStack(rp) 273 | case string: 274 | ret = errors.WithStack(errors.New(rp)) 275 | } 276 | } 277 | }() 278 | 279 | hargs := HelperContext{ 280 | Context: ev.context, 281 | Args: []interface{}{}, 282 | evalVisitor: ev, 283 | } 284 | 285 | rv := reflect.ValueOf(helper) 286 | if rv.Kind() == reflect.Ptr { 287 | rv = rv.Elem() 288 | } 289 | rt := rv.Type() 290 | 291 | args := []reflect.Value{} 292 | 293 | if rt.NumIn() > 0 { 294 | for _, p := range node.Params { 295 | v := p.Accept(ev) 296 | vv := reflect.ValueOf(v) 297 | hargs.Args = append(hargs.Args, v) 298 | args = append(args, vv) 299 | } 300 | 301 | last := rt.In(rt.NumIn() - 1) 302 | if last.Name() == helperContextKind { 303 | args = append(args, reflect.ValueOf(hargs)) 304 | } else if last.Kind() == reflect.Map { 305 | if node.Canonical() == "partial" { 306 | args = append(args, reflect.ValueOf(ev.context.export())) 307 | } else { 308 | args = append(args, reflect.ValueOf(ev.context.Options())) 309 | } 310 | } 311 | if len(args) > rt.NumIn() { 312 | err := errors.Errorf("Incorrect number of arguments being passed to %s (%d for %d)", node.Canonical(), len(args), rt.NumIn()) 313 | return errors.WithStack(err) 314 | } 315 | } 316 | vv := rv.Call(args) 317 | 318 | if len(vv) >= 1 { 319 | v := vv[0].Interface() 320 | if len(vv) >= 2 { 321 | if !vv[1].IsNil() { 322 | return errors.WithStack(vv[1].Interface().(error)) 323 | } 324 | } 325 | return v 326 | } 327 | 328 | return "" 329 | } 330 | 331 | func (ev *evalVisitor) helperName(h string) string { 332 | if h != "" { 333 | bp := ev.blockParams.current 334 | if len(bp) == 1 { 335 | if t := ev.context.Get("@value"); t != nil { 336 | ev.context.Set(bp[0], t) 337 | } 338 | } 339 | if len(bp) >= 2 { 340 | if t := ev.context.Get("@value"); t != nil { 341 | ev.context.Set(bp[1], t) 342 | } 343 | for _, k := range []string{"@index", "@key"} { 344 | if t := ev.context.Get(k); t != nil { 345 | ev.context.Set(bp[0], t) 346 | } 347 | } 348 | } 349 | return h 350 | } 351 | return "" 352 | } 353 | --------------------------------------------------------------------------------