├── .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"}}hi {{/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, "hi ")
20 | r.Contains(s, "hi ")
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 | hi
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 [](https://godoc.org/github.com/gobuffalo/velvet) [](https://travis-ci.org/gobuffalo/velvet) [](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 |
15 | {{#each names}}
16 | {{ @value }}
17 | {{/each}}
18 |
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 |
37 | John
38 | Paul
39 | George
40 | Ringo
41 |
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 |
89 | {{#each names}}
90 | {{ @index }} - {{ @value }}
91 | {{/each}}
92 |
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 |
99 | {{#each names as |name|}}
100 | {{ name }}
101 | {{/each}}
102 |
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 |
109 | {{#each names as |index, name|}}
110 | {{ index }} - {{ name }}
111 | {{/each}}
112 |
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 |
126 | {{#each users}}
127 | {{ @key }} - {{ @value }}
128 | {{/each}}
129 |
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 |
136 | {{#each users as |user|}}
137 | {{ @key }} - {{ user }}
138 | {{/each}}
139 |
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 |
146 | {{#each users as |key, user|}}
147 | {{ key }} - {{ user }}
148 | {{/each}}
149 |
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 |
--------------------------------------------------------------------------------