diff --git a/template/template.go b/template/template.go index 3295544a..73035647 100644 --- a/template/template.go +++ b/template/template.go @@ -168,11 +168,14 @@ func (t *Template) ExecuteHTMLString(html string, data interface{}) (string, err type FuncMap map[string]interface{} var DefaultFuncs = FuncMap{ - "toUpper": strings.ToUpper, - "toLower": strings.ToLower, + "toUpper": strings.ToUpper, + "toLower": strings.ToLower, + "title": func(text string) string { + // casers should not be shared between goroutines, instead + // create a new caser each time this function is called + return cases.Title(language.AmericanEnglish).String(text) + }, "trimSpace": strings.TrimSpace, - - "title": cases.Title(language.AmericanEnglish).String, // join is equal to strings.Join but inverts the argument order // for easier pipelining in templates. "join": func(sep string, s []string) string { diff --git a/template/template_test.go b/template/template_test.go index c28fa592..97d36fb1 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -16,6 +16,7 @@ package template import ( tmplhtml "html/template" "net/url" + "sync" "testing" tmpltext "text/template" "time" @@ -465,3 +466,60 @@ func TestTemplateExpansionWithOptions(t *testing.T) { }) } } + +// This test asserts that template functions are thread-safe. +func TestTemplateFuncs(t *testing.T) { + tmpl, err := FromGlobs([]string{}) + require.NoError(t, err) + + for _, tc := range []struct { + title string + in string + data interface{} + exp string + }{{ + title: "Template using toUpper", + in: `{{ "abc" | toUpper }}`, + exp: "ABC", + }, { + title: "Template using toLower", + in: `{{ "ABC" | toLower }}`, + exp: "abc", + }, { + title: "Template using title", + in: `{{ "abc" | title }}`, + exp: "Abc", + }, { + title: "Template using trimSpace", + in: `{{ " abc " | trimSpace }}`, + exp: "abc", + }, { + title: "Template using join", + in: `{{ . | join "," }}`, + data: []string{"abc", "def"}, + exp: "abc,def", + }, { + title: "Template using match", + in: `{{ match "[a-z]+" "abc" }}`, + exp: "true", + }, { + title: "Template using reReplaceAll", + in: `{{ reReplaceAll "ab" "AB" "abc" }}`, + exp: "ABc", + }} { + tc := tc + t.Run(tc.title, func(t *testing.T) { + wg := sync.WaitGroup{} + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + got, err := tmpl.ExecuteTextString(tc.in, tc.data) + require.NoError(t, err) + require.Equal(t, tc.exp, got) + }() + } + wg.Wait() + }) + } +}