diff --git a/Makefile b/Makefile index 40d9eb0..95462c5 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ build: test: packr - $(GO_BIN) test -tags ${TAGS} ./... + $(GO_BIN) test -cover -tags ${TAGS} ./... ci-test: $(GO_BIN) test -tags ${TAGS} -race ./... diff --git a/README.md b/README.md index bc8441e..c8442c6 100644 --- a/README.md +++ b/README.md @@ -264,83 +264,7 @@ fmt.Print(s) ## Helpers -### Builtin Helpers - -* `json` - converts the interface to a JSON object -* `jsEscape` - escapes the interface to be JavaScript safe -* `htmlEscape` - escapes the interface to be HTML safe -* `upcase` - converts the string to upper case -* `downcase` - converts the string to lower case -* `contentFor` - stores a block of HTML to be used later -* `contentOf` - retrieves a block of HTML previously stored with `contentFor` -* `markdown` - converts the string from Markdown into HTML -* `len` - returns the length of the interface -* `debug` - returns the `%+v` of the interface wrapped in `
` tags.
-* `inspect` - returns the `%+v` of the interface
-* `range` - interate between, and including two numbers
-* `between` - iterate between, but not including, two numbers
-* `until` - iterate until a number is reached
-* `groupBy` - splits a slice or array into `n` groups
-* `env` - returns the ENV variable for the specified key
-* `truncate` - truncates a string to a specified length
-* `form` - support for the [github.com/gobuffalo/tags/form](https://github.com/gobuffalo/tags/tree/master/form) package (Bootstrap version)
-* `form_for` - support for the [github.com/gobuffalo/tags/form](https://github.com/gobuffalo/tags/tree/master/form) package (Bootstrap version) to build a form for a model
-
-#### contentFor and contentOf
-
-Use the `contentFor` and `contentOf` helpers to dry up your templates with reusable components.
-
-For example, we can define a snippet that generates a fancy title using `contentFor`:
-
-```
-<% contentFor("fancy-title") { %>
-  

<%= title %>

-<% } %> -``` - -The `fancy-title` name is how we will invoke this with `contentOf` elsewhere -in our template: - -``` -<%= contentOf("fancy-title", {"title":"Welcome to Plush"}) %> -``` - -* The second map argument is optional, for static content just use `<%= contentOf("fancy-title") %>` - -Rendering this would generate this output: - -``` -

Welcome to Plush

-``` - -As you can see, the `<%= title %>` has been replaced with the `Welcome to Plush` string. - -#### truncate - -`truncate` takes two optional parameters: -* `size` - the maximum length of the returned string -* `trail` - the string to append at the end of a truncated string, defaults to `...` - -```html -

<%= truncate("a long string", {"size": 10, "trail": "[more]"})%>

-``` - -### From github.com/markbates/inflect - -* `asciffy` -* `camelize` -* `camelize_down_first` -* `capitalize` -* `dasherize` -* `humanize` -* `ordinalize` -* `parameterize` -* `pluralize` -* `pluralize_with_size` -* `singularize` -* `tableize` -* `typeify` -* `underscore` +For a full list, and documentation of, all the Helpers included in Plush, see [`github.com/gobuffalo/helpers`](https://godoc.org/github.com/gobuffalo/helpers). ### Custom Helpers diff --git a/compiler.go b/compiler.go index acc1d50..8d1c8e9 100644 --- a/compiler.go +++ b/compiler.go @@ -8,6 +8,7 @@ import ( "regexp" "time" + "github.com/gobuffalo/helpers/hctx" "github.com/gobuffalo/plush/ast" "github.com/pkg/errors" ) @@ -15,7 +16,7 @@ import ( var ErrUnknownIdentifier = errors.New("unknown identifier") type compiler struct { - ctx *Context + ctx hctx.Context program *ast.Program curStmt ast.Statement } @@ -525,7 +526,8 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er } hc := func(arg reflect.Type) { - if arg.ConvertibleTo(reflect.TypeOf(HelperContext{})) { + hhc := reflect.TypeOf((*hctx.HelperContext)(nil)).Elem() + if arg.ConvertibleTo(reflect.TypeOf(HelperContext{})) || arg.Implements(hhc) { hargs := HelperContext{ Context: c.ctx, compiler: c, @@ -634,14 +636,14 @@ func (c *compiler) evalCallExpression(node *ast.CallExpression) (interface{}, er } func (c *compiler) evalForExpression(node *ast.ForExpression) (interface{}, error) { - octx := c.ctx + octx := c.ctx.(*Context) defer func() { c.ctx = octx }() c.ctx = octx.New() // must copy all data from original (it includes application defined helpers) for k, v := range octx.data { - c.ctx.data[k] = v + c.ctx.Set(k, v) } iter, err := c.evalExpression(node.Iterable) diff --git a/content_helper.go b/content_helper.go deleted file mode 100644 index 49a9ecc..0000000 --- a/content_helper.go +++ /dev/null @@ -1,57 +0,0 @@ -package plush - -import ( - "html/template" - - "github.com/pkg/errors" -) - -// ContentFor stores a block of templating code to be re-used later in the template -// via the contentOf helper. -// An optional map of values can be passed to contentOf, -// which are made available to the contentFor block. -/* - <% contentFor("buttons") { %> - - <% } %> -*/ -func contentForHelper(name string, help HelperContext) { - help.Set("contentFor:"+name, func(data map[string]interface{}) (template.HTML, error) { - ctx := help.New() - for k, v := range data { - ctx.Set(k, v) - } - body, err := help.BlockWith(ctx) - if err != nil { - return "", err - } - return template.HTML(body), nil - }) -} - -// ContentOf retrieves a stored block for templating and renders it. -// You can pass an optional map of fields that will be set. -/* - <%= contentOf("buttons") %> - <%= contentOf("buttons", {"label": "Click me"}) %> -*/ -func contentOfHelper(name string, data map[string]interface{}, help HelperContext) (template.HTML, error) { - fn, ok := help.Value("contentFor:" + name).(func(data map[string]interface{}) (template.HTML, error)) - if !ok { - if !help.HasBlock() { - return template.HTML(""), errors.New("missing contentOf block: " + name) - } - - ctx := help.New() - for k, v := range data { - ctx.Set(k, v) - } - body, err := help.BlockWith(ctx) - if err != nil { - return template.HTML(""), err - } - - return template.HTML(body), nil - } - return fn(data) -} diff --git a/content_helper_test.go b/content_helper_test.go deleted file mode 100644 index f9702d5..0000000 --- a/content_helper_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package plush - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func Test_ContentForOf(t *testing.T) { - r := require.New(t) - input := ` - <% contentFor("buttons") { %><% } %> - <%= contentOf("buttons") %> - <%= contentOf("buttons") %> - ` - s, err := Render(input, NewContext()) - r.NoError(err) - r.Contains(s, "") - r.Contains(s, "") - r.Contains(s, "") -} - -func Test_ContentForOfWithData(t *testing.T) { - r := require.New(t) - input := ` - <% contentFor("buttons") { %><% } %> - <%= contentOf("buttons", {"label": "Button One"}) %> - <%= contentOf("buttons", {"label": "Button Two"}) %> - <%= label %> - ` - ctx := NewContext() - ctx.Set("label", "Outer label") - s, err := Render(input, ctx) - r.NoError(err) - r.Contains(s, "") - r.Contains(s, "") - r.Contains(s, "") - r.Contains(s, "Outer label", "the outer label shouldn't be affected by the map passed in") -} - -func Test_ContentForOf_MissingBlock(t *testing.T) { - r := require.New(t) - input := ` - <%= contentOf("buttons") %> - <%= contentOf("buttons") %> - ` - _, err := Render(input, NewContext()) - r.EqualError(err, "line 2: missing contentOf block: buttons") -} - -func Test_ContentForOf_MissingBlock_DefaultBlock(t *testing.T) { - r := require.New(t) - input := ` - <%= contentOf("my-block") { %>default<% } %> - ` - s, err := Render(input, NewContext()) - r.NoError(err) - r.Contains(s, "default") -} - -func Test_ContentForOf_MissingBlock_NoBlockContent(t *testing.T) { - r := require.New(t) - input := ` - <%= contentOf("buttons") %> - ` - _, err := Render(input, NewContext()) - r.EqualError(err, "line 2: missing contentOf block: buttons") -} - -func Test_ContentForOf_DefaultBlock(t *testing.T) { - r := require.New(t) - input := ` - <% contentFor("buttons") { %>custom<% } %> - <%= contentOf("buttons") { %>default<% } %> - ` - s, err := Render(input, NewContext()) - r.NoError(err) - r.Contains(s, "custom") -} diff --git a/context.go b/context.go index 84ac11f..740617e 100644 --- a/context.go +++ b/context.go @@ -3,6 +3,8 @@ package plush import ( "context" "sync" + + "github.com/gobuffalo/helpers/hctx" ) var _ context.Context = &Context{} @@ -18,7 +20,7 @@ type Context struct { // New context containing the current context. Values set on the new context // will not be set onto the original context, however, the original context's // values will be available to the new context. -func (c *Context) New() *Context { +func (c *Context) New() hctx.Context { cc := NewContext() cc.outer = c return cc diff --git a/forms.go b/forms.go index 9e64fec..bc2207b 100644 --- a/forms.go +++ b/forms.go @@ -1,70 +1,18 @@ package plush import ( - "fmt" - "html/template" - - "github.com/gobuffalo/tags" - "github.com/gobuffalo/tags/form" - "github.com/gobuffalo/tags/form/bootstrap" + "github.com/gobuffalo/helpers/forms" + "github.com/gobuffalo/helpers/forms/bootstrap" ) -// FormHelper implements a Plush helper around the -// form.New function in the github.com/gobuffalo/tags/form package -func FormHelper(opts tags.Options, help HelperContext) (template.HTML, error) { - return helper(opts, help, func(opts tags.Options) helperable { - return form.New(opts) - }) -} - -// FormForHelper implements a Plush helper around the -// form.NewFormFor function in the github.com/gobuffalo/tags/form package -func FormForHelper(model interface{}, opts tags.Options, help HelperContext) (template.HTML, error) { - return helper(opts, help, func(opts tags.Options) helperable { - return form.NewFormFor(model, opts) - }) -} - -// BootstrapFormHelper implements a Plush helper around the -// bootstrap.New function in the github.com/gobuffalo/tags/form/bootstrap package -func BootstrapFormHelper(opts tags.Options, help HelperContext) (template.HTML, error) { - return helper(opts, help, func(opts tags.Options) helperable { - return bootstrap.New(opts) - }) -} +// FormHelper is deprecated use github.com/gobuffalo/helpers/forms#Form instead. +var FormHelper = forms.Form -// BootstrapFormForHelper implements a Plush helper around the -// bootstrap.NewFormFor function in the github.com/gobuffalo/tags/form/bootstrap package -func BootstrapFormForHelper(model interface{}, opts tags.Options, help HelperContext) (template.HTML, error) { - return helper(opts, help, func(opts tags.Options) helperable { - return bootstrap.NewFormFor(model, opts) - }) -} +// FormForHelper is deprecated use github.com/gobuffalo/helpers/forms#FormFor instead. +var FormForHelper = forms.FormFor -type helperable interface { - SetAuthenticityToken(string) - Append(...tags.Body) - HTMLer -} +// BootstrapFormHelper is deprecated use github.com/gobuffalo/helpers/forms/bootstrap#Form instead. +var BootstrapFormHelper = bootstrap.Form -func helper(opts tags.Options, help HelperContext, fn func(opts tags.Options) helperable) (template.HTML, error) { - hn := "f" - if n, ok := opts["var"]; ok { - hn = n.(string) - delete(opts, "var") - } - if opts["errors"] == nil && help.Context.Value("errors") != nil { - opts["errors"] = help.Context.Value("errors") - } - form := fn(opts) - if help.Value("authenticity_token") != nil && opts["method"] != "GET" { - form.SetAuthenticityToken(fmt.Sprint(help.Value("authenticity_token"))) - } - help.Context.Set(hn, form) - s, err := help.Block() - if err != nil { - return "", err - } - form.Append(s) - return form.HTML(), nil -} +// BootstrapFormForHelper is deprecated use github.com/gobuffalo/helpers/forms/bootstrap#FormFor instead. +var BootstrapFormForHelper = bootstrap.FormFor diff --git a/helpers.go b/helpers.go index 906b25b..6a9e97e 100644 --- a/helpers.go +++ b/helpers.go @@ -2,17 +2,15 @@ package plush import ( "bytes" - "encoding/json" "fmt" - "html/template" - "reflect" - "strings" "sync" + "github.com/gobuffalo/helpers" + "github.com/gobuffalo/helpers/forms" + "github.com/gobuffalo/helpers/forms/bootstrap" + "github.com/gobuffalo/helpers/hctx" "github.com/gobuffalo/plush/ast" - "github.com/gobuffalo/envy" - "github.com/markbates/inflect" "github.com/pkg/errors" ) @@ -24,39 +22,21 @@ var Helpers = HelperMap{ } func init() { - Helpers.Add("json", toJSONHelper) - Helpers.Add("jsEscape", template.JSEscapeString) - Helpers.Add("htmlEscape", htmlEscape) - Helpers.Add("upcase", strings.ToUpper) - Helpers.Add("downcase", strings.ToLower) - Helpers.Add("contentFor", contentForHelper) - Helpers.Add("contentOf", contentOfHelper) - Helpers.Add("markdown", MarkdownHelper) - Helpers.Add("len", lenHelper) - Helpers.Add("debug", debugHelper) - Helpers.Add("inspect", inspectHelper) - Helpers.Add("range", rangeHelper) - Helpers.Add("between", betweenHelper) - Helpers.Add("until", untilHelper) - Helpers.Add("groupBy", groupByHelper) - Helpers.Add("form", BootstrapFormHelper) - Helpers.Add("form_for", BootstrapFormForHelper) - Helpers.Add("truncate", truncateHelper) - Helpers.Add("env", envy.MustGet) - Helpers.Add("envOr", envy.Get) Helpers.Add("partial", partialHelper) - Helpers.Add("raw", func(s string) template.HTML { - return template.HTML(s) - }) - Helpers.AddMany(inflect.Helpers) + Helpers.AddMany(helpers.ALL()) + Helpers.Add(forms.FormKey, bootstrap.Form) + Helpers.Add(forms.FormForKey, bootstrap.FormFor) + Helpers.Add("form_for", bootstrap.FormFor) } +var _ hctx.HelperContext = &HelperContext{} + // HelperContext is an optional last argument to helpers // that provides the current context of the call, and access // to an optional "block" of code that can be executed from // within the helper. type HelperContext struct { - *Context + hctx.Context compiler *compiler block *ast.BlockStatement } @@ -83,7 +63,11 @@ func (h HelperContext) Block() (string, error) { // BlockWith executes the block of template associated with // the helper, think the block inside of an "if" or "each" // statement, but with it's own context. -func (h HelperContext) BlockWith(ctx *Context) (string, error) { +func (h HelperContext) BlockWith(hc hctx.Context) (string, error) { + ctx, ok := hc.(*Context) + if !ok { + return "", fmt.Errorf("expected *Context, got %T", hc) + } octx := h.compiler.ctx defer func() { h.compiler.ctx = octx }() h.compiler.ctx = ctx @@ -99,58 +83,3 @@ func (h HelperContext) BlockWith(ctx *Context) (string, error) { h.compiler.write(bb, i) return bb.String(), nil } - -// toJSONHelper converts an interface into a string. -func toJSONHelper(v interface{}) (template.HTML, error) { - b, err := json.Marshal(v) - if err != nil { - return "", err - } - return template.HTML(b), nil -} - -func lenHelper(v interface{}) int { - rv := reflect.ValueOf(v) - if rv.Kind() == reflect.Ptr { - rv = rv.Elem() - } - return rv.Len() -} - -// Debug by verbosely printing out using 'pre' tags. -func debugHelper(v interface{}) template.HTML { - return template.HTML(fmt.Sprintf("
%s
", inspectHelper(v))) -} - -func inspectHelper(v interface{}) string { - return fmt.Sprintf("%+v", v) -} - -func htmlEscape(s string, help HelperContext) (string, error) { - var err error - if help.HasBlock() { - s, err = help.Block() - } - if err != nil { - return "", err - } - return template.HTMLEscapeString(s), nil -} - -func truncateHelper(s string, opts map[string]interface{}) string { - if opts["size"] == nil { - opts["size"] = 50 - } - if opts["trail"] == nil { - opts["trail"] = "..." - } - size := opts["size"].(int) - if len(s) <= size { - return s - } - trail := opts["trail"].(string) - if len(trail) >= size { - return trail - } - return s[:size-len(trail)] + trail -} diff --git a/helpers_test.go b/helpers_test.go index aab47fc..80c87a4 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -3,7 +3,6 @@ package plush import ( "testing" - "github.com/gobuffalo/envy" "github.com/stretchr/testify/require" ) @@ -36,91 +35,3 @@ func Test_Helpers_WithoutData(t *testing.T) { } } - -func Test_truncateHelper(t *testing.T) { - r := require.New(t) - x := "KEuFHyyImKUMhSkSolLqgqevKQNZUjpSZokrGbZqnUrUnWrTDwi" - s := truncateHelper(x, map[string]interface{}{}) - r.Len(s, 50) - r.Equal("...", s[47:]) - - s = truncateHelper(x, map[string]interface{}{ - "size": 10, - }) - r.Len(s, 10) - r.Equal("...", s[7:]) - - s = truncateHelper(x, map[string]interface{}{ - "size": 10, - "trail": "more", - }) - r.Len(s, 10) - r.Equal("more", s[6:]) - - // Case size < len(trail) - s = truncateHelper(x, map[string]interface{}{ - "size": 3, - "trail": "more", - }) - r.Len(s, 4) - r.Equal("more", s) -} - -func Test_inspectHelper(t *testing.T) { - r := require.New(t) - s := struct { - Name string - }{"Ringo"} - - o := inspectHelper(s) - r.Contains(o, "Ringo") -} - -func Test_env(t *testing.T) { - envy.Temp(func() { - r := require.New(t) - envy.Set("testKey", "test value") - input := `<%= env("testKey") %>` - - ctx := NewContext() - s, err := Render(input, ctx) - - r.NoError(err) - r.Equal("test value", s) - }) -} - -func Test_envMissing(t *testing.T) { - r := require.New(t) - input := `<%= env("testKey") %>` - - ctx := NewContext() - _, err := Render(input, ctx) - - r.Error(err) -} - -func Test_envOrHelper(t *testing.T) { - envy.Temp(func() { - r := require.New(t) - envy.Set("testKey", "test value") - input := `<%= envOr("testKey", "") %>` - - ctx := NewContext() - s, err := Render(input, ctx) - - r.NoError(err) - r.Equal("test value", s) - }) -} - -func Test_envOrHelperDefault(t *testing.T) { - r := require.New(t) - input := `<%= envOr("testKey", "default") %>` - - ctx := NewContext() - s, err := Render(input, ctx) - - r.NoError(err) - r.Equal("default", s) -} diff --git a/partial_helper.go b/partial_helper.go index c2f956b..8c72991 100644 --- a/partial_helper.go +++ b/partial_helper.go @@ -13,7 +13,7 @@ import ( type PartialFeeder func(string) (string, error) func partialHelper(name string, data map[string]interface{}, help HelperContext) (template.HTML, error) { - if help.Context == nil || help.Context.data == nil { + if help.Context == nil { return "", errors.New("invalid context. abort") } diff --git a/partial_helper_test.go b/partial_helper_test.go index 7161a5e..7191f3d 100644 --- a/partial_helper_test.go +++ b/partial_helper_test.go @@ -21,20 +21,6 @@ func Test_PartialHelper_Nil_Context(t *testing.T) { r.Equal("", string(html)) } -func Test_PartialHelper_Blank_Data(t *testing.T) { - r := require.New(t) - - name := "index" - data := map[string]interface{}{} - help := HelperContext{Context: NewContext()} - help.Context.data = nil - - html, err := partialHelper(name, data, help) - r.Error(err) - r.Contains(err.Error(), "invalid context") - r.Equal("", string(html)) -} - func Test_PartialHelper_Blank_Context(t *testing.T) { r := require.New(t) diff --git a/plush.go b/plush.go index f478231..c579b66 100644 --- a/plush.go +++ b/plush.go @@ -6,6 +6,8 @@ import ( "io" "io/ioutil" "sync" + + "github.com/gobuffalo/helpers/hctx" ) // DefaultTimeFormat is the default way of formatting a time.Time type. @@ -59,7 +61,7 @@ func Parse(input string) (*Template, error) { } // Render a string using the given the context. -func Render(input string, ctx *Context) (string, error) { +func Render(input string, ctx hctx.Context) (string, error) { t, err := Parse(input) if err != nil { return "", err @@ -67,7 +69,7 @@ func Render(input string, ctx *Context) (string, error) { return t.Exec(ctx) } -func RenderR(input io.Reader, ctx *Context) (string, error) { +func RenderR(input io.Reader, ctx hctx.Context) (string, error) { b, err := ioutil.ReadAll(input) if err != nil { return "", err @@ -76,7 +78,7 @@ func RenderR(input io.Reader, ctx *Context) (string, error) { } // RunScript allows for "pure" plush scripts to be executed. -func RunScript(input string, ctx *Context) error { +func RunScript(input string, ctx hctx.Context) error { input = "<% " + input + "%>" ctx = ctx.New() diff --git a/template.go b/template.go index 431db4a..ebf0fdf 100644 --- a/template.go +++ b/template.go @@ -1,6 +1,7 @@ package plush import ( + "github.com/gobuffalo/helpers/hctx" "github.com/gobuffalo/plush/ast" "github.com/gobuffalo/plush/parser" @@ -43,7 +44,7 @@ func (t *Template) Parse() error { } // Exec the template using the content and return the results -func (t *Template) Exec(ctx *Context) (string, error) { +func (t *Template) Exec(ctx hctx.Context) (string, error) { err := t.Parse() if err != nil { return "", err