Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/gohugoio/hugo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/tpl
diff options
context:
space:
mode:
authorCameron Moore <moorereason@gmail.com>2016-10-11 01:03:30 +0300
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2016-10-12 00:56:06 +0300
commit474eb454dfb6e150d0a7a79edf32906f7a2355d8 (patch)
treec3961719193210b0dae400804a1b65edb7077e5f /tpl
parentd2bc64bee3190d5f1850bec45bf7f51375f13c79 (diff)
tpl: Add partialCached template function
Supports an optional variant string parameter so that a given partial will be cached based upon the name+variant. Fixes #1368 Closes #2552
Diffstat (limited to 'tpl')
-rw-r--r--tpl/template_funcs.go153
-rw-r--r--tpl/template_funcs_test.go136
2 files changed, 236 insertions, 53 deletions
diff --git a/tpl/template_funcs.go b/tpl/template_funcs.go
index 099f9d74e..e820ccf8d 100644
--- a/tpl/template_funcs.go
+++ b/tpl/template_funcs.go
@@ -1392,6 +1392,52 @@ func replace(a, b, c interface{}) (string, error) {
return strings.Replace(aStr, bStr, cStr, -1), nil
}
+// partialCache represents a cache of partials protected by a mutex.
+type partialCache struct {
+ sync.RWMutex
+ p map[string]template.HTML
+}
+
+// Get retrieves partial output from the cache based upon the partial name.
+// If the partial is not found in the cache, the partial is rendered and added
+// to the cache.
+func (c *partialCache) Get(key, name string, context interface{}) (p template.HTML) {
+ var ok bool
+
+ c.RLock()
+ p, ok = c.p[key]
+ c.RUnlock()
+
+ if ok {
+ return p
+ }
+
+ c.Lock()
+ if p, ok = c.p[key]; !ok {
+ p = partial(name, context)
+ c.p[key] = p
+ }
+ c.Unlock()
+
+ return p
+}
+
+var cachedPartials = partialCache{p: make(map[string]template.HTML)}
+
+// partialCached executes and caches partial templates. An optional variant
+// string parameter (a string slice actually, but be only use a variadic
+// argument to make it optional) can be passed so that a given partial can have
+// multiple uses. The cache is created with name+variant as the key.
+func partialCached(name string, context interface{}, variant ...string) template.HTML {
+ key := name
+ if len(variant) > 0 {
+ for i := 0; i < len(variant); i++ {
+ key += variant[i]
+ }
+ }
+ return cachedPartials.Get(key, name, context)
+}
+
// regexpCache represents a cache of regexp objects protected by a mutex.
type regexpCache struct {
mu sync.RWMutex
@@ -1915,59 +1961,60 @@ func init() {
}
return template.HTML(helpers.AbsURL(s, true)), nil
},
- "add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') },
- "after": after,
- "apply": apply,
- "base64Decode": base64Decode,
- "base64Encode": base64Encode,
- "chomp": chomp,
- "countrunes": countRunes,
- "countwords": countWords,
- "default": dfault,
- "dateFormat": dateFormat,
- "delimit": delimit,
- "dict": dictionary,
- "div": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '/') },
- "echoParam": returnWhenSet,
- "emojify": emojify,
- "eq": eq,
- "findRE": findRE,
- "first": first,
- "ge": ge,
- "getCSV": getCSV,
- "getJSON": getJSON,
- "getenv": func(varName string) string { return os.Getenv(varName) },
- "gt": gt,
- "hasPrefix": func(a, b string) bool { return strings.HasPrefix(a, b) },
- "highlight": highlight,
- "htmlEscape": htmlEscape,
- "htmlUnescape": htmlUnescape,
- "humanize": humanize,
- "in": in,
- "index": index,
- "int": func(v interface{}) (int, error) { return cast.ToIntE(v) },
- "intersect": intersect,
- "isSet": isSet,
- "isset": isSet,
- "jsonify": jsonify,
- "last": last,
- "le": le,
- "lower": func(a string) string { return strings.ToLower(a) },
- "lt": lt,
- "markdownify": markdownify,
- "md5": md5,
- "mod": mod,
- "modBool": modBool,
- "mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') },
- "ne": ne,
- "partial": partial,
- "plainify": plainify,
- "pluralize": pluralize,
- "querify": querify,
- "readDir": readDirFromWorkingDir,
- "readFile": readFileFromWorkingDir,
- "ref": ref,
- "relURL": relURL,
+ "add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') },
+ "after": after,
+ "apply": apply,
+ "base64Decode": base64Decode,
+ "base64Encode": base64Encode,
+ "chomp": chomp,
+ "countrunes": countRunes,
+ "countwords": countWords,
+ "default": dfault,
+ "dateFormat": dateFormat,
+ "delimit": delimit,
+ "dict": dictionary,
+ "div": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '/') },
+ "echoParam": returnWhenSet,
+ "emojify": emojify,
+ "eq": eq,
+ "findRE": findRE,
+ "first": first,
+ "ge": ge,
+ "getCSV": getCSV,
+ "getJSON": getJSON,
+ "getenv": func(varName string) string { return os.Getenv(varName) },
+ "gt": gt,
+ "hasPrefix": func(a, b string) bool { return strings.HasPrefix(a, b) },
+ "highlight": highlight,
+ "htmlEscape": htmlEscape,
+ "htmlUnescape": htmlUnescape,
+ "humanize": humanize,
+ "in": in,
+ "index": index,
+ "int": func(v interface{}) (int, error) { return cast.ToIntE(v) },
+ "intersect": intersect,
+ "isSet": isSet,
+ "isset": isSet,
+ "jsonify": jsonify,
+ "last": last,
+ "le": le,
+ "lower": func(a string) string { return strings.ToLower(a) },
+ "lt": lt,
+ "markdownify": markdownify,
+ "md5": md5,
+ "mod": mod,
+ "modBool": modBool,
+ "mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') },
+ "ne": ne,
+ "partial": partial,
+ "partialCached": partialCached,
+ "plainify": plainify,
+ "pluralize": pluralize,
+ "querify": querify,
+ "readDir": readDirFromWorkingDir,
+ "readFile": readFileFromWorkingDir,
+ "ref": ref,
+ "relURL": relURL,
"relLangURL": func(i interface{}) (template.HTML, error) {
s, err := cast.ToStringE(i)
if err != nil {
diff --git a/tpl/template_funcs_test.go b/tpl/template_funcs_test.go
index 2ace8d257..7f46aeba9 100644
--- a/tpl/template_funcs_test.go
+++ b/tpl/template_funcs_test.go
@@ -2471,3 +2471,139 @@ func TestReadFile(t *testing.T) {
}
}
}
+
+func TestPartialCached(t *testing.T) {
+ testCases := []struct {
+ name string
+ partial string
+ tmpl string
+ variant string
+ }{
+ // name and partial should match between test cases.
+ {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . }}`, ""},
+ {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "header"},
+ {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "footer"},
+ {"test1", "{{ .Title }} seq: {{ shuffle (seq 1 20) }}", `{{ partialCached "test1" . "%s" }}`, "header"},
+ }
+
+ results := make(map[string]string, len(testCases))
+
+ var data struct {
+ Title string
+ Section string
+ Params map[string]interface{}
+ }
+
+ data.Title = "**BatMan**"
+ data.Section = "blog"
+ data.Params = map[string]interface{}{"langCode": "en"}
+
+ InitializeT()
+ for i, tc := range testCases {
+ var tmp string
+ if tc.variant != "" {
+ tmp = fmt.Sprintf(tc.tmpl, tc.variant)
+ } else {
+ tmp = tc.tmpl
+ }
+
+ tmpl, err := New().New("testroot").Parse(tmp)
+ if err != nil {
+ t.Fatalf("[%d] unable to create new html template: %s", i, err)
+ }
+
+ if tmpl == nil {
+ t.Fatalf("[%d] tmpl should not be nil!", i)
+ }
+
+ tmpl.New("partials/" + tc.name).Parse(tc.partial)
+
+ buf := new(bytes.Buffer)
+ err = tmpl.Execute(buf, &data)
+ if err != nil {
+ t.Fatalf("[%d] error executing template: %s", i, err)
+ }
+
+ for j := 0; j < 10; j++ {
+ buf2 := new(bytes.Buffer)
+ err = tmpl.Execute(buf2, nil)
+ if err != nil {
+ t.Fatalf("[%d] error executing template 2nd time: %s", i, err)
+ }
+
+ if !reflect.DeepEqual(buf, buf2) {
+ t.Fatalf("[%d] cached results do not match:\nResult 1:\n%q\nResult 2:\n%q", i, buf, buf2)
+ }
+ }
+
+ // double-check against previous test cases of the same variant
+ previous, ok := results[tc.name+tc.variant]
+ if !ok {
+ results[tc.name+tc.variant] = buf.String()
+ } else {
+ if previous != buf.String() {
+ t.Errorf("[%d] cached variant differs from previous rendering; got:\n%q\nwant:\n%q", i, buf.String(), previous)
+ }
+ }
+ }
+}
+
+func BenchmarkPartial(b *testing.B) {
+ InitializeT()
+ tmpl, err := New().New("testroot").Parse(`{{ partial "bench1" . }}`)
+ if err != nil {
+ b.Fatalf("unable to create new html template: %s", err)
+ }
+
+ tmpl.New("partials/bench1").Parse(`{{ shuffle (seq 1 10) }}`)
+ buf := new(bytes.Buffer)
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ if err = tmpl.Execute(buf, nil); err != nil {
+ b.Fatalf("error executing template: %s", err)
+ }
+ buf.Reset()
+ }
+}
+
+func BenchmarkPartialCached(b *testing.B) {
+ InitializeT()
+ tmpl, err := New().New("testroot").Parse(`{{ partialCached "bench1" . }}`)
+ if err != nil {
+ b.Fatalf("unable to create new html template: %s", err)
+ }
+
+ tmpl.New("partials/bench1").Parse(`{{ shuffle (seq 1 10) }}`)
+ buf := new(bytes.Buffer)
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ if err = tmpl.Execute(buf, nil); err != nil {
+ b.Fatalf("error executing template: %s", err)
+ }
+ buf.Reset()
+ }
+}
+
+func BenchmarkPartialCachedVariants(b *testing.B) {
+ InitializeT()
+ tmpl, err := New().New("testroot").Parse(`{{ partialCached "bench1" . "header" }}`)
+ if err != nil {
+ b.Fatalf("unable to create new html template: %s", err)
+ }
+
+ tmpl.New("partials/bench1").Parse(`{{ shuffle (seq 1 10) }}`)
+ buf := new(bytes.Buffer)
+
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ if err = tmpl.Execute(buf, nil); err != nil {
+ b.Fatalf("error executing template: %s", err)
+ }
+ buf.Reset()
+ }
+}