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
This commit is contained in:
Bjørn Erik Pedersen
2017-03-27 20:43:49 +02:00
parent 73c1c7b69d
commit 5c5efa03d2
31 changed files with 1208 additions and 840 deletions

View File

@@ -1,28 +1,103 @@
// 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 tpl
import (
"html/template"
"io"
"text/template/parse"
"html/template"
texttemplate "text/template"
bp "github.com/spf13/hugo/bufferpool"
)
// TODO(bep) make smaller
type Template interface {
ExecuteTemplate(wr io.Writer, name string, data interface{}) error
ExecuteTemplateToHTML(context interface{}, layouts ...string) template.HTML
Lookup(name string) *template.Template
Templates() []*template.Template
New(name string) *template.Template
GetClone() *template.Template
RebuildClone() *template.Template
LoadTemplates(absPath string)
LoadTemplatesWithPrefix(absPath, prefix string)
var (
_ TemplateExecutor = (*TemplateAdapter)(nil)
)
// TemplateHandler manages the collection of templates.
type TemplateHandler interface {
TemplateFinder
AddTemplate(name, tpl string) error
AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error
AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error
AddInternalTemplate(prefix, name, tpl string) error
AddInternalShortcode(name, tpl string) error
Partial(name string, contextList ...interface{}) template.HTML
AddLateTemplate(name, tpl string) error
LoadTemplates(absPath, prefix string)
PrintErrors()
Funcs(funcMap template.FuncMap)
MarkReady()
RebuildClone()
}
// TemplateFinder finds templates.
type TemplateFinder interface {
Lookup(name string) *TemplateAdapter
}
// Template is the common interface between text/template and html/template.
type Template interface {
Execute(wr io.Writer, data interface{}) error
Name() string
}
// TemplateExecutor adds some extras to Template.
type TemplateExecutor interface {
Template
ExecuteToString(data interface{}) (string, error)
Tree() string
}
// TemplateAdapter implements the TemplateExecutor interface.
type TemplateAdapter struct {
Template
}
// ExecuteToString executes the current template and returns the result as a
// string.
func (t *TemplateAdapter) ExecuteToString(data interface{}) (string, error) {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
if err := t.Execute(b, data); err != nil {
return "", err
}
return b.String(), nil
}
// Tree returns the template Parse tree as a string.
// Note: this isn't safe for parallel execution on the same template
// vs Lookup and Execute.
func (t *TemplateAdapter) Tree() string {
var tree *parse.Tree
switch tt := t.Template.(type) {
case *template.Template:
tree = tt.Tree
case *texttemplate.Template:
tree = tt.Tree
default:
panic("Unknown template")
}
if tree.Root == nil {
return ""
}
s := tree.Root.String()
return s
}
// TemplateTestMocker adds a way to override some template funcs during tests.
// The interface is named so it's not used in regular application code.
type TemplateTestMocker interface {
SetFuncs(funcMap map[string]interface{})
}

51
tpl/tplimpl/ace.go Normal file
View File

@@ -0,0 +1,51 @@
// 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 (
"path/filepath"
"strings"
"github.com/yosssi/ace"
)
func (t *templateHandler) 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.html.t.New(name), parsed, nil)
if err != nil {
t.errors = append(t.errors, &templateErr{name: name, err: err})
return err
}
return applyTemplateTransformersToHMLTTemplate(templ)
}

View File

@@ -19,7 +19,7 @@ import (
"github.com/eknkc/amber"
)
func (gt *GoHTMLTemplate) CompileAmberWithTemplate(b []byte, path string, t *template.Template) (*template.Template, error) {
func (t *templateHandler) compileAmberWithTemplate(b []byte, path string, templ *template.Template) (*template.Template, error) {
c := amber.New()
if err := c.ParseData(b, path); err != nil {
@@ -32,7 +32,7 @@ func (gt *GoHTMLTemplate) CompileAmberWithTemplate(b []byte, path string, t *tem
return nil, err
}
tpl, err := t.Funcs(gt.amberFuncMap).Parse(data)
tpl, err := templ.Funcs(t.amberFuncMap).Parse(data)
if err != nil {
return nil, err

View File

@@ -1,4 +1,4 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
// 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.
@@ -15,23 +15,39 @@ package tplimpl
import (
"html/template"
"io"
"os"
"path/filepath"
"strings"
"sync"
texttemplate "text/template"
"github.com/eknkc/amber"
"os"
"github.com/spf13/hugo/output"
"path/filepath"
"sync"
"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"
"github.com/spf13/hugo/tpl"
)
// TODO(bep) globals get rid of the rest of the jww.ERR etc.
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
@@ -41,8 +57,120 @@ type templateErr struct {
err error
}
type GoHTMLTemplate struct {
*template.Template
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
@@ -54,397 +182,201 @@ type GoHTMLTemplate struct {
// 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
func (t *htmlTemplates) setTemplateFuncster(f *templateFuncster) {
t.funcster = f
}
// 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,
func (t *htmlTemplates) Lookup(name string) *tpl.TemplateAdapter {
templ := t.lookup(name)
if templ == nil {
return nil
}
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
return &tpl.TemplateAdapter{templ}
}
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 {
// TODO(bep) output
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 {
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
}
}
// 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 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 *GoHTMLTemplate) GetClone() *template.Template {
return t.clone
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 *GoHTMLTemplate) RebuildClone() *template.Template {
t.clone = template.Must(t.cloneClone.Clone())
return t.clone
func (t *htmlTemplates) addTemplate(name, tpl string) error {
return t.addTemplateIn(t.t, name, tpl)
}
func (t *GoHTMLTemplate) LoadEmbedded() {
t.EmbedShortcodes()
t.EmbedTemplates()
func (t *htmlTemplates) addLateTemplate(name, tpl string) error {
return t.addTemplateIn(t.clone, name, tpl)
}
// MarkReady marks the template as "ready for execution". No changes allowed
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 *GoHTMLTemplate) MarkReady() {
if t.clone == nil {
t.clone = template.Must(t.Template.Clone())
t.cloneClone = template.Must(t.clone.Clone())
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())
}
}
func (t *GoHTMLTemplate) checkState() {
if t.clone != nil {
panic("template is cloned and cannot be modfified")
}
// 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 *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"
func isBaseTemplate(path string) bool {
return strings.Contains(path, baseFileBase)
}
func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
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 {
@@ -491,11 +423,12 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
relPath := path[li:]
descriptor := output.TemplateLookupDescriptor{
WorkingDir: workingDir,
LayoutDir: layoutDir,
RelPath: relPath,
Prefix: prefix,
Theme: t.PathSpec.Theme(),
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)
},
@@ -511,7 +444,7 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
return nil
}
if err := t.AddTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != 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)
}
@@ -523,16 +456,223 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {
}
}
func (t *GoHTMLTemplate) LoadTemplatesWithPrefix(absPath string, prefix string) {
t.loadTemplates(absPath, prefix)
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 *GoHTMLTemplate) LoadTemplates(absPath string) {
t.loadTemplates(absPath, "")
func (t *templateHandler) getTemplateHandler(name string) templateLoader {
if strings.HasPrefix(name, textTmplNamePrefix) {
return t.text
}
return t.html
}
func (t *GoHTMLTemplate) PrintErrors() {
for i, e := range t.errors {
t.Log.ERROR.Println(i, ":", e.err)
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)
}

View File

@@ -0,0 +1,86 @@
// 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 (
"fmt"
"html/template"
"strings"
bp "github.com/spf13/hugo/bufferpool"
"image"
"github.com/spf13/hugo/deps"
)
// Some of the template funcs are'nt entirely stateless.
type templateFuncster struct {
funcMap template.FuncMap
cachedPartials partialCache
image *imageHandler
// Make sure each funcster gets its own TemplateFinder to get
// proper text and HTML template separation.
Tmpl templateFuncsterTemplater
*deps.Deps
}
func newTemplateFuncster(deps *deps.Deps, t templateFuncsterTemplater) *templateFuncster {
return &templateFuncster{
Deps: deps,
Tmpl: t,
cachedPartials: partialCache{p: make(map[string]interface{})},
image: &imageHandler{fs: deps.Fs, imageConfigCache: map[string]image.Config{}},
}
}
// Partial executes the named partial and returns either a string,
// when called from text/template, for or a template.HTML.
func (t *templateFuncster) partial(name string, contextList ...interface{}) (interface{}, error) {
if strings.HasPrefix("partials/", name) {
name = name[8:]
}
var context interface{}
if len(contextList) == 0 {
context = nil
} else {
context = contextList[0]
}
for _, n := range []string{"partials/" + name, "theme/partials/" + name} {
templ := t.Tmpl.Lookup(n)
if templ != nil {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
if err := templ.Execute(b, context); err != nil {
return "", err
}
switch t.Tmpl.(type) {
case *htmlTemplates:
return template.HTML(b.String()), nil
case *textTemplates:
return b.String(), nil
default:
panic("Unknown type")
}
}
}
return "", fmt.Errorf("Partial %q not found", name)
}

View File

@@ -0,0 +1,59 @@
// 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 (
"github.com/spf13/hugo/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 {
newTmpl := newTemplateAdapter(deps)
deps.Tmpl = newTmpl
newTmpl.initFuncs()
newTmpl.loadEmbedded()
if deps.WithTemplate != nil {
err := deps.WithTemplate(newTmpl)
if err != nil {
newTmpl.addError("init", err)
}
}
newTmpl.MarkReady()
return nil
}
// Clone clones.
func (*TemplateProvider) Clone(d *deps.Deps) error {
t := d.Tmpl.(*templateHandler)
clone := t.clone(d)
d.Tmpl = clone
clone.MarkReady()
return nil
}

View File

@@ -17,6 +17,7 @@ import (
"errors"
"html/template"
"strings"
texttemplate "text/template"
"text/template/parse"
)
@@ -35,32 +36,57 @@ var paramsPaths = [][]string{
}
type templateContext struct {
decl decl
templ *template.Template
visited map[string]bool
decl decl
visited map[string]bool
lookupFn func(name string) *parse.Tree
}
func (c templateContext) getIfNotVisited(name string) *template.Template {
func (c templateContext) getIfNotVisited(name string) *parse.Tree {
if c.visited[name] {
return nil
}
c.visited[name] = true
return c.templ.Lookup(name)
return c.lookupFn(name)
}
func newTemplateContext(templ *template.Template) *templateContext {
return &templateContext{templ: templ, decl: make(map[string]string), visited: make(map[string]bool)}
func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext {
return &templateContext{lookupFn: lookupFn, decl: make(map[string]string), visited: make(map[string]bool)}
}
func applyTemplateTransformers(templ *template.Template) error {
if templ == nil || templ.Tree == nil {
func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree {
return func(nn string) *parse.Tree {
tt := templ.Lookup(nn)
if tt != nil {
return tt.Tree
}
return nil
}
}
func applyTemplateTransformersToHMLTTemplate(templ *template.Template) error {
return applyTemplateTransformers(templ.Tree, createParseTreeLookup(templ))
}
func applyTemplateTransformersToTextTemplate(templ *texttemplate.Template) error {
return applyTemplateTransformers(templ.Tree,
func(nn string) *parse.Tree {
tt := templ.Lookup(nn)
if tt != nil {
return tt.Tree
}
return nil
})
}
func applyTemplateTransformers(templ *parse.Tree, lookupFn func(name string) *parse.Tree) error {
if templ == nil {
return errors.New("expected template, but none provided")
}
c := newTemplateContext(templ)
c := newTemplateContext(lookupFn)
c.paramsKeysToLower(templ.Tree.Root)
c.paramsKeysToLower(templ.Root)
return nil
}
@@ -84,7 +110,7 @@ func (c *templateContext) paramsKeysToLower(n parse.Node) {
case *parse.TemplateNode:
subTempl := c.getIfNotVisited(x.Name)
if subTempl != nil {
c.paramsKeysToLowerForNodes(subTempl.Tree.Root)
c.paramsKeysToLowerForNodes(subTempl.Root)
}
case *parse.PipeNode:
for i, elem := range x.Decl {

View File

@@ -115,13 +115,13 @@ F3: {{ Echo (printf "themes/%s-theme" .Site.Params.LOWER) }}
func TestParamsKeysToLower(t *testing.T) {
t.Parallel()
require.Error(t, applyTemplateTransformers(nil))
require.Error(t, applyTemplateTransformers(nil, nil))
templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl)
require.NoError(t, err)
c := newTemplateContext(templ)
c := newTemplateContext(createParseTreeLookup(templ))
require.Equal(t, -1, c.decl.indexOfReplacementStart([]string{}))
@@ -185,7 +185,7 @@ func BenchmarkTemplateParamsKeysToLower(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
c := newTemplateContext(templates[i])
c := newTemplateContext(createParseTreeLookup(templates[i]))
c.paramsKeysToLower(templ.Tree.Root)
}
}
@@ -214,7 +214,7 @@ Blue: {{ $__amber_1.Blue}}
require.NoError(t, err)
c := newTemplateContext(templ)
c := newTemplateContext(createParseTreeLookup(templ))
c.paramsKeysToLower(templ.Tree.Root)
@@ -254,7 +254,7 @@ P2: {{ .Params.LOWER }}
require.NoError(t, err)
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
c := newTemplateContext(overlayTpl)
c := newTemplateContext(createParseTreeLookup(overlayTpl))
c.paramsKeysToLower(overlayTpl.Tree.Root)
@@ -284,7 +284,7 @@ func TestTransformRecursiveTemplate(t *testing.T) {
templ, err := template.New("foo").Parse(recursive)
require.NoError(t, err)
c := newTemplateContext(templ)
c := newTemplateContext(createParseTreeLookup(templ))
c.paramsKeysToLower(templ.Tree.Root)
}

View File

@@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// 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.
@@ -13,17 +13,12 @@
package tplimpl
type Tmpl struct {
Name string
Data string
}
func (t *GoHTMLTemplate) EmbedShortcodes() {
t.AddInternalShortcode("ref.html", `{{ .Get 0 | ref .Page }}`)
t.AddInternalShortcode("relref.html", `{{ .Get 0 | relref .Page }}`)
t.AddInternalShortcode("highlight.html", `{{ if len .Params | eq 2 }}{{ highlight .Inner (.Get 0) (.Get 1) }}{{ else }}{{ highlight .Inner (.Get 0) "" }}{{ end }}`)
t.AddInternalShortcode("test.html", `This is a simple Test`)
t.AddInternalShortcode("figure.html", `<!-- image -->
func (t *templateHandler) embedShortcodes() {
t.addInternalShortcode("ref.html", `{{ .Get 0 | ref .Page }}`)
t.addInternalShortcode("relref.html", `{{ .Get 0 | relref .Page }}`)
t.addInternalShortcode("highlight.html", `{{ if len .Params | eq 2 }}{{ highlight .Inner (.Get 0) (.Get 1) }}{{ else }}{{ highlight .Inner (.Get 0) "" }}{{ end }}`)
t.addInternalShortcode("test.html", `This is a simple Test`)
t.addInternalShortcode("figure.html", `<!-- image -->
<figure {{ with .Get "class" }}class="{{.}}"{{ end }}>
{{ with .Get "link"}}<a href="{{.}}">{{ end }}
<img src="{{ .Get "src" }}" {{ if or (.Get "alt") (.Get "caption") }}alt="{{ with .Get "alt"}}{{.}}{{else}}{{ .Get "caption" }}{{ end }}" {{ end }}{{ with .Get "width" }}width="{{.}}" {{ end }}/>
@@ -41,8 +36,8 @@ func (t *GoHTMLTemplate) EmbedShortcodes() {
{{ end }}
</figure>
<!-- image -->`)
t.AddInternalShortcode("speakerdeck.html", "<script async class='speakerdeck-embed' data-id='{{ index .Params 0 }}' data-ratio='1.33333333333333' src='//speakerdeck.com/assets/embed.js'></script>")
t.AddInternalShortcode("youtube.html", `{{ if .IsNamedParams }}
t.addInternalShortcode("speakerdeck.html", "<script async class='speakerdeck-embed' data-id='{{ index .Params 0 }}' data-ratio='1.33333333333333' src='//speakerdeck.com/assets/embed.js'></script>")
t.addInternalShortcode("youtube.html", `{{ if .IsNamedParams }}
<div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}>
<iframe src="//www.youtube.com/embed/{{ .Get "id" }}?{{ with .Get "autoplay" }}{{ if eq . "true" }}autoplay=1{{ end }}{{ end }}"
{{ if not (.Get "class") }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}allowfullscreen frameborder="0"></iframe>
@@ -51,21 +46,21 @@ func (t *GoHTMLTemplate) EmbedShortcodes() {
<iframe src="//www.youtube.com/embed/{{ .Get 0 }}" {{ if len .Params | eq 1 }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}allowfullscreen frameborder="0"></iframe>
</div>
{{ end }}`)
t.AddInternalShortcode("vimeo.html", `{{ if .IsNamedParams }}<div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}>
t.addInternalShortcode("vimeo.html", `{{ if .IsNamedParams }}<div {{ if .Get "class" }}class="{{ .Get "class" }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}>
<iframe src="//player.vimeo.com/video/{{ .Get "id" }}" {{ if not (.Get "class") }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
</div>{{ else }}
<div {{ if len .Params | eq 2 }}class="{{ .Get 1 }}"{{ else }}style="position: relative; padding-bottom: 56.25%; padding-top: 30px; height: 0; overflow: hidden;"{{ end }}>
<iframe src="//player.vimeo.com/video/{{ .Get 0 }}" {{ if len .Params | eq 1 }}style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;" {{ end }}webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
</div>
{{ end }}`)
t.AddInternalShortcode("gist.html", `<script src="//gist.github.com/{{ index .Params 0 }}/{{ index .Params 1 }}.js{{if len .Params | eq 3 }}?file={{ index .Params 2 }}{{end}}"></script>`)
t.AddInternalShortcode("tweet.html", `{{ (getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" (index .Params 0)).html | safeHTML }}`)
t.AddInternalShortcode("instagram.html", `{{ if len .Params | eq 2 }}{{ if eq (.Get 1) "hidecaption" }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=1" }}{{ .html | safeHTML }}{{ end }}{{ end }}{{ else }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=0" }}{{ .html | safeHTML }}{{ end }}{{ end }}`)
t.addInternalShortcode("gist.html", `<script src="//gist.github.com/{{ index .Params 0 }}/{{ index .Params 1 }}.js{{if len .Params | eq 3 }}?file={{ index .Params 2 }}{{end}}"></script>`)
t.addInternalShortcode("tweet.html", `{{ (getJSON "https://api.twitter.com/1/statuses/oembed.json?id=" (index .Params 0)).html | safeHTML }}`)
t.addInternalShortcode("instagram.html", `{{ if len .Params | eq 2 }}{{ if eq (.Get 1) "hidecaption" }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=1" }}{{ .html | safeHTML }}{{ end }}{{ end }}{{ else }}{{ with getJSON "https://api.instagram.com/oembed/?url=https://instagram.com/p/" (index .Params 0) "/&hidecaption=0" }}{{ .html | safeHTML }}{{ end }}{{ end }}`)
}
func (t *GoHTMLTemplate) EmbedTemplates() {
func (t *templateHandler) embedTemplates() {
t.AddInternalTemplate("_default", "rss.xml", `<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
t.addInternalTemplate("_default", "rss.xml", `<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }}</title>
<link>{{ .Permalink }}</link>
@@ -92,7 +87,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
</channel>
</rss>`)
t.AddInternalTemplate("_default", "sitemap.xml", `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
t.addInternalTemplate("_default", "sitemap.xml", `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{{ range .Data.Pages }}
<url>
<loc>{{ .Permalink }}</loc>{{ if not .Lastmod.IsZero }}
@@ -104,7 +99,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
</urlset>`)
// For multilanguage sites
t.AddInternalTemplate("_default", "sitemapindex.xml", `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
t.addInternalTemplate("_default", "sitemapindex.xml", `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{{ range . }}
<sitemap>
<loc>{{ .SitemapAbsURL }}</loc>
@@ -116,7 +111,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
</sitemapindex>
`)
t.AddInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }}
t.addInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }}
{{ if gt $pag.TotalPages 1 }}
<ul class="pagination">
{{ with $pag.First }}
@@ -144,7 +139,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
</ul>
{{ end }}`)
t.AddInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}<div id="disqus_thread"></div>
t.addInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}<div id="disqus_thread"></div>
<script type="text/javascript">
var disqus_shortname = '{{ .Site.DisqusShortname }}';
var disqus_identifier = '{{with .GetParam "disqus_identifier" }}{{ . }}{{ else }}{{ .Permalink }}{{end}}';
@@ -161,7 +156,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
<a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>{{end}}`)
// Add SEO & Social metadata
t.AddInternalTemplate("", "opengraph.html", `<meta property="og:title" content="{{ .Title }}" />
t.addInternalTemplate("", "opengraph.html", `<meta property="og:title" content="{{ .Title }}" />
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}" />
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
<meta property="og:url" content="{{ .Permalink }}" />
@@ -205,7 +200,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
<!-- Facebook Page Admin ID for Domain Insights -->
{{ with .Site.Social.facebook_admin }}<meta property="fb:admins" content="{{ . }}" />{{ end }}`)
t.AddInternalTemplate("", "twitter_cards.html", `{{ if .IsPage }}
t.addInternalTemplate("", "twitter_cards.html", `{{ if .IsPage }}
{{ with .Params.images }}
<!-- Twitter summary card with large image must be at least 280x150px -->
<meta name="twitter:card" content="summary_large_image"/>
@@ -223,11 +218,11 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
{{ with .twitter }}<meta name="twitter:creator" content="@{{ . }}"/>{{ end }}
{{ end }}{{ end }}`)
t.AddInternalTemplate("", "google_news.html", `{{ if .IsPage }}{{ with .Params.news_keywords }}
t.addInternalTemplate("", "google_news.html", `{{ if .IsPage }}{{ with .Params.news_keywords }}
<meta name="news_keywords" content="{{ range $i, $kw := first 10 . }}{{ if $i }},{{ end }}{{ $kw }}{{ end }}" />
{{ end }}{{ end }}`)
t.AddInternalTemplate("", "schema.html", `{{ with .Site.Social.GooglePlus }}<link rel="publisher" href="{{ . }}"/>{{ end }}
t.addInternalTemplate("", "schema.html", `{{ with .Site.Social.GooglePlus }}<link rel="publisher" href="{{ . }}"/>{{ end }}
<meta itemprop="name" content="{{ .Title }}">
<meta itemprop="description" content="{{ with .Description }}{{ . }}{{ else }}{{if .IsPage}}{{ .Summary }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}{{ end }}">
@@ -243,7 +238,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
<meta itemprop="keywords" content="{{ range $plural, $terms := .Site.Taxonomies }}{{ range $term, $val := $terms }}{{ printf "%s," $term }}{{ end }}{{ end }}" />
{{ end }}`)
t.AddInternalTemplate("", "google_analytics.html", `{{ with .Site.GoogleAnalytics }}
t.addInternalTemplate("", "google_analytics.html", `{{ with .Site.GoogleAnalytics }}
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
@@ -255,7 +250,7 @@ ga('send', 'pageview');
</script>
{{ end }}`)
t.AddInternalTemplate("", "google_analytics_async.html", `{{ with .Site.GoogleAnalytics }}
t.addInternalTemplate("", "google_analytics_async.html", `{{ with .Site.GoogleAnalytics }}
<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', '{{ . }}', 'auto');
@@ -264,5 +259,5 @@ ga('send', 'pageview');
<script async src='//www.google-analytics.com/analytics.js'></script>
{{ end }}`)
t.AddInternalTemplate("_default", "robots.txt", "User-agent: *")
t.addInternalTemplate("_default", "robots.txt", "User-agent: *")
}

View File

@@ -45,7 +45,6 @@ import (
"github.com/bep/inflect"
"github.com/spf13/afero"
"github.com/spf13/cast"
"github.com/spf13/hugo/deps"
"github.com/spf13/hugo/helpers"
jww "github.com/spf13/jwalterweatherman"
@@ -55,22 +54,6 @@ import (
_ "image/png"
)
// Some of the template funcs are'nt entirely stateless.
type templateFuncster struct {
funcMap template.FuncMap
cachedPartials partialCache
image *imageHandler
*deps.Deps
}
func newTemplateFuncster(deps *deps.Deps) *templateFuncster {
return &templateFuncster{
Deps: deps,
cachedPartials: partialCache{p: make(map[string]template.HTML)},
image: &imageHandler{fs: deps.Fs, imageConfigCache: map[string]image.Config{}},
}
}
// eq returns the boolean truth of arg1 == arg2.
func eq(x, y interface{}) bool {
normalize := func(v interface{}) interface{} {
@@ -1558,13 +1541,13 @@ func replace(a, b, c interface{}) (string, error) {
// partialCache represents a cache of partials protected by a mutex.
type partialCache struct {
sync.RWMutex
p map[string]template.HTML
p map[string]interface{}
}
// Get retrieves partial output from the cache based upon the partial name.
// If the partial is not found in the cache, the partial is rendered and added
// to the cache.
func (t *templateFuncster) Get(key, name string, context interface{}) (p template.HTML) {
func (t *templateFuncster) Get(key, name string, context interface{}) (p interface{}, err error) {
var ok bool
t.cachedPartials.RLock()
@@ -1572,13 +1555,13 @@ func (t *templateFuncster) Get(key, name string, context interface{}) (p templat
t.cachedPartials.RUnlock()
if ok {
return p
return
}
t.cachedPartials.Lock()
if p, ok = t.cachedPartials.p[key]; !ok {
t.cachedPartials.Unlock()
p = t.Tmpl.Partial(name, context)
p, err = t.partial(name, context)
t.cachedPartials.Lock()
t.cachedPartials.p[key] = p
@@ -1586,14 +1569,14 @@ func (t *templateFuncster) Get(key, name string, context interface{}) (p templat
}
t.cachedPartials.Unlock()
return p
return
}
// partialCached executes and caches partial templates. An optional variant
// string parameter (a string slice actually, but be only use a variadic
// argument to make it optional) can be passed so that a given partial can have
// multiple uses. The cache is created with name+variant as the key.
func (t *templateFuncster) partialCached(name string, context interface{}, variant ...string) template.HTML {
func (t *templateFuncster) partialCached(name string, context interface{}, variant ...string) (interface{}, error) {
key := name
if len(variant) > 0 {
for i := 0; i < len(variant); i++ {
@@ -2195,7 +2178,7 @@ func (t *templateFuncster) initFuncMap() {
"mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') },
"ne": ne,
"now": func() time.Time { return time.Now() },
"partial": t.Tmpl.Partial,
"partial": t.partial,
"partialCached": t.partialCached,
"plainify": plainify,
"pluralize": pluralize,
@@ -2249,5 +2232,5 @@ func (t *templateFuncster) initFuncMap() {
}
t.funcMap = funcMap
t.Tmpl.Funcs(funcMap)
t.Tmpl.setFuncs(funcMap)
}

View File

@@ -281,8 +281,8 @@ urlize: bat-man
v.Set("CurrentContentLanguage", helpers.NewLanguage("en", v))
config := newDepsConfig(v)
config.WithTemplate = func(templ tpl.Template) error {
if _, err := templ.New("test").Parse(in); err != nil {
config.WithTemplate = func(templ tpl.TemplateHandler) error {
if err := templ.AddTemplate("test", in); err != nil {
t.Fatal("Got error on parse", err)
}
return nil
@@ -2858,6 +2858,56 @@ func TestReadFile(t *testing.T) {
}
}
func TestPartialHTMLAndText(t *testing.T) {
t.Parallel()
config := newDepsConfig(viper.New())
data := struct {
Name string
}{
Name: "a+b+c", // This should get encoded in HTML.
}
config.WithTemplate = func(templ tpl.TemplateHandler) error {
if err := templ.AddTemplate("htmlTemplate.html", `HTML Test Partial: {{ partial "test.foo" . -}}`); err != nil {
return err
}
if err := templ.AddTemplate("_text/textTemplate.txt", `Text Test Partial: {{ partial "test.foo" . -}}`); err != nil {
return err
}
// Use "foo" here to say that the extension doesn't really matter in this scenario.
// It will look for templates in "partials/test.foo" and "partials/test.foo.html".
if err := templ.AddTemplate("partials/test.foo", "HTML Name: {{ .Name }}"); err != nil {
return err
}
if err := templ.AddTemplate("_text/partials/test.foo", "Text Name: {{ .Name }}"); err != nil {
return err
}
return nil
}
de, err := deps.New(config)
require.NoError(t, err)
require.NoError(t, de.LoadResources())
templ := de.Tmpl.Lookup("htmlTemplate.html")
require.NotNil(t, templ)
resultHTML, err := templ.ExecuteToString(data)
require.NoError(t, err)
templ = de.Tmpl.Lookup("_text/textTemplate.txt")
require.NotNil(t, templ)
resultText, err := templ.ExecuteToString(data)
require.NoError(t, err)
require.Contains(t, resultHTML, "HTML Test Partial: HTML Name: a&#43;b&#43;c")
require.Contains(t, resultText, "Text Test Partial: Text Name: a+b+c")
}
func TestPartialCached(t *testing.T) {
t.Parallel()
testCases := []struct {
@@ -2893,7 +2943,7 @@ func TestPartialCached(t *testing.T) {
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tpl.Template) error {
config.WithTemplate = func(templ tpl.TemplateHandler) error {
err := templ.AddTemplate("testroot", tmp)
if err != nil {
return err
@@ -2933,7 +2983,7 @@ func TestPartialCached(t *testing.T) {
func BenchmarkPartial(b *testing.B) {
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tpl.Template) error {
config.WithTemplate = func(templ tpl.TemplateHandler) error {
err := templ.AddTemplate("testroot", `{{ partial "bench1" . }}`)
if err != nil {
return err
@@ -2965,7 +3015,7 @@ func BenchmarkPartial(b *testing.B) {
func BenchmarkPartialCached(b *testing.B) {
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tpl.Template) error {
config.WithTemplate = func(templ tpl.TemplateHandler) error {
err := templ.AddTemplate("testroot", `{{ partialCached "bench1" . }}`)
if err != nil {
return err
@@ -3010,12 +3060,12 @@ func newTestFuncsterWithViper(v *viper.Viper) *templateFuncster {
panic(err)
}
return d.Tmpl.(*GoHTMLTemplate).funcster
return d.Tmpl.(*templateHandler).html.funcster
}
func newTestTemplate(t *testing.T, name, template string) *template.Template {
func newTestTemplate(t *testing.T, name, template string) tpl.Template {
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tpl.Template) error {
config.WithTemplate = func(templ tpl.TemplateHandler) error {
err := templ.AddTemplate(name, template)
if err != nil {
return err

View File

@@ -14,17 +14,10 @@
package tplimpl
import (
"bytes"
"errors"
"html/template"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/spf13/afero"
"github.com/spf13/hugo/deps"
"github.com/spf13/hugo/tpl"
@@ -32,223 +25,6 @@ import (
"github.com/stretchr/testify/require"
)
// Some tests for Issue #1178 -- Ace
func TestAceTemplates(t *testing.T) {
t.Parallel()
for i, this := range []struct {
basePath string
innerPath string
baseContent string
innerContent string
expect string
expectErr int
}{
{"", filepath.FromSlash("_default/single.ace"), "", "{{ . }}", "DATA", 0},
{filepath.FromSlash("_default/baseof.ace"), filepath.FromSlash("_default/single.ace"),
`= content main
h2 This is a content named "main" of an inner template. {{ . }}`,
`= doctype html
html lang=en
head
meta charset=utf-8
title Base and Inner Template
body
h1 This is a base template {{ . }}
= yield main`, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Base and Inner Template</title></head><body><h1>This is a base template DATA</h1></body></html>`, 0},
} {
for _, root := range []string{"", os.TempDir()} {
basePath := this.basePath
innerPath := this.innerPath
if basePath != "" && root != "" {
basePath = filepath.Join(root, basePath)
}
if innerPath != "" && root != "" {
innerPath = filepath.Join(root, innerPath)
}
d := "DATA"
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tpl.Template) error {
return templ.AddAceTemplate("mytemplate.ace", basePath, innerPath,
[]byte(this.baseContent), []byte(this.innerContent))
}
a, err := deps.New(config)
require.NoError(t, err)
if err := a.LoadResources(); err != nil {
t.Fatal(err)
}
templ := a.Tmpl.(*GoHTMLTemplate)
if len(templ.errors) > 0 && this.expectErr == 0 {
t.Errorf("Test %d with root '%s' errored: %v", i, root, templ.errors)
} else if len(templ.errors) == 0 && this.expectErr == 1 {
t.Errorf("#1 Test %d with root '%s' should have errored", i, root)
}
var buff bytes.Buffer
err = a.Tmpl.ExecuteTemplate(&buff, "mytemplate.html", d)
if err != nil && this.expectErr == 0 {
t.Errorf("Test %d with root '%s' errored: %s", i, root, err)
} else if err == nil && this.expectErr == 2 {
t.Errorf("#2 Test with root '%s' %d should have errored", root, i)
} else {
result := buff.String()
if result != this.expect {
t.Errorf("Test %d with root '%s' got\n%s\nexpected\n%s", i, root, result, this.expect)
}
}
}
}
}
func isAtLeastGo16() bool {
version := runtime.Version()
return strings.Contains(version, "1.6") || strings.Contains(version, "1.7")
}
func TestAddTemplateFileWithMaster(t *testing.T) {
t.Parallel()
if !isAtLeastGo16() {
t.Skip("This test only runs on Go >= 1.6")
}
for i, this := range []struct {
masterTplContent string
overlayTplContent string
writeSkipper int
expect interface{}
}{
{`A{{block "main" .}}C{{end}}C`, `{{define "main"}}B{{end}}`, 0, "ABC"},
{`A{{block "main" .}}C{{end}}C{{block "sub" .}}D{{end}}E`, `{{define "main"}}B{{end}}`, 0, "ABCDE"},
{`A{{block "main" .}}C{{end}}C{{block "sub" .}}D{{end}}E`, `{{define "main"}}B{{end}}{{define "sub"}}Z{{end}}`, 0, "ABCZE"},
{`tpl`, `tpl`, 1, false},
{`tpl`, `tpl`, 2, false},
{`{{.0.E}}`, `tpl`, 0, false},
{`tpl`, `{{.0.E}}`, 0, false},
} {
overlayTplName := "ot"
masterTplName := "mt"
finalTplName := "tp"
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tpl.Template) error {
err := templ.AddTemplateFileWithMaster(finalTplName, overlayTplName, masterTplName)
if b, ok := this.expect.(bool); ok && !b {
if err == nil {
t.Errorf("[%d] AddTemplateFileWithMaster didn't return an expected error", i)
}
} else {
if err != nil {
t.Errorf("[%d] AddTemplateFileWithMaster failed: %s", i, err)
return nil
}
resultTpl := templ.Lookup(finalTplName)
if resultTpl == nil {
t.Errorf("[%d] AddTemplateFileWithMaster: Result template not found", i)
return nil
}
var b bytes.Buffer
err := resultTpl.Execute(&b, nil)
if err != nil {
t.Errorf("[%d] AddTemplateFileWithMaster execute failed: %s", i, err)
return nil
}
resultContent := b.String()
if resultContent != this.expect {
t.Errorf("[%d] AddTemplateFileWithMaster got \n%s but expected \n%v", i, resultContent, this.expect)
}
}
return nil
}
if this.writeSkipper != 1 {
afero.WriteFile(config.Fs.Source, masterTplName, []byte(this.masterTplContent), 0644)
}
if this.writeSkipper != 2 {
afero.WriteFile(config.Fs.Source, overlayTplName, []byte(this.overlayTplContent), 0644)
}
deps.New(config)
}
}
// A Go stdlib test for linux/arm. Will remove later.
// See #1771
func TestBigIntegerFunc(t *testing.T) {
t.Parallel()
var func1 = func(v int64) error {
return nil
}
var funcs = map[string]interface{}{
"A": func1,
}
tpl, err := template.New("foo").Funcs(funcs).Parse("{{ A 3e80 }}")
if err != nil {
t.Fatal("Parse failed:", err)
}
err = tpl.Execute(ioutil.Discard, "foo")
if err == nil {
t.Fatal("Execute should have failed")
}
t.Log("Got expected error:", err)
}
// A Go stdlib test for linux/arm. Will remove later.
// See #1771
type BI struct {
}
func (b BI) A(v int64) error {
return nil
}
func TestBigIntegerMethod(t *testing.T) {
t.Parallel()
data := &BI{}
tpl, err := template.New("foo2").Parse("{{ .A 3e80 }}")
if err != nil {
t.Fatal("Parse failed:", err)
}
err = tpl.ExecuteTemplate(ioutil.Discard, "foo2", data)
if err == nil {
t.Fatal("Execute should have failed")
}
t.Log("Got expected error:", err)
}
// Test for bugs discovered by https://github.com/dvyukov/go-fuzz
func TestTplGoFuzzReports(t *testing.T) {
t.Parallel()
@@ -285,7 +61,7 @@ func TestTplGoFuzzReports(t *testing.T) {
config := newDepsConfig(viper.New())
config.WithTemplate = func(templ tpl.Template) error {
config.WithTemplate = func(templ tpl.TemplateHandler) error {
return templ.AddTemplate("fuzz", this.data)
}
@@ -293,7 +69,7 @@ func TestTplGoFuzzReports(t *testing.T) {
require.NoError(t, err)
require.NoError(t, de.LoadResources())
templ := de.Tmpl.(*GoHTMLTemplate)
templ := de.Tmpl.(*templateHandler)
if len(templ.errors) > 0 && this.expectErr == 0 {
t.Errorf("Test %d errored: %v", i, templ.errors)
@@ -301,7 +77,9 @@ func TestTplGoFuzzReports(t *testing.T) {
t.Errorf("#1 Test %d should have errored", i)
}
err = de.Tmpl.ExecuteTemplate(ioutil.Discard, "fuzz", d)
tt := de.Tmpl.Lookup("fuzz")
require.NotNil(t, tt)
err = tt.Execute(ioutil.Discard, d)
if err != nil && this.expectErr == 0 {
t.Fatalf("Test %d errored: %s", i, err)