hugo/tpl/tplimpl/template.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

678 lines
17 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 tplimpl
import (
"html/template"
"strings"
texttemplate "text/template"
"github.com/eknkc/amber"
"os"
"github.com/spf13/hugo/output"
"path/filepath"
"sync"
"github.com/spf13/afero"
"github.com/spf13/hugo/deps"
"github.com/spf13/hugo/helpers"
"github.com/spf13/hugo/tpl"
)
const (
textTmplNamePrefix = "_text/"
)
var (
_ tpl.TemplateHandler = (*templateHandler)(nil)
_ tpl.TemplateTestMocker = (*templateHandler)(nil)
_ tpl.TemplateFinder = (*htmlTemplates)(nil)
_ tpl.TemplateFinder = (*textTemplates)(nil)
_ templateLoader = (*htmlTemplates)(nil)
_ templateLoader = (*textTemplates)(nil)
_ templateLoader = (*templateHandler)(nil)
_ templateFuncsterTemplater = (*htmlTemplates)(nil)
_ templateFuncsterTemplater = (*textTemplates)(nil)
)
// Protecting global map access (Amber)
var amberMu sync.Mutex
type templateErr struct {
name string
err error
}
type templateLoader interface {
handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error
addTemplate(name, tpl string) error
addLateTemplate(name, tpl string) error
}
type templateFuncsterTemplater interface {
tpl.TemplateFinder
setFuncs(funcMap map[string]interface{})
setTemplateFuncster(f *templateFuncster)
}
// templateHandler holds the templates in play.
// It implements the templateLoader and tpl.TemplateHandler interfaces.
type templateHandler struct {
// text holds all the pure text templates.
text *textTemplates
html *htmlTemplates
amberFuncMap template.FuncMap
errors []*templateErr
*deps.Deps
}
func (t *templateHandler) addError(name string, err error) {
t.errors = append(t.errors, &templateErr{name, err})
}
// PrintErrors prints the accumulated errors as ERROR to the log.
func (t *templateHandler) PrintErrors() {
for _, e := range t.errors {
t.Log.ERROR.Println(e.name, ":", e.err)
}
}
// Lookup tries to find a template with the given name in both template
// collections: First HTML, then the plain text template collection.
func (t *templateHandler) Lookup(name string) *tpl.TemplateAdapter {
var te *tpl.TemplateAdapter
isTextTemplate := strings.HasPrefix(name, textTmplNamePrefix)
if isTextTemplate {
// The templates are stored without the prefix identificator.
name = strings.TrimPrefix(name, textTmplNamePrefix)
te = t.text.Lookup(name)
} else {
te = t.html.Lookup(name)
}
if te == nil {
return nil
}
return te
}
func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
c := &templateHandler{
Deps: d,
html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template)},
text: &textTemplates{t: texttemplate.Must(t.text.t.Clone()), overlays: make(map[string]*texttemplate.Template)},
errors: make([]*templateErr, 0),
}
d.Tmpl = c
c.initFuncs()
for k, v := range t.html.overlays {
vc := template.Must(v.Clone())
// The extra lookup is a workaround, see
// * https://github.com/golang/go/issues/16101
// * https://github.com/spf13/hugo/issues/2549
vc = vc.Lookup(vc.Name())
vc.Funcs(t.html.funcster.funcMap)
c.html.overlays[k] = vc
}
for k, v := range t.text.overlays {
vc := texttemplate.Must(v.Clone())
vc = vc.Lookup(vc.Name())
vc.Funcs(texttemplate.FuncMap(t.text.funcster.funcMap))
c.text.overlays[k] = vc
}
return c
}
func newTemplateAdapter(deps *deps.Deps) *templateHandler {
htmlT := &htmlTemplates{
t: template.New(""),
overlays: make(map[string]*template.Template),
}
textT := &textTemplates{
t: texttemplate.New(""),
overlays: make(map[string]*texttemplate.Template),
}
return &templateHandler{
Deps: deps,
html: htmlT,
text: textT,
errors: make([]*templateErr, 0),
}
}
type htmlTemplates struct {
funcster *templateFuncster
t *template.Template
// This looks, and is, strange.
// The clone is used by non-renderable content pages, and these need to be
// re-parsed on content change, and to avoid the
// "cannot Parse after Execute" error, we need to re-clone it from the original clone.
clone *template.Template
cloneClone *template.Template
// a separate storage for the overlays created from cloned master templates.
// note: No mutex protection, so we add these in one Go routine, then just read.
overlays map[string]*template.Template
}
func (t *htmlTemplates) setTemplateFuncster(f *templateFuncster) {
t.funcster = f
}
func (t *htmlTemplates) Lookup(name string) *tpl.TemplateAdapter {
templ := t.lookup(name)
if templ == nil {
return nil
}
return &tpl.TemplateAdapter{templ}
}
func (t *htmlTemplates) lookup(name string) *template.Template {
if templ := t.t.Lookup(name); templ != nil {
return templ
}
if t.overlays != nil {
if templ, ok := t.overlays[name]; ok {
return templ
}
}
if t.clone != nil {
return t.clone.Lookup(name)
}
return nil
}
type textTemplates struct {
funcster *templateFuncster
t *texttemplate.Template
clone *texttemplate.Template
cloneClone *texttemplate.Template
overlays map[string]*texttemplate.Template
}
func (t *textTemplates) setTemplateFuncster(f *templateFuncster) {
t.funcster = f
}
func (t *textTemplates) Lookup(name string) *tpl.TemplateAdapter {
templ := t.lookup(name)
if templ == nil {
return nil
}
return &tpl.TemplateAdapter{templ}
}
func (t *textTemplates) lookup(name string) *texttemplate.Template {
if templ := t.t.Lookup(name); templ != nil {
return templ
}
if t.overlays != nil {
if templ, ok := t.overlays[name]; ok {
return templ
}
}
if t.clone != nil {
return t.clone.Lookup(name)
}
return nil
}
func (t *templateHandler) setFuncs(funcMap map[string]interface{}) {
t.html.setFuncs(funcMap)
t.text.setFuncs(funcMap)
}
// SetFuncs replaces the funcs in the func maps with new definitions.
// This is only used in tests.
func (t *templateHandler) SetFuncs(funcMap map[string]interface{}) {
t.setFuncs(funcMap)
}
func (t *htmlTemplates) setFuncs(funcMap map[string]interface{}) {
t.t.Funcs(funcMap)
}
func (t *textTemplates) setFuncs(funcMap map[string]interface{}) {
t.t.Funcs(funcMap)
}
// LoadTemplates loads the templates, starting from the given absolute path.
// A prefix can be given to indicate a template namespace to load the templates
// into, i.e. "_internal" etc.
func (t *templateHandler) LoadTemplates(absPath, prefix string) {
// TODO(bep) output formats. Will have to get to complete list when that is ready.
t.loadTemplates(absPath, prefix, output.Formats{output.HTMLFormat, output.RSSFormat, output.CalendarFormat, output.AMPFormat, output.JSONFormat})
}
func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) error {
templ, err := tt.New(name).Parse(tpl)
if err != nil {
return err
}
if err := applyTemplateTransformersToHMLTTemplate(templ); err != nil {
return err
}
return nil
}
func (t *htmlTemplates) addTemplate(name, tpl string) error {
return t.addTemplateIn(t.t, name, tpl)
}
func (t *htmlTemplates) addLateTemplate(name, tpl string) error {
return t.addTemplateIn(t.clone, name, tpl)
}
func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) error {
name = strings.TrimPrefix(name, textTmplNamePrefix)
templ, err := tt.New(name).Parse(tpl)
if err != nil {
return err
}
if err := applyTemplateTransformersToTextTemplate(templ); err != nil {
return err
}
return nil
}
func (t *textTemplates) addTemplate(name, tpl string) error {
return t.addTemplateIn(t.t, name, tpl)
}
func (t *textTemplates) addLateTemplate(name, tpl string) error {
return t.addTemplateIn(t.clone, name, tpl)
}
func (t *templateHandler) addTemplate(name, tpl string) error {
return t.AddTemplate(name, tpl)
}
func (t *templateHandler) addLateTemplate(name, tpl string) error {
return t.AddLateTemplate(name, tpl)
}
// AddLateTemplate is used to add a template late, i.e. after the
// regular templates have started its execution.
func (t *templateHandler) AddLateTemplate(name, tpl string) error {
h := t.getTemplateHandler(name)
if err := h.addLateTemplate(name, tpl); err != nil {
t.addError(name, err)
return err
}
return nil
}
// AddTemplate parses and adds a template to the collection.
// Templates with name prefixed with "_text" will be handled as plain
// text templates.
func (t *templateHandler) AddTemplate(name, tpl string) error {
h := t.getTemplateHandler(name)
if err := h.addTemplate(name, tpl); err != nil {
t.addError(name, err)
return err
}
return nil
}
// MarkReady marks the templates as "ready for execution". No changes allowed
// after this is set.
// TODO(bep) if this proves to be resource heavy, we could detect
// earlier if we really need this, or make it lazy.
func (t *templateHandler) MarkReady() {
if t.html.clone == nil {
t.html.clone = template.Must(t.html.t.Clone())
t.html.cloneClone = template.Must(t.html.clone.Clone())
}
if t.text.clone == nil {
t.text.clone = texttemplate.Must(t.text.t.Clone())
t.text.cloneClone = texttemplate.Must(t.text.clone.Clone())
}
}
// RebuildClone rebuilds the cloned templates. Used for live-reloads.
func (t *templateHandler) RebuildClone() {
t.html.clone = template.Must(t.html.cloneClone.Clone())
t.text.clone = texttemplate.Must(t.text.cloneClone.Clone())
}
func (t *templateHandler) loadTemplates(absPath string, prefix string, formats output.Formats) {
t.Log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix)
walker := func(path string, fi os.FileInfo, err error) error {
if err != nil {
return nil
}
t.Log.DEBUG.Println("Template path", path)
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err := filepath.EvalSymlinks(absPath)
if err != nil {
t.Log.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", absPath, err)
return nil
}
linkfi, err := t.Fs.Source.Stat(link)
if err != nil {
t.Log.ERROR.Printf("Cannot stat '%s', error was: %s", link, err)
return nil
}
if !linkfi.Mode().IsRegular() {
t.Log.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", absPath)
}
return nil
}
if !fi.IsDir() {
if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) {
return nil
}
var (
workingDir = t.PathSpec.WorkingDir()
themeDir = t.PathSpec.GetThemeDir()
layoutDir = t.PathSpec.LayoutDir()
)
if themeDir != "" && strings.HasPrefix(absPath, themeDir) {
workingDir = themeDir
layoutDir = "layouts"
}
li := strings.LastIndex(path, layoutDir) + len(layoutDir) + 1
relPath := path[li:]
descriptor := output.TemplateLookupDescriptor{
WorkingDir: workingDir,
LayoutDir: layoutDir,
RelPath: relPath,
Prefix: prefix,
Theme: t.PathSpec.Theme(),
OutputFormats: formats,
FileExists: func(filename string) (bool, error) {
return helpers.Exists(filename, t.Fs.Source)
},
ContainsAny: func(filename string, subslices [][]byte) (bool, error) {
return helpers.FileContainsAny(filename, subslices, t.Fs.Source)
},
}
tplID, err := output.CreateTemplateNames(descriptor)
if err != nil {
t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err)
return nil
}
if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil {
t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, path, err)
}
}
return nil
}
if err := helpers.SymbolicWalk(t.Fs.Source, absPath, walker); err != nil {
t.Log.ERROR.Printf("Failed to load templates: %s", err)
}
}
func (t *templateHandler) initFuncs() {
// The template funcs need separation between text and html templates.
for _, funcsterHolder := range []templateFuncsterTemplater{t.html, t.text} {
funcster := newTemplateFuncster(t.Deps, funcsterHolder)
// The URL funcs in the funcMap is somewhat language dependent,
// so we need to wait until the language and site config is loaded.
funcster.initFuncMap()
funcsterHolder.setTemplateFuncster(funcster)
}
// Amber is HTML only.
t.amberFuncMap = template.FuncMap{}
amberMu.Lock()
for k, v := range amber.FuncMap {
t.amberFuncMap[k] = v
}
for k, v := range t.html.funcster.funcMap {
t.amberFuncMap[k] = v
// Hacky, but we need to make sure that the func names are in the global map.
amber.FuncMap[k] = func() string {
panic("should never be invoked")
}
}
amberMu.Unlock()
}
func (t *templateHandler) getTemplateHandler(name string) templateLoader {
if strings.HasPrefix(name, textTmplNamePrefix) {
return t.text
}
return t.html
}
func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {
h := t.getTemplateHandler(name)
return h.handleMaster(name, overlayFilename, masterFilename, onMissing)
}
func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {
masterTpl := t.lookup(masterFilename)
if masterTpl == nil {
templ, err := onMissing(masterFilename)
if err != nil {
return err
}
masterTpl, err = t.t.New(overlayFilename).Parse(templ)
if err != nil {
return err
}
}
templ, err := onMissing(overlayFilename)
if err != nil {
return err
}
overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ)
if err != nil {
return err
}
// The extra lookup is a workaround, see
// * https://github.com/golang/go/issues/16101
// * https://github.com/spf13/hugo/issues/2549
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
if err := applyTemplateTransformersToHMLTTemplate(overlayTpl); err != nil {
return err
}
t.overlays[name] = overlayTpl
return err
}
func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {
masterTpl := t.lookup(masterFilename)
if masterTpl == nil {
templ, err := onMissing(masterFilename)
if err != nil {
return err
}
masterTpl, err = t.t.New(overlayFilename).Parse(templ)
if err != nil {
return err
}
}
templ, err := onMissing(overlayFilename)
if err != nil {
return err
}
overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ)
if err != nil {
return err
}
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
if err := applyTemplateTransformersToTextTemplate(overlayTpl); err != nil {
return err
}
t.overlays[name] = overlayTpl
return err
}
func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) error {
t.checkState()
getTemplate := func(filename string) (string, error) {
b, err := afero.ReadFile(t.Fs.Source, filename)
if err != nil {
return "", err
}
return string(b), nil
}
// get the suffix and switch on that
ext := filepath.Ext(path)
switch ext {
case ".amber":
// Only HTML support for Amber
templateName := strings.TrimSuffix(name, filepath.Ext(name)) + ".html"
b, err := afero.ReadFile(t.Fs.Source, path)
if err != nil {
return err
}
amberMu.Lock()
templ, err := t.compileAmberWithTemplate(b, path, t.html.t.New(templateName))
amberMu.Unlock()
if err != nil {
return err
}
return applyTemplateTransformersToHMLTTemplate(templ)
case ".ace":
// Only HTML support for Ace
var innerContent, baseContent []byte
innerContent, err := afero.ReadFile(t.Fs.Source, path)
if err != nil {
return err
}
if baseTemplatePath != "" {
baseContent, err = afero.ReadFile(t.Fs.Source, baseTemplatePath)
if err != nil {
return err
}
}
return t.addAceTemplate(name, baseTemplatePath, path, baseContent, innerContent)
default:
if baseTemplatePath != "" {
return t.handleMaster(name, path, baseTemplatePath, getTemplate)
}
templ, err := getTemplate(path)
if err != nil {
return err
}
t.Log.DEBUG.Printf("Add template file from path %s", path)
return t.AddTemplate(name, templ)
}
}
func (t *templateHandler) loadEmbedded() {
t.embedShortcodes()
t.embedTemplates()
}
func (t *templateHandler) addInternalTemplate(prefix, name, tpl string) error {
if prefix != "" {
return t.AddTemplate("_internal/"+prefix+"/"+name, tpl)
}
return t.AddTemplate("_internal/"+name, tpl)
}
func (t *templateHandler) addInternalShortcode(name, content string) error {
return t.addInternalTemplate("shortcodes", name, content)
}
func (t *templateHandler) checkState() {
if t.html.clone != nil || t.text.clone != nil {
panic("template is cloned and cannot be modfified")
}
}
func isDotFile(path string) bool {
return filepath.Base(path)[0] == '.'
}
func isBackupFile(path string) bool {
return path[len(path)-1] == '~'
}
const baseFileBase = "baseof"
func isBaseTemplate(path string) bool {
return strings.Contains(path, baseFileBase)
}