diff --git a/commands/server.go b/commands/server.go index 709181507..7d884096c 100644 --- a/commands/server.go +++ b/commands/server.go @@ -16,6 +16,7 @@ package commands import ( "bytes" "fmt" + "io" "net" "net/http" "net/url" @@ -33,7 +34,6 @@ import ( "github.com/pkg/errors" "github.com/gohugoio/hugo/livereload" - "github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" @@ -287,7 +287,7 @@ func getRootWatchDirsStr(baseDir string, watchDirs []string) string { type fileServer struct { baseURLs []string roots []string - errorTemplate tpl.Template + errorTemplate func(err interface{}) (io.Reader, error) c *commandeer s *serverCmd } @@ -335,8 +335,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro err := f.c.getErrorWithContext() if err != nil { w.WriteHeader(500) - var b bytes.Buffer - err := f.errorTemplate.Execute(&b, err) + r, err := f.errorTemplate(err) if err != nil { f.c.logger.ERROR.Println(err) } @@ -344,7 +343,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro if !f.c.paused { port = f.c.Cfg.GetInt("liveReloadPort") } - fmt.Fprint(w, injectLiveReloadScript(&b, port)) + fmt.Fprint(w, injectLiveReloadScript(r, port)) return } @@ -422,11 +421,15 @@ func (c *commandeer) serve(s *serverCmd) error { } srv := &fileServer{ - baseURLs: baseURLs, - roots: roots, - c: c, - s: s, - errorTemplate: templ, + baseURLs: baseURLs, + roots: roots, + c: c, + s: s, + errorTemplate: func(ctx interface{}) (io.Reader, error) { + b := &bytes.Buffer{} + err := c.hugo().Tmpl.Execute(templ, b, ctx) + return b, err + }, } doLiveReload := !c.Cfg.GetBool("disableLiveReload") diff --git a/create/content_template_handler.go b/create/content_template_handler.go index 1576fabdb..b70cf02eb 100644 --- a/create/content_template_handler.go +++ b/create/content_template_handler.go @@ -129,7 +129,7 @@ func executeArcheTypeAsTemplate(s *hugolib.Site, name, kind, targetPath, archety archetypeTemplate = []byte(archetypeShortcodeReplacementsPre.Replace(string(archetypeTemplate))) // Reuse the Hugo template setup to get the template funcs properly set up. - templateHandler := s.Deps.Tmpl.(tpl.TemplateHandler) + templateHandler := s.Deps.Tmpl.(tpl.TemplateManager) templateName := "_text/" + helpers.Filename(archetypeFilename) if err := templateHandler.AddTemplate(templateName, string(archetypeTemplate)); err != nil { return nil, errors.Wrapf(err, "Failed to parse archetype file %q:", archetypeFilename) @@ -138,7 +138,7 @@ func executeArcheTypeAsTemplate(s *hugolib.Site, name, kind, targetPath, archety templ, _ := templateHandler.Lookup(templateName) var buff bytes.Buffer - if err := templ.Execute(&buff, data); err != nil { + if err := templateHandler.Execute(templ, &buff, data); err != nil { return nil, errors.Wrapf(err, "Failed to process archetype file %q:", archetypeFilename) } diff --git a/deps/deps.go b/deps/deps.go index d7b381ce9..ecbba2e56 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -37,8 +37,8 @@ type Deps struct { // Used to log warnings that may repeat itself many times. DistinctWarningLog *helpers.DistinctLogger - // The templates to use. This will usually implement the full tpl.TemplateHandler. - Tmpl tpl.TemplateFinder `json:"-"` + // The templates to use. This will usually implement the full tpl.TemplateManager. + Tmpl tpl.TemplateHandler `json:"-"` // We use this to parse and execute ad-hoc text templates. TextTmpl tpl.TemplateParseFinder `json:"-"` @@ -77,7 +77,10 @@ type Deps struct { OutputFormatsConfig output.Formats templateProvider ResourceProvider - WithTemplate func(templ tpl.TemplateHandler) error `json:"-"` + WithTemplate func(templ tpl.TemplateManager) error `json:"-"` + + // Used in tests + OverloadedTemplateFuncs map[string]interface{} translationProvider ResourceProvider @@ -151,8 +154,8 @@ type ResourceProvider interface { } // TemplateHandler returns the used tpl.TemplateFinder as tpl.TemplateHandler. -func (d *Deps) TemplateHandler() tpl.TemplateHandler { - return d.Tmpl.(tpl.TemplateHandler) +func (d *Deps) TemplateHandler() tpl.TemplateManager { + return d.Tmpl.(tpl.TemplateManager) } // LoadResources loads translations and templates. @@ -239,24 +242,25 @@ func New(cfg DepsCfg) (*Deps, error) { distinctWarnLogger := helpers.NewDistinctLogger(logger.WARN) d := &Deps{ - Fs: fs, - Log: logger, - DistinctErrorLog: distinctErrorLogger, - DistinctWarningLog: distinctWarnLogger, - templateProvider: cfg.TemplateProvider, - translationProvider: cfg.TranslationProvider, - WithTemplate: cfg.WithTemplate, - PathSpec: ps, - ContentSpec: contentSpec, - SourceSpec: sp, - ResourceSpec: resourceSpec, - Cfg: cfg.Language, - Language: cfg.Language, - Site: cfg.Site, - FileCaches: fileCaches, - BuildStartListeners: &Listeners{}, - Timeout: time.Duration(timeoutms) * time.Millisecond, - globalErrHandler: &globalErrHandler{}, + Fs: fs, + Log: logger, + DistinctErrorLog: distinctErrorLogger, + DistinctWarningLog: distinctWarnLogger, + templateProvider: cfg.TemplateProvider, + translationProvider: cfg.TranslationProvider, + WithTemplate: cfg.WithTemplate, + OverloadedTemplateFuncs: cfg.OverloadedTemplateFuncs, + PathSpec: ps, + ContentSpec: contentSpec, + SourceSpec: sp, + ResourceSpec: resourceSpec, + Cfg: cfg.Language, + Language: cfg.Language, + Site: cfg.Site, + FileCaches: fileCaches, + BuildStartListeners: &Listeners{}, + Timeout: time.Duration(timeoutms) * time.Millisecond, + globalErrHandler: &globalErrHandler{}, } if cfg.Cfg.GetBool("templateMetrics") { @@ -344,7 +348,9 @@ type DepsCfg struct { // Template handling. TemplateProvider ResourceProvider - WithTemplate func(templ tpl.TemplateHandler) error + WithTemplate func(templ tpl.TemplateManager) error + // Used in tests + OverloadedTemplateFuncs map[string]interface{} // i18n handling. TranslationProvider ResourceProvider diff --git a/hugolib/alias.go b/hugolib/alias.go index 972f7b01c..c80e7d0d2 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -15,6 +15,7 @@ package hugolib import ( "bytes" + "errors" "fmt" "html/template" "io" @@ -31,27 +32,15 @@ import ( "github.com/gohugoio/hugo/tpl" ) -const ( - alias = "{{ .Permalink }}" - aliasXHtml = "{{ .Permalink }}" -) - 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.TemplateFinder + t tpl.TemplateHandler log *loggers.Logger allowRoot bool } -func newAliasHandler(t tpl.TemplateFinder, l *loggers.Logger, allowRoot bool) aliasHandler { +func newAliasHandler(t tpl.TemplateHandler, l *loggers.Logger, allowRoot bool) aliasHandler { return aliasHandler{t, l, allowRoot} } @@ -60,33 +49,27 @@ type aliasPage struct { page.Page } -func (a aliasHandler) renderAlias(isXHTML bool, permalink string, p page.Page) (io.Reader, error) { - t := "alias" - if isXHTML { - t = "alias-xhtml" - } +func (a aliasHandler) renderAlias(permalink string, p page.Page) (io.Reader, error) { var templ tpl.Template var found bool - if a.t != nil { - templ, found = a.t.Lookup("alias.html") - } - + templ, found = a.t.Lookup("alias.html") if !found { - def := defaultAliasTemplates.Lookup(t) - if def != nil { - templ = &tpl.TemplateAdapter{Template: def} + // TODO(bep) consolidate + templ, found = a.t.Lookup("_internal/alias.html") + if !found { + return nil, errors.New("no alias template found") } - } + data := aliasPage{ permalink, p, } buffer := new(bytes.Buffer) - err := templ.Execute(buffer, data) + err := a.t.Execute(templ, buffer, data) if err != nil { return nil, err } @@ -100,8 +83,6 @@ func (s *Site) writeDestAlias(path, permalink string, outputFormat output.Format func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFormat output.Format, p page.Page) (err error) { handler := newAliasHandler(s.Tmpl, s.Log, allowRoot) - isXHTML := strings.HasSuffix(path, ".xhtml") - s.Log.DEBUG.Println("creating alias:", path, "redirecting to", permalink) targetPath, err := handler.targetPathAlias(path) @@ -109,7 +90,7 @@ func (s *Site) publishDestAlias(allowRoot bool, path, permalink string, outputFo return err } - aliasContent, err := handler.renderAlias(isXHTML, permalink, p) + aliasContent, err := handler.renderAlias(permalink, p) if err != nil { return err } diff --git a/hugolib/case_insensitive_test.go b/hugolib/case_insensitive_test.go index a8616ab06..42b9d7ef6 100644 --- a/hugolib/case_insensitive_test.go +++ b/hugolib/case_insensitive_test.go @@ -14,7 +14,6 @@ package hugolib import ( - "fmt" "path/filepath" "testing" @@ -232,76 +231,3 @@ Page2: {{ $page2.Params.ColoR }} "index2|Site: yellow|", ) } - -// TODO1 -func TestCaseInsensitiveConfigurationForAllTemplateEngines(t *testing.T) { - t.Parallel() - - noOp := func(s string) string { - return s - } - - for _, config := range []struct { - suffix string - templateFixer func(s string) string - }{ - //{"amber", amberFixer}, - {"html", noOp}, - //{"ace", noOp}, - } { - doTestCaseInsensitiveConfigurationForTemplateEngine(t, config.suffix, config.templateFixer) - - } - -} - -func doTestCaseInsensitiveConfigurationForTemplateEngine(t *testing.T, suffix string, templateFixer func(s string) string) { - c := qt.New(t) - mm := afero.NewMemMapFs() - - caseMixingTestsWriteCommonSources(t, mm) - - cfg, err := LoadConfigDefault(mm) - c.Assert(err, qt.IsNil) - - fs := hugofs.NewFrom(mm, cfg) - - th := newTestHelper(cfg, fs, t) - - t.Log("Testing", suffix) - - templTemplate := ` -p - | - | Page Colors: {{ .Params.CoLOR }}|{{ .Params.Colors.Blue }} - | Site Colors: {{ .Site.Params.COlOR }}|{{ .Site.Params.COLORS.YELLOW }} - | {{ .Content }} - -` - - templ := templateFixer(templTemplate) - - t.Log(templ) - - writeSource(t, fs, filepath.Join("layouts", "_default", fmt.Sprintf("single.%s", suffix)), templ) - - sites, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg}) - - if err != nil { - t.Fatalf("Failed to create sites: %s", err) - } - - err = sites.Build(BuildCfg{}) - - if err != nil { - t.Fatalf("Failed to build sites: %s", err) - } - - th.assertFileContent(filepath.Join("public", "nn", "sect1", "page1", "index.html"), - "Page Colors: red|heavenly", - "Site Colors: green|yellow", - "Shortcode Page: red|heavenly", - "Shortcode Site: green|yellow", - ) - -} diff --git a/hugolib/embedded_shortcodes_test.go b/hugolib/embedded_shortcodes_test.go index 64f2203e9..a998b85b7 100644 --- a/hugolib/embedded_shortcodes_test.go +++ b/hugolib/embedded_shortcodes_test.go @@ -27,7 +27,6 @@ import ( "github.com/gohugoio/hugo/deps" qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/tpl" ) const ( @@ -334,18 +333,13 @@ func TestShortcodeTweet(t *testing.T) { cfg.Set("privacy", this.privacy) - withTemplate := func(templ tpl.TemplateHandler) error { - templ.(tpl.TemplateTestMocker).SetFuncs(tweetFuncMap) - return nil - } - writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- title: Shorty --- %s`, this.in)) writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content }}`) - buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}, BuildCfg{}) + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg, OverloadedTemplateFuncs: tweetFuncMap}, BuildCfg{}) th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) @@ -389,18 +383,13 @@ func TestShortcodeInstagram(t *testing.T) { th = newTestHelper(cfg, fs, t) ) - withTemplate := func(templ tpl.TemplateHandler) error { - templ.(tpl.TemplateTestMocker).SetFuncs(instagramFuncMap) - return nil - } - writeSource(t, fs, filepath.Join("content", "simple.md"), fmt.Sprintf(`--- title: Shorty --- %s`, this.in)) writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), `{{ .Content | safeHTML }}`) - buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}, BuildCfg{}) + buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg, OverloadedTemplateFuncs: instagramFuncMap}, BuildCfg{}) th.assertFileContentRegexp(filepath.Join("public", "simple", "index.html"), this.expected) diff --git a/hugolib/hugo_modules_test.go b/hugolib/hugo_modules_test.go index 9ba039c74..40185e051 100644 --- a/hugolib/hugo_modules_test.go +++ b/hugolib/hugo_modules_test.go @@ -42,10 +42,10 @@ import ( func TestHugoModules(t *testing.T) { t.Parallel() - if hugo.GoMinorVersion() < 12 { + if !isCI() || hugo.GoMinorVersion() < 12 { // https://github.com/golang/go/issues/26794 // There were some concurrent issues with Go modules in < Go 12. - t.Skip("skip this for Go <= 1.11 due to a bug in Go's stdlib") + t.Skip("skip this on local host and for Go <= 1.11 due to a bug in Go's stdlib") } if testing.Short() { diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index c0d75c09f..c71dcaa59 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -426,8 +426,8 @@ 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 { +func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.TemplateManager) error) func(templ tpl.TemplateManager) error { + return func(templ tpl.TemplateManager) error { if err := templ.LoadTemplates(""); err != nil { return err } diff --git a/hugolib/hugo_smoke_test.go b/hugolib/hugo_smoke_test.go index 539e79729..406255d51 100644 --- a/hugolib/hugo_smoke_test.go +++ b/hugolib/hugo_smoke_test.go @@ -21,6 +21,27 @@ import ( qt "github.com/frankban/quicktest" ) +// The most basic build test. +func TestHello(t *testing.T) { + t.Parallel() + b := newTestSitesBuilder(t) + b.WithConfigFile("toml", ` +baseURL="https://example.org" +disableKinds = ["taxonomy", "taxonomyTerm", "section", "page"] +`) + b.WithContent("p1", ` +--- +title: Page +--- + +`) + b.WithTemplates("index.html", `Site: {{ .Site.Language.Lang | upper }}`) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", `Site: EN`) +} + func TestSmoke(t *testing.T) { t.Parallel() diff --git a/hugolib/page.go b/hugolib/page.go index b0e8c4359..56202f5e0 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -480,7 +480,7 @@ func (p *pageState) Render(layout ...string) template.HTML { templ, _ = p.s.Tmpl.Lookup(layout + ".html") } if templ != nil { - res, err := executeToString(templ, p) + res, err := executeToString(p.s.Tmpl, templ, p) if err != nil { p.s.SendError(p.wrapError(errors.Wrapf(err, ".Render: failed to execute template %q v", layout))) return "" diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 9697468ff..d3a32e15c 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -411,10 +411,10 @@ func (t targetPathsHolder) targetPaths() page.TargetPaths { return t.paths } -func executeToString(templ tpl.Template, data interface{}) (string, error) { +func executeToString(h tpl.TemplateHandler, templ tpl.Template, data interface{}) (string, error) { b := bp.GetBuffer() defer bp.PutBuffer(b) - if err := templ.Execute(b, data); err != nil { + if err := h.Execute(templ, b, data); err != nil { return "", err } return b.String(), nil diff --git a/hugolib/page_test.go b/hugolib/page_test.go index ff037a3cc..dc8bc821c 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -459,7 +459,7 @@ func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) { } cnt := content(p) - if cnt != "

The best static site generator.1

\n
\n
\n
    \n
  1. \n

    Many people say so. ↩︎

    \n
  2. \n
\n
" { + if cnt != "

The best static site generator.1

\n
\n
\n
    \n
  1. \n

    Many people say so.

    \n
  2. \n
\n
" { t.Fatalf("Got content:\n%q", cnt) } } diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index 5e916aeec..69bcb6d4f 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -393,7 +393,7 @@ func renderShortcode( } - result, err := renderShortcodeWithPage(tmpl, data) + result, err := renderShortcodeWithPage(s.Tmpl, tmpl, data) if err != nil && sc.isInline { fe := herrors.ToFileError("html", err) @@ -634,11 +634,11 @@ func replaceShortcodeTokens(source []byte, replacements map[string]string) ([]by return source, nil } -func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) (string, error) { +func renderShortcodeWithPage(h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) { buffer := bp.GetBuffer() defer bp.PutBuffer(buffer) - err := tmpl.Execute(buffer, data) + err := h.Execute(tmpl, buffer, data) if err != nil { return "", _errors.Wrap(err, "failed to process shortcode") } diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index fdf37b6c2..5e71db501 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -36,12 +36,12 @@ import ( qt "github.com/frankban/quicktest" ) -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.TemplateManager) error) { t.Helper() 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.TemplateManager) error, expectError bool) { t.Helper() cfg, fs := newTestCfg() @@ -95,7 +95,7 @@ func TestNonSC(t *testing.T) { // Issue #929 func TestHyphenatedSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("_internal/shortcodes/hyphenated-video.html", `Playing Video {{ .Get 0 }}`) return nil @@ -107,7 +107,7 @@ func TestHyphenatedSC(t *testing.T) { // Issue #1753 func TestNoTrailingNewline(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("_internal/shortcodes/a.html", `{{ .Get 0 }}`) return nil } @@ -117,7 +117,7 @@ func TestNoTrailingNewline(t *testing.T) { func TestPositionalParamSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ .Get 0 }}`) return nil } @@ -131,7 +131,7 @@ func TestPositionalParamSC(t *testing.T) { func TestPositionalParamIndexOutOfBounds(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("_internal/shortcodes/video.html", `Playing Video {{ with .Get 1 }}{{ . }}{{ else }}Missing{{ end }}`) return nil } @@ -141,7 +141,7 @@ func TestPositionalParamIndexOutOfBounds(t *testing.T) { // #5071 func TestShortcodeRelated(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("_internal/shortcodes/a.html", `{{ len (.Site.RegularPages.Related .Page) }}`) return nil } @@ -151,7 +151,7 @@ func TestShortcodeRelated(t *testing.T) { func TestShortcodeInnerMarkup(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("shortcodes/a.html", `
{{ .Inner }}
`) tem.AddTemplate("shortcodes/b.html", `**Bold**:
{{ .Inner }}
`) return nil @@ -175,7 +175,7 @@ func TestShortcodeInnerMarkup(t *testing.T) { func TestNamedParamSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("_internal/shortcodes/img.html", ``) return nil } @@ -190,7 +190,7 @@ func TestNamedParamSC(t *testing.T) { // Issue #2294 func TestNestedNamedMissingParam(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("_internal/shortcodes/acc.html", `
{{ .Inner }}
`) tem.AddTemplate("_internal/shortcodes/div.html", `
{{ .Inner }}
`) tem.AddTemplate("_internal/shortcodes/div2.html", `
{{ .Inner }}
`) @@ -203,7 +203,7 @@ func TestNestedNamedMissingParam(t *testing.T) { func TestIsNamedParamsSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("_internal/shortcodes/bynameorposition.html", `{{ with .Get "id" }}Named: {{ . }}{{ else }}Pos: {{ .Get 0 }}{{ end }}`) tem.AddTemplate("_internal/shortcodes/ifnamedparams.html", `
`) return nil @@ -216,7 +216,7 @@ func TestIsNamedParamsSC(t *testing.T) { func TestInnerSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("_internal/shortcodes/inside.html", `{{ .Inner }}
`) return nil } @@ -227,7 +227,7 @@ func TestInnerSC(t *testing.T) { func TestInnerSCWithMarkdown(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { // Note: In Hugo 0.55 we made it so any outer {{%'s inner content was rendered as part of the surrounding // markup. This solved lots of problems, but it also meant that this test had to be adjusted. tem.AddTemplate("_internal/shortcodes/wrapper.html", `{{ .Inner }}`) @@ -250,7 +250,7 @@ func TestEmbeddedSC(t *testing.T) { func TestNestedSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("_internal/shortcodes/scn1.html", `
Outer, inner is {{ .Inner }}
`) tem.AddTemplate("_internal/shortcodes/scn2.html", `
SC2
`) return nil @@ -262,7 +262,7 @@ func TestNestedSC(t *testing.T) { func TestNestedComplexSC(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) 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-`) @@ -278,7 +278,7 @@ func TestNestedComplexSC(t *testing.T) { func TestParentShortcode(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) 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 }}`) @@ -333,7 +333,7 @@ func TestFigureLinkWithTargetAndRel(t *testing.T) { // #1642 func TestShortcodeWrappedInPIssue(t *testing.T) { t.Parallel() - wt := func(tem tpl.TemplateHandler) error { + wt := func(tem tpl.TemplateManager) error { tem.AddTemplate("_internal/shortcodes/bug.html", `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`) return nil } @@ -564,7 +564,7 @@ title: "Foo" sources[i] = [2]string{filepath.FromSlash(test.contentPath), test.content} } - addTemplates := func(templ tpl.TemplateHandler) error { + addTemplates := func(templ tpl.TemplateManager) error { templ.AddTemplate("_default/single.html", "{{.Content}} Word Count: {{ .WordCount }}") templ.AddTemplate("_internal/shortcodes/b.html", `b`) diff --git a/hugolib/site.go b/hugolib/site.go index 1df7d6076..67ddff4d9 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -474,7 +474,7 @@ func NewSite(cfg deps.DepsCfg) (*Site, error) { // The site will have a template system loaded and ready to use. // Note: This is mainly used in single site tests. // TODO(bep) test refactor -- remove -func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) { +func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) { v := viper.New() if err := loadDefaultSettingsFor(v); err != nil { return nil, err @@ -486,7 +486,7 @@ func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateHandler) error) ( // The site will have a template system loaded and ready to use. // Note: This is mainly used in single site tests. // TODO(bep) test refactor -- remove -func NewEnglishSite(withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) { +func NewEnglishSite(withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) { v := viper.New() if err := loadDefaultSettingsFor(v); err != nil { return nil, err @@ -495,8 +495,8 @@ func NewEnglishSite(withTemplate ...func(templ tpl.TemplateHandler) error) (*Sit } // newSiteForLang creates a new site in the given language. -func newSiteForLang(lang *langs.Language, withTemplate ...func(templ tpl.TemplateHandler) error) (*Site, error) { - withTemplates := func(templ tpl.TemplateHandler) error { +func newSiteForLang(lang *langs.Language, withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) { + withTemplates := func(templ tpl.TemplateManager) error { for _, wt := range withTemplate { if err := wt(templ); err != nil { return err @@ -1589,7 +1589,7 @@ func (s *Site) renderForLayouts(name, outputFormat string, d interface{}, w io.W return nil } - if err = templ.Execute(w, d); err != nil { + if err = s.Tmpl.Execute(templ, w, d); err != nil { return _errors.Wrapf(err, "render of %q failed", name) } return diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go index 4dfb61ecd..27fbf11d8 100644 --- a/hugolib/sitemap_test.go +++ b/hugolib/sitemap_test.go @@ -50,7 +50,7 @@ func doTestSitemapOutput(t *testing.T, internal bool) { depsCfg := deps.DepsCfg{Fs: fs, Cfg: cfg} - depsCfg.WithTemplate = func(templ tpl.TemplateHandler) error { + depsCfg.WithTemplate = func(templ tpl.TemplateManager) error { if !internal { templ.AddTemplate("sitemap.xml", sitemapTemplate) } diff --git a/hugolib/template_engines_test.go b/hugolib/template_engines_test.go deleted file mode 100644 index ea0fca0b6..000000000 --- a/hugolib/template_engines_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2017 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 hugolib - -import ( - "fmt" - "path/filepath" - "testing" - - "github.com/gohugoio/hugo/deps" -) - -// TODO1 -func TestAllTemplateEngines(t *testing.T) { - noOp := func(s string) string { - return s - } - - for _, config := range []struct { - suffix string - templateFixer func(s string) string - }{ - //{"amber", amberFixer}, - {"html", noOp}, - //{"ace", noOp}, - } { - config := config - t.Run(config.suffix, - func(t *testing.T) { - t.Parallel() - doTestTemplateEngine(t, config.suffix, config.templateFixer) - }) - } - -} - -func doTestTemplateEngine(t *testing.T, suffix string, templateFixer func(s string) string) { - - cfg, fs := newTestCfg() - - t.Log("Testing", suffix) - - templTemplate := ` -p - | - | Page Title: {{ .Title }} - br - | Page Content: {{ .Content }} - br - | {{ title "hello world" }} - -` - - templShortcodeTemplate := ` -p - | - | Shortcode: {{ .IsNamedParams }} -` - - templ := templateFixer(templTemplate) - shortcodeTempl := templateFixer(templShortcodeTemplate) - - writeSource(t, fs, filepath.Join("content", "p.md"), ` ---- -title: My Title ---- -My Content - -Shortcode: {{< myShort >}} - -`) - - writeSource(t, fs, filepath.Join("layouts", "_default", fmt.Sprintf("single.%s", suffix)), templ) - writeSource(t, fs, filepath.Join("layouts", "shortcodes", fmt.Sprintf("myShort.%s", suffix)), shortcodeTempl) - - s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) - th := newTestHelper(s.Cfg, s.Fs, t) - - th.assertFileContent(filepath.Join("public", "p", "index.html"), - "Page Title: My Title", - "My Content", - "Hello World", - "Shortcode: false", - ) - -} diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index d861a5e09..ea1ee9674 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -845,9 +845,9 @@ func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layou return th, h } -func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.TemplateHandler) error { +func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.TemplateManager) error { - return func(templ tpl.TemplateHandler) error { + return func(templ tpl.TemplateManager) error { for i := 0; i < len(additionalTemplates); i += 2 { err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1]) if err != nil { diff --git a/langs/language.go b/langs/language.go index 67cb3689a..0e04324e9 100644 --- a/langs/language.go +++ b/langs/language.go @@ -16,6 +16,7 @@ package langs import ( "sort" "strings" + "sync" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/config" @@ -56,7 +57,9 @@ type Language struct { // These are params declared in the [params] section of the language merged with the // site's params, the most specific (language) wins on duplicate keys. - params map[string]interface{} + params map[string]interface{} + paramsMu sync.Mutex + paramsSet bool // These are config values, i.e. the settings declared outside of the [params] section of the language. // This is the map Hugo looks in when looking for configuration values (baseURL etc.). @@ -123,7 +126,15 @@ func (l Languages) Less(i, j int) bool { func (l Languages) Swap(i, j int) { l[i], l[j] = l[j], l[i] } // Params retunrs language-specific params merged with the global params. -func (l *Language) Params() map[string]interface{} { +func (l *Language) Params() maps.Params { + // TODO(bep) this construct should not be needed. Create the + // language params in one go. + l.paramsMu.Lock() + defer l.paramsMu.Unlock() + if !l.paramsSet { + maps.ToLower(l.params) + l.paramsSet = true + } return l.params } @@ -163,7 +174,12 @@ func (l Languages) IsMultihost() bool { // SetParam sets a param with the given key and value. // SetParam is case-insensitive. func (l *Language) SetParam(k string, v interface{}) { - l.params[strings.ToLower(k)] = v + l.paramsMu.Lock() + defer l.paramsMu.Unlock() + if l.paramsSet { + panic("params cannot be changed once set") + } + l.params[k] = v } // GetBool returns the value associated with the key as a boolean. diff --git a/resources/resource_transformers/templates/execute_as_template.go b/resources/resource_transformers/templates/execute_as_template.go index 422f1bbe1..953cccc04 100644 --- a/resources/resource_transformers/templates/execute_as_template.go +++ b/resources/resource_transformers/templates/execute_as_template.go @@ -27,25 +27,27 @@ import ( type Client struct { rs *resources.Spec - textTemplate tpl.TemplateParseFinder + templateHandler tpl.TemplateHandler + textTemplate tpl.TemplateParseFinder } // New creates a new Client with the given specification. -func New(rs *resources.Spec, textTemplate tpl.TemplateParseFinder) *Client { +func New(rs *resources.Spec, h tpl.TemplateHandler, textTemplate tpl.TemplateParseFinder) *Client { if rs == nil { panic("must provice a resource Spec") } if textTemplate == nil { panic("must provide a textTemplate") } - return &Client{rs: rs, textTemplate: textTemplate} + return &Client{rs: rs, templateHandler: h, textTemplate: textTemplate} } type executeAsTemplateTransform struct { - rs *resources.Spec - textTemplate tpl.TemplateParseFinder - targetPath string - data interface{} + rs *resources.Spec + textTemplate tpl.TemplateParseFinder + templateHandler tpl.TemplateHandler + targetPath string + data interface{} } func (t *executeAsTemplateTransform) Key() internal.ResourceTransformationKey { @@ -61,14 +63,15 @@ func (t *executeAsTemplateTransform) Transform(ctx *resources.ResourceTransforma ctx.OutPath = t.targetPath - return templ.Execute(ctx.To, t.data) + return t.templateHandler.Execute(templ, ctx.To, t.data) } func (c *Client) ExecuteAsTemplate(res resources.ResourceTransformer, targetPath string, data interface{}) (resource.Resource, error) { return res.Transform(&executeAsTemplateTransform{ - rs: c.rs, - targetPath: helpers.ToSlashTrimLeading(targetPath), - textTemplate: c.textTemplate, - data: data, + rs: c.rs, + targetPath: helpers.ToSlashTrimLeading(targetPath), + templateHandler: c.templateHandler, + textTemplate: c.textTemplate, + data: data, }) } diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go index 148785883..1cae78a43 100644 --- a/scripts/fork_go_templates/main.go +++ b/scripts/fork_go_templates/main.go @@ -57,6 +57,8 @@ var ( `"internal/fmtsort"`, `"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"`, // Rename types and function that we want to overload. "type state struct", "type stateOld struct", + "func (s *state) evalFunction", "func (s *state) evalFunctionOld", + "func (s *state) evalField(", "func (s *state) evalFieldOld(", ) htmlTemplateReplacers = strings.NewReplacer( diff --git a/tpl/collections/apply.go b/tpl/collections/apply.go index d715aeb00..d41a3b1da 100644 --- a/tpl/collections/apply.go +++ b/tpl/collections/apply.go @@ -106,17 +106,8 @@ func applyFnToThis(fn, this reflect.Value, args ...interface{}) (reflect.Value, func (ns *Namespace) lookupFunc(fname string) (reflect.Value, bool) { if !strings.ContainsRune(fname, '.') { - templ, ok := ns.deps.Tmpl.(tpl.TemplateFuncsGetter) - if !ok { - panic("Needs a tpl.TemplateFuncsGetter") - } - fm := templ.GetFuncs() - fn, found := fm[fname] - if !found { - return reflect.Value{}, false - } - - return reflect.ValueOf(fn), true + templ := ns.deps.Tmpl.(tpl.TemplateFuncGetter) + return templ.GetFunc(fname) } ss := strings.SplitN(fname, ".", 2) diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go index 96dd8896b..5b21d5a97 100644 --- a/tpl/collections/apply_test.go +++ b/tpl/collections/apply_test.go @@ -14,6 +14,8 @@ package collections import ( + "io" + "reflect" "testing" "fmt" @@ -33,10 +35,17 @@ func (templateFinder) LookupVariant(name string, variants tpl.TemplateVariants) return nil, false, false } -func (templateFinder) GetFuncs() map[string]interface{} { - return map[string]interface{}{ - "print": fmt.Sprint, +func (templateFinder) Execute(t tpl.Template, wr io.Writer, data interface{}) error { + return nil +} + +func (templateFinder) GetFunc(name string) (reflect.Value, bool) { + if name == "dobedobedo" { + return reflect.Value{}, false } + + return reflect.ValueOf(fmt.Sprint), true + } func TestApply(t *testing.T) { diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go index d47793320..db64edcb2 100644 --- a/tpl/internal/go_templates/texttemplate/exec.go +++ b/tpl/internal/go_templates/texttemplate/exec.go @@ -558,7 +558,7 @@ func (s *state) evalFieldChain(dot, receiver reflect.Value, node parse.Node, ide return s.evalField(dot, ident[n-1], node, args, final, receiver) } -func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd parse.Node, args []parse.Node, final reflect.Value) reflect.Value { +func (s *state) evalFunctionOld(dot reflect.Value, node *parse.IdentifierNode, cmd parse.Node, args []parse.Node, final reflect.Value) reflect.Value { s.at(node) name := node.Ident function, ok := findFunction(name, s.tmpl) @@ -571,7 +571,7 @@ func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd // evalField evaluates an expression like (.Field) or (.Field arg1 arg2). // The 'final' argument represents the return value from the preceding // value of the pipeline, if any. -func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, args []parse.Node, final, receiver reflect.Value) reflect.Value { +func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Node, args []parse.Node, final, receiver reflect.Value) reflect.Value { if !receiver.IsValid() { if s.tmpl.option.missingKey == mapError { // Treat invalid value as missing map key. s.errorf("nil data; no entry for key %q", fieldName) diff --git a/tpl/internal/go_templates/texttemplate/hugo_exec.go b/tpl/internal/go_templates/texttemplate/hugo_exec.go deleted file mode 100644 index cc3aeb2f1..000000000 --- a/tpl/internal/go_templates/texttemplate/hugo_exec.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019 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 template - -import ( - "io" - - "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" -) - -/* - -This files contains the Hugo related addons. All the other files in this -package is auto generated. - -*/ - -// state represents the state of an execution. It's not part of the -// template so that multiple executions of the same template -// can execute in parallel. -type state struct { - tmpl *Template - wr io.Writer - node parse.Node // current node, for errors - vars []variable // push-down stack of variable values. - depth int // the height of the stack of executing templates. -} diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go index e3252e7aa..c2738ec53 100644 --- a/tpl/internal/go_templates/texttemplate/hugo_template.go +++ b/tpl/internal/go_templates/texttemplate/hugo_template.go @@ -27,15 +27,31 @@ package is auto generated. */ -// TODO1 name +// Preparer prepares the template before execution. type Preparer interface { Prepare() (*Template, error) } -type TemplateExecutor struct { +// ExecHelper allows some custom eval hooks. +type ExecHelper interface { + GetFunc(name string) (reflect.Value, bool) + GetMapValue(receiver, key reflect.Value) (reflect.Value, bool) } -func (t *TemplateExecutor) Execute(p Preparer, wr io.Writer, data interface{}) error { +// Executer executes a given template. +type Executer interface { + Execute(p Preparer, wr io.Writer, data interface{}) error +} + +type executer struct { + helper ExecHelper +} + +func NewExecuter(helper ExecHelper) Executer { + return &executer{helper: helper} +} + +func (t *executer) Execute(p Preparer, wr io.Writer, data interface{}) error { tmpl, err := p.Prepare() if err != nil { return err @@ -47,9 +63,10 @@ func (t *TemplateExecutor) Execute(p Preparer, wr io.Writer, data interface{}) e } state := &state{ - tmpl: tmpl, - wr: wr, - vars: []variable{{"$", value}}, + helper: t.helper, + tmpl: tmpl, + wr: wr, + vars: []variable{{"$", value}}, } return tmpl.executeWithState(state, value) @@ -62,19 +79,6 @@ func (t *Template) Prepare() (*Template, error) { return t, nil } -// Below are modifed structs etc. - -// state represents the state of an execution. It's not part of the -// template so that multiple executions of the same template -// can execute in parallel. -type state struct { - tmpl *Template - wr io.Writer - node parse.Node // current node, for errors - vars []variable // push-down stack of variable values. - depth int // the height of the stack of executing templates. -} - func (t *Template) executeWithState(state *state, value reflect.Value) (err error) { defer errRecover(&err) if t.Tree == nil || t.Root == nil { @@ -83,3 +87,123 @@ func (t *Template) executeWithState(state *state, value reflect.Value) (err erro state.walk(value, t.Root) return } + +// Below are modifed structs etc. + +// state represents the state of an execution. It's not part of the +// template so that multiple executions of the same template +// can execute in parallel. +type state struct { + tmpl *Template + helper ExecHelper + wr io.Writer + node parse.Node // current node, for errors + vars []variable // push-down stack of variable values. + depth int // the height of the stack of executing templates. +} + +func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd parse.Node, args []parse.Node, final reflect.Value) reflect.Value { + s.at(node) + name := node.Ident + + var function reflect.Value + var ok bool + if s.helper != nil { + function, ok = s.helper.GetFunc(name) + } + + if !ok { + function, ok = findFunction(name, s.tmpl) + } + + if !ok { + s.errorf("%q is not a defined function", name) + } + return s.evalCall(dot, function, cmd, name, args, final) +} + +// evalField evaluates an expression like (.Field) or (.Field arg1 arg2). +// The 'final' argument represents the return value from the preceding +// value of the pipeline, if any. +func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, args []parse.Node, final, receiver reflect.Value) reflect.Value { + if !receiver.IsValid() { + if s.tmpl.option.missingKey == mapError { // Treat invalid value as missing map key. + s.errorf("nil data; no entry for key %q", fieldName) + } + return zero + } + typ := receiver.Type() + receiver, isNil := indirect(receiver) + if receiver.Kind() == reflect.Interface && isNil { + // Calling a method on a nil interface can't work. The + // MethodByName method call below would panic. + s.errorf("nil pointer evaluating %s.%s", typ, fieldName) + return zero + } + + // Unless it's an interface, need to get to a value of type *T to guarantee + // we see all methods of T and *T. + ptr := receiver + if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() { + ptr = ptr.Addr() + } + if method := ptr.MethodByName(fieldName); method.IsValid() { + return s.evalCall(dot, method, node, fieldName, args, final) + } + hasArgs := len(args) > 1 || final != missingVal + // It's not a method; must be a field of a struct or an element of a map. + switch receiver.Kind() { + case reflect.Struct: + tField, ok := receiver.Type().FieldByName(fieldName) + if ok { + field := receiver.FieldByIndex(tField.Index) + if tField.PkgPath != "" { // field is unexported + s.errorf("%s is an unexported field of struct type %s", fieldName, typ) + } + // If it's a function, we must call it. + if hasArgs { + s.errorf("%s has arguments but cannot be invoked as function", fieldName) + } + return field + } + case reflect.Map: + // If it's a map, attempt to use the field name as a key. + nameVal := reflect.ValueOf(fieldName) + if nameVal.Type().AssignableTo(receiver.Type().Key()) { + if hasArgs { + s.errorf("%s is not a method but has arguments", fieldName) + } + var result reflect.Value + if s.helper != nil { + result, _ = s.helper.GetMapValue(receiver, nameVal) + } else { + result = receiver.MapIndex(nameVal) + } + if !result.IsValid() { + switch s.tmpl.option.missingKey { + case mapInvalid: + // Just use the invalid value. + case mapZeroValue: + result = reflect.Zero(receiver.Type().Elem()) + case mapError: + s.errorf("map has no entry for key %q", fieldName) + } + } + return result + } + case reflect.Ptr: + etyp := receiver.Type().Elem() + if etyp.Kind() == reflect.Struct { + if _, ok := etyp.FieldByName(fieldName); !ok { + // If there's no such field, say "can't evaluate" + // instead of "nil pointer evaluating". + break + } + } + if isNil { + s.errorf("nil pointer evaluating %s.%s", typ, fieldName) + } + } + s.errorf("can't evaluate field %s in type %s", fieldName, typ) + panic("not reached") +} diff --git a/tpl/internal/go_templates/texttemplate/hugo_template_test.go b/tpl/internal/go_templates/texttemplate/hugo_template_test.go new file mode 100644 index 000000000..2424a0a48 --- /dev/null +++ b/tpl/internal/go_templates/texttemplate/hugo_template_test.go @@ -0,0 +1,71 @@ +// Copyright 2019 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 template + +import ( + "bytes" + "reflect" + "strings" + "testing" + + qt "github.com/frankban/quicktest" +) + +type TestStruct struct { + S string + M map[string]string +} + +type execHelper struct { +} + +func (e *execHelper) GetFunc(name string) (reflect.Value, bool) { + if name == "print" { + return zero, false + } + return reflect.ValueOf(func(s string) string { + return "hello " + s + }), true +} + +func (e *execHelper) GetMapValue(m, key reflect.Value) (reflect.Value, bool) { + key = reflect.ValueOf(strings.ToLower(key.String())) + return m.MapIndex(key), true +} + +func TestTemplateExecutor(t *testing.T) { + c := qt.New(t) + + templ, err := New("").Parse(` +{{ print "foo" }} +{{ printf "hugo" }} +Map: {{ .M.A }} + +`) + + c.Assert(err, qt.IsNil) + + ex := NewExecuter(&execHelper{}) + + var b bytes.Buffer + data := TestStruct{S: "sv", M: map[string]string{"a": "av"}} + + c.Assert(ex.Execute(templ, &b, data), qt.IsNil) + got := b.String() + + c.Assert(got, qt.Contains, "foo") + c.Assert(got, qt.Contains, "hello hugo") + c.Assert(got, qt.Contains, "Map: av") + +} diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go index 3380a5a9e..bfc3a82d3 100644 --- a/tpl/partials/partials.go +++ b/tpl/partials/partials.go @@ -139,7 +139,7 @@ func (ns *Namespace) Include(name string, contextList ...interface{}) (interface w = b } - if err := templ.Execute(w, context); err != nil { + if err := ns.deps.Tmpl.Execute(templ, w, context); err != nil { return "", err } diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go index 20c4d1b3a..9a7b29696 100644 --- a/tpl/resources/resources.go +++ b/tpl/resources/resources.go @@ -53,7 +53,7 @@ func New(deps *deps.Deps) (*Namespace, error) { integrityClient: integrity.New(deps.ResourceSpec), minifyClient: minifier.New(deps.ResourceSpec), postcssClient: postcss.New(deps.ResourceSpec), - templatesClient: templates.New(deps.ResourceSpec, deps.TextTmpl), + templatesClient: templates.New(deps.ResourceSpec, deps.Tmpl, deps.TextTmpl), }, nil } diff --git a/tpl/template.go b/tpl/template.go index 63bf42730..db715c306 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -14,39 +14,22 @@ package tpl import ( - "fmt" - "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" + "reflect" "io" - "path/filepath" "regexp" - "strings" - "time" "github.com/gohugoio/hugo/output" - "github.com/gohugoio/hugo/common/herrors" - - "github.com/gohugoio/hugo/hugofs" - - "github.com/spf13/afero" - texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" - "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" - - bp "github.com/gohugoio/hugo/bufferpool" - "github.com/gohugoio/hugo/metrics" - "github.com/pkg/errors" ) -var ( - _ TemplateExecutor = (*TemplateAdapter)(nil) - _ TemplateInfoProvider = (*TemplateAdapter)(nil) -) +var _ TemplateInfoProvider = (*TemplateInfo)(nil) -// TemplateHandler manages the collection of templates. -type TemplateHandler interface { - TemplateFinder +// TemplateManager manages the collection of templates. +type TemplateManager interface { + TemplateHandler + TemplateFuncGetter AddTemplate(name, tpl string) error AddLateTemplate(name, tpl string) error LoadTemplates(prefix string) error @@ -68,6 +51,12 @@ type TemplateFinder interface { TemplateLookupVariant } +// TemplateHandler finds and executes templates. +type TemplateHandler interface { + TemplateFinder + Execute(t Template, wr io.Writer, data interface{}) error +} + type TemplateLookup interface { Lookup(name string) (Template, bool) } @@ -87,8 +76,8 @@ type TemplateLookupVariant interface { // Template is the common interface between text/template and html/template. type Template interface { - Execute(wr io.Writer, data interface{}) error Name() string + Prepare() (*texttemplate.Template, error) } // TemplateInfoProvider provides some contextual information about a template. @@ -107,30 +96,15 @@ type TemplateParseFinder interface { TemplateFinder } -// TemplateExecutor adds some extras to Template. -type TemplateExecutor interface { - Template - ExecuteToString(data interface{}) (string, error) - Tree() string -} - // TemplateDebugger prints some debug info to stdoud. type TemplateDebugger interface { Debug() } -// TemplateAdapter implements the TemplateExecutor interface. -type TemplateAdapter struct { +// TemplateInfo wraps a Template with some additional information. +type TemplateInfo struct { Template - Metrics metrics.Provider - Info Info - - // The filesystem where the templates are stored. - Fs afero.Fs - - // Maps to base template if relevant. - NameBaseTemplateName map[string]string } var baseOfRe = regexp.MustCompile("template: (.*?):") @@ -143,165 +117,11 @@ func extractBaseOf(err string) string { return "" } -// Execute executes the current template. The actual execution is performed -// by the embedded text or html template, but we add an implementation here so -// we can add a timer for some metrics. -func (t *TemplateAdapter) Execute(w io.Writer, data interface{}) (execErr error) { - defer func() { - // Panics in templates are a little bit too common (nil pointers etc.) - // See https://github.com/gohugoio/hugo/issues/5327 - if r := recover(); r != nil { - execErr = t.addFileContext(t.Name(), fmt.Errorf(`panic in Execute: %s. See "https://github.com/gohugoio/hugo/issues/5327" for the reason why we cannot provide a better error message for this`, r)) - } - }() - - if t.Metrics != nil { - defer t.Metrics.MeasureSince(t.Name(), time.Now()) - } - - execErr = t.Template.Execute(w, data) - if execErr != nil { - execErr = t.addFileContext(t.Name(), execErr) - } - - return -} - -func (t *TemplateAdapter) TemplateInfo() Info { +func (t *TemplateInfo) TemplateInfo() Info { return t.Info } -// The identifiers may be truncated in the log, e.g. -// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image" -var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`) - -func (t *TemplateAdapter) extractIdentifiers(line string) []string { - m := identifiersRe.FindAllStringSubmatch(line, -1) - identifiers := make([]string, len(m)) - for i := 0; i < len(m); i++ { - identifiers[i] = m[i][1] - } - return identifiers -} - -func (t *TemplateAdapter) addFileContext(name string, inerr error) error { - if strings.HasPrefix(t.Name(), "_internal") { - return inerr - } - - f, realFilename, err := t.fileAndFilename(t.Name()) - if err != nil { - return inerr - - } - defer f.Close() - - master, hasMaster := t.NameBaseTemplateName[name] - - ferr := errors.Wrap(inerr, "execute of template failed") - - // Since this can be a composite of multiple template files (single.html + baseof.html etc.) - // we potentially need to look in both -- and cannot rely on line number alone. - lineMatcher := func(m herrors.LineMatcher) bool { - if m.Position.LineNumber != m.LineNumber { - return false - } - if !hasMaster { - return true - } - - identifiers := t.extractIdentifiers(m.Error.Error()) - - for _, id := range identifiers { - if strings.Contains(m.Line, id) { - return true - } - } - return false - } - - fe, ok := herrors.WithFileContext(ferr, realFilename, f, lineMatcher) - if ok || !hasMaster { - return fe - } - - // Try the base template if relevant - f, realFilename, err = t.fileAndFilename(master) - if err != nil { - return err - } - defer f.Close() - - fe, ok = herrors.WithFileContext(ferr, realFilename, f, lineMatcher) - - if !ok { - // Return the most specific. - return ferr - - } - return fe - -} - -func (t *TemplateAdapter) fileAndFilename(name string) (afero.File, string, error) { - fs := t.Fs - filename := filepath.FromSlash(name) - - fi, err := fs.Stat(filename) - if err != nil { - return nil, "", err - } - fim := fi.(hugofs.FileMetaInfo) - meta := fim.Meta() - - f, err := meta.Open() - if err != nil { - return nil, "", errors.Wrapf(err, "failed to open template file %q:", filename) - } - - return f, meta.Filename(), nil -} - -// 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 == nil || tree.Root == nil { - return "" - } - s := tree.Root.String() - - return s -} - -// TemplateFuncsGetter allows to get a map of functions. -type TemplateFuncsGetter interface { - GetFuncs() map[string]interface{} -} - -// 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{}) +// TemplateFuncGetter allows to find a template func by name. +type TemplateFuncGetter interface { + GetFunc(name string) (reflect.Value, bool) } diff --git a/tpl/tplimpl/embedded/templates.autogen.go b/tpl/tplimpl/embedded/templates.autogen.go index cb7fb9512..f64f18ee1 100644 --- a/tpl/tplimpl/embedded/templates.autogen.go +++ b/tpl/tplimpl/embedded/templates.autogen.go @@ -87,6 +87,7 @@ var EmbeddedTemplates = [][2]string{ {{ end }} `}, + {`alias.html`, `{{ .Permalink }}`}, {`disqus.html`, `{{- $pc := .Site.Config.Privacy.Disqus -}} {{- if not $pc.Disable -}} {{ if .Site.DisqusShortname }}
diff --git a/tpl/tplimpl/embedded/templates/alias.html b/tpl/tplimpl/embedded/templates/alias.html new file mode 100644 index 000000000..ee3f556e5 --- /dev/null +++ b/tpl/tplimpl/embedded/templates/alias.html @@ -0,0 +1 @@ +{{ .Permalink }} \ No newline at end of file diff --git a/tpl/tplimpl/shortcodes.go b/tpl/tplimpl/shortcodes.go index b41725463..e5dbabdd8 100644 --- a/tpl/tplimpl/shortcodes.go +++ b/tpl/tplimpl/shortcodes.go @@ -61,11 +61,6 @@ func (s *shortcodeTemplates) fromVariants(variants tpl.TemplateVariants) (shortc }) } -// Get the most specific template given a full name, e.g gtag.no.amp.html. -func (s *shortcodeTemplates) fromName(name string) (shortcodeVariant, bool) { - return s.fromVariantsSlice(templateVariants(name)) -} - func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVariant, bool) { var ( bestMatch shortcodeVariant diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go index 0feb3a0de..dd8de9067 100644 --- a/tpl/tplimpl/template.go +++ b/tpl/tplimpl/template.go @@ -15,6 +15,12 @@ package tplimpl import ( "fmt" + "io" + "reflect" + "regexp" + "time" + + "github.com/gohugoio/hugo/common/herrors" "strings" @@ -45,278 +51,33 @@ const ( ) var ( - _ tpl.TemplateHandler = (*templateHandler)(nil) - _ tpl.TemplateDebugger = (*templateHandler)(nil) - _ tpl.TemplateFuncsGetter = (*templateHandler)(nil) - _ tpl.TemplateTestMocker = (*templateHandler)(nil) - _ tpl.TemplateFinder = (*htmlTemplates)(nil) - _ tpl.TemplateFinder = (*textTemplates)(nil) - _ templateLoader = (*htmlTemplates)(nil) - _ templateLoader = (*textTemplates)(nil) - _ templateFuncsterTemplater = (*htmlTemplates)(nil) - _ templateFuncsterTemplater = (*textTemplates)(nil) + _ tpl.TemplateManager = (*templateHandler)(nil) + _ tpl.TemplateHandler = (*templateHandler)(nil) + _ tpl.TemplateDebugger = (*templateHandler)(nil) + _ tpl.TemplateFuncGetter = (*templateHandler)(nil) + _ tpl.TemplateFinder = (*htmlTemplates)(nil) + _ tpl.TemplateFinder = (*textTemplates)(nil) + _ templateLoader = (*htmlTemplates)(nil) + _ templateLoader = (*textTemplates)(nil) ) -type templateErr struct { - name string - err error -} - -type templateLoader interface { - handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error - addTemplate(name, tpl string) (*templateContext, error) - addLateTemplate(name, tpl string) error -} - -type templateFuncsterTemplater interface { - templateFuncsterSetter - tpl.TemplateFinder - setFuncs(funcMap map[string]interface{}) -} - -type templateFuncsterSetter interface { - setTemplateFuncster(f *templateFuncster) -} - -// templateHandler holds the templates in play. -// It implements the templateLoader and tpl.TemplateHandler interfaces. -type templateHandler struct { - mu sync.Mutex - - // shortcodes maps shortcode name to template variants - // (language, output format etc.) of that shortcode. - shortcodes map[string]*shortcodeTemplates - - // templateInfo maps template name to some additional information about that template. - // Note that for shortcodes that same information is embedded in the - // shortcodeTemplates type. - templateInfo map[string]tpl.Info - - // text holds all the pure text templates. - text *textTemplates - html *htmlTemplates - - errors []*templateErr - - // This is the filesystem to load the templates from. All the templates are - // stored in the root of this filesystem. - layoutsFs afero.Fs - - *deps.Deps -} - const ( shortcodesPathPrefix = "shortcodes/" internalPathPrefix = "_internal/" ) -// resolves _internal/shortcodes/param.html => param.html etc. -func templateBaseName(typ templateType, name string) string { - name = strings.TrimPrefix(name, internalPathPrefix) - switch typ { - case templateShortcode: - return strings.TrimPrefix(name, shortcodesPathPrefix) - default: - panic("not implemented") - } +// The identifiers may be truncated in the log, e.g. +// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image" +var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`) +var embeddedTemplatesAliases = map[string][]string{ + "shortcodes/twitter.html": {"shortcodes/tweet.html"}, } -func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) { - base := templateBaseName(templateShortcode, name) - - shortcodename, variants := templateNameAndVariants(base) - - templs, found := t.shortcodes[shortcodename] - if !found { - templs = &shortcodeTemplates{} - t.shortcodes[shortcodename] = templs - } - - sv := shortcodeVariant{variants: variants, info: info, templ: templ} - - i := templs.indexOf(variants) - - if i != -1 { - // Only replace if it's an override of an internal template. - if !isInternal(name) { - templs.variants[i] = sv - } - } else { - templs.variants = append(templs.variants, sv) - } -} - -func (t *templateHandler) wrapTextTemplate(tt *textTemplate) tpl.TemplateParseFinder { - return struct { - tpl.TemplateParser - tpl.TemplateLookup - tpl.TemplateLookupVariant - }{ - tt, - tt, - new(nopLookupVariant), - } - -} - -type nopLookupVariant int - -func (l nopLookupVariant) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { - return nil, false, false -} - -func (t *templateHandler) Debug() { - fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates()) - fmt.Println("\n\nText templates:\n", t.text.t.DefinedTemplates()) -} - -// 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.Template, bool) { - - if strings.HasPrefix(name, textTmplNamePrefix) { - // The caller has explicitly asked for a text template, so only look - // in the text template collection. - // The templates are stored without the prefix identificator. - name = strings.TrimPrefix(name, textTmplNamePrefix) - - return t.applyTemplateInfo(t.text.Lookup(name)) - } - - // Look in both - if te, found := t.html.Lookup(name); found { - return t.applyTemplateInfo(te, true) - } - - return t.applyTemplateInfo(t.text.Lookup(name)) - -} - -func (t *templateHandler) applyTemplateInfo(templ tpl.Template, found bool) (tpl.Template, bool) { - if adapter, ok := templ.(*tpl.TemplateAdapter); ok { - if adapter.Info.IsZero() { - if info, found := t.templateInfo[templ.Name()]; found { - adapter.Info = info - } - } - } - - return templ, found -} - -// This currently only applies to shortcodes and what we get here is the -// shortcode name. -func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { - name = templateBaseName(templateShortcode, name) - s, found := t.shortcodes[name] - if !found { - return nil, false, false - } - - sv, found := s.fromVariants(variants) - if !found { - return nil, false, false - } - - more := len(s.variants) > 1 - - return &tpl.TemplateAdapter{ - Template: sv.templ, - Info: sv.info, - Metrics: t.Deps.Metrics, - Fs: t.layoutsFs, - NameBaseTemplateName: t.html.nameBaseTemplateName}, true, more - -} - -func (t *textTemplates) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { - return t.handler.LookupVariant(name, variants) -} - -func (t *htmlTemplates) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { - return t.handler.LookupVariant(name, variants) -} - -func (t *templateHandler) lookupTemplate(in interface{}) tpl.Template { - switch templ := in.(type) { - case *texttemplate.Template: - return t.text.lookup(templ.Name()) - case *template.Template: - return t.html.lookup(templ.Name()) - } - - panic(fmt.Sprintf("%T is not a template", in)) -} - -func (t *templateHandler) setFuncMapInTemplate(in interface{}, funcs map[string]interface{}) { - switch templ := in.(type) { - case *texttemplate.Template: - templ.Funcs(funcs) - return - case *template.Template: - templ.Funcs(funcs) - return - } - - panic(fmt.Sprintf("%T is not a template", in)) -} - -func (t *templateHandler) clone(d *deps.Deps) *templateHandler { - c := &templateHandler{ - Deps: d, - layoutsFs: d.BaseFs.Layouts.Fs, - shortcodes: make(map[string]*shortcodeTemplates), - templateInfo: t.templateInfo, - html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template), templatesCommon: t.html.templatesCommon}, - text: &textTemplates{ - textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, - standalone: &textTemplate{t: texttemplate.New("")}, - overlays: make(map[string]*texttemplate.Template), templatesCommon: t.text.templatesCommon}, - errors: make([]*templateErr, 0), - } - - for k, v := range t.shortcodes { - other := *v - variantsc := make([]shortcodeVariant, len(v.variants)) - for i, variant := range v.variants { - variantsc[i] = shortcodeVariant{ - info: variant.info, - variants: variant.variants, - templ: c.lookupTemplate(variant.templ), - } - } - other.variants = variantsc - c.shortcodes[k] = &other - } - - d.Tmpl = c - d.TextTmpl = c.wrapTextTemplate(c.text.standalone) - - c.initFuncs() - - for k, v := range t.html.overlays { - vc := template.Must(v.Clone()) - // The extra lookup is a workaround, see - // * https://github.com/golang/go/issues/16101 - // * https://github.com/gohugoio/hugo/issues/2549 - vc = vc.Lookup(vc.Name()) - vc.Funcs(c.html.funcster.funcMap) - c.html.overlays[k] = vc - } - - for k, v := range t.text.overlays { - vc := texttemplate.Must(v.Clone()) - vc = vc.Lookup(vc.Name()) - vc.Funcs(texttemplate.FuncMap(c.text.funcster.funcMap)) - c.text.overlays[k] = vc - } - - return c - -} +const baseFileBase = "baseof" func newTemplateAdapter(deps *deps.Deps) *templateHandler { + common := &templatesCommon{ nameBaseTemplateName: make(map[string]string), transformNotFound: make(map[string]bool), @@ -327,20 +88,23 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler { overlays: make(map[string]*template.Template), templatesCommon: common, } + textT := &textTemplates{ textTemplate: &textTemplate{t: texttemplate.New("")}, standalone: &textTemplate{t: texttemplate.New("")}, overlays: make(map[string]*texttemplate.Template), templatesCommon: common, } + h := &templateHandler{ - Deps: deps, - layoutsFs: deps.BaseFs.Layouts.Fs, - shortcodes: make(map[string]*shortcodeTemplates), - templateInfo: make(map[string]tpl.Info), - html: htmlT, - text: textT, - errors: make([]*templateErr, 0), + Deps: deps, + layoutsFs: deps.BaseFs.Layouts.Fs, + templateHandlerCommon: &templateHandlerCommon{ + shortcodes: make(map[string]*shortcodeTemplates), + templateInfo: make(map[string]tpl.Info), + html: htmlT, + text: textT, + }, } common.handler = h @@ -349,21 +113,7 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler { } -// Shared by both HTML and text templates. -type templatesCommon struct { - handler *templateHandler - funcster *templateFuncster - - // Used to get proper filenames in errors - nameBaseTemplateName map[string]string - - // Holds names of the templates not found during the first AST transformation - // pass. - transformNotFound map[string]bool -} type htmlTemplates struct { - mu sync.RWMutex - *templatesCommon t *template.Template @@ -380,119 +130,29 @@ type htmlTemplates struct { overlays map[string]*template.Template } -func (t *htmlTemplates) setTemplateFuncster(f *templateFuncster) { - t.funcster = f -} - func (t *htmlTemplates) Lookup(name string) (tpl.Template, bool) { templ := t.lookup(name) if templ == nil { return nil, false } - return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true + return templ, true } -func (t *htmlTemplates) lookup(name string) *template.Template { - t.mu.RLock() - defer t.mu.RUnlock() - - // Need to check in the overlay registry first as it will also be found below. - if t.overlays != nil { - if templ, ok := t.overlays[name]; ok { - return templ - } - } - - if templ := t.t.Lookup(name); templ != nil { - return templ - } - - if t.clone != nil { - return t.clone.Lookup(name) - } - - return nil +func (t *htmlTemplates) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { + return t.handler.LookupVariant(name, variants) } -func (t *textTemplates) setTemplateFuncster(f *templateFuncster) { - t.funcster = f +func (t *htmlTemplates) addLateTemplate(name, tpl string) error { + _, err := t.addTemplateIn(t.clone, name, tpl) + return err } -type textTemplates struct { - *templatesCommon - *textTemplate - standalone *textTemplate - clone *texttemplate.Template - cloneClone *texttemplate.Template - - overlays map[string]*texttemplate.Template -} - -func (t *textTemplates) Lookup(name string) (tpl.Template, bool) { - templ := t.lookup(name) - if templ == nil { - return nil, false - } - return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true -} - -func (t *textTemplates) lookup(name string) *texttemplate.Template { - - // Need to check in the overlay registry first as it will also be found below. - if t.overlays != nil { - if templ, ok := t.overlays[name]; ok { - return templ - } - } - - if templ := t.t.Lookup(name); templ != nil { - return templ - } - - if t.clone != nil { - return t.clone.Lookup(name) - } - - return nil -} - -func (t *templateHandler) setFuncs(funcMap map[string]interface{}) { - t.html.setFuncs(funcMap) - t.text.setFuncs(funcMap) - t.setFuncMapInTemplate(t.text.standalone.t, 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 *templateHandler) GetFuncs() map[string]interface{} { - return t.html.funcster.funcMap -} - -func (t *htmlTemplates) setFuncs(funcMap map[string]interface{}) { - t.t.Funcs(funcMap) -} - -func (t *textTemplates) setFuncs(funcMap map[string]interface{}) { - t.t.Funcs(funcMap) -} - -// LoadTemplates loads the templates from the layouts filesystem. -// A prefix can be given to indicate a template namespace to load the templates -// into, i.e. "_internal" etc. -func (t *templateHandler) LoadTemplates(prefix string) error { - return t.loadTemplates(prefix) - +func (t *htmlTemplates) addTemplate(name, tpl string) (*templateContext, error) { + return t.addTemplateIn(t.t, name, tpl) } func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) (*templateContext, error) { - t.mu.Lock() - defer t.mu.Unlock() - templ, err := tt.New(name).Parse(tpl) if err != nil { return nil, err @@ -518,276 +178,6 @@ func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) ( return c, nil } -func (t *htmlTemplates) addTemplate(name, tpl string) (*templateContext, error) { - return t.addTemplateIn(t.t, name, tpl) -} - -func (t *htmlTemplates) addLateTemplate(name, tpl string) error { - _, err := t.addTemplateIn(t.clone, name, tpl) - return err -} - -type textTemplate struct { - mu sync.RWMutex - t *texttemplate.Template -} - -func (t *textTemplate) Parse(name, tpl string) (tpl.Template, error) { - return t.parseIn(t.t, name, tpl) -} - -func (t *textTemplate) Lookup(name string) (tpl.Template, bool) { - t.mu.RLock() - defer t.mu.RUnlock() - - tpl := t.t.Lookup(name) - return tpl, tpl != nil -} - -func (t *textTemplate) parseIn(tt *texttemplate.Template, name, tpl string) (*texttemplate.Template, error) { - t.mu.Lock() - defer t.mu.Unlock() - - templ, err := tt.New(name).Parse(tpl) - if err != nil { - return nil, err - } - - if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil { - return nil, err - } - return templ, nil -} - -func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) (*templateContext, error) { - name = strings.TrimPrefix(name, textTmplNamePrefix) - templ, err := t.parseIn(tt, name, tpl) - if err != nil { - return nil, err - } - - typ := resolveTemplateType(name) - - c, err := applyTemplateTransformersToTextTemplate(typ, templ) - if err != nil { - return nil, err - } - - for k := range c.notFound { - t.transformNotFound[k] = true - } - - if typ == templateShortcode { - t.handler.addShortcodeVariant(name, c.Info, templ) - } else { - t.handler.templateInfo[name] = c.Info - } - - return c, nil -} - -func (t *textTemplates) addTemplate(name, tpl string) (*templateContext, error) { - return t.addTemplateIn(t.t, name, tpl) -} - -func (t *textTemplates) addLateTemplate(name, tpl string) error { - _, err := t.addTemplateIn(t.clone, name, tpl) - return err -} - -func (t *templateHandler) addTemplate(name, tpl string) error { - return t.AddTemplate(name, tpl) -} - -func (t *templateHandler) postTransform() error { - if len(t.html.transformNotFound) == 0 && len(t.text.transformNotFound) == 0 { - return nil - } - - defer func() { - t.text.transformNotFound = make(map[string]bool) - t.html.transformNotFound = make(map[string]bool) - }() - - for _, s := range []struct { - lookup func(name string) *parse.Tree - transformNotFound map[string]bool - }{ - // html templates - {func(name string) *parse.Tree { - templ := t.html.lookup(name) - if templ == nil { - return nil - } - return templ.Tree - }, t.html.transformNotFound}, - // text templates - {func(name string) *parse.Tree { - templT := t.text.lookup(name) - if templT == nil { - return nil - } - return templT.Tree - }, t.text.transformNotFound}, - } { - for name := range s.transformNotFound { - templ := s.lookup(name) - if templ != nil { - _, err := applyTemplateTransformers(templateUndefined, templ, s.lookup) - if err != nil { - return err - } - } - } - } - - return nil -} - -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 { - 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. -// TODO(bep) clean up these addTemplate variants -func (t *templateHandler) AddTemplate(name, tpl string) error { - h := t.getTemplateHandler(name) - _, err := h.addTemplate(name, tpl) - if err != nil { - 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() error { - if err := t.postTransform(); err != nil { - return err - } - - 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()) - } - - return nil -} - -// RebuildClone rebuilds the cloned templates. Used for live-reloads. -func (t *templateHandler) RebuildClone() { - if t.html != nil && t.html.cloneClone != nil { - t.html.clone = template.Must(t.html.cloneClone.Clone()) - } - if t.text != nil && t.text.cloneClone != nil { - t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) - } -} - -func (t *templateHandler) loadTemplates(prefix string) error { - - walker := func(path string, fi hugofs.FileMetaInfo, err error) error { - if err != nil || fi.IsDir() { - return err - } - - if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) { - return nil - } - - workingDir := t.PathSpec.WorkingDir - - descriptor := output.TemplateLookupDescriptor{ - WorkingDir: workingDir, - RelPath: path, - Prefix: prefix, - OutputFormats: t.OutputFormatsConfig, - FileExists: func(filename string) (bool, error) { - return helpers.Exists(filename, t.Layouts.Fs) - }, - ContainsAny: func(filename string, subslices [][]byte) (bool, error) { - return helpers.FileContainsAny(filename, subslices, t.Layouts.Fs) - }, - } - - 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 { - return err - } - - return nil - } - - if err := helpers.SymbolicWalk(t.Layouts.Fs, "", walker); err != nil { - if !os.IsNotExist(err) { - return err - } - return nil - } - - return nil - -} - -func (t *templateHandler) initFuncs() { - - // Both template types will get their own funcster instance, which - // in the current case contains the same set of funcs. - funcMap := createFuncMap(t.Deps) - for _, funcsterHolder := range []templateFuncsterSetter{t.html, t.text} { - funcster := newTemplateFuncster(t.Deps) - - // 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(funcMap) - - funcsterHolder.setTemplateFuncster(funcster) - - } - - for _, v := range t.shortcodes { - for _, variant := range v.variants { - t.setFuncMapInTemplate(variant.templ, funcMap) - } - } - -} - -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) (templateInfo, 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) (templateInfo, error)) error { masterTpl := t.lookup(masterFilename) @@ -829,59 +219,292 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin } -func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { - - name = strings.TrimPrefix(name, textTmplNamePrefix) - masterTpl := t.lookup(masterFilename) - - if masterTpl == nil { - templ, err := onMissing(masterFilename) - if err != nil { - return err +func (t *htmlTemplates) lookup(name string) *template.Template { + // Need to check in the overlay registry first as it will also be found below. + if t.overlays != nil { + if templ, ok := t.overlays[name]; ok { + return templ } - - masterTpl, err = t.t.New(masterFilename).Parse(templ.template) - if err != nil { - return errors.Wrapf(err, "failed to parse %q:", templ.filename) - } - t.nameBaseTemplateName[masterFilename] = templ.filename } - templ, err := onMissing(overlayFilename) + if templ := t.t.Lookup(name); templ != nil { + return templ + } + + if t.clone != nil { + return t.clone.Lookup(name) + } + + return nil +} + +func (t htmlTemplates) withNewHandler(h *templateHandler) *htmlTemplates { + t.templatesCommon = t.templatesCommon.withNewHandler(h) + return &t +} + +type nopLookupVariant int + +func (l nopLookupVariant) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { + return nil, false, false +} + +// templateHandler holds the templates in play. +// It implements the templateLoader and tpl.TemplateHandler interfaces. +// There is one templateHandler created per Site. +type templateHandler struct { + executor texttemplate.Executer + funcs map[string]reflect.Value + + // This is the filesystem to load the templates from. All the templates are + // stored in the root of this filesystem. + layoutsFs afero.Fs + + *deps.Deps + + *templateHandlerCommon +} + +// 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 { + 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. +// TODO(bep) clean up these addTemplate variants +func (t *templateHandler) AddTemplate(name, tpl string) error { + h := t.getTemplateHandler(name) + _, err := h.addTemplate(name, tpl) if err != nil { return err } + return nil +} - overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ.template) - if err != nil { - return errors.Wrapf(err, "failed to parse %q:", templ.filename) +func (t *templateHandler) Debug() { + fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates()) + fmt.Println("\n\nText templates:\n", t.text.t.DefinedTemplates()) +} + +func (t *templateHandler) Execute(templ tpl.Template, wr io.Writer, data interface{}) error { + if t.Metrics != nil { + defer t.Metrics.MeasureSince(templ.Name(), time.Now()) } - overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil { - return err + execErr := t.executor.Execute(templ, wr, data) + if execErr != nil { + execErr = t.addFileContext(templ.Name(), execErr) } - t.overlays[name] = overlayTpl - t.nameBaseTemplateName[name] = templ.filename - return err + return execErr } -func removeLeadingBOM(s string) string { - const bom = '\ufeff' +func (t *templateHandler) GetFunc(name string) (reflect.Value, bool) { + v, found := t.funcs[name] + return v, found - for i, r := range s { - if i == 0 && r != bom { - return s - } - if i > 0 { - return s[i:] - } +} + +// LoadTemplates loads the templates from the layouts filesystem. +// A prefix can be given to indicate a template namespace to load the templates +// into, i.e. "_internal" etc. +func (t *templateHandler) LoadTemplates(prefix string) error { + return t.loadTemplates(prefix) + +} + +// 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.Template, bool) { + if strings.HasPrefix(name, textTmplNamePrefix) { + // The caller has explicitly asked for a text template, so only look + // in the text template collection. + // The templates are stored without the prefix identificator. + name = strings.TrimPrefix(name, textTmplNamePrefix) + + return t.applyTemplateInfo(t.text.Lookup(name)) } - return s + // Look in both + if te, found := t.html.Lookup(name); found { + return t.applyTemplateInfo(te, true) + } + return t.applyTemplateInfo(t.text.Lookup(name)) + +} + +// This currently only applies to shortcodes and what we get here is the +// shortcode name. +func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { + name = templateBaseName(templateShortcode, name) + s, found := t.shortcodes[name] + if !found { + return nil, false, false + } + + sv, found := s.fromVariants(variants) + if !found { + return nil, false, false + } + + more := len(s.variants) > 1 + + return &tpl.TemplateInfo{ + Template: sv.templ, + Info: sv.info, + }, true, more + +} + +// 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() error { + if err := t.postTransform(); err != nil { + return err + } + + 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()) + } + + return nil +} + +// RebuildClone rebuilds the cloned templates. Used for live-reloads. +func (t *templateHandler) RebuildClone() { + if t.html != nil && t.html.cloneClone != nil { + t.html.clone = template.Must(t.html.cloneClone.Clone()) + } + if t.text != nil && t.text.cloneClone != nil { + t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) + } +} + +func (h *templateHandler) initTemplateExecuter() { + exec, funcs := newTemplateExecuter(h.Deps) + h.executor = exec + h.funcs = funcs + funcMap := make(map[string]interface{}) + for k, v := range funcs { + funcMap[k] = v.Interface() + } + + // Note that these funcs are not the ones getting called + // on execution, but they are needed at parse time. + h.text.textTemplate.t.Funcs(funcMap) + h.text.standalone.t.Funcs(funcMap) + h.html.t.Funcs(funcMap) +} + +func (t *templateHandler) getTemplateHandler(name string) templateLoader { + if strings.HasPrefix(name, textTmplNamePrefix) { + return t.text + } + return t.html +} + +func (t *templateHandler) addFileContext(name string, inerr error) error { + if strings.HasPrefix(name, "_internal") { + return inerr + } + + f, realFilename, err := t.fileAndFilename(name) + if err != nil { + return inerr + + } + defer f.Close() + + master, hasMaster := t.html.nameBaseTemplateName[name] + + ferr := errors.Wrap(inerr, "execute of template failed") + + // Since this can be a composite of multiple template files (single.html + baseof.html etc.) + // we potentially need to look in both -- and cannot rely on line number alone. + lineMatcher := func(m herrors.LineMatcher) bool { + if m.Position.LineNumber != m.LineNumber { + return false + } + if !hasMaster { + return true + } + + identifiers := t.extractIdentifiers(m.Error.Error()) + + for _, id := range identifiers { + if strings.Contains(m.Line, id) { + return true + } + } + return false + } + + fe, ok := herrors.WithFileContext(ferr, realFilename, f, lineMatcher) + if ok || !hasMaster { + return fe + } + + // Try the base template if relevant + f, realFilename, err = t.fileAndFilename(master) + if err != nil { + return err + } + defer f.Close() + + fe, ok = herrors.WithFileContext(ferr, realFilename, f, lineMatcher) + + if !ok { + // Return the most specific. + return ferr + + } + return fe + +} + +func (t *templateHandler) addInternalTemplate(name, tpl string) error { + return t.AddTemplate("_internal/"+name, tpl) +} + +func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) { + base := templateBaseName(templateShortcode, name) + + shortcodename, variants := templateNameAndVariants(base) + + templs, found := t.shortcodes[shortcodename] + if !found { + templs = &shortcodeTemplates{} + t.shortcodes[shortcodename] = templs + } + + sv := shortcodeVariant{variants: variants, info: info, templ: templ} + + i := templs.indexOf(variants) + + if i != -1 { + // Only replace if it's an override of an internal template. + if !isInternal(name) { + templs.variants[i] = sv + } + } else { + templs.variants = append(templs.variants, sv) + } } func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) error { @@ -937,8 +560,77 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e } } -var embeddedTemplatesAliases = map[string][]string{ - "shortcodes/twitter.html": {"shortcodes/tweet.html"}, +func (t *templateHandler) applyTemplateInfo(templ tpl.Template, found bool) (tpl.Template, bool) { + if adapter, ok := templ.(*tpl.TemplateInfo); ok { + if adapter.Info.IsZero() { + if info, found := t.templateInfo[templ.Name()]; found { + adapter.Info = info + } + } + } else if templ != nil { + if info, found := t.templateInfo[templ.Name()]; found { + return &tpl.TemplateInfo{ + Template: templ, + Info: info, + }, true + } + } + + return templ, found +} + +func (t *templateHandler) checkState() { + if t.html.clone != nil || t.text.clone != nil { + panic("template is cloned and cannot be modfified") + } +} + +func (t *templateHandler) clone(d *deps.Deps) *templateHandler { + c := &templateHandler{ + Deps: d, + layoutsFs: d.BaseFs.Layouts.Fs, + } + + c.templateHandlerCommon = t.templateHandlerCommon.withNewHandler(c) + d.Tmpl = c + d.TextTmpl = c.wrapTextTemplate(c.text.standalone) + c.executor, c.funcs = newTemplateExecuter(d) + + return c + +} + +func (t *templateHandler) extractIdentifiers(line string) []string { + m := identifiersRe.FindAllStringSubmatch(line, -1) + identifiers := make([]string, len(m)) + for i := 0; i < len(m); i++ { + identifiers[i] = m[i][1] + } + return identifiers +} + +func (t *templateHandler) fileAndFilename(name string) (afero.File, string, error) { + fs := t.layoutsFs + filename := filepath.FromSlash(name) + + fi, err := fs.Stat(filename) + if err != nil { + return nil, "", err + } + fim := fi.(hugofs.FileMetaInfo) + meta := fim.Meta() + + f, err := meta.Open() + if err != nil { + return nil, "", errors.Wrapf(err, "failed to open template file %q:", filename) + } + + return f, meta.Filename(), nil +} + +func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { + h := t.getTemplateHandler(name) + return h.handleMaster(name, overlayFilename, masterFilename, onMissing) } func (t *templateHandler) loadEmbedded() error { @@ -961,26 +653,348 @@ func (t *templateHandler) loadEmbedded() error { } -func (t *templateHandler) addInternalTemplate(name, tpl string) error { - return t.AddTemplate("_internal/"+name, tpl) -} +func (t *templateHandler) loadTemplates(prefix string) error { -func (t *templateHandler) checkState() { - if t.html.clone != nil || t.text.clone != nil { - panic("template is cloned and cannot be modfified") + walker := func(path string, fi hugofs.FileMetaInfo, err error) error { + if err != nil || fi.IsDir() { + return err + } + + if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) { + return nil + } + + workingDir := t.PathSpec.WorkingDir + + descriptor := output.TemplateLookupDescriptor{ + WorkingDir: workingDir, + RelPath: path, + Prefix: prefix, + OutputFormats: t.OutputFormatsConfig, + FileExists: func(filename string) (bool, error) { + return helpers.Exists(filename, t.Layouts.Fs) + }, + ContainsAny: func(filename string, subslices [][]byte) (bool, error) { + return helpers.FileContainsAny(filename, subslices, t.Layouts.Fs) + }, + } + + 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 { + return err + } + + return nil } + + if err := helpers.SymbolicWalk(t.Layouts.Fs, "", walker); err != nil { + if !os.IsNotExist(err) { + return err + } + return nil + } + + return nil + } -func isDotFile(path string) bool { - return filepath.Base(path)[0] == '.' +func (t *templateHandler) postTransform() error { + if len(t.html.transformNotFound) == 0 && len(t.text.transformNotFound) == 0 { + return nil + } + + defer func() { + t.text.transformNotFound = make(map[string]bool) + t.html.transformNotFound = make(map[string]bool) + }() + + for _, s := range []struct { + lookup func(name string) *parse.Tree + transformNotFound map[string]bool + }{ + // html templates + {func(name string) *parse.Tree { + templ := t.html.lookup(name) + if templ == nil { + return nil + } + return templ.Tree + }, t.html.transformNotFound}, + // text templates + {func(name string) *parse.Tree { + templT := t.text.lookup(name) + if templT == nil { + return nil + } + return templT.Tree + }, t.text.transformNotFound}, + } { + for name := range s.transformNotFound { + templ := s.lookup(name) + if templ != nil { + _, err := applyTemplateTransformers(templateUndefined, templ, s.lookup) + if err != nil { + return err + } + } + } + } + + return nil +} + +func (t *templateHandler) wrapTextTemplate(tt *textTemplate) tpl.TemplateParseFinder { + return struct { + tpl.TemplateParser + tpl.TemplateLookup + tpl.TemplateLookupVariant + }{ + tt, + tt, + new(nopLookupVariant), + } + +} + +type templateHandlerCommon struct { + // shortcodes maps shortcode name to template variants + // (language, output format etc.) of that shortcode. + shortcodes map[string]*shortcodeTemplates + + // templateInfo maps template name to some additional information about that template. + // Note that for shortcodes that same information is embedded in the + // shortcodeTemplates type. + templateInfo map[string]tpl.Info + + // text holds all the pure text templates. + text *textTemplates + html *htmlTemplates +} + +func (t templateHandlerCommon) withNewHandler(h *templateHandler) *templateHandlerCommon { + t.text = t.text.withNewHandler(h) + t.html = t.html.withNewHandler(h) + return &t +} + +type templateLoader interface { + addLateTemplate(name, tpl string) error + addTemplate(name, tpl string) (*templateContext, error) + handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error +} + +// Shared by both HTML and text templates. +type templatesCommon struct { + handler *templateHandler + + // Used to get proper filenames in errors + nameBaseTemplateName map[string]string + + // Holds names of the templates not found during the first AST transformation + // pass. + transformNotFound map[string]bool +} + +func (t templatesCommon) withNewHandler(h *templateHandler) *templatesCommon { + t.handler = h + return &t +} + +type textTemplate struct { + mu sync.RWMutex + t *texttemplate.Template +} + +func (t *textTemplate) Lookup(name string) (tpl.Template, bool) { + t.mu.RLock() + defer t.mu.RUnlock() + + tpl := t.t.Lookup(name) + return tpl, tpl != nil +} + +func (t *textTemplate) Parse(name, tpl string) (tpl.Template, error) { + return t.parseIn(t.t, name, tpl) +} + +func (t *textTemplate) parseIn(tt *texttemplate.Template, name, tpl string) (*texttemplate.Template, error) { + t.mu.Lock() + defer t.mu.Unlock() + + templ, err := tt.New(name).Parse(tpl) + if err != nil { + return nil, err + } + + if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil { + return nil, err + } + return templ, nil +} + +type textTemplates struct { + *templatesCommon + *textTemplate + standalone *textTemplate + clone *texttemplate.Template + cloneClone *texttemplate.Template + + overlays map[string]*texttemplate.Template +} + +func (t *textTemplates) Lookup(name string) (tpl.Template, bool) { + templ := t.lookup(name) + if templ == nil { + return nil, false + } + return templ, true +} + +func (t *textTemplates) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { + return t.handler.LookupVariant(name, variants) +} + +func (t *textTemplates) addLateTemplate(name, tpl string) error { + _, err := t.addTemplateIn(t.clone, name, tpl) + return err +} + +func (t *textTemplates) addTemplate(name, tpl string) (*templateContext, error) { + return t.addTemplateIn(t.t, name, tpl) +} + +func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) (*templateContext, error) { + name = strings.TrimPrefix(name, textTmplNamePrefix) + templ, err := t.parseIn(tt, name, tpl) + if err != nil { + return nil, err + } + + typ := resolveTemplateType(name) + + c, err := applyTemplateTransformersToTextTemplate(typ, templ) + if err != nil { + return nil, err + } + + for k := range c.notFound { + t.transformNotFound[k] = true + } + + if typ == templateShortcode { + t.handler.addShortcodeVariant(name, c.Info, templ) + } else { + t.handler.templateInfo[name] = c.Info + } + + return c, nil +} + +func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { + + name = strings.TrimPrefix(name, textTmplNamePrefix) + masterTpl := t.lookup(masterFilename) + + if masterTpl == nil { + templ, err := onMissing(masterFilename) + if err != nil { + return err + } + + masterTpl, err = t.t.New(masterFilename).Parse(templ.template) + if err != nil { + return errors.Wrapf(err, "failed to parse %q:", templ.filename) + } + t.nameBaseTemplateName[masterFilename] = templ.filename + } + + templ, err := onMissing(overlayFilename) + if err != nil { + return err + } + + overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ.template) + if err != nil { + return errors.Wrapf(err, "failed to parse %q:", templ.filename) + } + + overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) + if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil { + return err + } + t.overlays[name] = overlayTpl + t.nameBaseTemplateName[name] = templ.filename + + return err + +} + +func (t *textTemplates) lookup(name string) *texttemplate.Template { + + // Need to check in the overlay registry first as it will also be found below. + if t.overlays != nil { + if templ, ok := t.overlays[name]; ok { + return templ + } + } + + if templ := t.t.Lookup(name); templ != nil { + return templ + } + + if t.clone != nil { + return t.clone.Lookup(name) + } + + return nil +} + +func (t textTemplates) withNewHandler(h *templateHandler) *textTemplates { + t.templatesCommon = t.templatesCommon.withNewHandler(h) + return &t } func isBackupFile(path string) bool { return path[len(path)-1] == '~' } -const baseFileBase = "baseof" - func isBaseTemplate(path string) bool { return strings.Contains(filepath.Base(path), baseFileBase) } + +func isDotFile(path string) bool { + return filepath.Base(path)[0] == '.' +} + +func removeLeadingBOM(s string) string { + const bom = '\ufeff' + + for i, r := range s { + if i == 0 && r != bom { + return s + } + if i > 0 { + return s[i:] + } + } + + return s + +} + +// resolves _internal/shortcodes/param.html => param.html etc. +func templateBaseName(typ templateType, name string) string { + name = strings.TrimPrefix(name, internalPathPrefix) + switch typ { + case templateShortcode: + return strings.TrimPrefix(name, shortcodesPathPrefix) + default: + panic("not implemented") + } + +} diff --git a/tpl/tplimpl/templateFuncster.go b/tpl/tplimpl/templateFuncster.go index ad51fbad7..96404f51b 100644 --- a/tpl/tplimpl/templateFuncster.go +++ b/tpl/tplimpl/templateFuncster.go @@ -12,22 +12,3 @@ // limitations under the License. package tplimpl - -import ( - "html/template" - - "github.com/gohugoio/hugo/deps" -) - -// Some of the template funcs are'nt entirely stateless. -type templateFuncster struct { - funcMap template.FuncMap - - *deps.Deps -} - -func newTemplateFuncster(deps *deps.Deps) *templateFuncster { - return &templateFuncster{ - Deps: deps, - } -} diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go index 605c47d87..910c0be89 100644 --- a/tpl/tplimpl/templateProvider.go +++ b/tpl/tplimpl/templateProvider.go @@ -29,8 +29,8 @@ func (*TemplateProvider) Update(deps *deps.Deps) error { newTmpl := newTemplateAdapter(deps) deps.Tmpl = newTmpl deps.TextTmpl = newTmpl.wrapTextTemplate(newTmpl.text.standalone) - - newTmpl.initFuncs() + // These needs to be there at parse time. + newTmpl.initTemplateExecuter() if err := newTmpl.loadEmbedded(); err != nil { return err diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go index 8a432f272..31d24b71d 100644 --- a/tpl/tplimpl/template_ast_transformers.go +++ b/tpl/tplimpl/template_ast_transformers.go @@ -14,9 +14,7 @@ package tplimpl import ( - "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" - - "strings" + template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" @@ -27,19 +25,6 @@ import ( "github.com/pkg/errors" ) -// decl keeps track of the variable mappings, i.e. $mysite => .Site etc. -type decl map[string]string - -const ( - paramsIdentifier = "Params" -) - -// Containers that may contain Params that we will not touch. -var reservedContainers = map[string]bool{ - // Aka .Site.Data.Params which must stay case sensitive. - "Data": true, -} - type templateType int const ( @@ -49,7 +34,6 @@ const ( ) type templateContext struct { - decl decl visited map[string]bool notFound map[string]bool lookupFn func(name string) *parse.Tree @@ -89,7 +73,6 @@ func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext return &templateContext{ Info: tpl.Info{Config: tpl.DefaultConfig}, lookupFn: lookupFn, - decl: make(map[string]string), visited: make(map[string]bool), notFound: make(map[string]bool)} } @@ -219,11 +202,6 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) { } case *parse.PipeNode: c.collectConfig(x) - if len(x.Decl) == 1 && len(x.Cmds) == 1 { - // maps $site => .Site etc. - c.decl[x.Decl[0].Ident[0]] = x.Cmds[0].String() - } - for i, cmd := range x.Cmds { keep, _ := c.applyTransformations(cmd) if !keep { @@ -237,17 +215,8 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) { for _, elem := range x.Args { switch an := elem.(type) { - case *parse.FieldNode: - c.updateIdentsIfNeeded(an.Ident) - case *parse.VariableNode: - c.updateIdentsIfNeeded(an.Ident) case *parse.PipeNode: c.applyTransformations(an) - case *parse.ChainNode: - // site.Params... - if len(an.Field) > 1 && an.Field[0] == paramsIdentifier { - c.updateIdentsIfNeeded(an.Field) - } } } return keep, c.err @@ -262,19 +231,6 @@ func (c *templateContext) applyTransformationsToNodes(nodes ...parse.Node) { } } -func (c *templateContext) updateIdentsIfNeeded(idents []string) { - index := c.decl.indexOfReplacementStart(idents) - - if index == -1 { - return - } - - for i := index; i < len(idents); i++ { - idents[i] = strings.ToLower(idents[i]) - } - -} - func (c *templateContext) hasIdent(idents []string, ident string) bool { for _, id := range idents { if id == ident { @@ -376,160 +332,3 @@ func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool { return false } - -// indexOfReplacementStart will return the index of where to start doing replacement, -// -1 if none needed. -func (d decl) indexOfReplacementStart(idents []string) int { - - l := len(idents) - - if l == 0 { - return -1 - } - - if l == 1 { - first := idents[0] - if first == "" || first == paramsIdentifier || first[0] == '$' { - // This can not be a Params.x - return -1 - } - } - - var lookFurther bool - var needsVarExpansion bool - for _, ident := range idents { - if ident[0] == '$' { - lookFurther = true - needsVarExpansion = true - break - } else if ident == paramsIdentifier { - lookFurther = true - break - } - } - - if !lookFurther { - return -1 - } - - var resolvedIdents []string - - if !needsVarExpansion { - resolvedIdents = idents - } else { - var ok bool - resolvedIdents, ok = d.resolveVariables(idents) - if !ok { - return -1 - } - } - - var paramFound bool - for i, ident := range resolvedIdents { - if ident == paramsIdentifier { - if i > 0 { - container := resolvedIdents[i-1] - if reservedContainers[container] { - // .Data.Params.someKey - return -1 - } - } - - paramFound = true - break - } - } - - if !paramFound { - return -1 - } - - var paramSeen bool - idx := -1 - for i, ident := range idents { - if ident == "" || ident[0] == '$' { - continue - } - - if ident == paramsIdentifier { - paramSeen = true - idx = -1 - - } else { - if paramSeen { - return i - } - if idx == -1 { - idx = i - } - } - } - return idx - -} - -func (d decl) resolveVariables(idents []string) ([]string, bool) { - var ( - replacements []string - replaced []string - ) - - // An Ident can start out as one of - // [Params] [$blue] [$colors.Blue] - // We need to resolve the variables, so - // $blue => [Params Colors Blue] - // etc. - replacements = []string{idents[0]} - - // Loop until there are no more $vars to resolve. - for i := 0; i < len(replacements); i++ { - - if i > 20 { - // bail out - return nil, false - } - - potentialVar := replacements[i] - - if potentialVar == "$" { - continue - } - - if potentialVar == "" || potentialVar[0] != '$' { - // leave it as is - replaced = append(replaced, strings.Split(potentialVar, ".")...) - continue - } - - replacement, ok := d[potentialVar] - - if !ok { - // Temporary range vars. We do not care about those. - return nil, false - } - - if !d.isKeyword(replacement) { - continue - } - - replacement = strings.TrimPrefix(replacement, ".") - - if replacement == "" { - continue - } - - if replacement[0] == '$' { - // Needs further expansion - replacements = append(replacements, strings.Split(replacement, ".")...) - } else { - replaced = append(replaced, strings.Split(replacement, ".")...) - } - } - - return append(replaced, idents[1:]...), true - -} - -func (d decl) isKeyword(s string) bool { - return !strings.ContainsAny(s, " -\"") -} diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go index 6a0bb8253..0dc91ac32 100644 --- a/tpl/tplimpl/template_ast_transformers_test.go +++ b/tpl/tplimpl/template_ast_transformers_test.go @@ -13,400 +13,18 @@ package tplimpl import ( - "bytes" - "fmt" - "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" + "strings" + + template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" "testing" "time" "github.com/gohugoio/hugo/tpl" - "github.com/spf13/cast" - qt "github.com/frankban/quicktest" ) -type paramsHolder struct { - params map[string]interface{} - page *paramsHolder -} - -func (p paramsHolder) Params() map[string]interface{} { - return p.params -} - -func (p paramsHolder) GetPage(arg string) *paramsHolder { - return p.page -} - -var ( - testFuncs = map[string]interface{}{ - "getif": func(v interface{}) interface{} { return v }, - "ToTime": func(v interface{}) interface{} { return cast.ToTime(v) }, - "First": func(v ...interface{}) interface{} { return v[0] }, - "Echo": func(v interface{}) interface{} { return v }, - "where": func(seq, key interface{}, args ...interface{}) (interface{}, error) { - return map[string]interface{}{ - "ByWeight": fmt.Sprintf("%v:%v:%v", seq, key, args), - }, nil - }, - "site": func() paramsHolder { - return paramsHolder{ - params: map[string]interface{}{ - "lower": "global-site", - }, - page: ¶msHolder{ - params: map[string]interface{}{ - "lower": "page", - }, - }, - } - }, - } - - paramsData = map[string]interface{}{ - - "NotParam": "Hi There", - "Slice": []int{1, 3}, - "Params": map[string]interface{}{ - "lower": "P1L", - "slice": []int{1, 3}, - "mydate": "1972-01-28", - }, - "Pages": map[string]interface{}{ - "ByWeight": []int{1, 3}, - }, - "CurrentSection": map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "pcurrentsection", - }, - }, - "Site": map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "P2L", - "slice": []int{1, 3}, - }, - "Language": map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "P22L", - "nested": map[string]interface{}{ - "lower": "P22L_nested", - }, - }, - }, - "Data": map[string]interface{}{ - "Params": map[string]interface{}{ - "NOLOW": "P3H", - }, - }, - }, - "Site2": paramsHolder{ - params: map[string]interface{}{ - "lower": "global-site", - }, - page: ¶msHolder{ - params: map[string]interface{}{ - "lower": "page", - }, - }, - }, - } - - paramsTempl = ` -{{ $page := . }} -{{ $pages := .Pages }} -{{ $pageParams := .Params }} -{{ $site := .Site }} -{{ $siteParams := .Site.Params }} -{{ $data := .Site.Data }} -{{ $notparam := .NotParam }} - -PCurrentSection: {{ .CurrentSection.Params.LOWER }} -P1: {{ .Params.LOWER }} -P1_2: {{ $.Params.LOWER }} -P1_3: {{ $page.Params.LOWER }} -P1_4: {{ $pageParams.LOWER }} -P2: {{ .Site.Params.LOWER }} -P2_2: {{ $.Site.Params.LOWER }} -P2_3: {{ $site.Params.LOWER }} -P2_4: {{ $siteParams.LOWER }} -P22: {{ .Site.Language.Params.LOWER }} -P22_nested: {{ .Site.Language.Params.NESTED.LOWER }} -P3: {{ .Site.Data.Params.NOLOW }} -P3_2: {{ $.Site.Data.Params.NOLOW }} -P3_3: {{ $site.Data.Params.NOLOW }} -P3_4: {{ $data.Params.NOLOW }} -P4: {{ range $i, $e := .Site.Params.SLICE }}{{ $e }}{{ end }} -P5: {{ Echo .Params.LOWER }} -P5_2: {{ Echo $site.Params.LOWER }} -{{ if .Params.LOWER }} -IF: {{ .Params.LOWER }} -{{ end }} -{{ if .Params.NOT_EXIST }} -{{ else }} -ELSE: {{ .Params.LOWER }} -{{ end }} - - -{{ with .Params.LOWER }} -WITH: {{ . }} -{{ end }} - - -{{ range .Slice }} -RANGE: {{ . }}: {{ $.Params.LOWER }} -{{ end }} -{{ index .Slice 1 }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ .NotParam }} -{{ $notparam }} - - -{{ $lower := .Site.Params.LOWER }} -F1: {{ printf "themes/%s-theme" .Site.Params.LOWER }} -F2: {{ Echo (printf "themes/%s-theme" $lower) }} -F3: {{ Echo (printf "themes/%s-theme" .Site.Params.LOWER) }} - -PSLICE: {{ range .Params.SLICE }}PSLICE{{.}}|{{ end }} - -{{ $pages := "foo" }} -{{ $pages := where $pages ".Params.toc_hide" "!=" true }} -PARAMS STRING: {{ $pages.ByWeight }} -PARAMS STRING2: {{ with $pages }}{{ .ByWeight }}{{ end }} -{{ $pages3 := where ".Params.TOC_HIDE" "!=" .Params.LOWER }} -PARAMS STRING3: {{ $pages3.ByWeight }} -{{ $first := First .Pages .Site.Params.LOWER }} -PARAMS COMPOSITE: {{ $first.ByWeight }} - - -{{ $time := $.Params.MyDate | ToTime }} -{{ $time = $time.AddDate 0 1 0 }} -PARAMS TIME: {{ $time.Format "2006-01-02" }} - -{{ $_x := $.Params.MyDate | ToTime }} -PARAMS TIME2: {{ $_x.AddDate 0 1 0 }} - -PARAMS SITE GLOBAL1: {{ site.Params.LOwER }} -{{ $lower := site.Params.LOwER }} -{{ $site := site }} -PARAMS SITE GLOBAL2: {{ $lower }} -PARAMS SITE GLOBAL3: {{ $site.Params.LOWER }} - -{{ $p := $site.GetPage "foo" }} -PARAMS GETPAGE: {{ $p.Params.LOWER }} -{{ $p := .Site2.GetPage "foo" }} -PARAMS GETPAGE2: {{ $p.Params.LOWER }} -` -) - -func TestParamsKeysToLower(t *testing.T) { - t.Parallel() - c := qt.New(t) - - _, err := applyTemplateTransformers(templateUndefined, nil, nil) - c.Assert(err, qt.Not(qt.IsNil)) - - templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) - - c.Assert(err, qt.IsNil) - - ctx := newTemplateContext(createParseTreeLookup(templ)) - - c.Assert(ctx.decl.indexOfReplacementStart([]string{}), qt.Equals, -1) - - ctx.applyTransformations(templ.Tree.Root) - - var b bytes.Buffer - - c.Assert(templ.Execute(&b, paramsData), qt.IsNil) - - result := b.String() - - c.Assert(result, qt.Contains, "P1: P1L") - c.Assert(result, qt.Contains, "P1_2: P1L") - c.Assert(result, qt.Contains, "P1_3: P1L") - c.Assert(result, qt.Contains, "P1_4: P1L") - c.Assert(result, qt.Contains, "P2: P2L") - c.Assert(result, qt.Contains, "P2_2: P2L") - c.Assert(result, qt.Contains, "P2_3: P2L") - c.Assert(result, qt.Contains, "P2_4: P2L") - c.Assert(result, qt.Contains, "P22: P22L") - c.Assert(result, qt.Contains, "P22_nested: P22L_nested") - c.Assert(result, qt.Contains, "P3: P3H") - c.Assert(result, qt.Contains, "P3_2: P3H") - c.Assert(result, qt.Contains, "P3_3: P3H") - c.Assert(result, qt.Contains, "P3_4: P3H") - c.Assert(result, qt.Contains, "P4: 13") - c.Assert(result, qt.Contains, "P5: P1L") - c.Assert(result, qt.Contains, "P5_2: P2L") - - c.Assert(result, qt.Contains, "IF: P1L") - c.Assert(result, qt.Contains, "ELSE: P1L") - - c.Assert(result, qt.Contains, "WITH: P1L") - - c.Assert(result, qt.Contains, "RANGE: 3: P1L") - - c.Assert(result, qt.Contains, "Hi There") - - // Issue #2740 - c.Assert(result, qt.Contains, "F1: themes/P2L-theme") - c.Assert(result, qt.Contains, "F2: themes/P2L-theme") - c.Assert(result, qt.Contains, "F3: themes/P2L-theme") - - c.Assert(result, qt.Contains, "PSLICE: PSLICE1|PSLICE3|") - c.Assert(result, qt.Contains, "PARAMS STRING: foo:.Params.toc_hide:[!= true]") - c.Assert(result, qt.Contains, "PARAMS STRING2: foo:.Params.toc_hide:[!= true]") - c.Assert(result, qt.Contains, "PARAMS STRING3: .Params.TOC_HIDE:!=:[P1L]") - - // Issue #5094 - c.Assert(result, qt.Contains, "PARAMS COMPOSITE: [1 3]") - - // Issue #5068 - c.Assert(result, qt.Contains, "PCurrentSection: pcurrentsection") - - // Issue #5541 - c.Assert(result, qt.Contains, "PARAMS TIME: 1972-02-28") - c.Assert(result, qt.Contains, "PARAMS TIME2: 1972-02-28") - - // Issue ##5615 - c.Assert(result, qt.Contains, "PARAMS SITE GLOBAL1: global-site") - c.Assert(result, qt.Contains, "PARAMS SITE GLOBAL2: global-site") - c.Assert(result, qt.Contains, "PARAMS SITE GLOBAL3: global-site") - - // - c.Assert(result, qt.Contains, "PARAMS GETPAGE: page") - c.Assert(result, qt.Contains, "PARAMS GETPAGE2: page") - -} - -func BenchmarkTemplateParamsKeysToLower(b *testing.B) { - templ, err := template.New("foo").Funcs(testFuncs).Parse(paramsTempl) - - if err != nil { - b.Fatal(err) - } - - templates := make([]*template.Template, b.N) - - for i := 0; i < b.N; i++ { - templates[i], err = templ.Clone() - if err != nil { - b.Fatal(err) - } - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - c := newTemplateContext(createParseTreeLookup(templates[i])) - c.applyTransformations(templ.Tree.Root) - } -} - -func TestParamsKeysToLowerVars(t *testing.T) { - t.Parallel() - c := qt.New(t) - - var ( - data = map[string]interface{}{ - "Params": map[string]interface{}{ - "colors": map[string]interface{}{ - "blue": "Amber", - "pretty": map[string]interface{}{ - "first": "Indigo", - }, - }, - }, - } - - // This is how Amber behaves: - paramsTempl = ` -{{$__amber_1 := .Params.Colors}} -{{$__amber_2 := $__amber_1.Blue}} -{{$__amber_3 := $__amber_1.Pretty}} -{{$__amber_4 := .Params}} - -Color: {{$__amber_2}} -Blue: {{ $__amber_1.Blue}} -Pretty First1: {{ $__amber_3.First}} -Pretty First2: {{ $__amber_1.Pretty.First}} -Pretty First3: {{ $__amber_4.COLORS.PRETTY.FIRST}} -` - ) - - templ, err := template.New("foo").Parse(paramsTempl) - - c.Assert(err, qt.IsNil) - - ctx := newTemplateContext(createParseTreeLookup(templ)) - - ctx.applyTransformations(templ.Tree.Root) - - var b bytes.Buffer - - c.Assert(templ.Execute(&b, data), qt.IsNil) - - result := b.String() - - c.Assert(result, qt.Contains, "Color: Amber") - c.Assert(result, qt.Contains, "Blue: Amber") - c.Assert(result, qt.Contains, "Pretty First1: Indigo") - c.Assert(result, qt.Contains, "Pretty First2: Indigo") - c.Assert(result, qt.Contains, "Pretty First3: Indigo") - -} - -func TestParamsKeysToLowerInBlockTemplate(t *testing.T) { - t.Parallel() - c := qt.New(t) - - var ( - data = map[string]interface{}{ - "Params": map[string]interface{}{ - "lower": "P1L", - }, - } - - master = ` -P1: {{ .Params.LOWER }} -{{ block "main" . }}DEFAULT{{ end }}` - overlay = ` -{{ define "main" }} -P2: {{ .Params.LOWER }} -{{ end }}` - ) - - masterTpl, err := template.New("foo").Parse(master) - c.Assert(err, qt.IsNil) - - overlayTpl, err := template.Must(masterTpl.Clone()).Parse(overlay) - c.Assert(err, qt.IsNil) - overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - - ctx := newTemplateContext(createParseTreeLookup(overlayTpl)) - - ctx.applyTransformations(overlayTpl.Tree.Root) - - var b bytes.Buffer - - c.Assert(overlayTpl.Execute(&b, data), qt.IsNil) - - result := b.String() - - c.Assert(result, qt.Contains, "P1: P1L") - c.Assert(result, qt.Contains, "P2: P1L") -} - // Issue #2927 func TestTransformRecursiveTemplate(t *testing.T) { c := qt.New(t) @@ -479,7 +97,7 @@ func TestInsertIsZeroFunc(t *testing.T) { ) d := newD(c) - h := d.Tmpl.(tpl.TemplateHandler) + h := d.Tmpl.(tpl.TemplateManager) // HTML templates c.Assert(h.AddTemplate("mytemplate.html", templ1), qt.IsNil) @@ -493,9 +111,13 @@ func TestInsertIsZeroFunc(t *testing.T) { for _, name := range []string{"mytemplate.html", "mytexttemplate.txt"} { tt, _ := d.Tmpl.Lookup(name) - result, err := tt.(tpl.TemplateExecutor).ExecuteToString(ctx) + sb := &strings.Builder{} + + err := d.Tmpl.Execute(tt, sb, ctx) c.Assert(err, qt.IsNil) + result := sb.String() + c.Assert(result, qt.Contains, ".True: TRUE") c.Assert(result, qt.Contains, ".TimeZero1: FALSE") c.Assert(result, qt.Contains, ".TimeZero2: FALSE") diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go index bbaf44ae2..2098732f6 100644 --- a/tpl/tplimpl/template_funcs.go +++ b/tpl/tplimpl/template_funcs.go @@ -16,7 +16,13 @@ package tplimpl import ( - "html/template" + "reflect" + "strings" + + "github.com/gohugoio/hugo/common/maps" + + template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" + texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" "github.com/gohugoio/hugo/deps" @@ -49,7 +55,55 @@ import ( _ "github.com/gohugoio/hugo/tpl/urls" ) +var _ texttemplate.ExecHelper = (*templateExecHelper)(nil) +var zero reflect.Value + +type templateExecHelper struct { + funcs map[string]reflect.Value +} + +func (t *templateExecHelper) GetFunc(name string) (reflect.Value, bool) { + if fn, found := t.funcs[name]; found { + return fn, true + } + return zero, false +} + +func (t *templateExecHelper) GetMapValue(receiver, key reflect.Value) (reflect.Value, bool) { + if params, ok := receiver.Interface().(maps.Params); ok { + // Case insensitive. + keystr := strings.ToLower(key.String()) + v, found := params[keystr] + if !found { + return zero, false + } + return reflect.ValueOf(v), true + } + + v := receiver.MapIndex(key) + + return v, v.IsValid() +} + +func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) { + funcs := createFuncMap(d) + funcsv := make(map[string]reflect.Value) + + for k, v := range funcs { + funcsv[k] = reflect.ValueOf(v) + } + + exeHelper := &templateExecHelper{ + funcs: funcsv, + } + + return texttemplate.NewExecuter( + exeHelper, + ), funcsv +} + func createFuncMap(d *deps.Deps) map[string]interface{} { + funcMap := template.FuncMap{} // Merge the namespace funcs @@ -71,10 +125,12 @@ func createFuncMap(d *deps.Deps) map[string]interface{} { } + if d.OverloadedTemplateFuncs != nil { + for k, v := range d.OverloadedTemplateFuncs { + funcMap[k] = v + } + } + return funcMap } -func (t *templateFuncster) initFuncMap(funcMap template.FuncMap) { - t.funcMap = funcMap - t.Tmpl.(*templateHandler).setFuncs(funcMap) -} diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go index 10fbc2375..6ca9de4da 100644 --- a/tpl/tplimpl/template_funcs_test.go +++ b/tpl/tplimpl/template_funcs_test.go @@ -119,7 +119,7 @@ func TestTemplateFuncsExamples(t *testing.T) { for _, mm := range ns.MethodMappings { for i, example := range mm.Examples { in, expected := example[0], example[1] - d.WithTemplate = func(templ tpl.TemplateHandler) error { + d.WithTemplate = func(templ tpl.TemplateManager) error { c.Assert(templ.AddTemplate("test", in), qt.IsNil) c.Assert(templ.AddTemplate("partials/header.html", "Hugo Rocks!"), qt.IsNil) return nil @@ -128,7 +128,7 @@ func TestTemplateFuncsExamples(t *testing.T) { var b bytes.Buffer templ, _ := d.Tmpl.Lookup("test") - c.Assert(templ.Execute(&b, &data), qt.IsNil) + c.Assert(d.Tmpl.Execute(templ, &b, &data), qt.IsNil) if b.String() != expected { t.Fatalf("%s[%d]: got %q expected %q", ns.Name, i, b.String(), expected) } @@ -154,7 +154,7 @@ func TestPartialCached(t *testing.T) { config := newDepsConfig(v) - config.WithTemplate = func(templ tpl.TemplateHandler) error { + config.WithTemplate = func(templ tpl.TemplateManager) error { err := templ.AddTemplate("partials/"+name, partial) if err != nil { return err @@ -208,7 +208,7 @@ func BenchmarkPartialCached(b *testing.B) { func doBenchmarkPartial(b *testing.B, f func(ns *partials.Namespace) error) { c := qt.New(b) config := newDepsConfig(viper.New()) - config.WithTemplate = func(templ tpl.TemplateHandler) error { + config.WithTemplate = func(templ tpl.TemplateManager) error { err := templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`) if err != nil { return err diff --git a/tpl/tplimpl/template_info_test.go b/tpl/tplimpl/template_info_test.go index c96e82d06..6841b4c47 100644 --- a/tpl/tplimpl/template_info_test.go +++ b/tpl/tplimpl/template_info_test.go @@ -24,7 +24,7 @@ import ( func TestTemplateInfoShortcode(t *testing.T) { c := qt.New(t) d := newD(c) - h := d.Tmpl.(tpl.TemplateHandler) + h := d.Tmpl.(tpl.TemplateManager) c.Assert(h.AddTemplate("shortcodes/mytemplate.html", ` {{ .Inner }}