diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-12-10 19:56:44 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-12-12 10:04:35 +0100 |
commit | a03c631c420a03f9d90699abdf9be7e4fca0ff61 (patch) | |
tree | fcc245c75aa6cc6dc5be40a614700b6aca26c84f | |
parent | 167c01530bb295c8b8d35921eb27ffa5bee76dfe (diff) |
Rework template handling for function and map lookups
This is a big commit, but it deletes lots of code and simplifies a lot.
* Resolving the template funcs at execution time means we don't have to create template clones per site
* Having a custom map resolver means that we can remove the AST lower case transformation for the special lower case Params map
Not only is the above easier to reason about, it's also faster, especially if you have more than one language, as in the benchmark below:
```
name old time/op new time/op delta
SiteNew/Deep_content_tree-16 53.7ms ± 0% 48.1ms ± 2% -10.38% (p=0.029 n=4+4)
name old alloc/op new alloc/op delta
SiteNew/Deep_content_tree-16 41.0MB ± 0% 36.8MB ± 0% -10.26% (p=0.029 n=4+4)
name old allocs/op new allocs/op delta
SiteNew/Deep_content_tree-16 481k ± 0% 410k ± 0% -14.66% (p=0.029 n=4+4)
```
This should be even better if you also have lots of templates.
Closes #6594
41 files changed, 1103 insertions, 1807 deletions
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 = "<!DOCTYPE html><html><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta name=\"robots\" content=\"noindex\"><meta charset=\"utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>" - aliasXHtml = "<!DOCTYPE html><html xmlns=\"http://www.w3.org/1999/xhtml\"><head><title>{{ .Permalink }}</title><link rel=\"canonical\" href=\"{{ .Permalink }}\"/><meta name=\"robots\" content=\"noindex\"><meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\" /><meta http-equiv=\"refresh\" content=\"0; url={{ .Permalink }}\" /></head></html>" -) - 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 != "<p>The <a href=\" |