mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-30 22:39:58 +02:00
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:
111
tpl/template.go
111
tpl/template.go
@@ -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
51
tpl/tplimpl/ace.go
Normal 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)
|
||||
}
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
86
tpl/tplimpl/templateFuncster.go
Normal file
86
tpl/tplimpl/templateFuncster.go
Normal 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)
|
||||
}
|
59
tpl/tplimpl/templateProvider.go
Normal file
59
tpl/tplimpl/templateProvider.go
Normal 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
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
|
||||
}
|
||||
|
@@ -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: *")
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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+b+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
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user