Add a handlebars template renderer

pull/6/head
Mickaël Menu 3 years ago
parent 3bfc0754c7
commit 76ac1d1bb1
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -0,0 +1 @@
Salut, <{{name}}>!

@ -0,0 +1,78 @@
package handlebars
import (
"html"
"io/ioutil"
"path/filepath"
"github.com/aymerick/raymond"
"github.com/mickael-menu/zk/util/errors"
)
// HandlebarsRenderer holds parsed handlebars template and renders them.
type HandlebarsRenderer struct {
templates map[string]*raymond.Template
}
// NewRenderer creates a new instance of HandlebarsRenderer.
func NewRenderer() *HandlebarsRenderer {
return &HandlebarsRenderer{
templates: make(map[string]*raymond.Template),
}
}
// Render renders a handlebars string template with the given context.
func (hr *HandlebarsRenderer) Render(template string, context interface{}) (string, error) {
template = html.EscapeString(template)
res, err := raymond.Render(template, context)
if err != nil {
return "", errors.Wrap(err, "render template failed")
}
return html.UnescapeString(res), nil
}
// RenderFile renders a handlebars template file with the given context.
func (hr *HandlebarsRenderer) RenderFile(path string, context interface{}) (string, error) {
wrap := errors.Wrapper("render template failed")
templ, err := hr.loadFileTemplate(path)
if err != nil {
return "", wrap(err)
}
res, err := templ.Exec(context)
if err != nil {
return "", wrap(err)
}
return html.UnescapeString(res), nil
}
// LoadFileTemplate loads the template at given path into the renderer if needed.
// Returns the parsed template.
func (hr *HandlebarsRenderer) loadFileTemplate(path string) (*raymond.Template, error) {
wrap := errors.Wrapperf("load template file failed: %v", path)
path, err := filepath.Abs(path)
if err != nil {
return nil, wrap(err)
}
// Already loaded?
templ, ok := hr.templates[path]
if ok {
return templ, nil
}
// Load new template.
bytes, err := ioutil.ReadFile(path)
if err != nil {
return nil, wrap(err)
}
templ, err = raymond.Parse(html.EscapeString(string(bytes)))
if err != nil {
return nil, wrap(err)
}
hr.templates[path] = templ
return templ, nil
}

@ -0,0 +1,41 @@
package handlebars
import (
"testing"
"github.com/mickael-menu/zk/util/assert"
"github.com/mickael-menu/zk/util/fixtures"
)
func TestRenderString(t *testing.T) {
sut := NewRenderer()
res, err := sut.Render("Goodbye, {{name}}", map[string]string{"name": "Ed"})
assert.Nil(t, err)
assert.Equal(t, res, "Goodbye, Ed")
}
func TestRenderFile(t *testing.T) {
sut := NewRenderer()
res, err := sut.RenderFile(fixtures.Path("template.txt"), map[string]string{"name": "Thom"})
assert.Nil(t, err)
assert.Equal(t, res, "Hello, Thom\n")
}
func TestUnknownVariable(t *testing.T) {
sut := NewRenderer()
res, err := sut.Render("Hi, {{unknown}}!", nil)
assert.Nil(t, err)
assert.Equal(t, res, "Hi, !")
}
func TestDoesntEscapeHTML(t *testing.T) {
sut := NewRenderer()
res, err := sut.Render("Salut, <{{name}}>!", map[string]string{"name": "l'ami"})
assert.Nil(t, err)
assert.Equal(t, res, "Salut, <l'ami>!")
res, err = sut.RenderFile(fixtures.Path("unescape.txt"), map[string]string{"name": "l'ami"})
assert.Nil(t, err)
assert.Equal(t, res, "Salut, <l'ami>!\n")
}

@ -4,6 +4,8 @@ go 1.15
require (
github.com/alecthomas/kong v0.2.12
github.com/aymerick/raymond v2.0.2+incompatible
github.com/google/go-cmp v0.3.1
github.com/hashicorp/hcl/v2 v2.8.1
gopkg.in/yaml.v2 v2.4.0 // indirect
)

@ -7,25 +7,34 @@ github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/apparentlymart/go-textseg/v12 v12.0.0 h1:bNEQyAGak9tojivJNkoqWErVCQbjdL7GzRt3F8NvfJ0=
github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0=
github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl/v2 v2.8.1 h1:FJ60CIYaMyJOKzPndhMyjiz353Fd+2jr6PodF5Xzb08=
github.com/hashicorp/hcl/v2 v2.8.1/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/zclconf/go-cty v1.2.0 h1:sPHsy7ADcIZQP3vILvTjrh74ZA175TFP5vqiNK1UmlI=
@ -33,6 +42,7 @@ github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -43,4 +53,8 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

@ -7,13 +7,13 @@ import (
func Nil(t *testing.T, value interface{}) {
if !isNil(value) {
t.Errorf("Expected %v (type %v) to be nil", value, reflect.TypeOf(value))
t.Errorf("Expected `%v` (type %v) to be nil", value, reflect.TypeOf(value))
}
}
func NotNil(t *testing.T, value interface{}) {
if isNil(value) {
t.Errorf("Expected %v (type %v) to not be nil", value, reflect.TypeOf(value))
t.Errorf("Expected `%v` (type %v) to not be nil", value, reflect.TypeOf(value))
}
}
@ -24,6 +24,6 @@ func isNil(value interface{}) bool {
func Equal(t *testing.T, actual, expected interface{}) {
if !reflect.DeepEqual(actual, expected) {
t.Errorf("Received %+v (type %v), expected %+v (type %v)", actual, reflect.TypeOf(actual), expected, reflect.TypeOf(expected))
t.Errorf("Received `%+v` (type %v), expected `%+v` (type %v)", actual, reflect.TypeOf(actual), expected, reflect.TypeOf(expected))
}
}

@ -4,12 +4,20 @@ import (
"fmt"
)
func Wrapperf(format string, args ...interface{}) func(error) error {
return Wrapper(fmt.Sprintf(format, args...))
}
func Wrapper(msg string) func(error) error {
return func(err error) error {
return Wrap(err, msg)
}
}
func Wrapf(err error, format string, args ...interface{}) error {
return Wrap(err, fmt.Sprintf(format, args...))
}
func Wrap(err error, msg string) error {
if err == nil {
return nil

@ -0,0 +1,15 @@
package fixtures
import (
"path/filepath"
"runtime"
)
// Path returns the absolute path to the given fixture.
func Path(name string) string {
_, callerPath, _, ok := runtime.Caller(1)
if !ok {
panic("failed to get the caller's path")
}
return filepath.Join(filepath.Dir(callerPath), "fixtures", name)
}

@ -21,7 +21,7 @@ var (
type Case int
const (
LowerCase Case = iota
LowerCase Case = iota + 1
UpperCase
MixedCase
)

Loading…
Cancel
Save