mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
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
This commit is contained in:
parent
d2bc64bee3
commit
474eb454df
2 changed files with 236 additions and 53 deletions
|
@ -1392,6 +1392,52 @@ func replace(a, b, c interface{}) (string, error) {
|
||||||
return strings.Replace(aStr, bStr, cStr, -1), nil
|
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.
|
// regexpCache represents a cache of regexp objects protected by a mutex.
|
||||||
type regexpCache struct {
|
type regexpCache struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
@ -1915,59 +1961,60 @@ func init() {
|
||||||
}
|
}
|
||||||
return template.HTML(helpers.AbsURL(s, true)), nil
|
return template.HTML(helpers.AbsURL(s, true)), nil
|
||||||
},
|
},
|
||||||
"add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') },
|
"add": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '+') },
|
||||||
"after": after,
|
"after": after,
|
||||||
"apply": apply,
|
"apply": apply,
|
||||||
"base64Decode": base64Decode,
|
"base64Decode": base64Decode,
|
||||||
"base64Encode": base64Encode,
|
"base64Encode": base64Encode,
|
||||||
"chomp": chomp,
|
"chomp": chomp,
|
||||||
"countrunes": countRunes,
|
"countrunes": countRunes,
|
||||||
"countwords": countWords,
|
"countwords": countWords,
|
||||||
"default": dfault,
|
"default": dfault,
|
||||||
"dateFormat": dateFormat,
|
"dateFormat": dateFormat,
|
||||||
"delimit": delimit,
|
"delimit": delimit,
|
||||||
"dict": dictionary,
|
"dict": dictionary,
|
||||||
"div": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '/') },
|
"div": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '/') },
|
||||||
"echoParam": returnWhenSet,
|
"echoParam": returnWhenSet,
|
||||||
"emojify": emojify,
|
"emojify": emojify,
|
||||||
"eq": eq,
|
"eq": eq,
|
||||||
"findRE": findRE,
|
"findRE": findRE,
|
||||||
"first": first,
|
"first": first,
|
||||||
"ge": ge,
|
"ge": ge,
|
||||||
"getCSV": getCSV,
|
"getCSV": getCSV,
|
||||||
"getJSON": getJSON,
|
"getJSON": getJSON,
|
||||||
"getenv": func(varName string) string { return os.Getenv(varName) },
|
"getenv": func(varName string) string { return os.Getenv(varName) },
|
||||||
"gt": gt,
|
"gt": gt,
|
||||||
"hasPrefix": func(a, b string) bool { return strings.HasPrefix(a, b) },
|
"hasPrefix": func(a, b string) bool { return strings.HasPrefix(a, b) },
|
||||||
"highlight": highlight,
|
"highlight": highlight,
|
||||||
"htmlEscape": htmlEscape,
|
"htmlEscape": htmlEscape,
|
||||||
"htmlUnescape": htmlUnescape,
|
"htmlUnescape": htmlUnescape,
|
||||||
"humanize": humanize,
|
"humanize": humanize,
|
||||||
"in": in,
|
"in": in,
|
||||||
"index": index,
|
"index": index,
|
||||||
"int": func(v interface{}) (int, error) { return cast.ToIntE(v) },
|
"int": func(v interface{}) (int, error) { return cast.ToIntE(v) },
|
||||||
"intersect": intersect,
|
"intersect": intersect,
|
||||||
"isSet": isSet,
|
"isSet": isSet,
|
||||||
"isset": isSet,
|
"isset": isSet,
|
||||||
"jsonify": jsonify,
|
"jsonify": jsonify,
|
||||||
"last": last,
|
"last": last,
|
||||||
"le": le,
|
"le": le,
|
||||||
"lower": func(a string) string { return strings.ToLower(a) },
|
"lower": func(a string) string { return strings.ToLower(a) },
|
||||||
"lt": lt,
|
"lt": lt,
|
||||||
"markdownify": markdownify,
|
"markdownify": markdownify,
|
||||||
"md5": md5,
|
"md5": md5,
|
||||||
"mod": mod,
|
"mod": mod,
|
||||||
"modBool": modBool,
|
"modBool": modBool,
|
||||||
"mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') },
|
"mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') },
|
||||||
"ne": ne,
|
"ne": ne,
|
||||||
"partial": partial,
|
"partial": partial,
|
||||||
"plainify": plainify,
|
"partialCached": partialCached,
|
||||||
"pluralize": pluralize,
|
"plainify": plainify,
|
||||||
"querify": querify,
|
"pluralize": pluralize,
|
||||||
"readDir": readDirFromWorkingDir,
|
"querify": querify,
|
||||||
"readFile": readFileFromWorkingDir,
|
"readDir": readDirFromWorkingDir,
|
||||||
"ref": ref,
|
"readFile": readFileFromWorkingDir,
|
||||||
"relURL": relURL,
|
"ref": ref,
|
||||||
|
"relURL": relURL,
|
||||||
"relLangURL": func(i interface{}) (template.HTML, error) {
|
"relLangURL": func(i interface{}) (template.HTML, error) {
|
||||||
s, err := cast.ToStringE(i)
|
s, err := cast.ToStringE(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue