From 8b5b558bb515e80da640f5e114169874771b61e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Mon, 27 Mar 2017 20:43:49 +0200 Subject: [PATCH] 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 --- deps/deps.go | 6 +- hugolib/alias.go | 27 +- hugolib/embedded_shortcodes_test.go | 8 +- hugolib/hugo_sites.go | 8 +- hugolib/menu_test.go | 4 +- hugolib/page.go | 3 +- hugolib/page_output.go | 24 +- hugolib/shortcode.go | 16 +- hugolib/shortcode_test.go | 103 +- hugolib/site.go | 32 +- hugolib/site_output_test.go | 75 +- hugolib/site_test.go | 18 - hugolib/sitemap_test.go | 2 +- hugolib/testhelpers_test.go | 15 +- output/layout.go | 28 +- output/layout_base.go | 19 +- output/layout_base_test.go | 6 + output/layout_test.go | 4 + output/outputFormat.go | 40 + output/outputFormat_test.go | 27 +- tpl/template.go | 111 ++- tpl/tplimpl/ace.go | 51 + tpl/tplimpl/amber_compiler.go | 4 +- tpl/tplimpl/template.go | 897 ++++++++++-------- tpl/tplimpl/templateFuncster.go | 86 ++ tpl/tplimpl/templateProvider.go | 59 ++ tpl/tplimpl/template_ast_transformers.go | 50 +- tpl/tplimpl/template_ast_transformers_test.go | 12 +- tpl/tplimpl/template_embedded.go | 57 +- tpl/tplimpl/template_funcs.go | 33 +- tpl/tplimpl/template_funcs_test.go | 106 ++- tpl/tplimpl/template_test.go | 232 +---- 32 files changed, 1313 insertions(+), 850 deletions(-) create mode 100644 tpl/tplimpl/ace.go create mode 100644 tpl/tplimpl/templateFuncster.go create mode 100644 tpl/tplimpl/templateProvider.go diff --git a/deps/deps.go b/deps/deps.go index 659f259dd..188863876 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -20,7 +20,7 @@ type Deps struct { 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 @@ type Deps struct { 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 @@ type DepsCfg struct { // Template handling. TemplateProvider ResourceProvider - WithTemplate func(templ tpl.Template) error + WithTemplate func(templ tpl.TemplateHandler) error // i18n handling. TranslationProvider ResourceProvider diff --git a/hugolib/alias.go b/hugolib/alias.go index d5eb35777..d1a1b5534 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -22,6 +22,8 @@ import ( "runtime" "strings" + "github.com/spf13/hugo/tpl" + jww "github.com/spf13/jwalterweatherman" "github.com/spf13/hugo/helpers" @@ -35,18 +37,19 @@ const ( 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)) } 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 @@ func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (i 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 @@ func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (i } 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) writeDestAlias(path, permalink string, p *Page) (err error) { } 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") diff --git a/hugolib/embedded_shortcodes_test.go b/hugolib/embedded_shortcodes_test.go index 92821d0ef..45de0bf09 100644 --- a/hugolib/embedded_shortcodes_test.go +++ b/hugolib/embedded_shortcodes_test.go @@ -335,8 +335,8 @@ func TestShortcodeTweet(t *testing.T) { 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 @@ func TestShortcodeInstagram(t *testing.T) { th = testHelper{cfg, fs, t} ) - withTemplate := func(templ tpl.Template) error { - templ.Funcs(instagramFuncMap) + withTemplate := func(templ tpl.TemplateHandler) error { + templ.(tpl.TemplateTestMocker).SetFuncs(instagramFuncMap) return nil } diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 2682e524d..491bbd809 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -129,11 +129,11 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { 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 { diff --git a/hugolib/menu_test.go b/hugolib/menu_test.go index 0c3badc7b..f044fb5e0 100644 --- a/hugolib/menu_test.go +++ b/hugolib/menu_test.go @@ -18,6 +18,8 @@ import ( "fmt" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" ) @@ -42,7 +44,7 @@ title = "Section Menu" 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) -}} diff --git a/hugolib/page.go b/hugolib/page.go index fa9f40922..5a04c6ce7 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -1384,13 +1384,14 @@ func (p *Page) prepareLayouts() error { 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 } p.selfLayout = self } } + return nil } diff --git a/hugolib/page_output.go b/hugolib/page_output.go index f47343cb5..58d09d688 100644 --- a/hugolib/page_output.go +++ b/hugolib/page_output.go @@ -110,9 +110,29 @@ func (p *PageOutput) Render(layout ...string) template.HTML { 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 { diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index d165c778b..d72a96faa 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -177,7 +177,7 @@ var isInnerShortcodeCache = struct { // 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 @@ func isInnerShortcode(t *template.Template) (bool, error) { 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 @@ Loop: 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 @@ func replaceShortcodeTokens(source []byte, prefix string, replacements map[strin 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 @@ func getShortcodeTemplate(name string, t tpl.Template) *template.Template { 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) diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index 28b03aa9b..3d1922462 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -30,7 +30,7 @@ import ( ) // 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 @@ func pageFromString(in, filename string, withTemplate ...func(templ tpl.Template 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 @@ func TestNonSC(t *testing.T) { // 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 @@ func TestHyphenatedSC(t *testing.T) { // 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 TestNoTrailingNewline(t *testing.T) { 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 TestPositionalParamSC(t *testing.T) { 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 TestPositionalParamIndexOutOfBounds(t *testing.T) { func TestNamedParamSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.Template) error { - tem.AddInternalShortcode("img.html", ``) + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/img.html", ``) return nil } CheckShortCodeMatch(t, `{{< img src="one" >}}`, ``, wt) @@ -161,10 +162,10 @@ func TestNamedParamSC(t *testing.T) { // Issue #2294 func TestNestedNamedMissingParam(t *testing.T) { t.Parallel() - wt := func(tem tpl.Template) error { - tem.AddInternalShortcode("acc.html", `
{{ .Inner }}
`) - tem.AddInternalShortcode("div.html", `
{{ .Inner }}
`) - tem.AddInternalShortcode("div2.html", `
{{ .Inner }}
`) + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/acc.html", `
{{ .Inner }}
`) + tem.AddTemplate("_internal/shortcodes/div.html", `
{{ .Inner }}
`) + tem.AddTemplate("_internal/shortcodes/div2.html", `
{{ .Inner }}
`) return nil } CheckShortCodeMatch(t, @@ -174,10 +175,10 @@ func TestNestedNamedMissingParam(t *testing.T) { func TestIsNamedParamsSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.Template) error { - tem.AddInternalShortcode("byposition.html", `
`) - tem.AddInternalShortcode("byname.html", `
`) - tem.AddInternalShortcode("ifnamedparams.html", `
`) + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/byposition.html", `
`) + tem.AddTemplate("_internal/shortcodes/byname.html", `
`) + tem.AddTemplate("_internal/shortcodes/ifnamedparams.html", `
`) return nil } CheckShortCodeMatch(t, `{{< ifnamedparams id="name" >}}`, `
`, wt) @@ -190,8 +191,8 @@ func TestIsNamedParamsSC(t *testing.T) { func TestInnerSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.Template) error { - tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/inside.html", `{{ .Inner }}
`) return nil } CheckShortCodeMatch(t, `{{< inside class="aspen" >}}`, `
`, wt) @@ -201,8 +202,8 @@ func TestInnerSC(t *testing.T) { func TestInnerSCWithMarkdown(t *testing.T) { t.Parallel() - wt := func(tem tpl.Template) error { - tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/inside.html", `{{ .Inner }}
`) return nil } CheckShortCodeMatch(t, `{{% inside %}} @@ -215,8 +216,8 @@ func TestInnerSCWithMarkdown(t *testing.T) { func TestInnerSCWithAndWithoutMarkdown(t *testing.T) { t.Parallel() - wt := func(tem tpl.Template) error { - tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/inside.html", `{{ .Inner }}
`) return nil } CheckShortCodeMatch(t, `{{% inside %}} @@ -246,9 +247,9 @@ func TestEmbeddedSC(t *testing.T) { func TestNestedSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.Template) error { - tem.AddInternalShortcode("scn1.html", `
Outer, inner is {{ .Inner }}
`) - tem.AddInternalShortcode("scn2.html", `
SC2
`) + wt := func(tem tpl.TemplateHandler) error { + tem.AddTemplate("_internal/shortcodes/scn1.html", `
Outer, inner is {{ .Inner }}
`) + tem.AddTemplate("_internal/shortcodes/scn2.html", `
SC2
`) return nil } CheckShortCodeMatch(t, `{{% scn1 %}}{{% scn2 %}}{{% /scn1 %}}`, "
Outer, inner is
SC2
\n
", wt) @@ -258,10 +259,10 @@ func TestNestedSC(t *testing.T) { 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 TestNestedComplexSC(t *testing.T) { 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 @@ func TestExtractShortcodes(t *testing.T) { 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 @@ tags: 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 diff --git a/hugolib/site.go b/hugolib/site.go index c42b938e8..40a7c44fb 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -188,7 +188,7 @@ func NewSite(cfg deps.DepsCfg) (*Site, error) { // 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,15 +197,15 @@ func NewSiteDefaultLang(withTemplate ...func(templ tpl.Template) error) (*Site, // 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...) } // 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 @@ Your rendered home page is blank: /index.html is zero-length } 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 @@ func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts 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) { diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go index 86e1a55ca..0edd8e1f0 100644 --- a/hugolib/site_output_test.go +++ b/hugolib/site_output_test.go @@ -18,6 +18,8 @@ import ( "strings" "testing" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" "fmt" @@ -75,6 +77,21 @@ disableKinds = ["page", "section", "taxonomy", "taxonomyTerm", "RSS", "sitemap", [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,27 +101,59 @@ outputs: %s # 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" }} + +{{ end }} +{{ .Site.Language.Lang }}: {{ T "elbow" -}} +{{ end }} +`, + "layouts/_default/list.html", `{{ define "main" }} +List HTML|{{.Title }}| +{{- with .OutputFormats.Get "HTML" -}} + +{{- 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{}) 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 @@ Output/Rel: {{ .Name -}}/{{ .Rel }}| 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 @@ Output/Rel: {{ .Name -}}/{{ .Rel }}| "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|`, + "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 + ``, + ) + th.assertFileContent("public/nn/index.json", + "List JSON|JSON Nynorsk Heim|", + "nn: Olboge", ) } diff --git a/hugolib/site_test.go b/hugolib/site_test.go index a3ec66880..5f66b153c 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -52,24 +52,6 @@ func pageMust(p *Page, err error) *Page { 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() diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go index df342953b..47f29c947 100644 --- a/hugolib/sitemap_test.go +++ b/hugolib/sitemap_test.go @@ -48,7 +48,7 @@ func doTestSitemapOutput(t *testing.T, internal bool) { 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 } diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index d50514529..cb529204a 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -124,18 +124,17 @@ func newTestSite(t testing.TB, configKeyValues ...interface{}) *Site { 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 newTestSitesFromConfig(t testing.TB, tomlConfig string, layoutPathContentPa } 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 newDebugLogger() *jww.Notepad { 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 { diff --git a/output/layout.go b/output/layout.go index a2bfd7717..6dba7f3b4 100644 --- a/output/layout.go +++ b/output/layout.go @@ -152,9 +152,11 @@ func (l *LayoutHandler) For(d LayoutDescriptor, layoutOverride string, f Format) } } - return layoutsWithThemeLayouts, nil + layouts = layoutsWithThemeLayouts } + layouts = prependTextPrefixIfNeeded(f, layouts...) + l.mu.Lock() l.cache[key] = layouts l.mu.Unlock() @@ -184,10 +186,26 @@ func resolveListTemplate(d LayoutDescriptor, f Format, } 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 { @@ -195,7 +213,9 @@ func replaceKeyValues(s string, oldNew ...string) string { 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 @@ func regularPageLayouts(types string, layout string, f Format) (layouts []string 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 } diff --git a/output/layout_base.go b/output/layout_base.go index 2bb89c20d..a0d2bc4eb 100644 --- a/output/layout_base.go +++ b/output/layout_base.go @@ -29,7 +29,10 @@ var ( ) 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 @@ type TemplateLookupDescriptor struct { // 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,6 +81,12 @@ func CreateTemplateNames(d TemplateLookupDescriptor) (TemplateNames, error) { // 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 @@ -90,6 +103,10 @@ func CreateTemplateNames(d TemplateLookupDescriptor) (TemplateNames, error) { 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) diff --git a/output/layout_base_test.go b/output/layout_base_test.go index f20d99bef..16be615f2 100644 --- a/output/layout_base_test.go +++ b/output/layout_base_test.go @@ -141,6 +141,7 @@ func TestLayoutBase(t *testing.T) { 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) @@ -150,6 +151,11 @@ func TestLayoutBase(t *testing.T) { 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) require.NoError(t, err) diff --git a/output/layout_test.go b/output/layout_test.go index e59a16fcb..6ea5a7617 100644 --- a/output/layout_test.go +++ b/output/layout_test.go @@ -64,6 +64,10 @@ func TestLayout(t *testing.T) { []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) diff --git a/output/outputFormat.go b/output/outputFormat.go index 76329a936..9d43b135a 100644 --- a/output/outputFormat.go +++ b/output/outputFormat.go @@ -33,6 +33,7 @@ var ( IsHTML: true, } + // CalendarFormat is AAA CalendarFormat = Format{ Name: "Calendar", MediaType: media.CalendarType, @@ -104,6 +105,45 @@ func (formats Formats) GetByName(name string) (f Format, found bool) { 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 +} + // Format represents an output representation, usually to a file on disk. type Format struct { // The Name is used as an identifier. Internal output formats (i.e. HTML and RSS) diff --git a/output/outputFormat_test.go b/output/outputFormat_test.go index e742012ba..b73e53f82 100644 --- a/output/outputFormat_test.go +++ b/output/outputFormat_test.go @@ -65,7 +65,7 @@ func TestDefaultTypes(t *testing.T) { } -func TestGetType(t *testing.T) { +func TestGetFormat(t *testing.T) { tp, _ := GetFormat("html") require.Equal(t, HTMLFormat, tp) tp, _ = GetFormat("HTML") @@ -73,3 +73,28 @@ func TestGetType(t *testing.T) { _, 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) +} diff --git a/tpl/template.go b/tpl/template.go index b94fc3242..617aa84ec 100644 --- 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{}) } diff --git a/tpl/tplimpl/ace.go b/tpl/tplimpl/ace.go new file mode 100644 index 000000000..fc3a1e1b1 --- /dev/null +++ b/tpl/tplimpl/ace.go @@ -0,0 +1,51 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +import ( + "path/filepath" + + "strings" + + "github.com/yosssi/ace" +) + +func (t *templateHandler) addAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error { + t.checkState() + var base, inner *ace.File + name = name[:len(name)-len(filepath.Ext(innerPath))] + ".html" + + // Fixes issue #1178 + basePath = strings.Replace(basePath, "\\", "/", -1) + innerPath = strings.Replace(innerPath, "\\", "/", -1) + + if basePath != "" { + base = ace.NewFile(basePath, baseContent) + inner = ace.NewFile(innerPath, innerContent) + } else { + base = ace.NewFile(innerPath, innerContent) + inner = ace.NewFile("", []byte{}) + } + parsed, err := ace.ParseSource(ace.NewSource(base, inner, []*ace.File{}), nil) + if err != nil { + t.errors = append(t.errors, &templateErr{name: name, err: err}) + return err + } + templ, err := ace.CompileResultWithTemplate(t.html.t.New(name), parsed, nil) + if err != nil { + t.errors = append(t.errors, &templateErr{name: name, err: err}) + return err + } + return applyTemplateTransformersToHMLTTemplate(templ) +} diff --git a/tpl/tplimpl/amber_compiler.go b/tpl/tplimpl/amber_compiler.go index 252c39ffb..10ed0443c 100644 --- a/tpl/tplimpl/amber_compiler.go +++ b/tpl/tplimpl/amber_compiler.go @@ -19,7 +19,7 @@ import ( "github.com/eknkc/amber" ) -func (gt *GoHTMLTemplate) CompileAmberWithTemplate(b []byte, path string, t *template.Template) (*template.Template, error) { +func (t *templateHandler) compileAmberWithTemplate(b []byte, path string, templ *template.Template) (*template.Template, error) { c := amber.New() if err := c.ParseData(b, path); err != nil { @@ -32,7 +32,7 @@ func (gt *GoHTMLTemplate) CompileAmberWithTemplate(b []byte, path string, t *tem return nil, err } - tpl, err := t.Funcs(gt.amberFuncMap).Parse(data) + tpl, err := templ.Funcs(t.amberFuncMap).Parse(data) if err != nil { return nil, err diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go index ea12cde7a..68528a775 100644 --- 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,23 +15,39 @@ package tplimpl import ( "html/template" - "io" - "os" - "path/filepath" "strings" - - "sync" + texttemplate "text/template" "github.com/eknkc/amber" + + "os" + + "github.com/spf13/hugo/output" + + "path/filepath" + "sync" + "github.com/spf13/afero" - bp "github.com/spf13/hugo/bufferpool" "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/output" - "github.com/yosssi/ace" + "github.com/spf13/hugo/tpl" ) -// TODO(bep) globals get rid of the rest of the jww.ERR etc. +const ( + textTmplNamePrefix = "_text/" +) + +var ( + _ tpl.TemplateHandler = (*templateHandler)(nil) + _ tpl.TemplateTestMocker = (*templateHandler)(nil) + _ tpl.TemplateFinder = (*htmlTemplates)(nil) + _ tpl.TemplateFinder = (*textTemplates)(nil) + _ templateLoader = (*htmlTemplates)(nil) + _ templateLoader = (*textTemplates)(nil) + _ templateLoader = (*templateHandler)(nil) + _ templateFuncsterTemplater = (*htmlTemplates)(nil) + _ templateFuncsterTemplater = (*textTemplates)(nil) +) // Protecting global map access (Amber) var amberMu sync.Mutex @@ -41,8 +57,120 @@ type templateErr struct { err error } -type GoHTMLTemplate struct { - *template.Template +type templateLoader interface { + handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error + addTemplate(name, tpl string) error + addLateTemplate(name, tpl string) error +} + +type templateFuncsterTemplater interface { + tpl.TemplateFinder + setFuncs(funcMap map[string]interface{}) + setTemplateFuncster(f *templateFuncster) +} + +// templateHandler holds the templates in play. +// It implements the templateLoader and tpl.TemplateHandler interfaces. +type templateHandler struct { + // text holds all the pure text templates. + text *textTemplates + html *htmlTemplates + + amberFuncMap template.FuncMap + + errors []*templateErr + + *deps.Deps +} + +func (t *templateHandler) addError(name string, err error) { + t.errors = append(t.errors, &templateErr{name, err}) +} + +// PrintErrors prints the accumulated errors as ERROR to the log. +func (t *templateHandler) PrintErrors() { + for _, e := range t.errors { + t.Log.ERROR.Println(e.name, ":", e.err) + } +} + +// Lookup tries to find a template with the given name in both template +// collections: First HTML, then the plain text template collection. +func (t *templateHandler) Lookup(name string) *tpl.TemplateAdapter { + var te *tpl.TemplateAdapter + + isTextTemplate := strings.HasPrefix(name, textTmplNamePrefix) + + if isTextTemplate { + // The templates are stored without the prefix identificator. + name = strings.TrimPrefix(name, textTmplNamePrefix) + te = t.text.Lookup(name) + } else { + te = t.html.Lookup(name) + } + + if te == nil { + return nil + } + + return te +} + +func (t *templateHandler) clone(d *deps.Deps) *templateHandler { + c := &templateHandler{ + Deps: d, + html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template)}, + text: &textTemplates{t: texttemplate.Must(t.text.t.Clone()), overlays: make(map[string]*texttemplate.Template)}, + errors: make([]*templateErr, 0), + } + + d.Tmpl = c + + c.initFuncs() + + for k, v := range t.html.overlays { + vc := template.Must(v.Clone()) + // The extra lookup is a workaround, see + // * https://github.com/golang/go/issues/16101 + // * https://github.com/spf13/hugo/issues/2549 + vc = vc.Lookup(vc.Name()) + vc.Funcs(c.html.funcster.funcMap) + c.html.overlays[k] = vc + } + + for k, v := range t.text.overlays { + vc := texttemplate.Must(v.Clone()) + vc = vc.Lookup(vc.Name()) + vc.Funcs(texttemplate.FuncMap(c.text.funcster.funcMap)) + c.text.overlays[k] = vc + } + + return c + +} + +func newTemplateAdapter(deps *deps.Deps) *templateHandler { + htmlT := &htmlTemplates{ + t: template.New(""), + overlays: make(map[string]*template.Template), + } + textT := &textTemplates{ + t: texttemplate.New(""), + overlays: make(map[string]*texttemplate.Template), + } + return &templateHandler{ + Deps: deps, + html: htmlT, + text: textT, + errors: make([]*templateErr, 0), + } + +} + +type htmlTemplates struct { + funcster *templateFuncster + + t *template.Template // This looks, and is, strange. // The clone is used by non-renderable content pages, and these need to be @@ -54,397 +182,201 @@ type GoHTMLTemplate struct { // a separate storage for the overlays created from cloned master templates. // note: No mutex protection, so we add these in one Go routine, then just read. overlays map[string]*template.Template - - errors []*templateErr - - funcster *templateFuncster - - amberFuncMap template.FuncMap - - *deps.Deps } -type TemplateProvider struct{} - -var DefaultTemplateProvider *TemplateProvider - -// Update updates the Hugo Template System in the provided Deps. -// with all the additional features, templates & functions -func (*TemplateProvider) Update(deps *deps.Deps) error { - tmpl := &GoHTMLTemplate{ - Template: template.New(""), - overlays: make(map[string]*template.Template), - errors: make([]*templateErr, 0), - Deps: deps, - } - - deps.Tmpl = tmpl - - tmpl.initFuncs(deps) - - tmpl.LoadEmbedded() - - if deps.WithTemplate != nil { - err := deps.WithTemplate(tmpl) - if err != nil { - tmpl.errors = append(tmpl.errors, &templateErr{"init", err}) - } - - } - - tmpl.MarkReady() - - return nil - +func (t *htmlTemplates) setTemplateFuncster(f *templateFuncster) { + t.funcster = f } -// Clone clones -func (*TemplateProvider) Clone(d *deps.Deps) error { - - t := d.Tmpl.(*GoHTMLTemplate) - - // 1. Clone the clone with new template funcs - // 2. Clone any overlays with new template funcs - - tmpl := &GoHTMLTemplate{ - Template: template.Must(t.Template.Clone()), - overlays: make(map[string]*template.Template), - errors: make([]*templateErr, 0), - Deps: d, +func (t *htmlTemplates) Lookup(name string) *tpl.TemplateAdapter { + templ := t.lookup(name) + if templ == nil { + return nil } - - d.Tmpl = tmpl - tmpl.initFuncs(d) - - for k, v := range t.overlays { - vc := template.Must(v.Clone()) - // The extra lookup is a workaround, see - // * https://github.com/golang/go/issues/16101 - // * https://github.com/spf13/hugo/issues/2549 - vc = vc.Lookup(vc.Name()) - vc.Funcs(tmpl.funcster.funcMap) - tmpl.overlays[k] = vc - } - - tmpl.MarkReady() - - return nil + return &tpl.TemplateAdapter{Template: templ} } -func (t *GoHTMLTemplate) initFuncs(d *deps.Deps) { - - t.funcster = newTemplateFuncster(d) - - // The URL funcs in the funcMap is somewhat language dependent, - // so we need to wait until the language and site config is loaded. - t.funcster.initFuncMap() - - t.amberFuncMap = template.FuncMap{} - - amberMu.Lock() - for k, v := range amber.FuncMap { - t.amberFuncMap[k] = v - } - - for k, v := range t.funcster.funcMap { - t.amberFuncMap[k] = v - // Hacky, but we need to make sure that the func names are in the global map. - amber.FuncMap[k] = func() string { - panic("should never be invoked") - } - } - amberMu.Unlock() - -} - -func (t *GoHTMLTemplate) Funcs(funcMap template.FuncMap) { - t.Template.Funcs(funcMap) -} - -func (t *GoHTMLTemplate) Partial(name string, contextList ...interface{}) template.HTML { - if strings.HasPrefix("partials/", name) { - name = name[8:] - } - var context interface{} - - if len(contextList) == 0 { - context = nil - } else { - context = contextList[0] - } - return t.ExecuteTemplateToHTML(context, "partials/"+name, "theme/partials/"+name) -} - -func (t *GoHTMLTemplate) executeTemplate(context interface{}, w io.Writer, layouts ...string) { - var worked bool - for _, layout := range layouts { - templ := t.Lookup(layout) - if templ == nil { - // TODO(bep) output - layout += ".html" - templ = t.Lookup(layout) - } - - if templ != nil { - if err := templ.Execute(w, context); err != nil { - helpers.DistinctErrorLog.Println(layout, err) - } - worked = true - break - } - } - if !worked { - t.Log.ERROR.Println("Unable to render", layouts) - t.Log.ERROR.Println("Expecting to find a template in either the theme/layouts or /layouts in one of the following relative locations", layouts) - } -} - -func (t *GoHTMLTemplate) ExecuteTemplateToHTML(context interface{}, layouts ...string) template.HTML { - b := bp.GetBuffer() - defer bp.PutBuffer(b) - t.executeTemplate(context, b, layouts...) - return template.HTML(b.String()) -} - -func (t *GoHTMLTemplate) Lookup(name string) *template.Template { - - if templ := t.Template.Lookup(name); templ != nil { +func (t *htmlTemplates) lookup(name string) *template.Template { + if templ := t.t.Lookup(name); templ != nil { return templ } - if t.overlays != nil { if templ, ok := t.overlays[name]; ok { return templ } } - // The clone is used for the non-renderable HTML pages (p.IsRenderable == false) that is parsed - // as Go templates late in the build process. if t.clone != nil { - if templ := t.clone.Lookup(name); templ != nil { + return t.clone.Lookup(name) + } + + return nil +} + +type textTemplates struct { + funcster *templateFuncster + + t *texttemplate.Template + + clone *texttemplate.Template + cloneClone *texttemplate.Template + + overlays map[string]*texttemplate.Template +} + +func (t *textTemplates) setTemplateFuncster(f *templateFuncster) { + t.funcster = f +} + +func (t *textTemplates) Lookup(name string) *tpl.TemplateAdapter { + templ := t.lookup(name) + if templ == nil { + return nil + } + return &tpl.TemplateAdapter{Template: templ} +} + +func (t *textTemplates) lookup(name string) *texttemplate.Template { + if templ := t.t.Lookup(name); templ != nil { + return templ + } + if t.overlays != nil { + if templ, ok := t.overlays[name]; ok { return templ } } + if t.clone != nil { + return t.clone.Lookup(name) + } + return nil +} + +func (t *templateHandler) setFuncs(funcMap map[string]interface{}) { + t.html.setFuncs(funcMap) + t.text.setFuncs(funcMap) +} + +// SetFuncs replaces the funcs in the func maps with new definitions. +// This is only used in tests. +func (t *templateHandler) SetFuncs(funcMap map[string]interface{}) { + t.setFuncs(funcMap) +} + +func (t *htmlTemplates) setFuncs(funcMap map[string]interface{}) { + t.t.Funcs(funcMap) +} + +func (t *textTemplates) setFuncs(funcMap map[string]interface{}) { + t.t.Funcs(funcMap) +} + +// LoadTemplates loads the templates, starting from the given absolute path. +// A prefix can be given to indicate a template namespace to load the templates +// into, i.e. "_internal" etc. +func (t *templateHandler) LoadTemplates(absPath, prefix string) { + // TODO(bep) output formats. Will have to get to complete list when that is ready. + t.loadTemplates(absPath, prefix, output.Formats{output.HTMLFormat, output.RSSFormat, output.CalendarFormat, output.AMPFormat, output.JSONFormat}) } -func (t *GoHTMLTemplate) GetClone() *template.Template { - return t.clone +func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) error { + templ, err := tt.New(name).Parse(tpl) + if err != nil { + return err + } + + if err := applyTemplateTransformersToHMLTTemplate(templ); err != nil { + return err + } + + return nil } -func (t *GoHTMLTemplate) RebuildClone() *template.Template { - t.clone = template.Must(t.cloneClone.Clone()) - return t.clone +func (t *htmlTemplates) addTemplate(name, tpl string) error { + return t.addTemplateIn(t.t, name, tpl) } -func (t *GoHTMLTemplate) LoadEmbedded() { - t.EmbedShortcodes() - t.EmbedTemplates() +func (t *htmlTemplates) addLateTemplate(name, tpl string) error { + return t.addTemplateIn(t.clone, name, tpl) } -// MarkReady marks the template as "ready for execution". No changes allowed +func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) error { + name = strings.TrimPrefix(name, textTmplNamePrefix) + templ, err := tt.New(name).Parse(tpl) + if err != nil { + return err + } + + if err := applyTemplateTransformersToTextTemplate(templ); err != nil { + return err + } + + return nil +} + +func (t *textTemplates) addTemplate(name, tpl string) error { + return t.addTemplateIn(t.t, name, tpl) +} + +func (t *textTemplates) addLateTemplate(name, tpl string) error { + return t.addTemplateIn(t.clone, name, tpl) +} + +func (t *templateHandler) addTemplate(name, tpl string) error { + return t.AddTemplate(name, tpl) +} + +func (t *templateHandler) addLateTemplate(name, tpl string) error { + return t.AddLateTemplate(name, tpl) +} + +// AddLateTemplate is used to add a template late, i.e. after the +// regular templates have started its execution. +func (t *templateHandler) AddLateTemplate(name, tpl string) error { + h := t.getTemplateHandler(name) + if err := h.addLateTemplate(name, tpl); err != nil { + t.addError(name, err) + return err + } + return nil +} + +// AddTemplate parses and adds a template to the collection. +// Templates with name prefixed with "_text" will be handled as plain +// text templates. +func (t *templateHandler) AddTemplate(name, tpl string) error { + h := t.getTemplateHandler(name) + if err := h.addTemplate(name, tpl); err != nil { + t.addError(name, err) + return err + } + return nil +} + +// MarkReady marks the templates as "ready for execution". No changes allowed // after this is set. // TODO(bep) if this proves to be resource heavy, we could detect // earlier if we really need this, or make it lazy. -func (t *GoHTMLTemplate) MarkReady() { - if t.clone == nil { - t.clone = template.Must(t.Template.Clone()) - t.cloneClone = template.Must(t.clone.Clone()) +func (t *templateHandler) MarkReady() { + if t.html.clone == nil { + t.html.clone = template.Must(t.html.t.Clone()) + t.html.cloneClone = template.Must(t.html.clone.Clone()) + } + if t.text.clone == nil { + t.text.clone = texttemplate.Must(t.text.t.Clone()) + t.text.cloneClone = texttemplate.Must(t.text.clone.Clone()) } } -func (t *GoHTMLTemplate) checkState() { - if t.clone != nil { - panic("template is cloned and cannot be modfified") - } +// RebuildClone rebuilds the cloned templates. Used for live-reloads. +func (t *templateHandler) RebuildClone() { + t.html.clone = template.Must(t.html.cloneClone.Clone()) + t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) } -func (t *GoHTMLTemplate) AddInternalTemplate(prefix, name, tpl string) error { - if prefix != "" { - return t.AddTemplate("_internal/"+prefix+"/"+name, tpl) - } - return t.AddTemplate("_internal/"+name, tpl) -} - -func (t *GoHTMLTemplate) AddInternalShortcode(name, content string) error { - return t.AddInternalTemplate("shortcodes", name, content) -} - -func (t *GoHTMLTemplate) AddTemplate(name, tpl string) error { - t.checkState() - templ, err := t.New(name).Parse(tpl) - if err != nil { - t.errors = append(t.errors, &templateErr{name: name, err: err}) - return err - } - if err := applyTemplateTransformers(templ); err != nil { - return err - } - - return nil -} - -func (t *GoHTMLTemplate) AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error { - - // There is currently no known way to associate a cloned template with an existing one. - // This funky master/overlay design will hopefully improve in a future version of Go. - // - // Simplicity is hard. - // - // Until then we'll have to live with this hackery. - // - // See https://github.com/golang/go/issues/14285 - // - // So, to do minimum amount of changes to get this to work: - // - // 1. Lookup or Parse the master - // 2. Parse and store the overlay in a separate map - - masterTpl := t.Lookup(masterFilename) - - if masterTpl == nil { - b, err := afero.ReadFile(t.Fs.Source, masterFilename) - if err != nil { - return err - } - masterTpl, err = t.New(masterFilename).Parse(string(b)) - - if err != nil { - // TODO(bep) Add a method that does this - t.errors = append(t.errors, &templateErr{name: name, err: err}) - return err - } - } - - b, err := afero.ReadFile(t.Fs.Source, overlayFilename) - if err != nil { - return err - } - - overlayTpl, err := template.Must(masterTpl.Clone()).Parse(string(b)) - if err != nil { - t.errors = append(t.errors, &templateErr{name: name, err: err}) - } else { - // The extra lookup is a workaround, see - // * https://github.com/golang/go/issues/16101 - // * https://github.com/spf13/hugo/issues/2549 - overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - if err := applyTemplateTransformers(overlayTpl); err != nil { - return err - } - t.overlays[name] = overlayTpl - } - - return err -} - -func (t *GoHTMLTemplate) AddAceTemplate(name, basePath, innerPath string, baseContent, innerContent []byte) error { - t.checkState() - var base, inner *ace.File - name = name[:len(name)-len(filepath.Ext(innerPath))] + ".html" - - // Fixes issue #1178 - basePath = strings.Replace(basePath, "\\", "/", -1) - innerPath = strings.Replace(innerPath, "\\", "/", -1) - - if basePath != "" { - base = ace.NewFile(basePath, baseContent) - inner = ace.NewFile(innerPath, innerContent) - } else { - base = ace.NewFile(innerPath, innerContent) - inner = ace.NewFile("", []byte{}) - } - parsed, err := ace.ParseSource(ace.NewSource(base, inner, []*ace.File{}), nil) - if err != nil { - t.errors = append(t.errors, &templateErr{name: name, err: err}) - return err - } - templ, err := ace.CompileResultWithTemplate(t.New(name), parsed, nil) - if err != nil { - t.errors = append(t.errors, &templateErr{name: name, err: err}) - return err - } - return applyTemplateTransformers(templ) -} - -func (t *GoHTMLTemplate) AddTemplateFile(name, baseTemplatePath, path string) error { - t.checkState() - // get the suffix and switch on that - ext := filepath.Ext(path) - switch ext { - case ".amber": - templateName := strings.TrimSuffix(name, filepath.Ext(name)) + ".html" - b, err := afero.ReadFile(t.Fs.Source, path) - - if err != nil { - return err - } - - amberMu.Lock() - templ, err := t.CompileAmberWithTemplate(b, path, t.New(templateName)) - amberMu.Unlock() - if err != nil { - return err - } - - return applyTemplateTransformers(templ) - case ".ace": - var innerContent, baseContent []byte - innerContent, err := afero.ReadFile(t.Fs.Source, path) - - if err != nil { - return err - } - - if baseTemplatePath != "" { - baseContent, err = afero.ReadFile(t.Fs.Source, baseTemplatePath) - if err != nil { - return err - } - } - - return t.AddAceTemplate(name, baseTemplatePath, path, baseContent, innerContent) - default: - - if baseTemplatePath != "" { - return t.AddTemplateFileWithMaster(name, path, baseTemplatePath) - } - - b, err := afero.ReadFile(t.Fs.Source, path) - - if err != nil { - return err - } - - t.Log.DEBUG.Printf("Add template file from path %s", path) - - return t.AddTemplate(name, string(b)) - } - -} - -func (t *GoHTMLTemplate) GenerateTemplateNameFrom(base, path string) string { - name, _ := filepath.Rel(base, path) - return filepath.ToSlash(name) -} - -func isDotFile(path string) bool { - return filepath.Base(path)[0] == '.' -} - -func isBackupFile(path string) bool { - return path[len(path)-1] == '~' -} - -const baseFileBase = "baseof" - -func isBaseTemplate(path string) bool { - return strings.Contains(path, baseFileBase) -} - -func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { +func (t *templateHandler) loadTemplates(absPath string, prefix string, formats output.Formats) { t.Log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix) walker := func(path string, fi os.FileInfo, err error) error { if err != nil { @@ -491,11 +423,12 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { relPath := path[li:] descriptor := output.TemplateLookupDescriptor{ - WorkingDir: workingDir, - LayoutDir: layoutDir, - RelPath: relPath, - Prefix: prefix, - Theme: t.PathSpec.Theme(), + WorkingDir: workingDir, + LayoutDir: layoutDir, + RelPath: relPath, + Prefix: prefix, + Theme: t.PathSpec.Theme(), + OutputFormats: formats, FileExists: func(filename string) (bool, error) { return helpers.Exists(filename, t.Fs.Source) }, @@ -511,7 +444,7 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { return nil } - if err := t.AddTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil { + if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil { t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, path, err) } @@ -523,16 +456,224 @@ func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { } } -func (t *GoHTMLTemplate) LoadTemplatesWithPrefix(absPath string, prefix string) { - t.loadTemplates(absPath, prefix) +func (t *templateHandler) initFuncs() { + + // The template funcs need separation between text and html templates. + for _, funcsterHolder := range []templateFuncsterTemplater{t.html, t.text} { + funcster := newTemplateFuncster(t.Deps, funcsterHolder) + + // The URL funcs in the funcMap is somewhat language dependent, + // so we need to wait until the language and site config is loaded. + funcster.initFuncMap() + + funcsterHolder.setTemplateFuncster(funcster) + + } + + // Amber is HTML only. + t.amberFuncMap = template.FuncMap{} + + amberMu.Lock() + for k, v := range amber.FuncMap { + t.amberFuncMap[k] = v + } + + for k, v := range t.html.funcster.funcMap { + t.amberFuncMap[k] = v + // Hacky, but we need to make sure that the func names are in the global map. + amber.FuncMap[k] = func() string { + panic("should never be invoked") + } + } + amberMu.Unlock() + } -func (t *GoHTMLTemplate) LoadTemplates(absPath string) { - t.loadTemplates(absPath, "") +func (t *templateHandler) getTemplateHandler(name string) templateLoader { + if strings.HasPrefix(name, textTmplNamePrefix) { + return t.text + } + return t.html } -func (t *GoHTMLTemplate) PrintErrors() { - for i, e := range t.errors { - t.Log.ERROR.Println(i, ":", e.err) +func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { + h := t.getTemplateHandler(name) + return h.handleMaster(name, overlayFilename, masterFilename, onMissing) +} + +func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { + masterTpl := t.lookup(masterFilename) + + if masterTpl == nil { + templ, err := onMissing(masterFilename) + if err != nil { + return err + } + + masterTpl, err = t.t.New(overlayFilename).Parse(templ) + if err != nil { + return err + } + } + + templ, err := onMissing(overlayFilename) + if err != nil { + return err + } + + overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ) + if err != nil { + return err + } + + // The extra lookup is a workaround, see + // * https://github.com/golang/go/issues/16101 + // * https://github.com/spf13/hugo/issues/2549 + overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) + if err := applyTemplateTransformersToHMLTTemplate(overlayTpl); err != nil { + return err + } + t.overlays[name] = overlayTpl + + return err + +} + +func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { + name = strings.TrimPrefix(name, textTmplNamePrefix) + masterTpl := t.lookup(masterFilename) + + if masterTpl == nil { + templ, err := onMissing(masterFilename) + if err != nil { + return err + } + + masterTpl, err = t.t.New(overlayFilename).Parse(templ) + if err != nil { + return err + } + } + + templ, err := onMissing(overlayFilename) + if err != nil { + return err + } + + overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ) + if err != nil { + return err + } + + overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) + if err := applyTemplateTransformersToTextTemplate(overlayTpl); err != nil { + return err + } + t.overlays[name] = overlayTpl + + return err + +} + +func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) error { + t.checkState() + + getTemplate := func(filename string) (string, error) { + b, err := afero.ReadFile(t.Fs.Source, filename) + if err != nil { + return "", err + } + return string(b), nil + } + + // get the suffix and switch on that + ext := filepath.Ext(path) + switch ext { + case ".amber": + // Only HTML support for Amber + templateName := strings.TrimSuffix(name, filepath.Ext(name)) + ".html" + b, err := afero.ReadFile(t.Fs.Source, path) + + if err != nil { + return err + } + + amberMu.Lock() + templ, err := t.compileAmberWithTemplate(b, path, t.html.t.New(templateName)) + amberMu.Unlock() + if err != nil { + return err + } + + return applyTemplateTransformersToHMLTTemplate(templ) + case ".ace": + // Only HTML support for Ace + var innerContent, baseContent []byte + innerContent, err := afero.ReadFile(t.Fs.Source, path) + + if err != nil { + return err + } + + if baseTemplatePath != "" { + baseContent, err = afero.ReadFile(t.Fs.Source, baseTemplatePath) + if err != nil { + return err + } + } + + return t.addAceTemplate(name, baseTemplatePath, path, baseContent, innerContent) + default: + + if baseTemplatePath != "" { + return t.handleMaster(name, path, baseTemplatePath, getTemplate) + } + + templ, err := getTemplate(path) + + if err != nil { + return err + } + + t.Log.DEBUG.Printf("Add template file from path %s", path) + + return t.AddTemplate(name, templ) + } + +} + +func (t *templateHandler) loadEmbedded() { + t.embedShortcodes() + t.embedTemplates() +} + +func (t *templateHandler) addInternalTemplate(prefix, name, tpl string) error { + if prefix != "" { + return t.AddTemplate("_internal/"+prefix+"/"+name, tpl) + } + return t.AddTemplate("_internal/"+name, tpl) +} + +func (t *templateHandler) addInternalShortcode(name, content string) error { + return t.addInternalTemplate("shortcodes", name, content) +} + +func (t *templateHandler) checkState() { + if t.html.clone != nil || t.text.clone != nil { + panic("template is cloned and cannot be modfified") } } + +func isDotFile(path string) bool { + return filepath.Base(path)[0] == '.' +} + +func isBackupFile(path string) bool { + return path[len(path)-1] == '~' +} + +const baseFileBase = "baseof" + +func isBaseTemplate(path string) bool { + return strings.Contains(path, baseFileBase) +} diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go new file mode 100644 index 000000000..1fbaebd43 --- /dev/null +++ b/tpl/tplimpl/templateFuncster.go @@ -0,0 +1,86 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +import ( + "fmt" + "html/template" + "strings" + + bp "github.com/spf13/hugo/bufferpool" + + "image" + + "github.com/spf13/hugo/deps" +) + +// Some of the template funcs are'nt entirely stateless. +type templateFuncster struct { + funcMap template.FuncMap + cachedPartials partialCache + image *imageHandler + + // Make sure each funcster gets its own TemplateFinder to get + // proper text and HTML template separation. + Tmpl templateFuncsterTemplater + + *deps.Deps +} + +func newTemplateFuncster(deps *deps.Deps, t templateFuncsterTemplater) *templateFuncster { + return &templateFuncster{ + Deps: deps, + Tmpl: t, + cachedPartials: partialCache{p: make(map[string]interface{})}, + image: &imageHandler{fs: deps.Fs, imageConfigCache: map[string]image.Config{}}, + } +} + +// Partial executes the named partial and returns either a string, +// when called from text/template, for or a template.HTML. +func (t *templateFuncster) partial(name string, contextList ...interface{}) (interface{}, error) { + if strings.HasPrefix("partials/", name) { + name = name[8:] + } + var context interface{} + + if len(contextList) == 0 { + context = nil + } else { + context = contextList[0] + } + + for _, n := range []string{"partials/" + name, "theme/partials/" + name} { + templ := t.Tmpl.Lookup(n) + if templ != nil { + b := bp.GetBuffer() + defer bp.PutBuffer(b) + + if err := templ.Execute(b, context); err != nil { + return "", err + } + + switch t.Tmpl.(type) { + case *htmlTemplates: + return template.HTML(b.String()), nil + case *textTemplates: + return b.String(), nil + default: + panic("Unknown type") + } + } + } + + return "", fmt.Errorf("Partial %q not found", name) +} diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go new file mode 100644 index 000000000..87cac01e5 --- /dev/null +++ b/tpl/tplimpl/templateProvider.go @@ -0,0 +1,59 @@ +// Copyright 2017-present The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tplimpl + +import ( + "github.com/spf13/hugo/deps" +) + +type TemplateProvider struct{} + +var DefaultTemplateProvider *TemplateProvider + +// Update updates the Hugo Template System in the provided Deps. +// with all the additional features, templates & functions +func (*TemplateProvider) Update(deps *deps.Deps) error { + + newTmpl := newTemplateAdapter(deps) + deps.Tmpl = newTmpl + + newTmpl.initFuncs() + newTmpl.loadEmbedded() + + if deps.WithTemplate != nil { + err := deps.WithTemplate(newTmpl) + if err != nil { + newTmpl.addError("init", err) + } + + } + + newTmpl.MarkReady() + + return nil + +} + +// Clone clones. +func (*TemplateProvider) Clone(d *deps.Deps) error { + + t := d.Tmpl.(*templateHandler) + clone := t.clone(d) + + d.Tmpl = clone + + clone.MarkReady() + + return nil +} diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go index 339e2264a..bbd0f28a4 100644 --- a/tpl/tplimpl/template_ast_transformers.go +++ b/tpl/tplimpl/template_ast_transformers.go @@ -17,6 +17,7 @@ import ( "errors" "html/template" "strings" + texttemplate "text/template" "text/template/parse" ) @@ -35,32 +36,57 @@ var paramsPaths = [][]string{ } type templateContext struct { - decl decl - templ *template.Template - visited map[string]bool + decl decl + visited map[string]bool + lookupFn func(name string) *parse.Tree } -func (c templateContext) getIfNotVisited(name string) *template.Template { +func (c templateContext) getIfNotVisited(name string) *parse.Tree { if c.visited[name] { return nil } c.visited[name] = true - return c.templ.Lookup(name) + return c.lookupFn(name) } -func newTemplateContext(templ *template.Template) *templateContext { - return &templateContext{templ: templ, decl: make(map[string]string), visited: make(map[string]bool)} +func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext { + return &templateContext{lookupFn: lookupFn, decl: make(map[string]string), visited: make(map[string]bool)} } -func applyTemplateTransformers(templ *template.Template) error { - if templ == nil || templ.Tree == nil { +func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree { + return func(nn string) *parse.Tree { + tt := templ.Lookup(nn) + if tt != nil { + return tt.Tree + } + return nil + } +} + +func applyTemplateTransformersToHMLTTemplate(templ *template.Template) error { + return applyTemplateTransformers(templ.Tree, createParseTreeLookup(templ)) +} + +func applyTemplateTransformersToTextTemplate(templ *texttemplate.Template) error { + return applyTemplateTransformers(templ.Tree, + func(nn string) *parse.Tree { + tt := templ.Lookup(nn) + if tt != nil { + return tt.Tree + } + return nil + }) +} + +func applyTemplateTransformers(templ *parse.Tree, lookupFn func(name string) *parse.Tree) error { + if templ == nil { return errors.New("expected template, but none provided") } - c := newTemplateContext(templ) + c := newTemplateContext(lookupFn) - c.paramsKeysToLower(templ.Tree.Root) + c.paramsKeysToLower(templ.Root) return nil } @@ -84,7 +110,7 @@ func (c *templateContext) paramsKeysToLower(n parse.Node) { case *parse.TemplateNode: subTempl := c.getIfNotVisited(x.Name) if subTempl != nil { - c.paramsKeysToLowerForNodes(subTempl.Tree.Root) + c.paramsKeysToLowerForNodes(subTempl.Root) } case *parse.PipeNode: for i, elem := range x.Decl { diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go index deeeae0a7..c3cf54940 100644 --- a/tpl/tplimpl/template_ast_transformers_test.go +++ b/tpl/tplimpl/template_ast_transformers_test.go @@ -115,13 +115,13 @@ F3: {{ Echo (printf "themes/%s-theme" .Site.Params.LOWER) }} func TestParamsKeysToLower(t *testing.T) { t.Parallel() - require.Error(t, applyTemplateTransformers(nil)) + require.Error(t, applyTemplateTransformers(nil, nil)) templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) require.NoError(t, err) - c := newTemplateContext(templ) + c := newTemplateContext(createParseTreeLookup(templ)) require.Equal(t, -1, c.decl.indexOfReplacementStart([]string{})) @@ -185,7 +185,7 @@ func BenchmarkTemplateParamsKeysToLower(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - c := newTemplateContext(templates[i]) + c := newTemplateContext(createParseTreeLookup(templates[i])) c.paramsKeysToLower(templ.Tree.Root) } } @@ -214,7 +214,7 @@ Blue: {{ $__amber_1.Blue}} require.NoError(t, err) - c := newTemplateContext(templ) + c := newTemplateContext(createParseTreeLookup(templ)) c.paramsKeysToLower(templ.Tree.Root) @@ -254,7 +254,7 @@ P2: {{ .Params.LOWER }} require.NoError(t, err) overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - c := newTemplateContext(overlayTpl) + c := newTemplateContext(createParseTreeLookup(overlayTpl)) c.paramsKeysToLower(overlayTpl.Tree.Root) @@ -284,7 +284,7 @@ func TestTransformRecursiveTemplate(t *testing.T) { templ, err := template.New("foo").Parse(recursive) require.NoError(t, err) - c := newTemplateContext(templ) + c := newTemplateContext(createParseTreeLookup(templ)) c.paramsKeysToLower(templ.Tree.Root) } diff --git a/tpl/tplimpl/template_embedded.go b/tpl/tplimpl/template_embedded.go index 56819b764..b1562a0e7 100644 --- 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", ` +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", `
{{ with .Get "link"}}{{ end }} @@ -41,8 +36,8 @@ func (t *GoHTMLTemplate) EmbedShortcodes() { {{ end }}
`) - t.AddInternalShortcode("speakerdeck.html", "") - t.AddInternalShortcode("youtube.html", `{{ if .IsNamedParams }} + t.addInternalShortcode("speakerdeck.html", "") + t.addInternalShortcode("youtube.html", `{{ if .IsNamedParams }}
@@ -51,21 +46,21 @@ func (t *GoHTMLTemplate) EmbedShortcodes() {
{{ end }}`) - t.AddInternalShortcode("vimeo.html", `{{ if .IsNamedParams }}
+ t.addInternalShortcode("vimeo.html", `{{ if .IsNamedParams }}
{{ else }}
{{ end }}`) - t.AddInternalShortcode("gist.html", ``) - 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", ``) + 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", ` + t.addInternalTemplate("_default", "rss.xml", ` {{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{.}} on {{ end }}{{ .Site.Title }}{{ end }} {{ .Permalink }} @@ -92,7 +87,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() { `) - t.AddInternalTemplate("_default", "sitemap.xml", ` + t.addInternalTemplate("_default", "sitemap.xml", ` {{ range .Data.Pages }} {{ .Permalink }}{{ if not .Lastmod.IsZero }} @@ -104,7 +99,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() { `) // For multilanguage sites - t.AddInternalTemplate("_default", "sitemapindex.xml", ` + t.addInternalTemplate("_default", "sitemapindex.xml", ` {{ range . }} {{ .SitemapAbsURL }} @@ -116,7 +111,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() { `) - t.AddInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }} + t.addInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }} {{ if gt $pag.TotalPages 1 }}
    {{ with $pag.First }} @@ -144,7 +139,7 @@ func (t *GoHTMLTemplate) EmbedTemplates() {
{{ end }}`) - t.AddInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}
+ t.addInternalTemplate("", "disqus.html", `{{ if .Site.DisqusShortname }}
{{ end }}`) - t.AddInternalTemplate("", "google_analytics_async.html", `{{ with .Site.GoogleAnalytics }} + t.addInternalTemplate("", "google_analytics_async.html", `{{ with .Site.GoogleAnalytics }} {{ end }}`) - t.AddInternalTemplate("_default", "robots.txt", "User-agent: *") + t.addInternalTemplate("_default", "robots.txt", "User-agent: *") } diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go index 1ec05b0c7..9703f6cff 100644 --- a/tpl/tplimpl/template_funcs.go +++ b/tpl/tplimpl/template_funcs.go @@ -45,7 +45,6 @@ import ( "github.com/bep/inflect" "github.com/spf13/afero" "github.com/spf13/cast" - "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" jww "github.com/spf13/jwalterweatherman" @@ -55,22 +54,6 @@ import ( _ "image/png" ) -// Some of the template funcs are'nt entirely stateless. -type templateFuncster struct { - funcMap template.FuncMap - cachedPartials partialCache - image *imageHandler - *deps.Deps -} - -func newTemplateFuncster(deps *deps.Deps) *templateFuncster { - return &templateFuncster{ - Deps: deps, - cachedPartials: partialCache{p: make(map[string]template.HTML)}, - image: &imageHandler{fs: deps.Fs, imageConfigCache: map[string]image.Config{}}, - } -} - // eq returns the boolean truth of arg1 == arg2. func eq(x, y interface{}) bool { normalize := func(v interface{}) interface{} { @@ -1558,13 +1541,13 @@ func replace(a, b, c interface{}) (string, error) { // partialCache represents a cache of partials protected by a mutex. type partialCache struct { sync.RWMutex - p map[string]template.HTML + p map[string]interface{} } // Get retrieves partial output from the cache based upon the partial name. // If the partial is not found in the cache, the partial is rendered and added // to the cache. -func (t *templateFuncster) Get(key, name string, context interface{}) (p template.HTML) { +func (t *templateFuncster) Get(key, name string, context interface{}) (p interface{}, err error) { var ok bool t.cachedPartials.RLock() @@ -1572,13 +1555,13 @@ func (t *templateFuncster) Get(key, name string, context interface{}) (p templat t.cachedPartials.RUnlock() if ok { - return p + return } t.cachedPartials.Lock() if p, ok = t.cachedPartials.p[key]; !ok { t.cachedPartials.Unlock() - p = t.Tmpl.Partial(name, context) + p, err = t.partial(name, context) t.cachedPartials.Lock() t.cachedPartials.p[key] = p @@ -1586,14 +1569,14 @@ func (t *templateFuncster) Get(key, name string, context interface{}) (p templat } t.cachedPartials.Unlock() - return p + return } // partialCached executes and caches partial templates. An optional variant // string parameter (a string slice actually, but be only use a variadic // argument to make it optional) can be passed so that a given partial can have // multiple uses. The cache is created with name+variant as the key. -func (t *templateFuncster) partialCached(name string, context interface{}, variant ...string) template.HTML { +func (t *templateFuncster) partialCached(name string, context interface{}, variant ...string) (interface{}, error) { key := name if len(variant) > 0 { for i := 0; i < len(variant); i++ { @@ -2195,7 +2178,7 @@ func (t *templateFuncster) initFuncMap() { "mul": func(a, b interface{}) (interface{}, error) { return helpers.DoArithmetic(a, b, '*') }, "ne": ne, "now": func() time.Time { return time.Now() }, - "partial": t.Tmpl.Partial, + "partial": t.partial, "partialCached": t.partialCached, "plainify": plainify, "pluralize": pluralize, @@ -2249,5 +2232,5 @@ func (t *templateFuncster) initFuncMap() { } t.funcMap = funcMap - t.Tmpl.Funcs(funcMap) + t.Tmpl.setFuncs(funcMap) } diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go index a9cf5e58b..b50765fcb 100644 --- a/tpl/tplimpl/template_funcs_test.go +++ b/tpl/tplimpl/template_funcs_test.go @@ -281,8 +281,8 @@ urlize: bat-man v.Set("CurrentContentLanguage", helpers.NewLanguage("en", v)) config := newDepsConfig(v) - config.WithTemplate = func(templ tpl.Template) error { - if _, err := templ.New("test").Parse(in); err != nil { + config.WithTemplate = func(templ tpl.TemplateHandler) error { + if err := templ.AddTemplate("test", in); err != nil { t.Fatal("Got error on parse", err) } return nil @@ -2858,6 +2858,96 @@ func TestReadFile(t *testing.T) { } } +func TestPartialHTMLAndText(t *testing.T) { + t.Parallel() + config := newDepsConfig(viper.New()) + + data := struct { + Name string + }{ + Name: "a+b+c", // This should get encoded in HTML. + } + + config.WithTemplate = func(templ tpl.TemplateHandler) error { + if err := templ.AddTemplate("htmlTemplate.html", `HTML Test Partial: {{ partial "test.foo" . -}}`); err != nil { + return err + } + + if err := templ.AddTemplate("_text/textTemplate.txt", `Text Test Partial: {{ partial "test.foo" . -}}`); err != nil { + return err + } + + // Use "foo" here to say that the extension doesn't really matter in this scenario. + // It will look for templates in "partials/test.foo" and "partials/test.foo.html". + if err := templ.AddTemplate("partials/test.foo", "HTML Name: {{ .Name }}"); err != nil { + return err + } + if err := templ.AddTemplate("_text/partials/test.foo", "Text Name: {{ .Name }}"); err != nil { + return err + } + + return nil + } + + de, err := deps.New(config) + require.NoError(t, err) + require.NoError(t, de.LoadResources()) + + templ := de.Tmpl.Lookup("htmlTemplate.html") + require.NotNil(t, templ) + resultHTML, err := templ.ExecuteToString(data) + require.NoError(t, err) + + templ = de.Tmpl.Lookup("_text/textTemplate.txt") + require.NotNil(t, templ) + resultText, err := templ.ExecuteToString(data) + require.NoError(t, err) + + require.Contains(t, resultHTML, "HTML Test Partial: HTML Name: a+b+c") + require.Contains(t, resultText, "Text Test Partial: Text Name: a+b+c") + +} + +func 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 `) + 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 @@ func TestPartialCached(t *testing.T) { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tpl.Template) error { + config.WithTemplate = func(templ tpl.TemplateHandler) error { err := templ.AddTemplate("testroot", tmp) if err != nil { return err @@ -2933,7 +3023,7 @@ func TestPartialCached(t *testing.T) { func BenchmarkPartial(b *testing.B) { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tpl.Template) error { + config.WithTemplate = func(templ tpl.TemplateHandler) error { err := templ.AddTemplate("testroot", `{{ partial "bench1" . }}`) if err != nil { return err @@ -2965,7 +3055,7 @@ func BenchmarkPartial(b *testing.B) { func BenchmarkPartialCached(b *testing.B) { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tpl.Template) error { + config.WithTemplate = func(templ tpl.TemplateHandler) error { err := templ.AddTemplate("testroot", `{{ partialCached "bench1" . }}`) if err != nil { return err @@ -3010,12 +3100,12 @@ func newTestFuncsterWithViper(v *viper.Viper) *templateFuncster { panic(err) } - return d.Tmpl.(*GoHTMLTemplate).funcster + return d.Tmpl.(*templateHandler).html.funcster } -func newTestTemplate(t *testing.T, name, template string) *template.Template { +func newTestTemplate(t *testing.T, name, template string) tpl.Template { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tpl.Template) error { + config.WithTemplate = func(templ tpl.TemplateHandler) error { err := templ.AddTemplate(name, template) if err != nil { return err diff --git a/tpl/tplimpl/template_test.go b/tpl/tplimpl/template_test.go index 43b834df2..998915a46 100644 --- 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 @@ import ( "github.com/stretchr/testify/require" ) -// Some tests for Issue #1178 -- Ace -func TestAceTemplates(t *testing.T) { - t.Parallel() - - for i, this := range []struct { - basePath string - innerPath string - baseContent string - innerContent string - expect string - expectErr int - }{ - {"", filepath.FromSlash("_default/single.ace"), "", "{{ . }}", "DATA", 0}, - {filepath.FromSlash("_default/baseof.ace"), filepath.FromSlash("_default/single.ace"), - `= content main - h2 This is a content named "main" of an inner template. {{ . }}`, - `= doctype html -html lang=en - head - meta charset=utf-8 - title Base and Inner Template - body - h1 This is a base template {{ . }} - = yield main`, `Base and Inner Template

This is a base template DATA

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