shithub: hugo

Download patch

ref: 474eb454dfb6e150d0a7a79edf32906f7a2355d8
parent: d2bc64bee3190d5f1850bec45bf7f51375f13c79
author: Cameron Moore <[email protected]>
date: Mon Oct 10 13:03:30 EDT 2016

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

--- a/tpl/template_funcs.go
+++ b/tpl/template_funcs.go
@@ -1392,6 +1392,52 @@
 	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 @@
 			}
 			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 {
--- a/tpl/template_funcs_test.go
+++ b/tpl/template_funcs_test.go
@@ -2471,3 +2471,139 @@
 		}
 	}
 }
+
+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()
+	}
+}