ref: 8b5b558bb515e80da640f5e114169874771b61e4
parent: 27610ddd011e8172d00e02275f948c3f1ed43e4f
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Mon Mar 27 16:43:49 EDT 2017
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
--- a/deps/deps.go
+++ b/deps/deps.go
@@ -20,7 +20,7 @@
Log *jww.Notepad `json:"-"`
// The templates to use.
- Tmpl tpl.Template `json:"-"`
+ Tmpl tpl.TemplateHandler `json:"-"`
// The file systems to use.
Fs *hugofs.Fs `json:"-"`
@@ -40,7 +40,7 @@
Language *helpers.Language
templateProvider ResourceProvider
- WithTemplate func(templ tpl.Template) error `json:"-"`
+ WithTemplate func(templ tpl.TemplateHandler) error `json:"-"`
translationProvider ResourceProvider
}
@@ -158,7 +158,7 @@
// Template handling.
TemplateProvider ResourceProvider
- WithTemplate func(templ tpl.Template) error
+ WithTemplate func(templ tpl.TemplateHandler) error
// i18n handling.
TranslationProvider ResourceProvider
--- a/hugolib/alias.go
+++ b/hugolib/alias.go
@@ -22,6 +22,8 @@
"runtime"
"strings"
+ "github.com/spf13/hugo/tpl"
+
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/hugo/helpers"
@@ -35,6 +37,7 @@
var defaultAliasTemplates *template.Template
func init() {+ //TODO(bep) consolidate
defaultAliasTemplates = template.New("") template.Must(defaultAliasTemplates.New("alias").Parse(alias)) template.Must(defaultAliasTemplates.New("alias-xhtml").Parse(aliasXHtml))@@ -41,12 +44,12 @@
}
type aliasHandler struct {- Templates *template.Template
+ t tpl.TemplateHandler
log *jww.Notepad
allowRoot bool
}
-func newAliasHandler(t *template.Template, l *jww.Notepad, allowRoot bool) aliasHandler {+func newAliasHandler(t tpl.TemplateHandler, l *jww.Notepad, allowRoot bool) aliasHandler { return aliasHandler{t, l, allowRoot}}
@@ -56,12 +59,19 @@
t = "alias-xhtml"
}
- template := defaultAliasTemplates
- if a.Templates != nil {- template = a.Templates
- t = "alias.html"
+ var templ *tpl.TemplateAdapter
+
+ if a.t != nil {+ templ = a.t.Lookup("alias.html")}
+ if templ == nil {+ def := defaultAliasTemplates.Lookup(t)
+ if def != nil {+ templ = &tpl.TemplateAdapter{Template: def}+ }
+
+ }
data := struct {Permalink string
Page *Page
@@ -71,7 +81,7 @@
}
buffer := new(bytes.Buffer)
- err := template.ExecuteTemplate(buffer, t, data)
+ err := templ.Execute(buffer, data)
if err != nil {return nil, err
}
@@ -83,8 +93,7 @@
}
func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, p *Page) (err error) {-
- handler := newAliasHandler(s.Tmpl.Lookup("alias.html"), s.Log, allowRoot)+ handler := newAliasHandler(s.Tmpl, s.Log, allowRoot)
isXHTML := strings.HasSuffix(path, ".xhtml")
--- a/hugolib/embedded_shortcodes_test.go
+++ b/hugolib/embedded_shortcodes_test.go
@@ -335,8 +335,8 @@
th = testHelper{cfg, fs, t})
- withTemplate := func(templ tpl.Template) error {- templ.Funcs(tweetFuncMap)
+ withTemplate := func(templ tpl.TemplateHandler) error {+ templ.(tpl.TemplateTestMocker).SetFuncs(tweetFuncMap)
return nil
}
@@ -390,8 +390,8 @@
om:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;\"\u003e\u003ca href=\"https://www.instagram.com/p/BMokmydjG-M/\" style=\" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;\" target=\"_blank\"\u003eA photo posted by Instagram (@instagram)\u003c/a\u003e on \u003ctime style=\" font-family:Arial,sans-serif; font-size:14px; line-height:17px;\" datetime=\"2016-11-10T15:02:28+00:00\"\u003eNov 10, 2016 at 7:02am PST\u003c/time\u003e\u003c/p\u003e\u003c/div\u003e\u003c/blockquote\u003e\n\u003cscript async defer src=\"//platform.instagram.com/en_US/embeds.js\"\u003e\u003c/script\u003e", "width": 658, "version": "1.0", "author_url": "https://www.instagram.com/instagram", "author_id": 25025320, "type": "rich"}`,
`(?s)<blockquote class="instagram-media" data-instgrm-version="7" style=" background:#FFF; border:0; .*<script async defer src="//platform.instagram.com/en_US/embeds.js"></script>`,
},
-dding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;\"\u003e\u003ca href=\"https://www.instagram.com/p/BMokmydjG-M/\" style=\" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;\" target=\"_blank\"\u003eA photo posted by Instagram (@instagram)\u003c/a\u003e on \u003ctime style=\" font-family:Arial,sans-serif; font-size:14px; line-height:17px;\" datetime=\"2016-11-10T15:02:28+00:00\"\u003eNov 10, 2016 at 7:02am PST\u003c/time\u003e\u003c/p\u003e\u003c/div\u003e\u003c/blockquote\u003e\n\u003cscript async defer src=\"//platform.instagram.com/en_US/embeds.js\"\u003e\u003c/script\u003e", "width": 658, "version": "1.0", "author_url": "https://www.instagram.com/instagram", "author_id": 25025320, "type": "rich"}`,
- `(?s)<blockquote class="instagram-media" data-instgrm-version="7" style=" background:#FFF; border:0; .*<script async defer src="//platform.instagram.com/en_US/embeds.js"></script>`,
+dding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;\"\u003e\u003ca href=\"https://www.instagram.com/p/BMokmydjG-M/\" style=\" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;\" target=\"_blank\"\u003eA photo posted by Instagram (@instagram)\u003c/a\u003e on \u003ctime style=\" font-family:Arial,sans-serif; font-size:14px; line-height:17px;\" datetime=\"2016-11-10T15:02:28+00:00\"\u003eNov 10, 2016 at 7:02am PST\u003c/time\u003e\u003c/p\u003e\u003c/div\u003e\u003c/blockquote\u003e\n\u003cscript async defer src=\"//platform.instagram.com/en_US/embeds.js\"\u003e\u003c/script\u003e", "width": 658, "version": "1.0", "author_url": "https://www.instagram.com/instagram", "author_id": 25025320, "type": "rich"}`,
+ `(?s)<blockquote class="instagram-media" data-instgrm-version="7" style=" background:#FFF; border:0; .*<script async defer src="//platform.instagram.com/en_US/embeds.js"></script>`,
03e\u003ca href=\"https://www.instagram.com/p/BMokmydjG-M/\" style=\" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;\" target=\"_blank\"\u003eA photo posted by Instagram (@instagram)\u003c/a\u003e on \u003ctime style=\" font-family:Arial,sans-serif; font-size:14px; line-height:17px;\" datetime=\"2016-11-10T15:02:28+00:00\"\u003eNov 10, 2016 at 7:02am PST\u003c/time\u003e\u003c/p\u003e\u003c/div\u003e\u003c/blockquote\u003e\n\u003cscript async defer src=\"//platform.instagram.com/en_US/embeds.js\"\u003e\u003c/script\u003e", "width": 658, "version": "1.0", "author_url": "https://www.instagram.com/instagram", "author_id": 25025320, "type": "rich"}`,
`(?s)<blockquote class="instagram-media" data-instgrm-version="7" style=" background:#FFF; border:0; .*<script async defer src="//platform.instagram.com/en_US/embeds.js"></script>`,
},
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -129,11 +129,11 @@
return newHugoSites(cfg, sites...)
}
-func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.Template) error) func(templ tpl.Template) error {- return func(templ tpl.Template) error {- templ.LoadTemplates(s.PathSpec.GetLayoutDirPath())
+func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.TemplateHandler) error) func(templ tpl.TemplateHandler) error {+ return func(templ tpl.TemplateHandler) error {+ templ.LoadTemplates(s.PathSpec.GetLayoutDirPath(), "")
if s.PathSpec.ThemeSet() {- templ.LoadTemplatesWithPrefix(s.PathSpec.GetThemeDir()+"/layouts", "theme")
+ templ.LoadTemplates(s.PathSpec.GetThemeDir()+"/layouts", "theme")
}
for _, wt := range withTemplates {--- a/hugolib/menu_test.go
+++ b/hugolib/menu_test.go
@@ -18,6 +18,8 @@
"fmt"
+ "github.com/spf13/afero"
+
"github.com/stretchr/testify/require"
)
@@ -42,7 +44,7 @@
sectionPagesMenu = "sect"
`
- th, h := newTestSitesFromConfig(t, siteConfig,
+ th, h := newTestSitesFromConfig(t, afero.NewMemMapFs(), siteConfig,
"layouts/partials/menu.html", `{{- $p := .page -}} {{- $m := .menu -}} {{ range (index $p.Site.Menus $m) -}}--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -1384,7 +1384,7 @@
if p.Kind == KindPage { if !p.IsRenderable() {self := "__" + p.UniqueID()
- _, err := p.s.Tmpl.GetClone().New(self).Parse(string(p.Content))
+ err := p.s.Tmpl.AddLateTemplate(self, string(p.Content))
if err != nil {return err
}
@@ -1391,6 +1391,7 @@
p.selfLayout = self
}
}
+
return nil
}
--- a/hugolib/page_output.go
+++ b/hugolib/page_output.go
@@ -110,9 +110,29 @@
l, err := p.layouts(layout...)
if err != nil { helpers.DistinctErrorLog.Printf("in .Render: Failed to resolve layout %q for page %q", layout, p.pathOrTitle())- return template.HTML("")+ return ""
}
- return p.s.Tmpl.ExecuteTemplateToHTML(p, l...)
+
+ for _, layout := range l {+ templ := p.s.Tmpl.Lookup(layout)
+ if templ == nil {+ // This is legacy from when we had only one output format and
+ // HTML templates only. Some have references to layouts without suffix.
+ // We default to good old HTML.
+ templ = p.s.Tmpl.Lookup(layout + ".html")
+ }
+ if templ != nil {+ res, err := templ.ExecuteToString(p)
+ if err != nil {+ helpers.DistinctErrorLog.Printf("in .Render: Failed to execute template %q for page %q", layout, p.pathOrTitle())+ return template.HTML("")+ }
+ return template.HTML(res)
+ }
+ }
+
+ return ""
+
}
func (p *Page) Render(layout ...string) template.HTML {--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -177,7 +177,7 @@
// to avoid potential costly look-aheads for closing tags we look inside the template itself
// we could change the syntax to self-closing tags, but that would make users cry
// the value found is cached
-func isInnerShortcode(t *template.Template) (bool, error) {+func isInnerShortcode(t tpl.TemplateExecutor) (bool, error) {isInnerShortcodeCache.RLock()
m, ok := isInnerShortcodeCache.m[t.Name()]
isInnerShortcodeCache.RUnlock()
@@ -188,10 +188,7 @@
isInnerShortcodeCache.Lock()
defer isInnerShortcodeCache.Unlock()
- if t.Tree == nil {- return false, errors.New("Template failed to compile")- }
- match, _ := regexp.MatchString("{{.*?\\.Inner.*?}}", t.Tree.Root.String())+ match, _ := regexp.MatchString("{{.*?\\.Inner.*?}}", t.Tree())isInnerShortcodeCache.m[t.Name()] = match
return match, nil
@@ -398,8 +395,6 @@
case tScName:
sc.name = currItem.val
tmpl := getShortcodeTemplate(sc.name, p.s.Tmpl)
- {- }
if tmpl == nil { return sc, fmt.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path())}
@@ -570,7 +565,10 @@
return source, nil
}
-func getShortcodeTemplate(name string, t tpl.Template) *template.Template {+func getShortcodeTemplate(name string, t tpl.TemplateHandler) *tpl.TemplateAdapter {+ isInnerShortcodeCache.RLock()
+ defer isInnerShortcodeCache.RUnlock()
+
if x := t.Lookup("shortcodes/" + name + ".html"); x != nil {return x
}
@@ -580,7 +578,7 @@
return t.Lookup("_internal/shortcodes/" + name + ".html")}
-func renderShortcodeWithPage(tmpl *template.Template, data *ShortcodeWithPage) string {+func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) string {buffer := bp.GetBuffer()
defer bp.PutBuffer(buffer)
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -30,7 +30,7 @@
)
// TODO(bep) remove
-func pageFromString(in, filename string, withTemplate ...func(templ tpl.Template) error) (*Page, error) {+func pageFromString(in, filename string, withTemplate ...func(templ tpl.TemplateHandler) error) (*Page, error) {s := newTestSite(nil)
if len(withTemplate) > 0 {// Have to create a new site
@@ -47,11 +47,11 @@
return s.NewPageFrom(strings.NewReader(in), filename)
}
-func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.Template) error) {+func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error) {CheckShortCodeMatchAndError(t, input, expected, withTemplate, false)
}
-func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.Template) error, expectError bool) {+func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) {cfg, fs := newTestCfg()
@@ -100,8 +100,9 @@
// Issue #929
func TestHyphenatedSC(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("hyphenated-video.html", `Playing Video {{ .Get 0 }}`)+ wt := func(tem tpl.TemplateHandler) error {+
+ tem.AddTemplate("_internal/shortcodes/hyphenated-video.html", `Playing Video {{ .Get 0 }}`)return nil
}
@@ -111,8 +112,8 @@
// Issue #1753
func TestNoTrailingNewline(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("a.html", `{{ .Get 0 }}`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/a.html", `{{ .Get 0 }}`)return nil
}
@@ -121,8 +122,8 @@
func TestPositionalParamSC(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("video.html", `Playing Video {{ .Get 0 }}`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 0 }}`)return nil
}
@@ -135,8 +136,8 @@
func TestPositionalParamIndexOutOfBounds(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("video.html", `Playing Video {{ .Get 1 }}`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 1 }}`)return nil
}
CheckShortCodeMatch(t, "{{< video 47238zzb >}}", "Playing Video error: index out of range for positional param at position 1", wt)@@ -146,8 +147,8 @@
func TestNamedParamSC(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("img.html", `<img{{ with .Get "src" }} src="{{.}}"{{end}}{{with .Get "class"}} class="{{.}}"{{end}}>`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/img.html", `<img{{ with .Get "src" }} src="{{.}}"{{end}}{{with .Get "class"}} class="{{.}}"{{end}}>`)return nil
}
CheckShortCodeMatch(t, `{{< img src="one" >}}`, `<img src="one">`, wt)@@ -161,10 +162,10 @@
// Issue #2294
func TestNestedNamedMissingParam(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("acc.html", `<div class="acc">{{ .Inner }}</div>`)- tem.AddInternalShortcode("div.html", `<div {{with .Get "class"}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)- tem.AddInternalShortcode("div2.html", `<div {{with .Get 0}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/acc.html", `<div class="acc">{{ .Inner }}</div>`)+ tem.AddTemplate("_internal/shortcodes/div.html", `<div {{with .Get "class"}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)+ tem.AddTemplate("_internal/shortcodes/div2.html", `<div {{with .Get 0}} class="{{ . }}"{{ end }}>{{ .Inner }}</div>`)return nil
}
CheckShortCodeMatch(t,
@@ -174,10 +175,10 @@
func TestIsNamedParamsSC(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("byposition.html", `<div id="{{ .Get 0 }}">`)- tem.AddInternalShortcode("byname.html", `<div id="{{ .Get "id" }}">`)- tem.AddInternalShortcode("ifnamedparams.html", `<div id="{{ if .IsNamedParams }}{{ .Get "id" }}{{ else }}{{ .Get 0 }}{{end}}">`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/byposition.html", `<div id="{{ .Get 0 }}">`)+ tem.AddTemplate("_internal/shortcodes/byname.html", `<div id="{{ .Get "id" }}">`)+ tem.AddTemplate("_internal/shortcodes/ifnamedparams.html", `<div id="{{ if .IsNamedParams }}{{ .Get "id" }}{{ else }}{{ .Get 0 }}{{end}}">`)return nil
}
CheckShortCodeMatch(t, `{{< ifnamedparams id="name" >}}`, `<div id="name">`, wt)@@ -190,8 +191,8 @@
func TestInnerSC(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)return nil
}
CheckShortCodeMatch(t, `{{< inside class="aspen" >}}`, `<div class="aspen"></div>`, wt)@@ -201,8 +202,8 @@
func TestInnerSCWithMarkdown(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)return nil
}
CheckShortCodeMatch(t, `{{% inside %}}@@ -215,8 +216,8 @@
func TestInnerSCWithAndWithoutMarkdown(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/inside.html", `<div{{with .Get "class"}} class="{{.}}"{{end}}>{{ .Inner }}</div>`)return nil
}
CheckShortCodeMatch(t, `{{% inside %}}@@ -246,9 +247,9 @@
func TestNestedSC(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("scn1.html", `<div>Outer, inner is {{ .Inner }}</div>`)- tem.AddInternalShortcode("scn2.html", `<div>SC2</div>`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/scn1.html", `<div>Outer, inner is {{ .Inner }}</div>`)+ tem.AddTemplate("_internal/shortcodes/scn2.html", `<div>SC2</div>`)return nil
}
CheckShortCodeMatch(t, `{{% scn1 %}}{{% scn2 %}}{{% /scn1 %}}`, "<div>Outer, inner is <div>SC2</div>\n</div>", wt)@@ -258,10 +259,10 @@
func TestNestedComplexSC(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("row.html", `-row-{{ .Inner}}-rowStop-`)- tem.AddInternalShortcode("column.html", `-col-{{.Inner }}-colStop-`)- tem.AddInternalShortcode("aside.html", `-aside-{{ .Inner }}-asideStop-`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/row.html", `-row-{{ .Inner}}-rowStop-`)+ tem.AddTemplate("_internal/shortcodes/column.html", `-col-{{.Inner }}-colStop-`)+ tem.AddTemplate("_internal/shortcodes/aside.html", `-aside-{{ .Inner }}-asideStop-`)return nil
}
CheckShortCodeMatch(t, `{{< row >}}1-s{{% column %}}2-**s**{{< aside >}}3-**s**{{< /aside >}}4-s{{% /column %}}5-s{{< /row >}}6-s`,@@ -274,10 +275,10 @@
func TestParentShortcode(t *testing.T) {t.Parallel()
- wt := func(tem tpl.Template) error {- tem.AddInternalShortcode("r1.html", `1: {{ .Get "pr1" }} {{ .Inner }}`)- tem.AddInternalShortcode("r2.html", `2: {{ .Parent.Get "pr1" }}{{ .Get "pr2" }} {{ .Inner }}`)- tem.AddInternalShortcode("r3.html", `3: {{ .Parent.Parent.Get "pr1" }}{{ .Parent.Get "pr2" }}{{ .Get "pr3" }} {{ .Inner }}`)+ wt := func(tem tpl.TemplateHandler) error {+ tem.AddTemplate("_internal/shortcodes/r1.html", `1: {{ .Get "pr1" }} {{ .Inner }}`)+ tem.AddTemplate("_internal/shortcodes/r2.html", `2: {{ .Parent.Get "pr1" }}{{ .Get "pr2" }} {{ .Inner }}`)+ tem.AddTemplate("_internal/shortcodes/r3.html", `3: {{ .Parent.Parent.Get "pr1" }}{{ .Parent.Get "pr2" }}{{ .Get "pr3" }} {{ .Inner }}`)return nil
}
CheckShortCodeMatch(t, `{{< r1 pr1="p1" >}}1: {{< r2 pr2="p2" >}}2: {{< r3 pr3="p3" >}}{{< /r3 >}}{{< /r2 >}}{{< /r1 >}}`,@@ -342,13 +343,13 @@
fmt.Sprintf("Hello %sworld%s. And that's it.", testScPlaceholderRegexp, testScPlaceholderRegexp), ""}, } {- p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.Template) error {- templ.AddInternalShortcode("tag.html", `tag`)- templ.AddInternalShortcode("sc1.html", `sc1`)- templ.AddInternalShortcode("sc2.html", `sc2`)- templ.AddInternalShortcode("inner.html", `{{with .Inner }}{{ . }}{{ end }}`)- templ.AddInternalShortcode("inner2.html", `{{.Inner}}`)- templ.AddInternalShortcode("inner3.html", `{{.Inner}}`)+ p, _ := pageFromString(simplePage, "simple.md", func(templ tpl.TemplateHandler) error {+ templ.AddTemplate("_internal/shortcodes/tag.html", `tag`)+ templ.AddTemplate("_internal/shortcodes/sc1.html", `sc1`)+ templ.AddTemplate("_internal/shortcodes/sc2.html", `sc2`)+ templ.AddTemplate("_internal/shortcodes/inner.html", `{{with .Inner }}{{ . }}{{ end }}`)+ templ.AddTemplate("_internal/shortcodes/inner2.html", `{{.Inner}}`)+ templ.AddTemplate("_internal/shortcodes/inner3.html", `{{.Inner}}`)return nil
})
@@ -517,14 +518,14 @@
sources[i] = source.ByteSource{Name: filepath.FromSlash(test.contentPath), Content: []byte(test.content)}}
- addTemplates := func(templ tpl.Template) error {+ addTemplates := func(templ tpl.TemplateHandler) error { templ.AddTemplate("_default/single.html", "{{.Content}}")- templ.AddInternalShortcode("b.html", `b`)- templ.AddInternalShortcode("c.html", `c`)- templ.AddInternalShortcode("d.html", `d`)- templ.AddInternalShortcode("menu.html", `{{ len (index .Page.Menus "main").Children }}`)- templ.AddInternalShortcode("tags.html", `{{ len .Page.Site.Taxonomies.tags }}`)+ templ.AddTemplate("_internal/shortcodes/b.html", `b`)+ templ.AddTemplate("_internal/shortcodes/c.html", `c`)+ templ.AddTemplate("_internal/shortcodes/d.html", `d`)+ templ.AddTemplate("_internal/shortcodes/menu.html", `{{ len (index .Page.Menus "main").Children }}`)+ templ.AddTemplate("_internal/shortcodes/tags.html", `{{ len .Page.Site.Taxonomies.tags }}`)return nil
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -188,7 +188,7 @@
// NewSiteDefaultLang creates a new site in the default language.
// The site will have a template system loaded and ready to use.
// Note: This is mainly used in single site tests.
-func NewSiteDefaultLang(withTemplate ...func(templ tpl.Template) error) (*Site, error) {+func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {v := viper.New()
loadDefaultSettingsFor(v)
return newSiteForLang(helpers.NewDefaultLanguage(v), withTemplate...)
@@ -197,7 +197,7 @@
// NewEnglishSite creates a new site in English language.
// The site will have a template system loaded and ready to use.
// Note: This is mainly used in single site tests.
-func NewEnglishSite(withTemplate ...func(templ tpl.Template) error) (*Site, error) {+func NewEnglishSite(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {v := viper.New()
loadDefaultSettingsFor(v)
return newSiteForLang(helpers.NewLanguage("en", v), withTemplate...)@@ -204,8 +204,8 @@
}
// newSiteForLang creates a new site in the given language.
-func newSiteForLang(lang *helpers.Language, withTemplate ...func(templ tpl.Template) error) (*Site, error) {- withTemplates := func(templ tpl.Template) error {+func newSiteForLang(lang *helpers.Language, withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) {+ withTemplates := func(templ tpl.TemplateHandler) error { for _, wt := range withTemplate { if err := wt(templ); err != nil {return err
@@ -1906,13 +1906,13 @@
}
func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts ...string) error {- layout, found := s.findFirstLayout(layouts...)
- if !found {+ templ := s.findFirstTemplate(layouts...)
+ if templ == nil { helpers.DistinctWarnLog.Printf("[%s] Unable to locate layout for %s: %s\n", s.Language.Lang, name, layouts)return nil
}
- if err := s.renderThing(d, layout, w); err != nil {+ if err := templ.Execute(w, d); err != nil {// Behavior here should be dependent on if running in server or watch mode.
helpers.DistinctErrorLog.Printf("Error while rendering %q: %s", name, err)@@ -1927,23 +1927,13 @@
return nil
}
-func (s *Site) findFirstLayout(layouts ...string) (string, bool) {+func (s *Site) findFirstTemplate(layouts ...string) tpl.Template { for _, layout := range layouts {- if s.Tmpl.Lookup(layout) != nil {- return layout, true
+ if templ := s.Tmpl.Lookup(layout); templ != nil {+ return templ
}
}
- return "", false
-}
-
-func (s *Site) renderThing(d interface{}, layout string, w io.Writer) error {-
- // If the template doesn't exist, then return, but leave the Writer open
- if templ := s.Tmpl.Lookup(layout); templ != nil {- return templ.Execute(w, d)
- }
- return fmt.Errorf("Layout not found: %s", layout)-
+ return nil
}
func (s *Site) publish(path string, r io.Reader) (err error) {--- a/hugolib/site_output_test.go
+++ b/hugolib/site_output_test.go
@@ -18,6 +18,8 @@
"strings"
"testing"
+ "github.com/spf13/afero"
+
"github.com/stretchr/testify/require"
"fmt"
@@ -75,6 +77,21 @@
[Taxonomies]
tag = "tags"
category = "categories"
+
+defaultContentLanguage = "en"
+
+[languages]
+
+[languages.en]
+title = "Title in English"
+languageName = "English"
+weight = 1
+
+[languages.nn]
+languageName = "Nynorsk"
+weight = 2
+title = "Tittel på Nynorsk"
+
`
pageTemplate := `---
@@ -84,21 +101,51 @@
# Doc
`
- th, h := newTestSitesFromConfig(t, siteConfig,
- "layouts/_default/list.json", `List JSON|{{ .Title }}|{{ .Content }}|Alt formats: {{ len .AlternativeOutputFormats -}}|+ mf := afero.NewMemMapFs()
+
+ writeToFs(t, mf, "i18n/en.toml", `
+[elbow]
+other = "Elbow"
+`)
+ writeToFs(t, mf, "i18n/nn.toml", `
+[elbow]
+other = "Olboge"
+`)
+
+ th, h := newTestSitesFromConfig(t, mf, siteConfig,
+
+ "layouts/_default/baseof.json", `START JSON:{{block "main" .}}default content{{ end }}:END JSON`,+ "layouts/_default/baseof.html", `START HTML:{{block "main" .}}default content{{ end }}:END HTML`,+
+ "layouts/_default/list.json", `{{ define "main" }}+List JSON|{{ .Title }}|{{ .Content }}|Alt formats: {{ len .AlternativeOutputFormats -}}| {{- range .AlternativeOutputFormats -}} Alt Output: {{ .Name -}}| {{- end -}}| {{- range .OutputFormats -}}-Output/Rel: {{ .Name -}}/{{ .Rel }}|+Output/Rel: {{ .Name -}}/{{ .Rel }}|{{ .MediaType }} {{- end -}}+ {{ with .OutputFormats.Get "JSON" }}+<atom:link href={{ .Permalink }} rel="self" type="{{ .MediaType }}" />+{{ end }}+{{ .Site.Language.Lang }}: {{ T "elbow" -}}+{{ end }}`,
+ "layouts/_default/list.html", `{{ define "main" }}+List HTML|{{.Title }}|+{{- with .OutputFormats.Get "HTML" -}}+<atom:link href={{ .Permalink }} rel="self" type="{{ .MediaType }}" />+{{- end -}}+{{ .Site.Language.Lang }}: {{ T "elbow" -}}+{{ end }}+`,
)
- require.Len(t, h.Sites, 1)
+ require.Len(t, h.Sites, 2)
fs := th.Fs
writeSource(t, fs, "content/_index.md", fmt.Sprintf(pageTemplate, "JSON Home", outputsStr))
+ writeSource(t, fs, "content/_index.nn.md", fmt.Sprintf(pageTemplate, "JSON Nynorsk Heim", outputsStr))
err := h.Build(BuildCfg{})@@ -105,6 +152,8 @@
require.NoError(t, err)
s := h.Sites[0]
+ require.Equal(t, "en", s.Language.Lang)
+
home := s.getPage(KindHome)
require.NotNil(t, home)
@@ -113,7 +162,6 @@
require.Len(t, home.outputFormats, lenOut)
- // TODO(bep) output assert template/text
// There is currently always a JSON output to make it simpler ...
altFormats := lenOut - 1
hasHTML := helpers.InStringArray(outputs, "html")
@@ -127,10 +175,27 @@
"Alt Output: HTML",
"Output/Rel: JSON/alternate|",
"Output/Rel: HTML/canonical|",
+ "en: Elbow",
)
+
+ th.assertFileContent("public/index.html",+ // The HTML entity is a deliberate part of this test: The HTML templates are
+ // parsed with html/template.
+ `List HTML|JSON Home|<atom:link href=http://example.com/blog/ rel="self" type="text/html+html" />`,
+ "en: Elbow",
+ )
+ th.assertFileContent("public/nn/index.html",+ "List HTML|JSON Nynorsk Heim|",
+ "nn: Olboge")
} else { th.assertFileContent("public/index.json","Output/Rel: JSON/canonical|",
+ // JSON is plain text, so no need to safeHTML this and that
+ `<atom:link href=http://example.com/blog/index.json rel="self" type="application/json+json" />`,
+ )
+ th.assertFileContent("public/nn/index.json",+ "List JSON|JSON Nynorsk Heim|",
+ "nn: Olboge",
)
}
--- a/hugolib/site_test.go
+++ b/hugolib/site_test.go
@@ -52,24 +52,6 @@
return p
}
-func TestDegenerateRenderThingMissingTemplate(t *testing.T) {- t.Parallel()
- cfg, fs := newTestCfg()
-
- writeSource(t, fs, filepath.Join("content", "a", "file.md"), pageSimpleTitle)-
- s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{})-
- require.Len(t, s.RegularPages, 1)
-
- p := s.RegularPages[0]
-
- err := s.renderThing(p, "foobar", nil)
- if err == nil {- t.Errorf("Expected err to be returned when missing the template.")- }
-}
-
func TestRenderWithInvalidTemplate(t *testing.T) {t.Parallel()
cfg, fs := newTestCfg()
--- a/hugolib/sitemap_test.go
+++ b/hugolib/sitemap_test.go
@@ -48,7 +48,7 @@
depsCfg := deps.DepsCfg{Fs: fs, Cfg: cfg} if !internal {- depsCfg.WithTemplate = func(templ tpl.Template) error {+ depsCfg.WithTemplate = func(templ tpl.TemplateHandler) error { templ.AddTemplate("sitemap.xml", sitemapTemplate)return nil
}
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -124,18 +124,17 @@
return s
}
-func newTestSitesFromConfig(t testing.TB, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) {+func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) { if len(layoutPathContentPairs)%2 != 0 { t.Fatalf("Layouts must be provided in pairs")}
- mf := afero.NewMemMapFs()
- writeToFs(t, mf, "config.toml", tomlConfig)
+ writeToFs(t, afs, "config.toml", tomlConfig)
- cfg, err := LoadConfig(mf, "", "config.toml")
+ cfg, err := LoadConfig(afs, "", "config.toml")
require.NoError(t, err)
- fs := hugofs.NewFrom(mf, cfg)
+ fs := hugofs.NewFrom(afs, cfg)
th := testHelper{cfg, fs, t} for i := 0; i < len(layoutPathContentPairs); i += 2 {@@ -150,7 +149,7 @@
}
func newTestSitesFromConfigWithDefaultTemplates(t testing.TB, tomlConfig string) (testHelper, *HugoSites) {- return newTestSitesFromConfig(t, tomlConfig,
+ return newTestSitesFromConfig(t, afero.NewMemMapFs(), tomlConfig,
"layouts/_default/single.html", "Single|{{ .Title }}|{{ .Content }}", "layouts/_default/list.html", "List|{{ .Title }}|{{ .Content }}", "layouts/_default/terms.html", "Terms List|{{ .Title }}|{{ .Content }}",@@ -164,9 +163,9 @@
func newErrorLogger() *jww.Notepad {return jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
}
-func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.Template) error {+func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.TemplateHandler) error {- return func(templ tpl.Template) error {+ return func(templ tpl.TemplateHandler) error { for i := 0; i < len(additionalTemplates); i += 2 {err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1])
if err != nil {--- a/output/layout.go
+++ b/output/layout.go
@@ -152,9 +152,11 @@
}
}
- return layoutsWithThemeLayouts, nil
+ layouts = layoutsWithThemeLayouts
}
+ layouts = prependTextPrefixIfNeeded(f, layouts...)
+
l.mu.Lock()
l.cache[key] = layouts
l.mu.Unlock()
@@ -184,18 +186,36 @@
}
func resolveTemplate(templ string, d LayoutDescriptor, f Format) []string {- return strings.Fields(replaceKeyValues(templ,
+ layouts := strings.Fields(replaceKeyValues(templ,
"SUFFIX", f.MediaType.Suffix,
"NAME", strings.ToLower(f.Name),
"SECTION", d.Section))
+
+ return layouts
}
+func prependTextPrefixIfNeeded(f Format, layouts ...string) []string {+ if !f.IsPlainText {+ return layouts
+ }
+
+ newLayouts := make([]string, len(layouts))
+
+ for i, l := range layouts {+ newLayouts[i] = "_text/" + l
+ }
+
+ return newLayouts
+}
+
func replaceKeyValues(s string, oldNew ...string) string {replacer := strings.NewReplacer(oldNew...)
return replacer.Replace(s)
}
-func regularPageLayouts(types string, layout string, f Format) (layouts []string) {+func regularPageLayouts(types string, layout string, f Format) []string {+ var layouts []string
+
if layout == "" {layout = "single"
}
@@ -219,5 +239,5 @@
layouts = append(layouts, fmt.Sprintf("_default/%s.%s.%s", layout, name, suffix)) layouts = append(layouts, fmt.Sprintf("_default/%s.%s", layout, suffix))- return
+ return layouts
}
--- a/output/layout_base.go
+++ b/output/layout_base.go
@@ -29,7 +29,10 @@
)
type TemplateNames struct {- Name string
+ // The name used as key in the template map. Note that this will be
+ // prefixed with "_text/" if it should be parsed with text/template.
+ Name string
+
OverlayFilename string
MasterFilename string
}
@@ -51,6 +54,10 @@
// The theme name if active.
Theme string
+ // All the output formats in play. This is used to decide if text/template or
+ // html/template.
+ OutputFormats Formats
+
FileExists func(filename string) (bool, error)
ContainsAny func(filename string, subslices [][]byte) (bool, error)
}
@@ -74,7 +81,13 @@
// index.amp.html
// index.json
filename := filepath.Base(d.RelPath)
+ isPlainText := false
+ outputFormat, found := d.OutputFormats.FromFilename(filename)
+ if found && outputFormat.IsPlainText {+ isPlainText = true
+ }
+
var ext, outFormat string
parts := strings.Split(filename, ".")
@@ -89,6 +102,10 @@
id.OverlayFilename = fullPath
id.Name = name
+
+ if isPlainText {+ id.Name = "_text/" + id.Name
+ }
// Ace and Go templates may have both a base and inner template.
pathDir := filepath.Dir(fullPath)
--- a/output/layout_base_test.go
+++ b/output/layout_base_test.go
@@ -141,6 +141,7 @@
return this.needsBase, nil
}
+ this.d.OutputFormats = Formats{AMPFormat, HTMLFormat, RSSFormat, JSONFormat}this.d.WorkingDir = filepath.FromSlash(this.d.WorkingDir)
this.d.LayoutDir = filepath.FromSlash(this.d.LayoutDir)
this.d.RelPath = filepath.FromSlash(this.d.RelPath)
@@ -149,6 +150,11 @@
this.expect.MasterFilename = filepath.FromSlash(this.expect.MasterFilename)
this.expect.OverlayFilename = filepath.FromSlash(this.expect.OverlayFilename)
+
+ if strings.Contains(this.d.RelPath, "json") {+ // currently the only plain text templates in this test.
+ this.expect.Name = "_text/" + this.expect.Name
+ }
id, err := CreateTemplateNames(this.d)
--- a/output/layout_test.go
+++ b/output/layout_test.go
@@ -64,6 +64,10 @@
[]string{"taxonomy/tag.rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}}, {"RSS Taxonomy term", LayoutDescriptor{Kind: "taxonomyTerm", Section: "tag"}, false, "", RSSFormat, []string{"taxonomy/tag.terms.rss.xml", "_default/rss.xml", "rss.xml", "_internal/_default/rss.xml"}},+ {"Home plain text", LayoutDescriptor{Kind: "home"}, true, "", JSONFormat,+ []string{"_text/index.json.json", "_text/index.json", "_text/_default/list.json.json", "_text/_default/list.json", "_text/theme/index.json.json", "_text/theme/index.json"}},+ {"Page plain text", LayoutDescriptor{Kind: "page"}, true, "", JSONFormat,+ []string{"_text/_default/single.json.json", "_text/_default/single.json", "_text/theme/_default/single.json.json"}}, } { t.Run(this.name, func(t *testing.T) {l := NewLayoutHandler(this.hasTheme)
--- a/output/outputFormat.go
+++ b/output/outputFormat.go
@@ -33,6 +33,7 @@
IsHTML: true,
}
+ // CalendarFormat is AAA
CalendarFormat = Format{Name: "Calendar",
MediaType: media.CalendarType,
@@ -100,6 +101,45 @@
found = true
return
}
+ }
+ return
+}
+
+func (formats Formats) GetBySuffix(name string) (f Format, found bool) {+ for _, ff := range formats {+ if name == ff.MediaType.Suffix {+ if found {+ // ambiguous
+ found = false
+ return
+ }
+ f = ff
+ found = true
+ }
+ }
+ return
+}
+
+func (formats Formats) FromFilename(filename string) (f Format, found bool) {+ // mytemplate.amp.html
+ // mytemplate.html
+ // mytemplate
+ var ext, outFormat string
+
+ parts := strings.Split(filename, ".")
+ if len(parts) > 2 {+ outFormat = parts[1]
+ ext = parts[2]
+ } else if len(parts) > 1 {+ ext = parts[1]
+ }
+
+ if outFormat != "" {+ return formats.GetByName(outFormat)
+ }
+
+ if ext != "" {+ return formats.GetBySuffix(ext)
}
return
}
--- a/output/outputFormat_test.go
+++ b/output/outputFormat_test.go
@@ -65,11 +65,36 @@
}
-func TestGetType(t *testing.T) {+func TestGetFormat(t *testing.T) { tp, _ := GetFormat("html")require.Equal(t, HTMLFormat, tp)
tp, _ = GetFormat("HTML")require.Equal(t, HTMLFormat, tp)
_, found := GetFormat("FOO")+ require.False(t, found)
+}
+
+func TestGeGetFormatByName(t *testing.T) {+ formats := Formats{AMPFormat, CalendarFormat}+ tp, _ := formats.GetByName("AMP")+ require.Equal(t, AMPFormat, tp)
+ _, found := formats.GetByName("HTML")+ require.False(t, found)
+ _, found = formats.GetByName("FOO")+ require.False(t, found)
+}
+
+func TestGeGetFormatByExt(t *testing.T) {+ formats1 := Formats{AMPFormat, CalendarFormat}+ formats2 := Formats{AMPFormat, HTMLFormat, CalendarFormat}+ tp, _ := formats1.GetBySuffix("html")+ require.Equal(t, AMPFormat, tp)
+ tp, _ = formats1.GetBySuffix("ics")+ require.Equal(t, CalendarFormat, tp)
+ _, found := formats1.GetBySuffix("not")+ require.False(t, found)
+
+ // ambiguous
+ _, found = formats2.GetByName("html")require.False(t, found)
}
--- a/tpl/template.go
+++ b/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{})}
--- /dev/null
+++ b/tpl/tplimpl/ace.go
@@ -1,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)
+}
--- a/tpl/tplimpl/amber_compiler.go
+++ b/tpl/tplimpl/amber_compiler.go
@@ -19,7 +19,7 @@
"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 @@
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
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -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,24 +15,40 @@
import (
"html/template"
- "io"
- "os"
- "path/filepath"
"strings"
+ texttemplate "text/template"
+ "github.com/eknkc/amber"
+
+ "os"
+
+ "github.com/spf13/hugo/output"
+
+ "path/filepath"
"sync"
- "github.com/eknkc/amber"
"github.com/spf13/afero"
- bp "github.com/spf13/hugo/bufferpool"
"github.com/spf13/hugo/deps"
"github.com/spf13/hugo/helpers"
- "github.com/spf13/hugo/output"
- "github.com/yosssi/ace"
+ "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,177 +57,189 @@
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
+}
- // This looks, and is, strange.
- // The clone is used by non-renderable content pages, and these need to be
- // re-parsed on content change, and to avoid the
- // "cannot Parse after Execute" error, we need to re-clone it from the original clone.
- clone *template.Template
- cloneClone *template.Template
+type templateFuncsterTemplater interface {+ tpl.TemplateFinder
+ setFuncs(funcMap map[string]interface{})+ setTemplateFuncster(f *templateFuncster)
+}
- // 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
+// 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
- errors []*templateErr
-
- funcster *templateFuncster
-
amberFuncMap template.FuncMap
+ errors []*templateErr
+
*deps.Deps
}
-type TemplateProvider struct{}+func (t *templateHandler) addError(name string, err error) {+ t.errors = append(t.errors, &templateErr{name, err})+}
-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,
+// 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)
}
+}
- deps.Tmpl = tmpl
+// 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
- tmpl.initFuncs(deps)
+ isTextTemplate := strings.HasPrefix(name, textTmplNamePrefix)
- tmpl.LoadEmbedded()
+ 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 deps.WithTemplate != nil {- err := deps.WithTemplate(tmpl)
- if err != nil {- tmpl.errors = append(tmpl.errors, &templateErr{"init", err})- }
-
+ if te == nil {+ return nil
}
- tmpl.MarkReady()
-
- return nil
-
+ return te
}
-// 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 *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 = tmpl
- tmpl.initFuncs(d)
+ d.Tmpl = c
- for k, v := range t.overlays {+ 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(tmpl.funcster.funcMap)
- tmpl.overlays[k] = vc
+ vc.Funcs(c.html.funcster.funcMap)
+ c.html.overlays[k] = vc
}
- tmpl.MarkReady()
+ for k, v := range t.text.overlays {+ vc := texttemplate.Must(v.Clone())
+ vc = vc.Lookup(vc.Name())
+ vc.Funcs(texttemplate.FuncMap(c.text.funcster.funcMap))
+ c.text.overlays[k] = vc
+ }
- return nil
+ return c
+
}
-func (t *GoHTMLTemplate) initFuncs(d *deps.Deps) {+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),
+ }
- 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()
+type htmlTemplates struct {+ funcster *templateFuncster
- t.amberFuncMap = template.FuncMap{}+ t *template.Template
- amberMu.Lock()
- for k, v := range amber.FuncMap {- t.amberFuncMap[k] = v
- }
+ // This looks, and is, strange.
+ // The clone is used by non-renderable content pages, and these need to be
+ // re-parsed on content change, and to avoid the
+ // "cannot Parse after Execute" error, we need to re-clone it from the original clone.
+ clone *template.Template
+ cloneClone *template.Template
- 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()
+ // a separate storage for the overlays created from cloned master templates.
+ // note: No mutex protection, so we add these in one Go routine, then just read.
+ overlays map[string]*template.Template
+}
+func (t *htmlTemplates) setTemplateFuncster(f *templateFuncster) {+ t.funcster = f
}
-func (t *GoHTMLTemplate) Funcs(funcMap template.FuncMap) {- t.Template.Funcs(funcMap)
+func (t *htmlTemplates) Lookup(name string) *tpl.TemplateAdapter {+ templ := t.lookup(name)
+ if templ == nil {+ return nil
+ }
+ return &tpl.TemplateAdapter{Template: templ}}
-func (t *GoHTMLTemplate) Partial(name string, contextList ...interface{}) template.HTML {- if strings.HasPrefix("partials/", name) {- name = name[8:]
+func (t *htmlTemplates) lookup(name string) *template.Template {+ if templ := t.t.Lookup(name); templ != nil {+ return templ
}
- var context interface{}+ if t.overlays != nil {+ if templ, ok := t.overlays[name]; ok {+ return templ
+ }
+ }
- if len(contextList) == 0 {- context = nil
- } else {- context = contextList[0]
+ if t.clone != nil {+ return t.clone.Lookup(name)
}
- return t.ExecuteTemplateToHTML(context, "partials/"+name, "theme/partials/"+name)
+
+ return nil
}
-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)
- }
+type textTemplates struct {+ funcster *templateFuncster
- 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)- }
+ t *texttemplate.Template
+
+ clone *texttemplate.Template
+ cloneClone *texttemplate.Template
+
+ overlays map[string]*texttemplate.Template
}
-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 *textTemplates) setTemplateFuncster(f *templateFuncster) {+ t.funcster = f
}
-func (t *GoHTMLTemplate) Lookup(name string) *template.Template {+func (t *textTemplates) Lookup(name string) *tpl.TemplateAdapter {+ templ := t.lookup(name)
+ if templ == nil {+ return nil
+ }
+ return &tpl.TemplateAdapter{Template: templ}+}
- if templ := t.Template.Lookup(name); templ != nil {+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
@@ -218,68 +246,70 @@
}
}
- // The clone is used for the non-renderable HTML pages (p.IsRenderable == false) that is parsed
- // as Go templates late in the build process.
if t.clone != nil {- if templ := t.clone.Lookup(name); templ != nil {- return templ
- }
+ return t.clone.Lookup(name)
}
return nil
+}
+func (t *templateHandler) setFuncs(funcMap map[string]interface{}) {+ t.html.setFuncs(funcMap)
+ t.text.setFuncs(funcMap)
}
-func (t *GoHTMLTemplate) GetClone() *template.Template {- return t.clone
+// 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 *GoHTMLTemplate) RebuildClone() *template.Template {- t.clone = template.Must(t.cloneClone.Clone())
- return t.clone
+func (t *htmlTemplates) setFuncs(funcMap map[string]interface{}) {+ t.t.Funcs(funcMap)
}
-func (t *GoHTMLTemplate) LoadEmbedded() {- t.EmbedShortcodes()
- t.EmbedTemplates()
+func (t *textTemplates) setFuncs(funcMap map[string]interface{}) {+ t.t.Funcs(funcMap)
}
-// MarkReady marks the template as "ready for execution". No changes allowed
-// after this is set.
-// TODO(bep) if this proves to be resource heavy, we could detect
-// earlier if we really need this, or make it lazy.
-func (t *GoHTMLTemplate) MarkReady() {- if t.clone == nil {- t.clone = template.Must(t.Template.Clone())
- t.cloneClone = template.Must(t.clone.Clone())
- }
+// 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) checkState() {- if t.clone != nil {- panic("template is cloned and cannot be modfified")+func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) error {+ templ, err := tt.New(name).Parse(tpl)
+ if err != nil {+ return err
}
-}
-func (t *GoHTMLTemplate) AddInternalTemplate(prefix, name, tpl string) error {- if prefix != "" {- return t.AddTemplate("_internal/"+prefix+"/"+name, tpl)+ if err := applyTemplateTransformersToHMLTTemplate(templ); err != nil {+ return err
}
- return t.AddTemplate("_internal/"+name, tpl)+
+ return nil
}
-func (t *GoHTMLTemplate) AddInternalShortcode(name, content string) error {- return t.AddInternalTemplate("shortcodes", name, content)+func (t *htmlTemplates) addTemplate(name, tpl string) error {+ return t.addTemplateIn(t.t, name, tpl)
}
-func (t *GoHTMLTemplate) AddTemplate(name, tpl string) error {- t.checkState()
- templ, err := t.New(name).Parse(tpl)
+func (t *htmlTemplates) addLateTemplate(name, tpl string) error {+ return t.addTemplateIn(t.clone, name, tpl)
+}
+
+func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) error {+ name = strings.TrimPrefix(name, textTmplNamePrefix)
+ templ, err := tt.New(name).Parse(tpl)
if err != nil {- t.errors = append(t.errors, &templateErr{name: name, err: err})return err
}
- if err := applyTemplateTransformers(templ); err != nil {+
+ if err := applyTemplateTransformersToTextTemplate(templ); err != nil {return err
}
@@ -286,95 +316,281 @@
return nil
}
-func (t *GoHTMLTemplate) AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error {+func (t *textTemplates) addTemplate(name, tpl string) error {+ return t.addTemplateIn(t.t, name, tpl)
+}
- // 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
+func (t *textTemplates) addLateTemplate(name, tpl string) error {+ return t.addTemplateIn(t.clone, name, tpl)
+}
- masterTpl := t.Lookup(masterFilename)
+func (t *templateHandler) addTemplate(name, tpl string) error {+ return t.AddTemplate(name, tpl)
+}
+func (t *templateHandler) addLateTemplate(name, tpl string) error {+ return t.AddLateTemplate(name, tpl)
+}
+
+// AddLateTemplate is used to add a template late, i.e. after the
+// regular templates have started its execution.
+func (t *templateHandler) AddLateTemplate(name, tpl string) error {+ h := t.getTemplateHandler(name)
+ if err := h.addLateTemplate(name, tpl); err != nil {+ t.addError(name, err)
+ return err
+ }
+ return nil
+}
+
+// AddTemplate parses and adds a template to the collection.
+// Templates with name prefixed with "_text" will be handled as plain
+// text templates.
+func (t *templateHandler) AddTemplate(name, tpl string) error {+ h := t.getTemplateHandler(name)
+ if err := h.addTemplate(name, tpl); err != nil {+ t.addError(name, err)
+ return err
+ }
+ return nil
+}
+
+// MarkReady marks the templates as "ready for execution". No changes allowed
+// after this is set.
+// TODO(bep) if this proves to be resource heavy, we could detect
+// earlier if we really need this, or make it lazy.
+func (t *templateHandler) MarkReady() {+ if t.html.clone == nil {+ t.html.clone = template.Must(t.html.t.Clone())
+ t.html.cloneClone = template.Must(t.html.clone.Clone())
+ }
+ if t.text.clone == nil {+ t.text.clone = texttemplate.Must(t.text.t.Clone())
+ t.text.cloneClone = texttemplate.Must(t.text.clone.Clone())
+ }
+}
+
+// RebuildClone rebuilds the cloned templates. Used for live-reloads.
+func (t *templateHandler) RebuildClone() {+ t.html.clone = template.Must(t.html.cloneClone.Clone())
+ t.text.clone = texttemplate.Must(t.text.cloneClone.Clone())
+}
+
+func (t *templateHandler) loadTemplates(absPath string, prefix string, formats output.Formats) {+ t.Log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix)+ walker := func(path string, fi os.FileInfo, err error) error {+ if err != nil {+ return nil
+ }
+
+ t.Log.DEBUG.Println("Template path", path)+ if fi.Mode()&os.ModeSymlink == os.ModeSymlink {+ link, err := filepath.EvalSymlinks(absPath)
+ if err != nil {+ t.Log.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", absPath, err)+ return nil
+ }
+
+ linkfi, err := t.Fs.Source.Stat(link)
+ if err != nil {+ t.Log.ERROR.Printf("Cannot stat '%s', error was: %s", link, err)+ return nil
+ }
+
+ if !linkfi.Mode().IsRegular() {+ t.Log.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", absPath)+ }
+ return nil
+ }
+
+ if !fi.IsDir() {+ if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) {+ return nil
+ }
+
+ var (
+ workingDir = t.PathSpec.WorkingDir()
+ themeDir = t.PathSpec.GetThemeDir()
+ layoutDir = t.PathSpec.LayoutDir()
+ )
+
+ if themeDir != "" && strings.HasPrefix(absPath, themeDir) {+ workingDir = themeDir
+ layoutDir = "layouts"
+ }
+
+ li := strings.LastIndex(path, layoutDir) + len(layoutDir) + 1
+ relPath := path[li:]
+
+ descriptor := output.TemplateLookupDescriptor{+ WorkingDir: workingDir,
+ LayoutDir: layoutDir,
+ RelPath: relPath,
+ Prefix: prefix,
+ Theme: t.PathSpec.Theme(),
+ OutputFormats: formats,
+ FileExists: func(filename string) (bool, error) {+ return helpers.Exists(filename, t.Fs.Source)
+ },
+ ContainsAny: func(filename string, subslices [][]byte) (bool, error) {+ return helpers.FileContainsAny(filename, subslices, t.Fs.Source)
+ },
+ }
+
+ tplID, err := output.CreateTemplateNames(descriptor)
+ if err != nil {+ t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err)+
+ return nil
+ }
+
+ if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil {+ t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, path, err)+ }
+
+ }
+ return nil
+ }
+ if err := helpers.SymbolicWalk(t.Fs.Source, absPath, walker); err != nil {+ t.Log.ERROR.Printf("Failed to load templates: %s", err)+ }
+}
+
+func (t *templateHandler) initFuncs() {+
+ // The template funcs need separation between text and html templates.
+ for _, funcsterHolder := range []templateFuncsterTemplater{t.html, t.text} {+ funcster := newTemplateFuncster(t.Deps, funcsterHolder)
+
+ // The URL funcs in the funcMap is somewhat language dependent,
+ // so we need to wait until the language and site config is loaded.
+ funcster.initFuncMap()
+
+ funcsterHolder.setTemplateFuncster(funcster)
+
+ }
+
+ // Amber is HTML only.
+ t.amberFuncMap = template.FuncMap{}+
+ amberMu.Lock()
+ for k, v := range amber.FuncMap {+ t.amberFuncMap[k] = v
+ }
+
+ for k, v := range t.html.funcster.funcMap {+ t.amberFuncMap[k] = v
+ // Hacky, but we need to make sure that the func names are in the global map.
+ amber.FuncMap[k] = func() string {+ panic("should never be invoked")+ }
+ }
+ amberMu.Unlock()
+
+}
+
+func (t *templateHandler) getTemplateHandler(name string) templateLoader {+ if strings.HasPrefix(name, textTmplNamePrefix) {+ return t.text
+ }
+ return t.html
+}
+
+func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {+ h := t.getTemplateHandler(name)
+ return h.handleMaster(name, overlayFilename, masterFilename, onMissing)
+}
+
+func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {+ masterTpl := t.lookup(masterFilename)
+
if masterTpl == nil {- b, err := afero.ReadFile(t.Fs.Source, masterFilename)
+ templ, err := onMissing(masterFilename)
if err != nil {return err
}
- masterTpl, err = t.New(masterFilename).Parse(string(b))
+ masterTpl, err = t.t.New(overlayFilename).Parse(templ)
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)
+ templ, err := onMissing(overlayFilename)
if err != nil {return err
}
- overlayTpl, err := template.Must(masterTpl.Clone()).Parse(string(b))
+ overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ)
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
}
+ // 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 *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"
+func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error {+ name = strings.TrimPrefix(name, textTmplNamePrefix)
+ masterTpl := t.lookup(masterFilename)
- // Fixes issue #1178
- basePath = strings.Replace(basePath, "\\", "/", -1)
- innerPath = strings.Replace(innerPath, "\\", "/", -1)
+ if masterTpl == nil {+ templ, err := onMissing(masterFilename)
+ if err != nil {+ return err
+ }
- if basePath != "" {- base = ace.NewFile(basePath, baseContent)
- inner = ace.NewFile(innerPath, innerContent)
- } else {- base = ace.NewFile(innerPath, innerContent)
- inner = ace.NewFile("", []byte{})+ masterTpl, err = t.t.New(overlayFilename).Parse(templ)
+ if err != nil {+ return err
+ }
}
- parsed, err := ace.ParseSource(ace.NewSource(base, inner, []*ace.File{}), nil)+
+ templ, err := onMissing(overlayFilename)
if err != nil {- t.errors = append(t.errors, &templateErr{name: name, err: err})return err
}
- templ, err := ace.CompileResultWithTemplate(t.New(name), parsed, nil)
+
+ overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ)
if err != nil {- t.errors = append(t.errors, &templateErr{name: name, err: err})return err
}
- return applyTemplateTransformers(templ)
+
+ overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
+ if err := applyTemplateTransformersToTextTemplate(overlayTpl); err != nil {+ return err
+ }
+ t.overlays[name] = overlayTpl
+
+ return err
+
}
-func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) error {+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)
@@ -383,14 +599,15 @@
}
amberMu.Lock()
- templ, err := t.CompileAmberWithTemplate(b, path, t.New(templateName))
+ templ, err := t.compileAmberWithTemplate(b, path, t.html.t.New(templateName))
amberMu.Unlock()
if err != nil {return err
}
- return applyTemplateTransformers(templ)
+ return applyTemplateTransformersToHMLTTemplate(templ)
case ".ace":
+ // Only HTML support for Ace
var innerContent, baseContent []byte
innerContent, err := afero.ReadFile(t.Fs.Source, path)
@@ -405,14 +622,14 @@
}
}
- return t.AddAceTemplate(name, baseTemplatePath, path, baseContent, innerContent)
+ return t.addAceTemplate(name, baseTemplatePath, path, baseContent, innerContent)
default:
if baseTemplatePath != "" {- return t.AddTemplateFileWithMaster(name, path, baseTemplatePath)
+ return t.handleMaster(name, path, baseTemplatePath, getTemplate)
}
- b, err := afero.ReadFile(t.Fs.Source, path)
+ templ, err := getTemplate(path)
if err != nil {return err
@@ -420,16 +637,33 @@
t.Log.DEBUG.Printf("Add template file from path %s", path)- return t.AddTemplate(name, string(b))
+ return t.AddTemplate(name, templ)
}
}
-func (t *GoHTMLTemplate) GenerateTemplateNameFrom(base, path string) string {- name, _ := filepath.Rel(base, path)
- return filepath.ToSlash(name)
+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] == '.'
}
@@ -442,97 +676,4 @@
func isBaseTemplate(path string) bool {return strings.Contains(path, baseFileBase)
-}
-
-func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) {- t.Log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix)- walker := func(path string, fi os.FileInfo, err error) error {- if err != nil {- return nil
- }
-
- t.Log.DEBUG.Println("Template path", path)- if fi.Mode()&os.ModeSymlink == os.ModeSymlink {- link, err := filepath.EvalSymlinks(absPath)
- if err != nil {- t.Log.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", absPath, err)- return nil
- }
-
- linkfi, err := t.Fs.Source.Stat(link)
- if err != nil {- t.Log.ERROR.Printf("Cannot stat '%s', error was: %s", link, err)- return nil
- }
-
- if !linkfi.Mode().IsRegular() {- t.Log.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", absPath)- }
- return nil
- }
-
- if !fi.IsDir() {- if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) {- return nil
- }
-
- var (
- workingDir = t.PathSpec.WorkingDir()
- themeDir = t.PathSpec.GetThemeDir()
- layoutDir = t.PathSpec.LayoutDir()
- )
-
- if themeDir != "" && strings.HasPrefix(absPath, themeDir) {- workingDir = themeDir
- layoutDir = "layouts"
- }
-
- li := strings.LastIndex(path, layoutDir) + len(layoutDir) + 1
- relPath := path[li:]
-
- descriptor := output.TemplateLookupDescriptor{- WorkingDir: workingDir,
- LayoutDir: layoutDir,
- RelPath: relPath,
- Prefix: prefix,
- Theme: t.PathSpec.Theme(),
- FileExists: func(filename string) (bool, error) {- return helpers.Exists(filename, t.Fs.Source)
- },
- ContainsAny: func(filename string, subslices [][]byte) (bool, error) {- return helpers.FileContainsAny(filename, subslices, t.Fs.Source)
- },
- }
-
- tplID, err := output.CreateTemplateNames(descriptor)
- if err != nil {- t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err)-
- return nil
- }
-
- if err := t.AddTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil {- t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, path, err)- }
-
- }
- return nil
- }
- if err := helpers.SymbolicWalk(t.Fs.Source, absPath, walker); err != nil {- t.Log.ERROR.Printf("Failed to load templates: %s", err)- }
-}
-
-func (t *GoHTMLTemplate) LoadTemplatesWithPrefix(absPath string, prefix string) {- t.loadTemplates(absPath, prefix)
-}
-
-func (t *GoHTMLTemplate) LoadTemplates(absPath string) {- t.loadTemplates(absPath, "")
-}
-
-func (t *GoHTMLTemplate) PrintErrors() {- for i, e := range t.errors {- t.Log.ERROR.Println(i, ":", e.err)
- }
}
--- /dev/null
+++ b/tpl/tplimpl/templateFuncster.go
@@ -1,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)+}
--- /dev/null
+++ b/tpl/tplimpl/templateProvider.go
@@ -1,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
+}
--- a/tpl/tplimpl/template_ast_transformers.go
+++ b/tpl/tplimpl/template_ast_transformers.go
@@ -17,6 +17,7 @@
"errors"
"html/template"
"strings"
+ texttemplate "text/template"
"text/template/parse"
)
@@ -35,32 +36,57 @@
}
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 @@
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 {--- a/tpl/tplimpl/template_ast_transformers_test.go
+++ b/tpl/tplimpl/template_ast_transformers_test.go
@@ -115,13 +115,13 @@
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 @@
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 @@
require.NoError(t, err)
- c := newTemplateContext(templ)
+ c := newTemplateContext(createParseTreeLookup(templ))
c.paramsKeysToLower(templ.Tree.Root)
@@ -254,7 +254,7 @@
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 @@
templ, err := template.New("foo").Parse(recursive)require.NoError(t, err)
- c := newTemplateContext(templ)
+ c := newTemplateContext(createParseTreeLookup(templ))
c.paramsKeysToLower(templ.Tree.Root)
}
--- a/tpl/tplimpl/template_embedded.go
+++ b/tpl/tplimpl/template_embedded.go
@@ -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 @@
{{ 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,7 +46,7 @@
<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 }}>@@ -58,14 +53,14 @@
<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 @@
</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 @@
</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 @@
</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 @@
</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 @@
<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 @@
<!-- 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 @@
{{ 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 @@
<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 @@
</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 @@
<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: *")}
--- a/tpl/tplimpl/template_funcs.go
+++ b/tpl/tplimpl/template_funcs.go
@@ -45,7 +45,6 @@
"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 @@
_ "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 @@
// 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 @@
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,7 +1569,7 @@
}
t.cachedPartials.Unlock()
- return p
+ return
}
// partialCached executes and caches partial templates. An optional variant
@@ -1593,7 +1576,7 @@
// 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 @@
"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 @@
}
t.funcMap = funcMap
- t.Tmpl.Funcs(funcMap)
+ t.Tmpl.setFuncs(funcMap)
}
--- a/tpl/tplimpl/template_funcs_test.go
+++ b/tpl/tplimpl/template_funcs_test.go
@@ -281,8 +281,8 @@
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,96 @@
}
}
+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 TestPartialWithError(t *testing.T) {+ t.Parallel()
+ config := newDepsConfig(viper.New())
+
+ data := struct {+ Name string
+ }{+ Name: "bep",
+ }
+
+ config.WithTemplate = func(templ tpl.TemplateHandler) error {+ if err := templ.AddTemplate("container.html", `HTML Test Partial: {{ partial "fail.foo" . -}}`); err != nil {+ return err
+ }
+
+ if err := templ.AddTemplate("partials/fail.foo", "Template: {{ .DoesNotExist }}"); err != nil {+ return err
+ }
+
+ return nil
+ }
+
+ de, err := deps.New(config)
+ require.NoError(t, err)
+ require.NoError(t, de.LoadResources())
+
+ templ := de.Tmpl.Lookup("container.html")+ require.NotNil(t, templ)
+ result, err := templ.ExecuteToString(data)
+ require.Error(t, err)
+
+ errStr := err.Error()
+
+ require.Contains(t, errStr, `template: container.html:1:22: executing "container.html" at <partial "fail.foo" .>`)
+ require.Contains(t, errStr, `can't evaluate field DoesNotExist`)
+
+ require.Empty(t, result)
+
+}
+
func TestPartialCached(t *testing.T) {t.Parallel()
testCases := []struct {@@ -2893,7 +2983,7 @@
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 +3023,7 @@
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 +3055,7 @@
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 +3100,12 @@
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
--- a/tpl/tplimpl/template_test.go
+++ b/tpl/tplimpl/template_test.go
@@ -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 @@
"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 @@
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 @@
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 @@
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)--
⑨