hugo/tpl/tplimpl/template.go
Bjørn Erik Pedersen baa29f6534 output: Rework the base template logic
Extract the logic to a testable function and add support for custom output types.

Fixes #2995
2017-03-27 15:43:56 +02:00

542 lines
14 KiB
Go

// Copyright 2016 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"
"io"
"os"
"path/filepath"
"strings"
"sync"
"github.com/eknkc/amber"
"github.com/spf13/afero"
bp "github.com/spf13/hugo/bufferpool"
"github.com/spf13/hugo/deps"
"github.com/spf13/hugo/helpers"
"github.com/spf13/hugo/output"
"github.com/yosssi/ace"
)
// TODO(bep) globals get rid of the rest of the jww.ERR etc.
// Protecting global map access (Amber)
var amberMu sync.Mutex
type templateErr struct {
name string
err error
}
type GoHTMLTemplate struct {
*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
errors []*templateErr
funcster *templateFuncster
amberFuncMap template.FuncMap
*deps.Deps
}
type TemplateProvider struct{}
var DefaultTemplateProvider *TemplateProvider
// Update updates the Hugo Template System in the provided Deps.
// with all the additional features, templates & functions
func (*TemplateProvider) Update(deps *deps.Deps) error {
tmpl := &GoHTMLTemplate{
Template: template.New(""),
overlays: make(map[string]*template.Template),
errors: make([]*templateErr, 0),
Deps: deps,
}
deps.Tmpl = tmpl
tmpl.initFuncs(deps)
tmpl.LoadEmbedded()
if deps.WithTemplate != nil {
err := deps.WithTemplate(tmpl)
if err != nil {
tmpl.errors = append(tmpl.errors, &templateErr{"init", err})
}
}
tmpl.MarkReady()
return nil
}
// Clone clones
func (*TemplateProvider) Clone(d *deps.Deps) error {
t := d.Tmpl.(*GoHTMLTemplate)
// 1. Clone the clone with new template funcs
// 2. Clone any overlays with new template funcs
tmpl := &GoHTMLTemplate{
Template: template.Must(t.Template.Clone()),
overlays: make(map[string]*template.Template),
errors: make([]*templateErr, 0),
Deps: d,
}
d.Tmpl = tmpl
tmpl.initFuncs(d)
for k, v := range t.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(tmpl.funcster.funcMap)
tmpl.overlays[k] = vc
}
tmpl.MarkReady()
return nil
}
func (t *GoHTMLTemplate) initFuncs(d *deps.Deps) {
t.funcster = newTemplateFuncster(d)
// The URL funcs in the funcMap is somewhat language dependent,
// so we need to wait until the language and site config is loaded.
t.funcster.initFuncMap()
t.amberFuncMap = template.FuncMap{}
amberMu.Lock()
for k, v := range amber.FuncMap {
t.amberFuncMap[k] = v
}
for k, v := range t.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 *GoHTMLTemplate) Funcs(funcMap template.FuncMap) {
t.Template.Funcs(funcMap)
}
func (t *GoHTMLTemplate) Partial(name string, contextList ...interface{}) template.HTML {
if strings.HasPrefix("partials/", name) {
name = name[8:]
}
var context interface{}
if len(contextList) == 0 {
context = nil
} else {
context = contextList[0]
}
return t.ExecuteTemplateToHTML(context, "partials/"+name, "theme/partials/"+name)
}
func (t *GoHTMLTemplate) executeTemplate(context interface{}, w io.Writer, layouts ...string) {
var worked bool
for _, layout := range layouts {
templ := t.Lookup(layout)
if templ == nil {
layout += ".html"
templ = t.Lookup(layout)
}
if templ != nil {
if err := templ.Execute(w, context); err != nil {
helpers.DistinctErrorLog.Println(layout, err)
}
worked = true
break
}
}
if !worked {
t.Log.ERROR.Println("Unable to render", layouts)
t.Log.ERROR.Println("Expecting to find a template in either the theme/layouts or /layouts in one of the following relative locations", layouts)
}
}
func (t *GoHTMLTemplate) ExecuteTemplateToHTML(context interface{}, layouts ...string) template.HTML {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
t.executeTemplate(context, b, layouts...)
return template.HTML(b.String())
}
func (t *GoHTMLTemplate) Lookup(name string) *template.Template {
if templ := t.Template.Lookup(name); templ != nil {
return templ
}
if t.overlays != nil {
if templ, ok := t.overlays[name]; ok {
return templ
}
}
// The clone is used for the non-renderable HTML pages (p.IsRenderable == false) that is parsed
// as Go templates late in the build process.
if t.clone != nil {
if templ := t.clone.Lookup(name); templ != nil {
return templ
}
}
return nil
}
func (t *GoHTMLTemplate) GetClone() *template.Template {
return t.clone
}
func (t *GoHTMLTemplate) RebuildClone() *template.Template {
t.clone = template.Must(t.cloneClone.Clone())
return t.clone
}
func (t *GoHTMLTemplate) LoadEmbedded() {
t.EmbedShortcodes()
t.EmbedTemplates()
}
// MarkReady marks the template 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 *GoHTMLTemplate) MarkReady() {
if t.clone == nil {
t.clone = template.Must(t.Template.Clone())
t.cloneClone = template.Must(t.clone.Clone())
}
}
func (t *GoHTMLTemplate) checkState() {
if t.clone != nil {
panic("template is cloned and cannot be modfified")
}
}
func (t *GoHTMLTemplate) AddInternalTemplate(prefix, name, tpl string) error {
if prefix != "" {
return t.AddTemplate("_internal/"+prefix+"/"+name, tpl)
}
return t.AddTemplate("_internal/"+name, tpl)
}
func (t *GoHTMLTemplate) AddInternalShortcode(name, content string) error {
return t.AddInternalTemplate("shortcodes", name, content)
}
func (t *GoHTMLTemplate) AddTemplate(name, tpl string) error {
t.checkState()
templ, err := t.New(name).Parse(tpl)
if err != nil {
t.errors = append(t.errors, &templateErr{name: name, err: err})
return err
}
if err := applyTemplateTransformers(templ); err != nil {
return err
}
return nil
}
func (t *GoHTMLTemplate) AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error {
// There is currently no known way to associate a cloned template with an existing one.
// This funky master/overlay design will hopefully improve in a future version of Go.
//
// Simplicity is hard.
//
// Until then we'll have to live with this hackery.
//
// See https://github.com/golang/go/issues/14285
//
// So, to do minimum amount of changes to get this to work:
//
// 1. Lookup or Parse the master
// 2. Parse and store the overlay in a separate map
masterTpl := t.Lookup(masterFilename)
if masterTpl == nil {
b, err := afero.ReadFile(t.Fs.Source, masterFilename)
if err != nil {
return err
}
masterTpl, err = t.New(masterFilename).Parse(string(b))
if err != nil {
// TODO(bep) Add a method that does this
t.errors = append(t.errors, &templateErr{name: name, err: err})
return err
}
}
b, err := afero.ReadFile(t.Fs.Source, overlayFilename)
if err != nil {
return err
}
overlayTpl, err := template.Must(masterTpl.Clone()).Parse(string(b))
if err != nil {
t.errors = append(t.errors, &templateErr{name: name, err: err})
} else {
// 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 := applyTemplateTransformers(overlayTpl); err != nil {
return err
}
t.overlays[name] = overlayTpl
}
return err
}
func (t *GoHTMLTemplate) AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error {
t.checkState()
var base, inner *ace.File
name = name[:len(name)-len(filepath.Ext(innerPath))] + ".html"
// Fixes issue #1178
basePath = strings.Replace(basePath, "\\", "/", -1)
innerPath = strings.Replace(innerPath, "\\", "/", -1)
if basePath != "" {
base = ace.NewFile(basePath, baseContent)
inner = ace.NewFile(innerPath, innerContent)
} else {
base = ace.NewFile(innerPath, innerContent)
inner = ace.NewFile("", []byte{})
}
parsed, err := ace.ParseSource(ace.NewSource(base, inner, []*ace.File{}), nil)
if err != nil {
t.errors = append(t.errors, &templateErr{name: name, err: err})
return err
}
templ, err := ace.CompileResultWithTemplate(t.New(name), parsed, nil)
if err != nil {
t.errors = append(t.errors, &templateErr{name: name, err: err})
return err
}
return applyTemplateTransformers(templ)
}
func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) error {
t.checkState()
// get the suffix and switch on that
ext := filepath.Ext(path)
switch ext {
case ".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.New(templateName))
amberMu.Unlock()
if err != nil {
return err
}
return applyTemplateTransformers(templ)
case ".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.AddTemplateFileWithMaster(name, path, baseTemplatePath)
}
b, err := afero.ReadFile(t.Fs.Source, path)
if err != nil {
return err
}
t.Log.DEBUG.Printf("Add template file from path %s", path)
return t.AddTemplate(name, string(b))
}
}
func (t *GoHTMLTemplate) GenerateTemplateNameFrom(base, path string) string {
name, _ := filepath.Rel(base, path)
return filepath.ToSlash(name)
}
func isDotFile(path string) bool {
return filepath.Base(path)[0] == '.'
}
func isBackupFile(path string) bool {
return path[len(path)-1] == '~'
}
const baseFileBase = "baseof"
var aceTemplateInnerMarkers = [][]byte{[]byte("= content")}
var goTemplateInnerMarkers = [][]byte{[]byte("{{define"), []byte("{{ define")}
func isBaseTemplate(path string) bool {
return strings.Contains(path, baseFileBase)
}
func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
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
}
workingDir := t.PathSpec.WorkingDir()
themeDir := t.PathSpec.GetThemeDir()
if themeDir != "" && strings.HasPrefix(absPath, themeDir) {
workingDir = themeDir
}
li := strings.LastIndex(path, t.PathSpec.LayoutDir()) + len(t.PathSpec.LayoutDir()) + 1
if li < 0 {
// Possibly a theme
li = strings.LastIndex(path, "layouts") + 8
}
relPath := path[li:]
descriptor := output.TemplateLookupDescriptor{
WorkingDir: workingDir,
LayoutDir: t.PathSpec.LayoutDir(),
RelPath: relPath,
Prefix: prefix,
Theme: t.PathSpec.Theme(),
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.CreateTemplateID(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 *GoHTMLTemplate) LoadTemplatesWithPrefix(absPath string, prefix string) {
t.loadTemplates(absPath, prefix)
}
func (t *GoHTMLTemplate) LoadTemplates(absPath string) {
t.loadTemplates(absPath, "")
}
func (t *GoHTMLTemplate) PrintErrors() {
for i, e := range t.errors {
t.Log.ERROR.Println(i, ":", e.err)
}
}