hugo/output/layout.go
Bjørn Erik Pedersen ba1d0051b4 media: Make Type comparable
So we can use it and output.Format as map key etc.

This commit also fixes the media.Type implementation so it does not need to mutate itself to handle different suffixes for the same MIME type, e.g. jpg vs. jpeg.

This means that there are no Suffix or FullSuffix on media.Type anymore.

Fixes #8317
Fixes #8324
2021-03-14 15:21:54 +01:00

291 lines
6.8 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 (
"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" && d.Kind != "404"
}
// 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 && d.Layout != "" {
b.addLayoutVariations(d.Layout)
}
if d.Type != "" {
b.addTypeVariations(d.Type)
}
if d.RenderingHook {
b.addLayoutVariations(d.Kind)
b.addSectionType()
}
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 "term":
b.addKind()
if d.Section != "" {
b.addLayoutVariations(d.Section)
}
b.addLayoutVariations("taxonomy")
b.addTypeVariations("taxonomy")
b.addSectionType()
case "taxonomy":
if d.Section != "" {
b.addLayoutVariations(d.Section + ".terms")
}
b.addSectionType()
b.addLayoutVariations("terms")
// For legacy reasons this is deliberately put last.
b.addKind()
case "404":
b.addLayoutVariations("404")
b.addTypeVariations("")
}
isRSS := f.Name == RSSFormat.Name
if !d.RenderingHook && !d.Baseof && isRSS {
// The historic and common rss.xml case
b.addLayoutVariations("")
}
if d.Baseof || d.Kind != "404" {
// Most 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{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
}
s := constructLayoutPath(typeVar, layoutVar, variation, l.f.MediaType.FirstSuffix.Suffix)
if s != "" {
layouts = append(layouts, s)
}
}
}
}
return layouts
}
// constructLayoutPath constructs a layout path given a type, layout,
// variations, and extension. The path constructed follows the pattern of
// type/layout.variations.extension. If any value is empty, it will be left out
// of the path construction.
//
// Path construction requires at least 2 of 3 out of layout, variations, and extension.
// If more than one of those is empty, an empty string is returned.
func constructLayoutPath(typ, layout, variations, extension string) string {
// we already know that layout and variations are not both empty because of
// checks in resolveVariants().
if extension == "" && (layout == "" || variations == "") {
return ""
}
// Commence valid path construction...
var (
p strings.Builder
needDot bool
)
if typ != "" {
p.WriteString(typ)
p.WriteString("/")
}
if layout != "" {
p.WriteString(layout)
needDot = true
}
if variations != "" {
if needDot {
p.WriteString(".")
}
p.WriteString(variations)
needDot = true
}
if extension != "" {
if needDot {
p.WriteString(".")
}
p.WriteString(extension)
}
return p.String()
}