diff --git a/deps/deps.go b/deps/deps.go index 18886387689..659f259dd93 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.TemplateHandler `json:"-"` + Tmpl tpl.Template `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.TemplateHandler) error `json:"-"` + WithTemplate func(templ tpl.Template) error `json:"-"` translationProvider ResourceProvider } @@ -158,7 +158,7 @@ type DepsCfg struct { // Template handling. TemplateProvider ResourceProvider - WithTemplate func(templ tpl.TemplateHandler) error + WithTemplate func(templ tpl.Template) error // i18n handling. TranslationProvider ResourceProvider diff --git a/hugolib/alias.go b/hugolib/alias.go index 454744d9b3c..d5eb35777df 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -22,8 +22,6 @@ import ( "runtime" "strings" - "github.com/spf13/hugo/tpl" - jww "github.com/spf13/jwalterweatherman" "github.com/spf13/hugo/helpers" @@ -37,19 +35,18 @@ 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 { - t tpl.TemplateHandler + Templates *template.Template log *jww.Notepad allowRoot bool } -func newAliasHandler(t tpl.TemplateHandler, l *jww.Notepad, allowRoot bool) aliasHandler { +func newAliasHandler(t *template.Template, l *jww.Notepad, allowRoot bool) aliasHandler { return aliasHandler{t, l, allowRoot} } @@ -59,19 +56,12 @@ func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (i t = "alias-xhtml" } - var templ *tpl.TemplateAdapter - - if a.t != nil { - templ = a.t.Lookup("alias.html") + template := defaultAliasTemplates + if a.Templates != nil { + template = a.Templates + t = "alias.html" } - if templ == nil { - def := defaultAliasTemplates.Lookup(t) - if def != nil { - templ = &tpl.TemplateAdapter{def} - } - - } data := struct { Permalink string Page *Page @@ -81,7 +71,7 @@ func (a aliasHandler) renderAlias(isXHTML bool, permalink string, page *Page) (i } buffer := new(bytes.Buffer) - err := templ.Execute(buffer, data) + err := template.ExecuteTemplate(buffer, t, data) if err != nil { return nil, err } @@ -93,7 +83,8 @@ 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, s.Log, allowRoot) + + handler := newAliasHandler(s.Tmpl.Lookup("alias.html"), s.Log, allowRoot) isXHTML := strings.HasSuffix(path, ".xhtml") diff --git a/hugolib/embedded_shortcodes_test.go b/hugolib/embedded_shortcodes_test.go index 45de0bf0968..92821d0ef2a 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.TemplateHandler) error { - templ.(tpl.TemplateTestMocker).SetFuncs(tweetFuncMap) + withTemplate := func(templ tpl.Template) error { + templ.Funcs(tweetFuncMap) return nil } @@ -390,8 +390,8 @@ func TestShortcodeInstagram(t *testing.T) { th = testHelper{cfg, fs, t} ) - withTemplate := func(templ tpl.TemplateHandler) error { - templ.(tpl.TemplateTestMocker).SetFuncs(instagramFuncMap) + withTemplate := func(templ tpl.Template) error { + templ.Funcs(instagramFuncMap) return nil } diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 1f090cf5e6b..6c737f65af2 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -127,11 +127,11 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { return newHugoSites(cfg, sites...) } -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(), "") +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()) if s.PathSpec.ThemeSet() { - templ.LoadTemplates(s.PathSpec.GetThemeDir()+"/layouts", "theme") + templ.LoadTemplatesWithPrefix(s.PathSpec.GetThemeDir()+"/layouts", "theme") } for _, wt := range withTemplates { diff --git a/hugolib/page.go b/hugolib/page.go index 5a04c6ce70c..fa9f409223a 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -1384,14 +1384,13 @@ func (p *Page) prepareLayouts() error { if p.Kind == KindPage { if !p.IsRenderable() { self := "__" + p.UniqueID() - err := p.s.Tmpl.AddLateTemplate(self, string(p.Content)) + _, err := p.s.Tmpl.GetClone().New(self).Parse(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 58d09d6881c..f47343cb565 100644 --- a/hugolib/page_output.go +++ b/hugolib/page_output.go @@ -110,29 +110,9 @@ 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 "" + return template.HTML("") } - - 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 "" - + return p.s.Tmpl.ExecuteTemplateToHTML(p, l...) } func (p *Page) Render(layout ...string) template.HTML { diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index d72a96faafe..d165c778b22 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 tpl.TemplateExecutor) (bool, error) { +func isInnerShortcode(t *template.Template) (bool, error) { isInnerShortcodeCache.RLock() m, ok := isInnerShortcodeCache.m[t.Name()] isInnerShortcodeCache.RUnlock() @@ -188,7 +188,10 @@ func isInnerShortcode(t tpl.TemplateExecutor) (bool, error) { isInnerShortcodeCache.Lock() defer isInnerShortcodeCache.Unlock() - match, _ := regexp.MatchString("{{.*?\\.Inner.*?}}", t.Tree()) + if t.Tree == nil { + return false, errors.New("Template failed to compile") + } + match, _ := regexp.MatchString("{{.*?\\.Inner.*?}}", t.Tree.Root.String()) isInnerShortcodeCache.m[t.Name()] = match return match, nil @@ -395,6 +398,8 @@ 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()) } @@ -565,10 +570,7 @@ func replaceShortcodeTokens(source []byte, prefix string, replacements map[strin return source, nil } -func getShortcodeTemplate(name string, t tpl.TemplateHandler) *tpl.TemplateAdapter { - isInnerShortcodeCache.RLock() - defer isInnerShortcodeCache.RUnlock() - +func getShortcodeTemplate(name string, t tpl.Template) *template.Template { if x := t.Lookup("shortcodes/" + name + ".html"); x != nil { return x } @@ -578,7 +580,7 @@ func getShortcodeTemplate(name string, t tpl.TemplateHandler) *tpl.TemplateAdapt return t.Lookup("_internal/shortcodes/" + name + ".html") } -func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) string { +func renderShortcodeWithPage(tmpl *template.Template, data *ShortcodeWithPage) string { buffer := bp.GetBuffer() defer bp.PutBuffer(buffer) diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index 3d192246270..28b03aa9bba 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.TemplateHandler) error) (*Page, error) { +func pageFromString(in, filename string, withTemplate ...func(templ tpl.Template) 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.TemplateHandler) error) { +func CheckShortCodeMatch(t *testing.T, input, expected string, withTemplate func(templ tpl.Template) error) { CheckShortCodeMatchAndError(t, input, expected, withTemplate, false) } -func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.TemplateHandler) error, expectError bool) { +func CheckShortCodeMatchAndError(t *testing.T, input, expected string, withTemplate func(templ tpl.Template) error, expectError bool) { cfg, fs := newTestCfg() @@ -100,9 +100,8 @@ func TestNonSC(t *testing.T) { // Issue #929 func TestHyphenatedSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { - - tem.AddTemplate("_internal/shortcodes/hyphenated-video.html", `Playing Video {{ .Get 0 }}`) + wt := func(tem tpl.Template) error { + tem.AddInternalShortcode("hyphenated-video.html", `Playing Video {{ .Get 0 }}`) return nil } @@ -112,8 +111,8 @@ func TestHyphenatedSC(t *testing.T) { // Issue #1753 func TestNoTrailingNewline(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { - tem.AddTemplate("_internal/shortcodes/a.html", `{{ .Get 0 }}`) + wt := func(tem tpl.Template) error { + tem.AddInternalShortcode("a.html", `{{ .Get 0 }}`) return nil } @@ -122,8 +121,8 @@ func TestNoTrailingNewline(t *testing.T) { func TestPositionalParamSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { - tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 0 }}`) + wt := func(tem tpl.Template) error { + tem.AddInternalShortcode("video.html", `Playing Video {{ .Get 0 }}`) return nil } @@ -136,8 +135,8 @@ func TestPositionalParamSC(t *testing.T) { func TestPositionalParamIndexOutOfBounds(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { - tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 1 }}`) + wt := func(tem tpl.Template) error { + tem.AddInternalShortcode("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) @@ -147,8 +146,8 @@ func TestPositionalParamIndexOutOfBounds(t *testing.T) { func TestNamedParamSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { - tem.AddTemplate("_internal/shortcodes/img.html", ``) + wt := func(tem tpl.Template) error { + tem.AddInternalShortcode("img.html", ``) return nil } CheckShortCodeMatch(t, `{{< img src="one" >}}`, ``, wt) @@ -162,10 +161,10 @@ func TestNamedParamSC(t *testing.T) { // Issue #2294 func TestNestedNamedMissingParam(t *testing.T) { t.Parallel() - 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 }}
`) + wt := func(tem tpl.Template) error { + tem.AddInternalShortcode("acc.html", `
{{ .Inner }}
`) + tem.AddInternalShortcode("div.html", `
{{ .Inner }}
`) + tem.AddInternalShortcode("div2.html", `
{{ .Inner }}
`) return nil } CheckShortCodeMatch(t, @@ -175,10 +174,10 @@ func TestNestedNamedMissingParam(t *testing.T) { func TestIsNamedParamsSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { - tem.AddTemplate("_internal/shortcodes/byposition.html", `
`) - tem.AddTemplate("_internal/shortcodes/byname.html", `
`) - tem.AddTemplate("_internal/shortcodes/ifnamedparams.html", `
`) + wt := func(tem tpl.Template) error { + tem.AddInternalShortcode("byposition.html", `
`) + tem.AddInternalShortcode("byname.html", `
`) + tem.AddInternalShortcode("ifnamedparams.html", `
`) return nil } CheckShortCodeMatch(t, `{{< ifnamedparams id="name" >}}`, `
`, wt) @@ -191,8 +190,8 @@ func TestIsNamedParamsSC(t *testing.T) { func TestInnerSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { - tem.AddTemplate("_internal/shortcodes/inside.html", `{{ .Inner }}
`) + wt := func(tem tpl.Template) error { + tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) return nil } CheckShortCodeMatch(t, `{{< inside class="aspen" >}}`, `
`, wt) @@ -202,8 +201,8 @@ func TestInnerSC(t *testing.T) { func TestInnerSCWithMarkdown(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { - tem.AddTemplate("_internal/shortcodes/inside.html", `{{ .Inner }}
`) + wt := func(tem tpl.Template) error { + tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) return nil } CheckShortCodeMatch(t, `{{% inside %}} @@ -216,8 +215,8 @@ func TestInnerSCWithMarkdown(t *testing.T) { func TestInnerSCWithAndWithoutMarkdown(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { - tem.AddTemplate("_internal/shortcodes/inside.html", `{{ .Inner }}
`) + wt := func(tem tpl.Template) error { + tem.AddInternalShortcode("inside.html", `{{ .Inner }}
`) return nil } CheckShortCodeMatch(t, `{{% inside %}} @@ -247,9 +246,9 @@ func TestEmbeddedSC(t *testing.T) { func TestNestedSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { - tem.AddTemplate("_internal/shortcodes/scn1.html", `
Outer, inner is {{ .Inner }}
`) - tem.AddTemplate("_internal/shortcodes/scn2.html", `
SC2
`) + wt := func(tem tpl.Template) error { + tem.AddInternalShortcode("scn1.html", `
Outer, inner is {{ .Inner }}
`) + tem.AddInternalShortcode("scn2.html", `
SC2
`) return nil } CheckShortCodeMatch(t, `{{% scn1 %}}{{% scn2 %}}{{% /scn1 %}}`, "
Outer, inner is
SC2
\n
", wt) @@ -259,10 +258,10 @@ func TestNestedSC(t *testing.T) { func TestNestedComplexSC(t *testing.T) { t.Parallel() - 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-`) + 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-`) return nil } CheckShortCodeMatch(t, `{{< row >}}1-s{{% column %}}2-**s**{{< aside >}}3-**s**{{< /aside >}}4-s{{% /column %}}5-s{{< /row >}}6-s`, @@ -275,10 +274,10 @@ func TestNestedComplexSC(t *testing.T) { func TestParentShortcode(t *testing.T) { t.Parallel() - 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 }}`) + 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 }}`) return nil } CheckShortCodeMatch(t, `{{< r1 pr1="p1" >}}1: {{< r2 pr2="p2" >}}2: {{< r3 pr3="p3" >}}{{< /r3 >}}{{< /r2 >}}{{< /r1 >}}`, @@ -343,13 +342,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.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}}`) + 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}}`) return nil }) @@ -518,14 +517,14 @@ tags: sources[i] = source.ByteSource{Name: filepath.FromSlash(test.contentPath), Content: []byte(test.content)} } - addTemplates := func(templ tpl.TemplateHandler) error { + addTemplates := func(templ tpl.Template) error { templ.AddTemplate("_default/single.html", "{{.Content}}") - 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 }}`) + 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 }}`) return nil diff --git a/hugolib/site.go b/hugolib/site.go index 61348586436..c42b938e85c 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.TemplateHandler) error) (*Site, error) { +func NewSiteDefaultLang(withTemplate ...func(templ tpl.Template) error) (*Site, error) { v := viper.New() loadDefaultSettingsFor(v) return newSiteForLang(helpers.NewDefaultLanguage(v), withTemplate...) @@ -197,15 +197,15 @@ func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) ( // 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.TemplateHandler) error) (*Site, error) { +func NewEnglishSite(withTemplate ...func(templ tpl.Template) 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.TemplateHandler) error) (*Site, error) { - withTemplates := func(templ tpl.TemplateHandler) error { +func newSiteForLang(lang *helpers.Language, withTemplate ...func(templ tpl.Template) error) (*Site, error) { + withTemplates := func(templ tpl.Template) error { for _, wt := range withTemplate { if err := wt(templ); err != nil { return err @@ -1906,14 +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 { - templ := s.findFirstTemplate(layouts...) - if templ == nil { + layout, found := s.findFirstLayout(layouts...) + if !found { helpers.DistinctWarnLog.Printf("[%s] Unable to locate layout for %s: %s\n", s.Language.Lang, name, layouts) - return nil } - if err := templ.Execute(w, d); err != nil { + if err := s.renderThing(d, layout, w); 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) @@ -1928,13 +1927,23 @@ func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts return nil } -func (s *Site) findFirstTemplate(layouts ...string) tpl.Template { +func (s *Site) findFirstLayout(layouts ...string) (string, bool) { for _, layout := range layouts { - if templ := s.Tmpl.Lookup(layout); templ != nil { - return templ + if s.Tmpl.Lookup(layout) != nil { + return layout, true } } - return nil + 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) + } 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 06c55a69c83..86e1a55ca96 100644 --- a/hugolib/site_output_test.go +++ b/hugolib/site_output_test.go @@ -90,15 +90,9 @@ outputs: %s Alt Output: {{ .Name -}}| {{- end -}}| {{- range .OutputFormats -}} -Output/Rel: {{ .Name -}}/{{ .Rel }}|{{ .MediaType }} +Output/Rel: {{ .Name -}}/{{ .Rel }}| {{- end -}} - {{ with .OutputFormats.Get "JSON" }} - -{{ end }} `, - "layouts/_default/list.html", `List HTML|{{ with .OutputFormats.Get "HTML" -}} - -{{- end -}}`, ) require.Len(t, h.Sites, 1) @@ -119,6 +113,7 @@ Output/Rel: {{ .Name -}}/{{ .Rel }}|{{ .MediaType }} 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") @@ -133,16 +128,9 @@ Output/Rel: {{ .Name -}}/{{ .Rel }}|{{ .MediaType }} "Output/Rel: JSON/alternate|", "Output/Rel: HTML/canonical|", ) - 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|`, - ) } else { th.assertFileContent("public/index.json", "Output/Rel: JSON/canonical|", - // JSON is plain text, so no need to safeHTML this and that - ``, ) } diff --git a/hugolib/site_test.go b/hugolib/site_test.go index 5f66b153cb1..a3ec668805a 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -52,6 +52,24 @@ 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 47f29c94774..df342953b8c 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.TemplateHandler) error { + depsCfg.WithTemplate = func(templ tpl.Template) error { templ.AddTemplate("sitemap.xml", sitemapTemplate) return nil } diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index fbaab7192dc..d50514529c0 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -164,9 +164,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.TemplateHandler) error { +func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.Template) error { - return func(templ tpl.TemplateHandler) error { + return func(templ tpl.Template) 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 6dba7f3b4c5..a2bfd771785 100644 --- a/output/layout.go +++ b/output/layout.go @@ -152,11 +152,9 @@ func (l *LayoutHandler) For(d LayoutDescriptor, layoutOverride string, f Format) } } - layouts = layoutsWithThemeLayouts + return layoutsWithThemeLayouts, nil } - layouts = prependTextPrefixIfNeeded(f, layouts...) - l.mu.Lock() l.cache[key] = layouts l.mu.Unlock() @@ -186,26 +184,10 @@ func resolveListTemplate(d LayoutDescriptor, f Format, } func resolveTemplate(templ string, d LayoutDescriptor, f Format) []string { - layouts := strings.Fields(replaceKeyValues(templ, + return 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 { @@ -213,9 +195,7 @@ func replaceKeyValues(s string, oldNew ...string) string { return replacer.Replace(s) } -func regularPageLayouts(types string, layout string, f Format) []string { - var layouts []string - +func regularPageLayouts(types string, layout string, f Format) (layouts []string) { if layout == "" { layout = "single" } @@ -239,5 +219,5 @@ func regularPageLayouts(types string, layout string, f Format) []string { layouts = append(layouts, fmt.Sprintf("_default/%s.%s.%s", layout, name, suffix)) layouts = append(layouts, fmt.Sprintf("_default/%s.%s", layout, suffix)) - return layouts + return } diff --git a/output/layout_base.go b/output/layout_base.go index a0d2bc4eb7a..2bb89c20dd4 100644 --- a/output/layout_base.go +++ b/output/layout_base.go @@ -29,10 +29,7 @@ var ( ) type TemplateNames struct { - // 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 - + Name string OverlayFilename string MasterFilename string } @@ -54,10 +51,6 @@ 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) } @@ -81,12 +74,6 @@ 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 @@ -103,10 +90,6 @@ 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 16be615f2c3..f20d99befd9 100644 --- a/output/layout_base_test.go +++ b/output/layout_base_test.go @@ -141,7 +141,6 @@ 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) @@ -151,11 +150,6 @@ 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 6ea5a761707..e59a16fcbb8 100644 --- a/output/layout_test.go +++ b/output/layout_test.go @@ -64,10 +64,6 @@ 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 9d43b135a69..76329a9360f 100644 --- a/output/outputFormat.go +++ b/output/outputFormat.go @@ -33,7 +33,6 @@ var ( IsHTML: true, } - // CalendarFormat is AAA CalendarFormat = Format{ Name: "Calendar", MediaType: media.CalendarType, @@ -105,45 +104,6 @@ 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 b73e53f8265..e742012ba84 100644 --- a/output/outputFormat_test.go +++ b/output/outputFormat_test.go @@ -65,7 +65,7 @@ func TestDefaultTypes(t *testing.T) { } -func TestGetFormat(t *testing.T) { +func TestGetType(t *testing.T) { tp, _ := GetFormat("html") require.Equal(t, HTMLFormat, tp) tp, _ = GetFormat("HTML") @@ -73,28 +73,3 @@ func TestGetFormat(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 617aa84ecbe..b94fc3242ac 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -1,103 +1,28 @@ -// 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 ( - "io" - - "text/template/parse" - "html/template" - texttemplate "text/template" - - bp "github.com/spf13/hugo/bufferpool" -) - -var ( - _ TemplateExecutor = (*TemplateAdapter)(nil) + "io" ) -// TemplateHandler manages the collection of templates. -type TemplateHandler interface { - TemplateFinder +// 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) AddTemplate(name, tpl string) error - AddLateTemplate(name, tpl string) error - LoadTemplates(absPath, prefix string) + 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 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 deleted file mode 100644 index fc3a1e1b1de..00000000000 --- a/tpl/tplimpl/ace.go +++ /dev/null @@ -1,51 +0,0 @@ -// 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 10ed0443c21..252c39ffb5d 100644 --- a/tpl/tplimpl/amber_compiler.go +++ b/tpl/tplimpl/amber_compiler.go @@ -19,7 +19,7 @@ import ( "github.com/eknkc/amber" ) -func (t *templateHandler) compileAmberWithTemplate(b []byte, path string, templ *template.Template) (*template.Template, error) { +func (gt *GoHTMLTemplate) CompileAmberWithTemplate(b []byte, path string, t *template.Template) (*template.Template, error) { c := amber.New() if err := c.ParseData(b, path); err != nil { @@ -32,7 +32,7 @@ func (t *templateHandler) compileAmberWithTemplate(b []byte, path string, templ return nil, err } - tpl, err := templ.Funcs(t.amberFuncMap).Parse(data) + tpl, err := t.Funcs(gt.amberFuncMap).Parse(data) if err != nil { return nil, err diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go index 49be4a7696a..ea12cde7a70 100644 --- a/tpl/tplimpl/template.go +++ b/tpl/tplimpl/template.go @@ -1,4 +1,4 @@ -// Copyright 2017-present The Hugo Authors. All rights reserved. +// Copyright 2016 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,39 +15,23 @@ package tplimpl import ( "html/template" - "strings" - texttemplate "text/template" - - "github.com/eknkc/amber" - + "io" "os" - - "github.com/spf13/hugo/output" - "path/filepath" + "strings" + "sync" + "github.com/eknkc/amber" "github.com/spf13/afero" + bp "github.com/spf13/hugo/bufferpool" "github.com/spf13/hugo/deps" "github.com/spf13/hugo/helpers" - "github.com/spf13/hugo/tpl" -) - -const ( - textTmplNamePrefix = "_text/" + "github.com/spf13/hugo/output" + "github.com/yosssi/ace" ) -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) -) +// TODO(bep) globals get rid of the rest of the jww.ERR etc. // Protecting global map access (Amber) var amberMu sync.Mutex @@ -57,539 +41,340 @@ type templateErr struct { err error } -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) -} +type GoHTMLTemplate struct { + *template.Template -// templateHandler holds the templates in play. -// It implements the templateLoader and tpl.TemplateHandler interfaces. -type templateHandler struct { - // text holds all the pure text templates. - text *textTemplates - html *htmlTemplates + // This looks, and is, strange. + // The clone is used by non-renderable content pages, and these need to be + // re-parsed on content change, and to avoid the + // "cannot Parse after Execute" error, we need to re-clone it from the original clone. + clone *template.Template + cloneClone *template.Template - amberFuncMap template.FuncMap + // 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 } -func (t *templateHandler) addError(name string, err error) { - t.errors = append(t.errors, &templateErr{name, err}) -} +type TemplateProvider struct{} + +var DefaultTemplateProvider *TemplateProvider -// 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) +// 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, } -} -// 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 + deps.Tmpl = tmpl - isTextTemplate := strings.HasPrefix(name, textTmplNamePrefix) + tmpl.initFuncs(deps) - 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) - } + tmpl.LoadEmbedded() + + if deps.WithTemplate != nil { + err := deps.WithTemplate(tmpl) + if err != nil { + tmpl.errors = append(tmpl.errors, &templateErr{"init", err}) + } - if te == nil { - return nil } - return te + tmpl.MarkReady() + + return nil + } -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), - } +// Clone clones +func (*TemplateProvider) Clone(d *deps.Deps) error { - d.Tmpl = c + t := d.Tmpl.(*GoHTMLTemplate) - c.initFuncs() + // 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, + } - for k, v := range t.html.overlays { + 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(t.html.funcster.funcMap) - c.html.overlays[k] = vc - } - - for k, v := range t.text.overlays { - vc := texttemplate.Must(v.Clone()) - vc = vc.Lookup(vc.Name()) - vc.Funcs(texttemplate.FuncMap(t.text.funcster.funcMap)) - c.text.overlays[k] = vc + vc.Funcs(tmpl.funcster.funcMap) + tmpl.overlays[k] = vc } - return c + tmpl.MarkReady() + return nil } -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), - } +func (t *GoHTMLTemplate) initFuncs(d *deps.Deps) { -} + t.funcster = newTemplateFuncster(d) -type htmlTemplates struct { - funcster *templateFuncster + // 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 *template.Template + t.amberFuncMap = template.FuncMap{} - // This looks, and is, strange. - // The clone is used by non-renderable content pages, and these need to be - // re-parsed on content change, and to avoid the - // "cannot Parse after Execute" error, we need to re-clone it from the original clone. - clone *template.Template - cloneClone *template.Template + amberMu.Lock() + for k, v := range amber.FuncMap { + t.amberFuncMap[k] = v + } - // 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 -} + 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 *htmlTemplates) setTemplateFuncster(f *templateFuncster) { - t.funcster = f } -func (t *htmlTemplates) Lookup(name string) *tpl.TemplateAdapter { - templ := t.lookup(name) - if templ == nil { - return nil - } - return &tpl.TemplateAdapter{templ} +func (t *GoHTMLTemplate) Funcs(funcMap template.FuncMap) { + t.Template.Funcs(funcMap) } -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 - } +func (t *GoHTMLTemplate) Partial(name string, contextList ...interface{}) template.HTML { + if strings.HasPrefix("partials/", name) { + name = name[8:] } + var context interface{} - if t.clone != nil { - return t.clone.Lookup(name) + if len(contextList) == 0 { + context = nil + } else { + context = contextList[0] } - - return nil + return t.ExecuteTemplateToHTML(context, "partials/"+name, "theme/partials/"+name) } -type textTemplates struct { - funcster *templateFuncster - - t *texttemplate.Template - - clone *texttemplate.Template - cloneClone *texttemplate.Template +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) + } - overlays map[string]*texttemplate.Template + 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 *textTemplates) setTemplateFuncster(f *templateFuncster) { - t.funcster = f +func (t *GoHTMLTemplate) ExecuteTemplateToHTML(context interface{}, layouts ...string) template.HTML { + b := bp.GetBuffer() + defer bp.PutBuffer(b) + t.executeTemplate(context, b, layouts...) + return template.HTML(b.String()) } -func (t *textTemplates) Lookup(name string) *tpl.TemplateAdapter { - templ := t.lookup(name) - if templ == nil { - return nil - } - return &tpl.TemplateAdapter{templ} -} +func (t *GoHTMLTemplate) Lookup(name string) *template.Template { -func (t *textTemplates) lookup(name string) *texttemplate.Template { - if templ := t.t.Lookup(name); templ != nil { + if templ := t.Template.Lookup(name); templ != nil { return templ } + if t.overlays != nil { if templ, ok := t.overlays[name]; ok { return templ } } + // The clone is used for the non-renderable HTML pages (p.IsRenderable == false) that is parsed + // as Go templates late in the build process. if t.clone != nil { - return t.clone.Lookup(name) + if templ := t.clone.Lookup(name); templ != nil { + return templ + } } 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 *GoHTMLTemplate) GetClone() *template.Template { + return t.clone } -func (t *textTemplates) setFuncs(funcMap map[string]interface{}) { - t.t.Funcs(funcMap) +func (t *GoHTMLTemplate) RebuildClone() *template.Template { + t.clone = template.Must(t.cloneClone.Clone()) + return t.clone } -// LoadTemplates loads the templates, starting from the given absolute path. -// A prefix can be given to indicate a template namespace to load the templates -// into, i.e. "_internal" etc. -func (t *templateHandler) LoadTemplates(absPath, prefix string) { - // TODO(bep) output formats. Will have to get to complete list when that is ready. - t.loadTemplates(absPath, prefix, output.Formats{output.HTMLFormat, output.RSSFormat, output.CalendarFormat, output.AMPFormat, output.JSONFormat}) - +func (t *GoHTMLTemplate) LoadEmbedded() { + t.EmbedShortcodes() + t.EmbedTemplates() } -func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) error { - templ, err := tt.New(name).Parse(tpl) - if err != nil { - return err +// MarkReady marks the template as "ready for execution". No changes allowed +// after this is set. +// TODO(bep) if this proves to be resource heavy, we could detect +// earlier if we really need this, or make it lazy. +func (t *GoHTMLTemplate) MarkReady() { + if t.clone == nil { + t.clone = template.Must(t.Template.Clone()) + t.cloneClone = template.Must(t.clone.Clone()) } +} - if err := applyTemplateTransformersToHMLTTemplate(templ); err != nil { - return err +func (t *GoHTMLTemplate) checkState() { + if t.clone != nil { + panic("template is cloned and cannot be modfified") } - - return nil } -func (t *htmlTemplates) addTemplate(name, tpl string) error { - return t.addTemplateIn(t.t, name, tpl) +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 *htmlTemplates) addLateTemplate(name, tpl string) error { - return t.addTemplateIn(t.clone, name, tpl) +func (t *GoHTMLTemplate) AddInternalShortcode(name, content string) error { + return t.AddInternalTemplate("shortcodes", name, content) } -func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) error { - name = strings.TrimPrefix(name, textTmplNamePrefix) - templ, err := tt.New(name).Parse(tpl) +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 := 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) + if err := applyTemplateTransformers(templ); err != nil { return err } - return nil -} -// AddTemplate parses and adds a template to the collection. -// Templates with name prefixed with "_text" will be handled as plain -// text templates. -func (t *templateHandler) AddTemplate(name, tpl string) error { - h := t.getTemplateHandler(name) - if err := h.addTemplate(name, tpl); err != nil { - t.addError(name, err) - return err - } return nil } -// MarkReady marks the templates as "ready for execution". No changes allowed -// after this is set. -// TODO(bep) if this proves to be resource heavy, we could detect -// earlier if we really need this, or make it lazy. -func (t *templateHandler) MarkReady() { - if t.html.clone == nil { - t.html.clone = template.Must(t.html.t.Clone()) - t.html.cloneClone = template.Must(t.html.clone.Clone()) - } - if t.text.clone == nil { - t.text.clone = texttemplate.Must(t.text.t.Clone()) - t.text.cloneClone = texttemplate.Must(t.text.clone.Clone()) - } -} - -// RebuildClone rebuilds the cloned templates. Used for live-reloads. -func (t *templateHandler) RebuildClone() { - t.html.clone = template.Must(t.html.cloneClone.Clone()) - t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) -} - -func (t *templateHandler) loadTemplates(absPath string, prefix string, formats output.Formats) { - t.Log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix) - walker := func(path string, fi os.FileInfo, err error) error { - if err != nil { - return nil - } - - t.Log.DEBUG.Println("Template path", path) - if fi.Mode()&os.ModeSymlink == os.ModeSymlink { - link, err := filepath.EvalSymlinks(absPath) - if err != nil { - t.Log.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", absPath, err) - return nil - } - - linkfi, err := t.Fs.Source.Stat(link) - if err != nil { - t.Log.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) - return nil - } - - if !linkfi.Mode().IsRegular() { - t.Log.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", absPath) - } - return nil - } - - if !fi.IsDir() { - if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) { - return nil - } - - var ( - workingDir = t.PathSpec.WorkingDir() - themeDir = t.PathSpec.GetThemeDir() - layoutDir = t.PathSpec.LayoutDir() - ) - - if themeDir != "" && strings.HasPrefix(absPath, themeDir) { - workingDir = themeDir - layoutDir = "layouts" - } - - li := strings.LastIndex(path, layoutDir) + len(layoutDir) + 1 - relPath := path[li:] - - descriptor := output.TemplateLookupDescriptor{ - WorkingDir: workingDir, - LayoutDir: layoutDir, - RelPath: relPath, - Prefix: prefix, - Theme: t.PathSpec.Theme(), - OutputFormats: formats, - FileExists: func(filename string) (bool, error) { - return helpers.Exists(filename, t.Fs.Source) - }, - ContainsAny: func(filename string, subslices [][]byte) (bool, error) { - return helpers.FileContainsAny(filename, subslices, t.Fs.Source) - }, - } - - tplID, err := output.CreateTemplateNames(descriptor) - if err != nil { - t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err) - - return nil - } - - if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil { - t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, path, err) - } - - } - return nil - } - if err := helpers.SymbolicWalk(t.Fs.Source, absPath, walker); err != nil { - t.Log.ERROR.Printf("Failed to load templates: %s", err) - } -} - -func (t *templateHandler) initFuncs() { - - // The template funcs need separation between text and html templates. - for _, funcsterHolder := range []templateFuncsterTemplater{t.html, t.text} { - funcster := newTemplateFuncster(t.Deps, funcsterHolder) - - // The URL funcs in the funcMap is somewhat language dependent, - // so we need to wait until the language and site config is loaded. - funcster.initFuncMap() - - funcsterHolder.setTemplateFuncster(funcster) - - } +func (t *GoHTMLTemplate) AddTemplateFileWithMaster(name, overlayFilename, masterFilename string) error { - // Amber is HTML only. - t.amberFuncMap = template.FuncMap{} + // 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 - amberMu.Lock() - for k, v := range amber.FuncMap { - t.amberFuncMap[k] = v - } - - for k, v := range t.html.funcster.funcMap { - t.amberFuncMap[k] = v - // Hacky, but we need to make sure that the func names are in the global map. - amber.FuncMap[k] = func() string { - panic("should never be invoked") - } - } - amberMu.Unlock() - -} - -func (t *templateHandler) getTemplateHandler(name string) templateLoader { - if strings.HasPrefix(name, textTmplNamePrefix) { - return t.text - } - return t.html -} - -func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { - h := t.getTemplateHandler(name) - return h.handleMaster(name, overlayFilename, masterFilename, onMissing) -} - -func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { - masterTpl := t.lookup(masterFilename) + masterTpl := t.Lookup(masterFilename) if masterTpl == nil { - templ, err := onMissing(masterFilename) + b, err := afero.ReadFile(t.Fs.Source, masterFilename) if err != nil { return err } + masterTpl, err = t.New(masterFilename).Parse(string(b)) - masterTpl, err = t.t.New(overlayFilename).Parse(templ) if err != nil { + // TODO(bep) Add a method that does this + t.errors = append(t.errors, &templateErr{name: name, err: err}) return err } } - templ, err := onMissing(overlayFilename) + b, err := afero.ReadFile(t.Fs.Source, overlayFilename) if err != nil { return err } - overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ) + overlayTpl, err := template.Must(masterTpl.Clone()).Parse(string(b)) 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.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 } - t.overlays[name] = overlayTpl return err - } -func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { - masterTpl := t.lookup(masterFilename) +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" - if masterTpl == nil { - templ, err := onMissing(masterFilename) - if err != nil { - return err - } + // Fixes issue #1178 + basePath = strings.Replace(basePath, "\\", "/", -1) + innerPath = strings.Replace(innerPath, "\\", "/", -1) - masterTpl, err = t.t.New(overlayFilename).Parse(templ) - if err != nil { - return err - } + if basePath != "" { + base = ace.NewFile(basePath, baseContent) + inner = ace.NewFile(innerPath, innerContent) + } else { + base = ace.NewFile(innerPath, innerContent) + inner = ace.NewFile("", []byte{}) } - - templ, err := onMissing(overlayFilename) + 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 } - - overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ) + templ, err := ace.CompileResultWithTemplate(t.New(name), parsed, nil) if err != nil { + t.errors = append(t.errors, &templateErr{name: name, err: err}) return err } - - overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - if err := applyTemplateTransformersToTextTemplate(overlayTpl); err != nil { - return err - } - t.overlays[name] = overlayTpl - - return err - + return applyTemplateTransformers(templ) } -func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) error { +func (t *GoHTMLTemplate) 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) @@ -598,15 +383,14 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e } amberMu.Lock() - templ, err := t.compileAmberWithTemplate(b, path, t.html.t.New(templateName)) + templ, err := t.CompileAmberWithTemplate(b, path, t.New(templateName)) amberMu.Unlock() if err != nil { return err } - return applyTemplateTransformersToHMLTTemplate(templ) + return applyTemplateTransformers(templ) case ".ace": - // Only HTML support for Ace var innerContent, baseContent []byte innerContent, err := afero.ReadFile(t.Fs.Source, path) @@ -621,14 +405,14 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e } } - return t.addAceTemplate(name, baseTemplatePath, path, baseContent, innerContent) + return t.AddAceTemplate(name, baseTemplatePath, path, baseContent, innerContent) default: if baseTemplatePath != "" { - return t.handleMaster(name, path, baseTemplatePath, getTemplate) + return t.AddTemplateFileWithMaster(name, path, baseTemplatePath) } - templ, err := getTemplate(path) + b, err := afero.ReadFile(t.Fs.Source, path) if err != nil { return err @@ -636,31 +420,14 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e 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(name, string(b)) } - 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 (t *GoHTMLTemplate) GenerateTemplateNameFrom(base, path string) string { + name, _ := filepath.Rel(base, path) + return filepath.ToSlash(name) } func isDotFile(path string) bool { @@ -676,3 +443,96 @@ const baseFileBase = "baseof" func isBaseTemplate(path string) bool { return strings.Contains(path, baseFileBase) } + +func (t *GoHTMLTemplate) loadTemplates(absPath string, prefix string) { + t.Log.DEBUG.Printf("Load templates from path %q prefix %q", absPath, prefix) + walker := func(path string, fi os.FileInfo, err error) error { + if err != nil { + return nil + } + + t.Log.DEBUG.Println("Template path", path) + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + link, err := filepath.EvalSymlinks(absPath) + if err != nil { + t.Log.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", absPath, err) + return nil + } + + linkfi, err := t.Fs.Source.Stat(link) + if err != nil { + t.Log.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) + return nil + } + + if !linkfi.Mode().IsRegular() { + t.Log.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", absPath) + } + return nil + } + + if !fi.IsDir() { + if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) { + return nil + } + + var ( + workingDir = t.PathSpec.WorkingDir() + themeDir = t.PathSpec.GetThemeDir() + layoutDir = t.PathSpec.LayoutDir() + ) + + if themeDir != "" && strings.HasPrefix(absPath, themeDir) { + workingDir = themeDir + layoutDir = "layouts" + } + + li := strings.LastIndex(path, layoutDir) + len(layoutDir) + 1 + relPath := path[li:] + + descriptor := output.TemplateLookupDescriptor{ + WorkingDir: workingDir, + LayoutDir: layoutDir, + RelPath: relPath, + Prefix: prefix, + Theme: t.PathSpec.Theme(), + FileExists: func(filename string) (bool, error) { + return helpers.Exists(filename, t.Fs.Source) + }, + ContainsAny: func(filename string, subslices [][]byte) (bool, error) { + return helpers.FileContainsAny(filename, subslices, t.Fs.Source) + }, + } + + tplID, err := output.CreateTemplateNames(descriptor) + if err != nil { + t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err) + + return nil + } + + if err := t.AddTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil { + t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, path, err) + } + + } + return nil + } + if err := helpers.SymbolicWalk(t.Fs.Source, absPath, walker); err != nil { + t.Log.ERROR.Printf("Failed to load templates: %s", err) + } +} + +func (t *GoHTMLTemplate) LoadTemplatesWithPrefix(absPath string, prefix string) { + t.loadTemplates(absPath, prefix) +} + +func (t *GoHTMLTemplate) LoadTemplates(absPath string) { + t.loadTemplates(absPath, "") +} + +func (t *GoHTMLTemplate) PrintErrors() { + for i, e := range t.errors { + t.Log.ERROR.Println(i, ":", e.err) + } +} diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go deleted file mode 100644 index 1fbaebd43f9..00000000000 --- a/tpl/tplimpl/templateFuncster.go +++ /dev/null @@ -1,86 +0,0 @@ -// 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 deleted file mode 100644 index 87cac01e582..00000000000 --- a/tpl/tplimpl/templateProvider.go +++ /dev/null @@ -1,59 +0,0 @@ -// 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 bbd0f28a46f..339e2264a24 100644 --- a/tpl/tplimpl/template_ast_transformers.go +++ b/tpl/tplimpl/template_ast_transformers.go @@ -17,7 +17,6 @@ import ( "errors" "html/template" "strings" - texttemplate "text/template" "text/template/parse" ) @@ -36,57 +35,32 @@ var paramsPaths = [][]string{ } type templateContext struct { - decl decl - visited map[string]bool - lookupFn func(name string) *parse.Tree + decl decl + templ *template.Template + visited map[string]bool } -func (c templateContext) getIfNotVisited(name string) *parse.Tree { +func (c templateContext) getIfNotVisited(name string) *template.Template { if c.visited[name] { return nil } c.visited[name] = true - return c.lookupFn(name) + return c.templ.Lookup(name) } -func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext { - return &templateContext{lookupFn: lookupFn, decl: make(map[string]string), visited: make(map[string]bool)} +func newTemplateContext(templ *template.Template) *templateContext { + return &templateContext{templ: templ, decl: make(map[string]string), visited: make(map[string]bool)} } -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 { +func applyTemplateTransformers(templ *template.Template) error { + if templ == nil || templ.Tree == nil { return errors.New("expected template, but none provided") } - c := newTemplateContext(lookupFn) + c := newTemplateContext(templ) - c.paramsKeysToLower(templ.Root) + c.paramsKeysToLower(templ.Tree.Root) return nil } @@ -110,7 +84,7 @@ func (c *templateContext) paramsKeysToLower(n parse.Node) { case *parse.TemplateNode: subTempl := c.getIfNotVisited(x.Name) if subTempl != nil { - c.paramsKeysToLowerForNodes(subTempl.Root) + c.paramsKeysToLowerForNodes(subTempl.Tree.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 c3cf549407c..deeeae0a7e6 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, nil)) + require.Error(t, applyTemplateTransformers(nil)) templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) require.NoError(t, err) - c := newTemplateContext(createParseTreeLookup(templ)) + c := newTemplateContext(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(createParseTreeLookup(templates[i])) + c := newTemplateContext(templates[i]) c.paramsKeysToLower(templ.Tree.Root) } } @@ -214,7 +214,7 @@ Blue: {{ $__amber_1.Blue}} require.NoError(t, err) - c := newTemplateContext(createParseTreeLookup(templ)) + c := newTemplateContext(templ) c.paramsKeysToLower(templ.Tree.Root) @@ -254,7 +254,7 @@ P2: {{ .Params.LOWER }} require.NoError(t, err) overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - c := newTemplateContext(createParseTreeLookup(overlayTpl)) + c := newTemplateContext(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(createParseTreeLookup(templ)) + c := newTemplateContext(templ) c.paramsKeysToLower(templ.Tree.Root) } diff --git a/tpl/tplimpl/template_embedded.go b/tpl/tplimpl/template_embedded.go index b1562a0e786..56819b764cb 100644 --- a/tpl/tplimpl/template_embedded.go +++ b/tpl/tplimpl/template_embedded.go @@ -1,4 +1,4 @@ -// Copyright 2017-present The Hugo Authors. All rights reserved. +// Copyright 2015 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,12 +13,17 @@ package tplimpl -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", ` +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", `
{{ with .Get "link"}}{{ end }} @@ -36,8 +41,8 @@ func (t *templateHandler) embedShortcodes() { {{ end }}
`) - t.addInternalShortcode("speakerdeck.html", "") - t.addInternalShortcode("youtube.html", `{{ if .IsNamedParams }} + t.AddInternalShortcode("speakerdeck.html", "") + t.AddInternalShortcode("youtube.html", `{{ if .IsNamedParams }}
@@ -46,21 +51,21 @@ func (t *templateHandler) 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 *templateHandler) embedTemplates() { +func (t *GoHTMLTemplate) 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 }} @@ -87,7 +92,7 @@ func (t *templateHandler) embedTemplates() { `) - t.addInternalTemplate("_default", "sitemap.xml", ` + t.AddInternalTemplate("_default", "sitemap.xml", ` {{ range .Data.Pages }} {{ .Permalink }}{{ if not .Lastmod.IsZero }} @@ -99,7 +104,7 @@ func (t *templateHandler) embedTemplates() { `) // For multilanguage sites - t.addInternalTemplate("_default", "sitemapindex.xml", ` + t.AddInternalTemplate("_default", "sitemapindex.xml", ` {{ range . }} {{ .SitemapAbsURL }} @@ -111,7 +116,7 @@ func (t *templateHandler) embedTemplates() { `) - t.addInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }} + t.AddInternalTemplate("", "pagination.html", `{{ $pag := $.Paginator }} {{ if gt $pag.TotalPages 1 }}
    {{ with $pag.First }} @@ -139,7 +144,7 @@ func (t *templateHandler) 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 9703f6cff51..1ec05b0c724 100644 --- a/tpl/tplimpl/template_funcs.go +++ b/tpl/tplimpl/template_funcs.go @@ -45,6 +45,7 @@ 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" @@ -54,6 +55,22 @@ 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{} { @@ -1541,13 +1558,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]interface{} + p map[string]template.HTML } // 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 interface{}, err error) { +func (t *templateFuncster) Get(key, name string, context interface{}) (p template.HTML) { var ok bool t.cachedPartials.RLock() @@ -1555,13 +1572,13 @@ func (t *templateFuncster) Get(key, name string, context interface{}) (p interfa t.cachedPartials.RUnlock() if ok { - return + return p } t.cachedPartials.Lock() if p, ok = t.cachedPartials.p[key]; !ok { t.cachedPartials.Unlock() - p, err = t.partial(name, context) + p = t.Tmpl.Partial(name, context) t.cachedPartials.Lock() t.cachedPartials.p[key] = p @@ -1569,14 +1586,14 @@ func (t *templateFuncster) Get(key, name string, context interface{}) (p interfa } t.cachedPartials.Unlock() - return + return p } // 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) (interface{}, error) { +func (t *templateFuncster) partialCached(name string, context interface{}, variant ...string) template.HTML { key := name if len(variant) > 0 { for i := 0; i < len(variant); i++ { @@ -2178,7 +2195,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.partial, + "partial": t.Tmpl.Partial, "partialCached": t.partialCached, "plainify": plainify, "pluralize": pluralize, @@ -2232,5 +2249,5 @@ func (t *templateFuncster) initFuncMap() { } t.funcMap = funcMap - t.Tmpl.setFuncs(funcMap) + t.Tmpl.Funcs(funcMap) } diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go index b50765fcb95..a9cf5e58bf8 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.TemplateHandler) error { - if err := templ.AddTemplate("test", in); err != nil { + config.WithTemplate = func(templ tpl.Template) error { + if _, err := templ.New("test").Parse(in); err != nil { t.Fatal("Got error on parse", err) } return nil @@ -2858,96 +2858,6 @@ 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 { @@ -2983,7 +2893,7 @@ func TestPartialCached(t *testing.T) { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tpl.TemplateHandler) error { + config.WithTemplate = func(templ tpl.Template) error { err := templ.AddTemplate("testroot", tmp) if err != nil { return err @@ -3023,7 +2933,7 @@ func TestPartialCached(t *testing.T) { func BenchmarkPartial(b *testing.B) { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tpl.TemplateHandler) error { + config.WithTemplate = func(templ tpl.Template) error { err := templ.AddTemplate("testroot", `{{ partial "bench1" . }}`) if err != nil { return err @@ -3055,7 +2965,7 @@ func BenchmarkPartial(b *testing.B) { func BenchmarkPartialCached(b *testing.B) { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tpl.TemplateHandler) error { + config.WithTemplate = func(templ tpl.Template) error { err := templ.AddTemplate("testroot", `{{ partialCached "bench1" . }}`) if err != nil { return err @@ -3100,12 +3010,12 @@ func newTestFuncsterWithViper(v *viper.Viper) *templateFuncster { panic(err) } - return d.Tmpl.(*templateHandler).html.funcster + return d.Tmpl.(*GoHTMLTemplate).funcster } -func newTestTemplate(t *testing.T, name, template string) tpl.Template { +func newTestTemplate(t *testing.T, name, template string) *template.Template { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tpl.TemplateHandler) error { + config.WithTemplate = func(templ tpl.Template) 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 998915a467b..43b834df249 100644 --- a/tpl/tplimpl/template_test.go +++ b/tpl/tplimpl/template_test.go @@ -14,10 +14,17 @@ 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" @@ -25,6 +32,223 @@ 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() @@ -61,7 +285,7 @@ func TestTplGoFuzzReports(t *testing.T) { config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tpl.TemplateHandler) error { + config.WithTemplate = func(templ tpl.Template) error { return templ.AddTemplate("fuzz", this.data) } @@ -69,7 +293,7 @@ func TestTplGoFuzzReports(t *testing.T) { require.NoError(t, err) require.NoError(t, de.LoadResources()) - templ := de.Tmpl.(*templateHandler) + templ := de.Tmpl.(*GoHTMLTemplate) if len(templ.errors) > 0 && this.expectErr == 0 { t.Errorf("Test %d errored: %v", i, templ.errors) @@ -77,9 +301,7 @@ func TestTplGoFuzzReports(t *testing.T) { t.Errorf("#1 Test %d should have errored", i) } - tt := de.Tmpl.Lookup("fuzz") - require.NotNil(t, tt) - err = tt.Execute(ioutil.Discard, d) + err = de.Tmpl.ExecuteTemplate(ioutil.Discard, "fuzz", d) if err != nil && this.expectErr == 0 { t.Fatalf("Test %d errored: %s", i, err)