mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-29 02:02:09 -05:00
c6d650c8c8
This more or less completes the simplification of the template handling code in Hugo started in v0.62. The main motivation was to fix a long lasting issue about a crash in HTML content files without front matter. But this commit also comes with a big functional improvement. As we now have moved the base template evaluation to the build stage we now use the same lookup rules for `baseof` as for `list` etc. type of templates. This means that in this simple example you can have a `baseof` template for the `blog` section without having to duplicate the others: ``` layouts ├── _default │ ├── baseof.html │ ├── list.html │ └── single.html └── blog └── baseof.html ``` Also, when simplifying code, you often get rid of some double work, as shown in the "site building" benchmarks below. These benchmarks looks suspiciously good, but I have repeated the below with ca. the same result. Compared to master: ``` name old time/op new time/op delta SiteNew/Bundle_with_image-16 13.1ms ± 1% 10.5ms ± 1% -19.34% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file-16 13.0ms ± 0% 10.7ms ± 1% -18.05% (p=0.029 n=4+4) SiteNew/Tags_and_categories-16 46.4ms ± 2% 43.1ms ± 1% -7.15% (p=0.029 n=4+4) SiteNew/Canonify_URLs-16 52.2ms ± 2% 47.8ms ± 1% -8.30% (p=0.029 n=4+4) SiteNew/Deep_content_tree-16 77.9ms ± 1% 70.9ms ± 1% -9.01% (p=0.029 n=4+4) SiteNew/Many_HTML_templates-16 43.0ms ± 0% 37.2ms ± 1% -13.54% (p=0.029 n=4+4) SiteNew/Page_collections-16 58.2ms ± 1% 52.4ms ± 1% -9.95% (p=0.029 n=4+4) name old alloc/op new alloc/op delta SiteNew/Bundle_with_image-16 3.81MB ± 0% 2.22MB ± 0% -41.70% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file-16 3.60MB ± 0% 2.01MB ± 0% -44.20% (p=0.029 n=4+4) SiteNew/Tags_and_categories-16 19.3MB ± 1% 14.1MB ± 0% -26.91% (p=0.029 n=4+4) SiteNew/Canonify_URLs-16 70.7MB ± 0% 69.0MB ± 0% -2.40% (p=0.029 n=4+4) SiteNew/Deep_content_tree-16 37.1MB ± 0% 31.2MB ± 0% -15.94% (p=0.029 n=4+4) SiteNew/Many_HTML_templates-16 17.6MB ± 0% 10.6MB ± 0% -39.92% (p=0.029 n=4+4) SiteNew/Page_collections-16 25.9MB ± 0% 21.2MB ± 0% -17.99% (p=0.029 n=4+4) name old allocs/op new allocs/op delta SiteNew/Bundle_with_image-16 52.3k ± 0% 26.1k ± 0% -50.18% (p=0.029 n=4+4) SiteNew/Bundle_with_JSON_file-16 52.3k ± 0% 26.1k ± 0% -50.16% (p=0.029 n=4+4) SiteNew/Tags_and_categories-16 336k ± 1% 269k ± 0% -19.90% (p=0.029 n=4+4) SiteNew/Canonify_URLs-16 422k ± 0% 395k ± 0% -6.43% (p=0.029 n=4+4) SiteNew/Deep_content_tree-16 401k ± 0% 313k ± 0% -21.79% (p=0.029 n=4+4) SiteNew/Many_HTML_templates-16 247k ± 0% 143k ± 0% -42.17% (p=0.029 n=4+4) SiteNew/Page_collections-16 282k ± 0% 207k ± 0% -26.55% (p=0.029 n=4+4) ``` Fixes #6716 Fixes #6760 Fixes #6768 Fixes #6778
280 lines
6.3 KiB
Go
280 lines
6.3 KiB
Go
// Copyright 2017-present The Hugo Authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package output
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/gohugoio/hugo/helpers"
|
|
)
|
|
|
|
// These may be used as content sections with potential conflicts. Avoid that.
|
|
var reservedSections = map[string]bool{
|
|
"shortcodes": true,
|
|
"partials": true,
|
|
}
|
|
|
|
// LayoutDescriptor describes how a layout should be chosen. This is
|
|
// typically built from a Page.
|
|
type LayoutDescriptor struct {
|
|
Type string
|
|
Section string
|
|
Kind string
|
|
Lang string
|
|
Layout string
|
|
// LayoutOverride indicates what we should only look for the above layout.
|
|
LayoutOverride bool
|
|
|
|
RenderingHook bool
|
|
Baseof bool
|
|
}
|
|
|
|
func (d LayoutDescriptor) isList() bool {
|
|
return !d.RenderingHook && d.Kind != "page"
|
|
}
|
|
|
|
// LayoutHandler calculates the layout template to use to render a given output type.
|
|
type LayoutHandler struct {
|
|
mu sync.RWMutex
|
|
cache map[layoutCacheKey][]string
|
|
}
|
|
|
|
type layoutCacheKey struct {
|
|
d LayoutDescriptor
|
|
f string
|
|
}
|
|
|
|
// NewLayoutHandler creates a new LayoutHandler.
|
|
func NewLayoutHandler() *LayoutHandler {
|
|
return &LayoutHandler{cache: make(map[layoutCacheKey][]string)}
|
|
}
|
|
|
|
// For returns a layout for the given LayoutDescriptor and options.
|
|
// Layouts are rendered and cached internally.
|
|
func (l *LayoutHandler) For(d LayoutDescriptor, f Format) ([]string, error) {
|
|
|
|
// We will get lots of requests for the same layouts, so avoid recalculations.
|
|
key := layoutCacheKey{d, f.Name}
|
|
l.mu.RLock()
|
|
if cacheVal, found := l.cache[key]; found {
|
|
l.mu.RUnlock()
|
|
return cacheVal, nil
|
|
}
|
|
l.mu.RUnlock()
|
|
|
|
layouts := resolvePageTemplate(d, f)
|
|
|
|
layouts = helpers.UniqueStringsReuse(layouts)
|
|
|
|
l.mu.Lock()
|
|
l.cache[key] = layouts
|
|
l.mu.Unlock()
|
|
|
|
return layouts, nil
|
|
}
|
|
|
|
type layoutBuilder struct {
|
|
layoutVariations []string
|
|
typeVariations []string
|
|
d LayoutDescriptor
|
|
f Format
|
|
}
|
|
|
|
func (l *layoutBuilder) addLayoutVariations(vars ...string) {
|
|
for _, layoutVar := range vars {
|
|
if l.d.Baseof && layoutVar != "baseof" {
|
|
l.layoutVariations = append(l.layoutVariations, layoutVar+"-baseof")
|
|
continue
|
|
}
|
|
if !l.d.RenderingHook && !l.d.Baseof && l.d.LayoutOverride && layoutVar != l.d.Layout {
|
|
continue
|
|
}
|
|
l.layoutVariations = append(l.layoutVariations, layoutVar)
|
|
}
|
|
}
|
|
|
|
func (l *layoutBuilder) addTypeVariations(vars ...string) {
|
|
for _, typeVar := range vars {
|
|
if !reservedSections[typeVar] {
|
|
if l.d.RenderingHook {
|
|
typeVar = typeVar + renderingHookRoot
|
|
}
|
|
l.typeVariations = append(l.typeVariations, typeVar)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *layoutBuilder) addSectionType() {
|
|
if l.d.Section != "" {
|
|
l.addTypeVariations(l.d.Section)
|
|
}
|
|
}
|
|
|
|
func (l *layoutBuilder) addKind() {
|
|
l.addLayoutVariations(l.d.Kind)
|
|
l.addTypeVariations(l.d.Kind)
|
|
}
|
|
|
|
const renderingHookRoot = "/_markup"
|
|
|
|
func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
|
|
|
|
b := &layoutBuilder{d: d, f: f}
|
|
|
|
if d.RenderingHook {
|
|
b.addLayoutVariations(d.Kind)
|
|
} else {
|
|
if d.Layout != "" {
|
|
b.addLayoutVariations(d.Layout)
|
|
}
|
|
if d.Type != "" {
|
|
b.addTypeVariations(d.Type)
|
|
}
|
|
}
|
|
|
|
switch d.Kind {
|
|
case "page":
|
|
b.addLayoutVariations("single")
|
|
b.addSectionType()
|
|
case "home":
|
|
b.addLayoutVariations("index", "home")
|
|
// Also look in the root
|
|
b.addTypeVariations("")
|
|
case "section":
|
|
if d.Section != "" {
|
|
b.addLayoutVariations(d.Section)
|
|
}
|
|
b.addSectionType()
|
|
b.addKind()
|
|
case "taxonomy":
|
|
if d.Section != "" {
|
|
b.addLayoutVariations(d.Section)
|
|
}
|
|
b.addKind()
|
|
b.addSectionType()
|
|
|
|
case "taxonomyTerm":
|
|
if d.Section != "" {
|
|
b.addLayoutVariations(d.Section + ".terms")
|
|
}
|
|
b.addTypeVariations("taxonomy")
|
|
b.addSectionType()
|
|
b.addLayoutVariations("terms")
|
|
|
|
}
|
|
|
|
isRSS := f.Name == RSSFormat.Name
|
|
if !d.RenderingHook && !d.Baseof && isRSS {
|
|
// The historic and common rss.xml case
|
|
b.addLayoutVariations("")
|
|
}
|
|
|
|
// All have _default in their lookup path
|
|
b.addTypeVariations("_default")
|
|
|
|
if d.isList() {
|
|
// Add the common list type
|
|
b.addLayoutVariations("list")
|
|
}
|
|
|
|
if d.Baseof {
|
|
b.addLayoutVariations("baseof")
|
|
}
|
|
|
|
layouts := b.resolveVariations()
|
|
|
|
if !d.RenderingHook && !d.Baseof && isRSS {
|
|
layouts = append(layouts, "_internal/_default/rss.xml")
|
|
}
|
|
|
|
return layouts
|
|
|
|
}
|
|
|
|
func (l *layoutBuilder) resolveVariations() []string {
|
|
|
|
var layouts []string
|
|
|
|
var variations []string
|
|
name := strings.ToLower(l.f.Name)
|
|
|
|
if l.d.Lang != "" {
|
|
// We prefer the most specific type before language.
|
|
variations = append(variations, []string{fmt.Sprintf("%s.%s", l.d.Lang, name), name, l.d.Lang}...)
|
|
} else {
|
|
variations = append(variations, name)
|
|
}
|
|
|
|
variations = append(variations, "")
|
|
|
|
for _, typeVar := range l.typeVariations {
|
|
for _, variation := range variations {
|
|
for _, layoutVar := range l.layoutVariations {
|
|
if variation == "" && layoutVar == "" {
|
|
continue
|
|
}
|
|
template := layoutTemplate(typeVar, layoutVar)
|
|
layouts = append(layouts, replaceKeyValues(template,
|
|
"TYPE", typeVar,
|
|
"LAYOUT", layoutVar,
|
|
"VARIATIONS", variation,
|
|
"EXTENSION", l.f.MediaType.Suffix(),
|
|
))
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return filterDotLess(layouts)
|
|
}
|
|
|
|
func layoutTemplate(typeVar, layoutVar string) string {
|
|
|
|
var l string
|
|
|
|
if typeVar != "" {
|
|
l = "TYPE/"
|
|
}
|
|
|
|
if layoutVar != "" {
|
|
l += "LAYOUT.VARIATIONS.EXTENSION"
|
|
} else {
|
|
l += "VARIATIONS.EXTENSION"
|
|
}
|
|
|
|
return l
|
|
}
|
|
|
|
func filterDotLess(layouts []string) []string {
|
|
var filteredLayouts []string
|
|
|
|
for _, l := range layouts {
|
|
l = strings.Replace(l, "..", ".", -1)
|
|
l = strings.Trim(l, ".")
|
|
// If media type has no suffix, we have "index" type of layouts in this list, which
|
|
// doesn't make much sense.
|
|
if strings.Contains(l, ".") {
|
|
filteredLayouts = append(filteredLayouts, l)
|
|
}
|
|
}
|
|
|
|
return filteredLayouts
|
|
}
|
|
|
|
func replaceKeyValues(s string, oldNew ...string) string {
|
|
replacer := strings.NewReplacer(oldNew...)
|
|
return replacer.Replace(s)
|
|
}
|