diff --git a/README.md b/README.md index d9a6d9a..4563e9e 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ func main() { } // parse template - tpl, err := raymond.Parse(source) + tpl, err := raymond.Parse(source, nil) if err != nil { panic(err) } @@ -179,7 +179,7 @@ You can use `MustParse()` and `MustExec()` functions if you don't want to deal w ```go // parse template -tpl := raymond.MustParse(source) +tpl := raymond.MustParse(source, nil) // render template result := tpl.MustExec(ctx) @@ -281,7 +281,7 @@ ctx := map[string]string{ "body": "
This is a post about <p> tags
", } -tpl := raymond.MustParse(source) +tpl := raymond.MustParse(source, nil) result := tpl.MustExec(ctx) fmt.Print(result) @@ -305,7 +305,7 @@ raymond.RegisterHelper("link", func(url, text string) raymond.SafeString { return raymond.SafeString("" + raymond.Escape(text) + "") }) -tpl := raymond.MustParse("{{link url text}}") +tpl := raymond.MustParse("{{link url text}}", nil) ctx := map[string]string{ "url": "http://www.aymerick.com/", @@ -322,6 +322,29 @@ Output: This is a <em>cool</em> website ``` +### Custom Escaping + +If you are rendering something which is not HTML, the default escaping may not be what you want. In that case, you may specify your own escaper: + +```go +type NoopEscaper struct {} +func (n NoopEscaper) Escape(s string) string { + return s +} + + +opts := &raymond.TemplateOptions{ + Escaper: &NoopEscaper{}, +} +tpl := raymond.MustParse("Hey {{ text }}", opts) + +ctx := map[string]string{ + "text": "<<< cool >>>", +} + +result := tpl.MustExec(ctx) +fmt.Print(result) // would print "Hey <<< cool >>>" +``` ## Helpers @@ -433,7 +456,7 @@ RegisterHelper("fullName", func(person Person) string { You can register a helper on a specific template, and in that case that helper will be available to that template only: ```go -tpl := raymond.MustParse("User: {{fullName user.firstName user.lastName}}") +tpl := raymond.MustParse("User: {{fullName user.firstName user.lastName}}", nil) tpl.RegisterHelper("fullName", func(firstName, lastName string) string { return firstName + " " + lastName @@ -1116,7 +1139,7 @@ Those context functions behave like helper functions: they can be called with pa You can register template partials before execution: ```go -tpl := raymond.MustParse("{{> foo}} baz") +tpl := raymond.MustParse("{{> foo}} baz", nil) tpl.RegisterPartial("foo", "bar") result := tpl.MustExec(nil) @@ -1132,7 +1155,7 @@ Output: You can register several partials at once: ```go -tpl := raymond.MustParse("{{> foo}} and {{> baz}}") +tpl := raymond.MustParse("{{> foo}} and {{> baz}}", nil) tpl.RegisterPartials(map[string]string{ "foo": "bar", "baz": "bat", @@ -1156,7 +1179,7 @@ You can registers global partials that will be accessible by all templates: ```go raymond.RegisterPartial("foo", "bar") -tpl := raymond.MustParse("{{> foo}} baz") +tpl := raymond.MustParse("{{> foo}} baz", nil) result := tpl.MustExec(nil) fmt.Print(result) ``` @@ -1169,7 +1192,7 @@ raymond.RegisterPartials(map[string]string{ "baz": "bat", }) -tpl := raymond.MustParse("{{> foo}} and {{> baz}}") +tpl := raymond.MustParse("{{> foo}} and {{> baz}}", nil) result := tpl.MustExec(nil) fmt.Print(result) ``` @@ -1182,7 +1205,7 @@ It's possible to dynamically select the partial to be executed by using sub expr For example, that template randomly evaluates the `foo` or `baz` partial: ```go -tpl := raymond.MustParse("{{> (whichPartial) }}") +tpl := raymond.MustParse("{{> (whichPartial) }}", nil) tpl.RegisterPartials(map[string]string{ "foo": "bar", "baz": "bat", @@ -1209,7 +1232,7 @@ It's possible to execute partials on a custom context by passing in the context For example: ```go -tpl := raymond.MustParse("User: {{> userDetails user }}") +tpl := raymond.MustParse("User: {{> userDetails user }}", nil) tpl.RegisterPartial("userDetails", "{{firstname}} {{lastname}}") ctx := map[string]interface{}{ @@ -1237,7 +1260,7 @@ Custom data can be passed to partials through hash parameters. For example: ```go -tpl := raymond.MustParse("{{> myPartial name=hero }}") +tpl := raymond.MustParse("{{> myPartial name=hero }}", nil) tpl.RegisterPartial("myPartial", "My hero is {{name}}") ctx := map[string]interface{}{ diff --git a/base_test.go b/base_test.go index b769331..5716931 100644 --- a/base_test.go +++ b/base_test.go @@ -25,7 +25,7 @@ func launchTests(t *testing.T, tests []Test) { var tpl *Template // parse template - tpl, err = Parse(test.input) + tpl, err = Parse(test.input, nil) if err != nil { t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err) } else { @@ -91,7 +91,7 @@ func launchErrorTests(t *testing.T, tests []Test) { var tpl *Template // parse template - tpl, err = Parse(test.input) + tpl, err = Parse(test.input, nil) if err != nil { t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err) } else { diff --git a/benchmark_test.go b/benchmark_test.go index f9ea74c..aa01e72 100644 --- a/benchmark_test.go +++ b/benchmark_test.go @@ -16,7 +16,7 @@ func BenchmarkArguments(b *testing.B) { "bar": true, } - tpl := MustParse(source) + tpl := MustParse(source, nil) tpl.RegisterHelper("foo", func(a, b, c, d interface{}) string { return "" }) b.ResetTimer() @@ -37,7 +37,7 @@ func BenchmarkArrayEach(b *testing.B) { }, } - tpl := MustParse(source) + tpl := MustParse(source, nil) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -57,7 +57,7 @@ func BenchmarkArrayMustache(b *testing.B) { }, } - tpl := MustParse(source) + tpl := MustParse(source, nil) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -92,7 +92,7 @@ func BenchmarkComplex(b *testing.B) { }, } - tpl := MustParse(source) + tpl := MustParse(source, nil) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -112,7 +112,7 @@ func BenchmarkData(b *testing.B) { }, } - tpl := MustParse(source) + tpl := MustParse(source, nil) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -133,7 +133,7 @@ func BenchmarkDepth1(b *testing.B) { "foo": "bar", } - tpl := MustParse(source) + tpl := MustParse(source, nil) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -154,7 +154,7 @@ func BenchmarkDepth2(b *testing.B) { "foo": "bar", } - tpl := MustParse(source) + tpl := MustParse(source, nil) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -172,7 +172,7 @@ func BenchmarkObjectMustache(b *testing.B) { }, } - tpl := MustParse(source) + tpl := MustParse(source, nil) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -190,7 +190,7 @@ func BenchmarkObject(b *testing.B) { }, } - tpl := MustParse(source) + tpl := MustParse(source, nil) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -216,9 +216,9 @@ func BenchmarkPartialRecursion(b *testing.B) { }, } - tpl := MustParse(source) + tpl := MustParse(source, nil) - partial := MustParse(`{{name}}{{#each kids}}{{>recursion}}{{/each}}`) + partial := MustParse(`{{name}}{{#each kids}}{{>recursion}}{{/each}}`, nil) tpl.RegisterPartialTemplate("recursion", partial) b.ResetTimer() @@ -238,9 +238,9 @@ func BenchmarkPartial(b *testing.B) { }, } - tpl := MustParse(source) + tpl := MustParse(source, nil) - partial := MustParse(`Hello {{name}}! You have {{count}} new messages.`) + partial := MustParse(`Hello {{name}}! You have {{count}} new messages.`, nil) tpl.RegisterPartialTemplate("variables", partial) b.ResetTimer() @@ -263,7 +263,7 @@ func BenchmarkPath(b *testing.B) { }, } - tpl := MustParse(source) + tpl := MustParse(source, nil) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -274,7 +274,7 @@ func BenchmarkPath(b *testing.B) { func BenchmarkString(b *testing.B) { source := `Hello world` - tpl := MustParse(source) + tpl := MustParse(source, nil) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -287,7 +287,7 @@ func BenchmarkSubExpression(b *testing.B) { ctx := map[string]interface{}{} - tpl := MustParse(source) + tpl := MustParse(source, nil) tpl.RegisterHelpers(map[string]interface{}{ "echo": func(v string) string { return "foo " + v }, "header": func() string { return "Colors" }, @@ -307,7 +307,7 @@ func BenchmarkVariables(b *testing.B) { "count": 30, } - tpl := MustParse(source) + tpl := MustParse(source, nil) b.ResetTimer() for i := 0; i < b.N; i++ { diff --git a/escape.go b/escape.go index 6a0363c..c078de7 100644 --- a/escape.go +++ b/escape.go @@ -52,10 +52,18 @@ func escape(w writer, s string) error { return err } -// Escape escapes special HTML characters. -// -// It can be used by helpers that return a SafeString and that need to escape some content by themselves. -func Escape(s string) string { +type escapeFunc func(string) string + +// Escaper defines an object which can be used to escape strings. +type Escaper interface { + Escape(string) string +} + +// HTMLEscaper is an Escaper which escapes HTML-unsafe characters in strings. +type HTMLEscaper struct{} + +// Escape HTML characters in a string +func (h HTMLEscaper) Escape(s string) string { if strings.IndexAny(s, escapedChars) == -1 { return s } @@ -63,3 +71,10 @@ func Escape(s string) string { escape(&buf, s) return buf.String() } + +var defaultEscaper = HTMLEscaper{} + +// Escape escapes special HTML characters. +// +// It can be used by helpers that return a SafeString and that need to escape some content by themselves. +var Escape = HTMLEscaper.Escape diff --git a/escape_test.go b/escape_test.go index b77bb09..85c476f 100644 --- a/escape_test.go +++ b/escape_test.go @@ -1,12 +1,17 @@ package raymond -import "fmt" +import ( + "strings" + "testing" +) -func ExampleEscape() { - tpl := MustParse("{{link url text}}") +func TestDefaultEscape(t *testing.T) { + expected := "This is a <em>cool</em> website" + tpl := MustParse("{{link url text}}", nil) + esc := &HTMLEscaper{} tpl.RegisterHelper("link", func(url string, text string) SafeString { - return SafeString("" + Escape(text) + "") + return SafeString("" + esc.Escape(text) + "") }) ctx := map[string]string{ @@ -15,6 +20,30 @@ func ExampleEscape() { } result := tpl.MustExec(ctx) - fmt.Print(result) - // Output: This is a <em>cool</em> website + if result != expected { + t.Errorf("expected %s, got %s", expected, result) + } +} + +type TestEscaper struct{} + +func (t TestEscaper) Escape(s string) string { + return strings.Replace(s, "em", "EM", -1) +} + +func TestCustomEscape(t *testing.T) { + expected := "This is a cool website" + opts := &TemplateOptions{ + Escaper: &TestEscaper{}, + } + tpl := MustParse("{{ text }}", opts) + + ctx := map[string]string{ + "text": "This is a cool website", + } + + result := tpl.MustExec(ctx) + if result != expected { + t.Errorf("expected %s, got %s", expected, result) + } } diff --git a/eval.go b/eval.go index 7683f4e..bdadf6d 100644 --- a/eval.go +++ b/eval.go @@ -712,7 +712,7 @@ func (v *evalVisitor) partialContext(node *ast.PartialStatement) reflect.Value { // evalPartial evaluates a partial func (v *evalVisitor) evalPartial(p *partial, node *ast.PartialStatement) string { // get partial template - partialTpl, err := p.template() + partialTpl, err := p.template(v.tpl.opts) if err != nil { v.errPanic(err) } @@ -804,7 +804,7 @@ func (v *evalVisitor) VisitMustache(node *ast.MustacheStatement) interface{} { str := Str(expr) if !isSafe && !node.Unescaped { // escape html - str = Escape(str) + str = v.tpl.escape(str) } return str diff --git a/handlebars/base_test.go b/handlebars/base_test.go index 254b1a9..c0199fd 100644 --- a/handlebars/base_test.go +++ b/handlebars/base_test.go @@ -41,7 +41,7 @@ func launchTests(t *testing.T, tests []Test) { } // parse template - tpl, err = raymond.Parse(test.input) + tpl, err = raymond.Parse(test.input, nil) if err != nil { t.Errorf("Test '%s' failed - Failed to parse template\ninput:\n\t'%s'\nerror:\n\t%s", test.name, test.input, err) } else { diff --git a/handlebars/basic_test.go b/handlebars/basic_test.go index 6325341..8894e77 100644 --- a/handlebars/basic_test.go +++ b/handlebars/basic_test.go @@ -633,7 +633,7 @@ func TestBasicErrors(t *testing.T) { expectedError := regexp.QuoteMeta("Invalid path: text/this") for _, input := range inputs { - _, err = raymond.Parse(input) + _, err = raymond.Parse(input, nil) if err == nil { t.Errorf("Test failed - Error expected") } diff --git a/partial.go b/partial.go index 3299d02..c621905 100644 --- a/partial.go +++ b/partial.go @@ -71,11 +71,11 @@ func findPartial(name string) *partial { } // template returns parsed partial template -func (p *partial) template() (*Template, error) { +func (p *partial) template(opts *TemplateOptions) (*Template, error) { if p.tpl == nil { var err error - p.tpl, err = Parse(p.source) + p.tpl, err = Parse(p.source, opts) if err != nil { return nil, err } diff --git a/raymond.go b/raymond.go index c6df6b3..565799a 100644 --- a/raymond.go +++ b/raymond.go @@ -6,7 +6,7 @@ package raymond // Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead. func Render(source string, ctx interface{}) (string, error) { // parse template - tpl, err := Parse(source) + tpl, err := Parse(source, nil) if err != nil { return "", err } @@ -24,5 +24,5 @@ func Render(source string, ctx interface{}) (string, error) { // // Note that this function call is not optimal as your template is parsed everytime you call it. You should use Parse() function instead. func MustRender(source string, ctx interface{}) string { - return MustParse(source).MustExec(ctx) + return MustParse(source, nil).MustExec(ctx) } diff --git a/raymond_test.go b/raymond_test.go index ab8609b..8f30403 100644 --- a/raymond_test.go +++ b/raymond_test.go @@ -11,7 +11,7 @@ func Example() { } // parse template - tpl := MustParse(source) + tpl := MustParse(source, nil) // evaluate template with context output := tpl.MustExec(ctx) diff --git a/string_test.go b/string_test.go index eaec630..07b78a3 100644 --- a/string_test.go +++ b/string_test.go @@ -51,7 +51,7 @@ func ExampleSafeString() { return SafeString("FOO BAR") }) - tpl := MustParse("{{em}}") + tpl := MustParse("{{em}}", nil) result := tpl.MustExec(nil) fmt.Print(result) diff --git a/template.go b/template.go index f16ed2f..2fcb70d 100644 --- a/template.go +++ b/template.go @@ -11,6 +11,11 @@ import ( "github.com/aymerick/raymond/parser" ) +// TemplateOptions specifies options for rendering a template. +type TemplateOptions struct { + Escaper Escaper +} + // Template represents a handlebars template. type Template struct { source string @@ -18,20 +23,32 @@ type Template struct { helpers map[string]reflect.Value partials map[string]*partial mutex sync.RWMutex // protects helpers and partials + opts *TemplateOptions + escape escapeFunc } // newTemplate instanciate a new template without parsing it -func newTemplate(source string) *Template { +func newTemplate(source string, opts *TemplateOptions) *Template { + if opts == nil { + opts = &TemplateOptions{} + } + + if opts.Escaper == nil { + opts.Escaper = defaultEscaper + } + return &Template{ source: source, helpers: make(map[string]reflect.Value), partials: make(map[string]*partial), + opts: opts, + escape: opts.Escaper.Escape, } } // Parse instanciates a template by parsing given source. -func Parse(source string) (*Template, error) { - tpl := newTemplate(source) +func Parse(source string, opts *TemplateOptions) (*Template, error) { + tpl := newTemplate(source, opts) // parse template if err := tpl.parse(); err != nil { @@ -42,8 +59,8 @@ func Parse(source string) (*Template, error) { } // MustParse instanciates a template by parsing given source. It panics on error. -func MustParse(source string) *Template { - result, err := Parse(source) +func MustParse(source string, opts *TemplateOptions) *Template { + result, err := Parse(source, opts) if err != nil { panic(err) } @@ -51,13 +68,13 @@ func MustParse(source string) *Template { } // ParseFile reads given file and returns parsed template. -func ParseFile(filePath string) (*Template, error) { +func ParseFile(filePath string, opts *TemplateOptions) (*Template, error) { b, err := ioutil.ReadFile(filePath) if err != nil { return nil, err } - return Parse(string(b)) + return Parse(string(b), opts) } // parse parses the template @@ -78,7 +95,7 @@ func (tpl *Template) parse() error { // Clone returns a copy of that template. func (tpl *Template) Clone() *Template { - result := newTemplate(tpl.source) + result := newTemplate(tpl.source, tpl.opts) result.program = tpl.program diff --git a/template_test.go b/template_test.go index c194b28..36c9772 100644 --- a/template_test.go +++ b/template_test.go @@ -27,7 +27,7 @@ CONTENT[ ' func TestNewTemplate(t *testing.T) { t.Parallel() - tpl := newTemplate(sourceBasic) + tpl := newTemplate(sourceBasic, nil) if tpl.source != sourceBasic { t.Errorf("Failed to instantiate template") } @@ -36,7 +36,7 @@ func TestNewTemplate(t *testing.T) { func TestParse(t *testing.T) { t.Parallel() - tpl, err := Parse(sourceBasic) + tpl, err := Parse(sourceBasic, nil) if err != nil || (tpl.source != sourceBasic) { t.Errorf("Failed to parse template") } @@ -52,7 +52,7 @@ func TestClone(t *testing.T) { sourcePartial := `I am a {{wat}} partial` sourcePartial2 := `Partial for the {{wat}}` - tpl := MustParse(sourceBasic) + tpl := MustParse(sourceBasic, nil) tpl.RegisterPartial("p", sourcePartial) if (len(tpl.partials) != 1) || (tpl.partials["p"] == nil) { @@ -85,7 +85,7 @@ func ExampleTemplate_Exec() { } // parse template - tpl := MustParse(source) + tpl := MustParse(source, nil) // evaluate template with context output, err := tpl.Exec(ctx) @@ -106,7 +106,7 @@ func ExampleTemplate_MustExec() { } // parse template - tpl := MustParse(source) + tpl := MustParse(source, nil) // evaluate template with context output := tpl.MustExec(ctx) @@ -124,7 +124,7 @@ func ExampleTemplate_ExecWith() { } // parse template - tpl := MustParse(source) + tpl := MustParse(source, nil) // computes private data frame frame := NewDataFrame() @@ -144,7 +144,7 @@ func ExampleTemplate_PrintAST() { source := "{{#body}}{{content}} and {{@baz.bat}}{{/body}}
" // parse template - tpl := MustParse(source) + tpl := MustParse(source, nil) // print AST output := tpl.PrintAST()