hugo/output/layout.go
Bjørn Erik Pedersen 5c5efa03d2 tpl: Rework to handle both text and HTML templates
Before this commit, Hugo used `html/template` for all Go templates.

While this is a fine choice for HTML and maybe also RSS feeds, it is painful for plain text formats such as CSV, JSON etc.

This commit fixes that by using the `IsPlainText` attribute on the output format to decide what to use.

A couple of notes:

* The above requires a nonambiguous template name to type mapping. I.e. `/layouts/_default/list.json` will only work if there is only one JSON output format, `/layouts/_default/list.mytype.json` will always work.
* Ambiguous types will fall back to HTML.
* Partials inherits the text vs HTML identificator of the container template. This also means that plain text templates can only include plain text partials.
* Shortcode templates are, by definition, currently HTML templates only.

Fixes #3221
2017-04-02 11:37:30 +02:00

243 lines
6.7 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"
"path"
"strings"
"sync"
)
// LayoutDescriptor describes how a layout should be chosen. This is
// typically built from a Page.
type LayoutDescriptor struct {
Type string
Section string
Kind string
Layout string
}
// Layout calculates the layout template to use to render a given output type.
type LayoutHandler struct {
hasTheme bool
mu sync.RWMutex
cache map[layoutCacheKey][]string
}
type layoutCacheKey struct {
d LayoutDescriptor
layoutOverride string
f Format
}
func NewLayoutHandler(hasTheme bool) *LayoutHandler {
return &LayoutHandler{hasTheme: hasTheme, cache: make(map[layoutCacheKey][]string)}
}
// RSS:
// Home:"rss.xml", "_default/rss.xml", "_internal/_default/rss.xml"
// Section: "section/" + section + ".rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"
// Taxonomy "taxonomy/" + singular + ".rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"
// Tax term: taxonomy/" + singular + ".terms.rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"
const (
// The RSS templates doesn't map easily into the regular pages.
layoutsRSSHome = `NAME.SUFFIX _default/NAME.SUFFIX _internal/_default/rss.xml`
layoutsRSSSection = `section/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
layoutsRSSTaxonomy = `taxonomy/SECTION.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
layoutsRSSTaxonomyTerm = `taxonomy/SECTION.terms.NAME.SUFFIX _default/NAME.SUFFIX NAME.SUFFIX _internal/_default/rss.xml`
layoutsHome = "index.NAME.SUFFIX index.SUFFIX _default/list.NAME.SUFFIX _default/list.SUFFIX"
layoutsSection = `
section/SECTION.NAME.SUFFIX section/SECTION.SUFFIX
SECTION/list.NAME.SUFFIX SECTION/list.SUFFIX
_default/section.NAME.SUFFIX _default/section.SUFFIX
_default/list.NAME.SUFFIX _default/list.SUFFIX
indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX
_default/indexes.NAME.SUFFIX _default/indexes.SUFFIX
`
layoutsTaxonomy = `
taxonomy/SECTION.NAME.SUFFIX taxonomy/SECTION.SUFFIX
indexes/SECTION.NAME.SUFFIX indexes/SECTION.SUFFIX
_default/taxonomy.NAME.SUFFIX _default/taxonomy.SUFFIX
_default/list.NAME.SUFFIX _default/list.SUFFIX
`
layoutsTaxonomyTerm = `
taxonomy/SECTION.terms.NAME.SUFFIX taxonomy/SECTION.terms.SUFFIX
_default/terms.NAME.SUFFIX _default/terms.SUFFIX
indexes/indexes.NAME.SUFFIX indexes/indexes.SUFFIX
`
)
func (l *LayoutHandler) For(d LayoutDescriptor, layoutOverride string, f Format) ([]string, error) {
// We will get lots of requests for the same layouts, so avoid recalculations.
key := layoutCacheKey{d, layoutOverride, f}
l.mu.RLock()
if cacheVal, found := l.cache[key]; found {
l.mu.RUnlock()
return cacheVal, nil
}
l.mu.RUnlock()
var layouts []string
if layoutOverride != "" && d.Kind != "page" {
return layouts, fmt.Errorf("Custom layout (%q) only supported for regular pages, not kind %q", layoutOverride, d.Kind)
}
layout := d.Layout
if layoutOverride != "" {
layout = layoutOverride
}
isRSS := f.Name == RSSFormat.Name
if d.Kind == "page" {
if isRSS {
return []string{}, nil
}
layouts = regularPageLayouts(d.Type, layout, f)
} else {
if isRSS {
layouts = resolveListTemplate(d, f,
layoutsRSSHome,
layoutsRSSSection,
layoutsRSSTaxonomy,
layoutsRSSTaxonomyTerm)
} else {
layouts = resolveListTemplate(d, f,
layoutsHome,
layoutsSection,
layoutsTaxonomy,
layoutsTaxonomyTerm)
}
}
if l.hasTheme {
layoutsWithThemeLayouts := []string{}
// First place all non internal templates
for _, t := range layouts {
if !strings.HasPrefix(t, "_internal/") {
layoutsWithThemeLayouts = append(layoutsWithThemeLayouts, t)
}
}
// Then place theme templates with the same names
for _, t := range layouts {
if !strings.HasPrefix(t, "_internal/") {
layoutsWithThemeLayouts = append(layoutsWithThemeLayouts, "theme/"+t)
}
}
// Lastly place internal templates
for _, t := range layouts {
if strings.HasPrefix(t, "_internal/") {
layoutsWithThemeLayouts = append(layoutsWithThemeLayouts, t)
}
}
layouts = layoutsWithThemeLayouts
}
layouts = prependTextPrefixIfNeeded(f, layouts...)
l.mu.Lock()
l.cache[key] = layouts
l.mu.Unlock()
return layouts, nil
}
func resolveListTemplate(d LayoutDescriptor, f Format,
homeLayouts,
sectionLayouts,
taxonomyLayouts,
taxonomyTermLayouts string) []string {
var layouts []string
switch d.Kind {
case "home":
layouts = resolveTemplate(homeLayouts, d, f)
case "section":
layouts = resolveTemplate(sectionLayouts, d, f)
case "taxonomy":
layouts = resolveTemplate(taxonomyLayouts, d, f)
case "taxonomyTerm":
layouts = resolveTemplate(taxonomyTermLayouts, d, f)
}
return layouts
}
func resolveTemplate(templ string, d LayoutDescriptor, f Format) []string {
layouts := strings.Fields(replaceKeyValues(templ,
"SUFFIX", f.MediaType.Suffix,
"NAME", strings.ToLower(f.Name),
"SECTION", d.Section))
return layouts
}
func prependTextPrefixIfNeeded(f Format, layouts ...string) []string {
if !f.IsPlainText {
return layouts
}
newLayouts := make([]string, len(layouts))
for i, l := range layouts {
newLayouts[i] = "_text/" + l
}
return newLayouts
}
func replaceKeyValues(s string, oldNew ...string) string {
replacer := strings.NewReplacer(oldNew...)
return replacer.Replace(s)
}
func regularPageLayouts(types string, layout string, f Format) []string {
var layouts []string
if layout == "" {
layout = "single"
}
suffix := f.MediaType.Suffix
name := strings.ToLower(f.Name)
if types != "" {
t := strings.Split(types, "/")
// Add type/layout.html
for i := range t {
search := t[:len(t)-i]
layouts = append(layouts, fmt.Sprintf("%s/%s.%s.%s", strings.ToLower(path.Join(search...)), layout, name, suffix))
layouts = append(layouts, fmt.Sprintf("%s/%s.%s", strings.ToLower(path.Join(search...)), layout, suffix))
}
}
// Add _default/layout.html
layouts = append(layouts, fmt.Sprintf("_default/%s.%s.%s", layout, name, suffix))
layouts = append(layouts, fmt.Sprintf("_default/%s.%s", layout, suffix))
return layouts
}