mirror of
https://github.com/gohugoio/hugo.git
synced 2024-12-04 14:25:19 -05:00
c507e2717d
Now: * The template API lives in /tpl * The rest lives in /tpl/tplimpl This is bound te be more improved in the future. Updates #2701
575 lines
15 KiB
Go
575 lines
15 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 (
|
|
"fmt"
|
|
"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/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
|
|
|
|
clone *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 {
|
|
// TODO(bep) check that this isn't called too many times.
|
|
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) LoadEmbedded() {
|
|
t.EmbedShortcodes()
|
|
t.EmbedTemplates()
|
|
}
|
|
|
|
// MarkReady marks the template as "ready for execution". No changes allowed
|
|
// after this is set.
|
|
func (t *GoHTMLTemplate) MarkReady() {
|
|
if t.clone == nil {
|
|
t.clone = template.Must(t.Template.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
|
|
}
|
|
|
|
tplName := t.GenerateTemplateNameFrom(absPath, path)
|
|
|
|
if prefix != "" {
|
|
tplName = strings.Trim(prefix, "/") + "/" + tplName
|
|
}
|
|
|
|
var baseTemplatePath string
|
|
|
|
// Ace and Go templates may have both a base and inner template.
|
|
pathDir := filepath.Dir(path)
|
|
if filepath.Ext(path) != ".amber" && !strings.HasSuffix(pathDir, "partials") && !strings.HasSuffix(pathDir, "shortcodes") {
|
|
|
|
innerMarkers := goTemplateInnerMarkers
|
|
baseFileName := fmt.Sprintf("%s.html", baseFileBase)
|
|
|
|
if filepath.Ext(path) == ".ace" {
|
|
innerMarkers = aceTemplateInnerMarkers
|
|
baseFileName = fmt.Sprintf("%s.ace", baseFileBase)
|
|
}
|
|
|
|
// This may be a view that shouldn't have base template
|
|
// Have to look inside it to make sure
|
|
needsBase, err := helpers.FileContainsAny(path, innerMarkers, t.Fs.Source)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if needsBase {
|
|
|
|
layoutDir := t.PathSpec.GetLayoutDirPath()
|
|
currBaseFilename := fmt.Sprintf("%s-%s", helpers.Filename(path), baseFileName)
|
|
templateDir := filepath.Dir(path)
|
|
themeDir := filepath.Join(t.PathSpec.GetThemeDir())
|
|
relativeThemeLayoutsDir := filepath.Join(t.PathSpec.GetRelativeThemeDir(), "layouts")
|
|
|
|
var baseTemplatedDir string
|
|
|
|
if strings.HasPrefix(templateDir, relativeThemeLayoutsDir) {
|
|
baseTemplatedDir = strings.TrimPrefix(templateDir, relativeThemeLayoutsDir)
|
|
} else {
|
|
baseTemplatedDir = strings.TrimPrefix(templateDir, layoutDir)
|
|
}
|
|
|
|
baseTemplatedDir = strings.TrimPrefix(baseTemplatedDir, helpers.FilePathSeparator)
|
|
|
|
// Look for base template in the follwing order:
|
|
// 1. <current-path>/<template-name>-baseof.<suffix>, e.g. list-baseof.<suffix>.
|
|
// 2. <current-path>/baseof.<suffix>
|
|
// 3. _default/<template-name>-baseof.<suffix>, e.g. list-baseof.<suffix>.
|
|
// 4. _default/baseof.<suffix>
|
|
// For each of the steps above, it will first look in the project, then, if theme is set,
|
|
// in the theme's layouts folder.
|
|
|
|
pairsToCheck := [][]string{
|
|
[]string{baseTemplatedDir, currBaseFilename},
|
|
[]string{baseTemplatedDir, baseFileName},
|
|
[]string{"_default", currBaseFilename},
|
|
[]string{"_default", baseFileName},
|
|
}
|
|
|
|
Loop:
|
|
for _, pair := range pairsToCheck {
|
|
pathsToCheck := basePathsToCheck(pair, layoutDir, themeDir)
|
|
for _, pathToCheck := range pathsToCheck {
|
|
if ok, err := helpers.Exists(pathToCheck, t.Fs.Source); err == nil && ok {
|
|
baseTemplatePath = pathToCheck
|
|
break Loop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := t.AddTemplateFile(tplName, baseTemplatePath, path); err != nil {
|
|
t.Log.ERROR.Printf("Failed to add template %s in path %s: %s", tplName, 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 basePathsToCheck(path []string, layoutDir, themeDir string) []string {
|
|
// Always look in the project.
|
|
pathsToCheck := []string{filepath.Join((append([]string{layoutDir}, path...))...)}
|
|
|
|
// May have a theme
|
|
if themeDir != "" {
|
|
pathsToCheck = append(pathsToCheck, filepath.Join((append([]string{themeDir, "layouts"}, path...))...))
|
|
}
|
|
|
|
return pathsToCheck
|
|
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|