From e625088ef5a970388ad50e464e87db56b358dac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 27 Nov 2019 13:42:36 +0100 Subject: [PATCH] Add render template hooks for links and images This commit also * revises the change detection for templates used by content files in server mode. * Adds a Page.RenderString method Fixes #6545 Fixes #4663 Closes #6043 --- docs/content/en/functions/RenderString.md | 37 ++ .../getting-started/configuration-markup.md | 59 +++ helpers/content.go | 4 +- hugolib/content_render_hooks_test.go | 244 +++++++++++++ hugolib/filesystems/basefs.go | 49 ++- hugolib/hugo_modules_test.go | 3 + hugolib/hugo_sites.go | 39 +- hugolib/hugo_sites_build.go | 2 +- hugolib/hugo_sites_build_test.go | 16 + hugolib/page.go | 184 +++++++++- hugolib/page__content.go | 3 +- hugolib/page__meta.go | 38 +- hugolib/page__new.go | 24 +- hugolib/page__output.go | 69 ++-- hugolib/page__per_output.go | 339 ++++++++++-------- hugolib/page_test.go | 7 +- hugolib/page_unwrap_test.go | 1 + hugolib/pagebundler_test.go | 1 + hugolib/pagecollections.go | 10 - hugolib/shortcode.go | 19 +- hugolib/shortcode_page.go | 19 + hugolib/shortcode_test.go | 7 +- hugolib/site.go | 118 +++--- hugolib/site_benchmark_new_test.go | 30 ++ hugolib/template_test.go | 87 +++++ hugolib/testhelpers_test.go | 9 +- identity/identity.go | 131 +++++++ identity/identity_test.go | 42 +++ markup/asciidoc/convert.go | 5 + markup/blackfriday/convert.go | 5 + markup/converter/converter.go | 13 +- markup/converter/hooks/hooks.go | 57 +++ markup/goldmark/convert.go | 116 ++++-- markup/goldmark/convert_test.go | 15 +- markup/goldmark/render_link.go | 208 +++++++++++ markup/mmark/convert.go | 5 + markup/org/convert.go | 6 + markup/pandoc/convert.go | 5 + markup/rst/convert.go | 5 + output/layout.go | 34 +- output/layout_test.go | 3 + resources/page/page.go | 5 +- resources/page/page_nop.go | 8 +- resources/page/testhelpers_test.go | 6 +- scripts/fork_go_templates/main.go | 1 + .../go_templates/texttemplate/exec.go | 2 +- .../texttemplate/hugo_template.go | 104 +++++- .../texttemplate/hugo_template_test.go | 22 +- tpl/partials/partials.go | 6 +- tpl/template.go | 39 +- tpl/template_info.go | 52 ++- tpl/tplimpl/shortcodes.go | 4 +- tpl/tplimpl/shortcodes_test.go | 8 +- tpl/tplimpl/template.go | 175 +++++---- tpl/tplimpl/templateProvider.go | 9 +- tpl/tplimpl/template_ast_transformers.go | 170 ++++++--- tpl/tplimpl/template_ast_transformers_test.go | 66 ++-- tpl/tplimpl/template_funcs.go | 24 +- tpl/tplimpl/template_info_test.go | 7 +- 59 files changed, 2234 insertions(+), 542 deletions(-) create mode 100644 docs/content/en/functions/RenderString.md create mode 100644 hugolib/content_render_hooks_test.go create mode 100644 identity/identity.go create mode 100644 identity/identity_test.go create mode 100644 markup/converter/hooks/hooks.go create mode 100644 markup/goldmark/render_link.go diff --git a/docs/content/en/functions/RenderString.md b/docs/content/en/functions/RenderString.md new file mode 100644 index 000000000..61f5d6417 --- /dev/null +++ b/docs/content/en/functions/RenderString.md @@ -0,0 +1,37 @@ +--- +title: .RenderString +description: "Renders markup to HTML." +godocref: +date: 2019-12-18 +categories: [functions] +menu: + docs: + parent: "functions" +keywords: [markdown,goldmark,render] +signature: [".RenderString MARKUP"] +--- + +{{< new-in "0.62.0" >}} + +`.RenderString` is a method on `Page` that renders some markup to HTML using the content renderer defined for that page (if not set in the options). + +The method takes an optional map argument with these options: + +display ("inline") +: `inline` or `block`. If `inline` (default), surrounding ´

` on short snippets will be trimmed. + +markup (defaults to the Page's markup) +: See identifiers in [List of content formats](/content-management/formats/#list-of-content-formats). + +Some examples: + +```go-html-template +{{ $optBlock := dict "display" "block" }} +{{ $optOrg := dict "markup" "org" }} +{{ "**Bold Markdown**" | $p.RenderString }} +{{ "**Bold Block Markdown**" | $p.RenderString $optBlock }} +{{ "/italic org mode/" | $p.RenderString $optOrg }}:REND +``` + + +**Note** that this method is more powerful than the similar [markdownify](functions/markdownify/) function as it also supports [Render Hooks](/getting-started/configuration-markup/#markdown-render-hooks) and it has options to render other markup formats. \ No newline at end of file diff --git a/docs/content/en/getting-started/configuration-markup.md b/docs/content/en/getting-started/configuration-markup.md index ff0095024..f254b9012 100644 --- a/docs/content/en/getting-started/configuration-markup.md +++ b/docs/content/en/getting-started/configuration-markup.md @@ -74,3 +74,62 @@ endLevel ordered : Whether or not to generate an ordered list instead of an unordered list. + + +## Markdown Render Hooks + +{{< new-in "0.62.0" >}} + +Note that this is only supported with the [Goldmark](#goldmark) renderer. + +These Render Hooks allow custom templates to render links and images from markdown. + +You can do this by creating templates with base names `render-link` and/or `render-image` inside `layouts/_default`. + +You can define [Output Format](/templates/output-formats) specific templates if needed.[^1] Your `layouts` folder may then look like this: + +```bash +layouts +└── _default + └── markup + ├── render-image.html + ├── render-image.rss.xml + └── render-link.html +``` + +Some use cases for the above: + +* Resolve link references using `.GetPage`. This would make links more portable as you could translate `./my-post.md` (and similar constructs that would work on GitHub) into `/blog/2019/01/01/my-post/` etc. +* Add `target=blank` to external links. +* Resolve (look in the page bundle, inside `/assets` etc.) and [transform](/content-management/image-processing) images. + + +[^1]: It's currently only possible to have one set of render hook templates, e.g. not per `Type` or `Section`. We may consider that in a future version. + +### Render Hook Templates + +Both `render-link` and `render-image` templates will receive this context: + +Page +: The [Page](/variables/page/) being rendered. + +Destination +: The URL. + +Title +: The title attribute. + +Text +: The link text. + +A Markdown example for a inline-style link with title: + +```md +[Text](https://www.gohugo.io "Title") +``` + +A very simple template example given the above: + +{{< code file="layouts/_default/render-link.html" >}} +{{ .Text }}{{ with .Page }} (in page {{ .Title }}){{ end }}" +{{< /code >}} diff --git a/helpers/content.go b/helpers/content.go index 4dc4cd413..1c780fefe 100644 --- a/helpers/content.go +++ b/helpers/content.go @@ -25,13 +25,14 @@ import ( "github.com/gohugoio/hugo/common/loggers" + "github.com/spf13/afero" + "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup" bp "github.com/gohugoio/hugo/bufferpool" "github.com/gohugoio/hugo/config" - "github.com/spf13/afero" "strings" ) @@ -78,6 +79,7 @@ func NewContentSpec(cfg config.Provider, logger *loggers.Logger, contentFs afero ContentFs: contentFs, Logger: logger, }) + if err != nil { return nil, err } diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go new file mode 100644 index 000000000..aa697220d --- /dev/null +++ b/hugolib/content_render_hooks_test.go @@ -0,0 +1,244 @@ +// 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 requiredF 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 "testing" + +func TestRenderHooks(t *testing.T) { + config := ` +baseURL="https://example.org" +workingDir="/mywork" +` + b := newTestSitesBuilder(t).WithWorkingDir("/mywork").WithConfigFile("toml", config).Running() + b.WithTemplatesAdded("_default/single.html", `{{ .Content }}`) + b.WithTemplatesAdded("shortcodes/myshortcode1.html", `{{ partial "mypartial1" }}`) + b.WithTemplatesAdded("shortcodes/myshortcode2.html", `{{ partial "mypartial2" }}`) + b.WithTemplatesAdded("shortcodes/myshortcode3.html", `SHORT3|`) + b.WithTemplatesAdded("shortcodes/myshortcode4.html", ` +
+{{ .Inner | markdownify }} +
+`) + b.WithTemplatesAdded("shortcodes/myshortcode5.html", ` +Inner Inline: {{ .Inner | .Page.RenderString }} +Inner Block: {{ .Inner | .Page.RenderString (dict "display" "block" ) }} +`) + + b.WithTemplatesAdded("shortcodes/myshortcode6.html", `.Render: {{ .Page.Render "myrender" }}`) + b.WithTemplatesAdded("partials/mypartial1.html", `PARTIAL1`) + b.WithTemplatesAdded("partials/mypartial2.html", `PARTIAL2 {{ partial "mypartial3.html" }}`) + b.WithTemplatesAdded("partials/mypartial3.html", `PARTIAL3`) + b.WithTemplatesAdded("partials/mypartial4.html", `PARTIAL4`) + b.WithTemplatesAdded("customview/myrender.html", `myrender: {{ .Title }}|P4: {{ partial "mypartial4" }}`) + b.WithTemplatesAdded("_default/_markup/render-link.html", `{{ with .Page }}{{ .Title }}{{ end }}|{{ .Destination | safeURL }}|Title: {{ .Title | safeHTML }}|Text: {{ .Text | safeHTML }}|END`) + b.WithTemplatesAdded("docs/_markup/render-link.html", `Link docs section: {{ .Text | safeHTML }}|END`) + b.WithTemplatesAdded("_default/_markup/render-image.html", `IMAGE: {{ .Page.Title }}||{{ .Destination | safeURL }}|Title: {{ .Title | safeHTML }}|Text: {{ .Text | safeHTML }}|END`) + + b.WithContent("customview/p1.md", `--- +title: Custom View +--- + +{{< myshortcode6 >}} + + `, "blog/p1.md", `--- +title: Cool Page +--- + +[First Link](https://www.google.com "Google's Homepage") + +{{< myshortcode3 >}} + +[Second Link](https://www.google.com "Google's Homepage") + +Image: + +![Drag Racing](/images/Dragster.jpg "image title") + + +`, "blog/p2.md", `--- +title: Cool Page2 +layout: mylayout +--- + +{{< myshortcode1 >}} + +[Some Text](https://www.google.com "Google's Homepage") + + + +`, "blog/p3.md", `--- +title: Cool Page3 +--- + +{{< myshortcode2 >}} + + +`, "docs/docs1.md", `--- +title: Docs 1 +--- + + +[Docs 1](https://www.google.com "Google's Homepage") + + +`, "blog/p4.md", `--- +title: Cool Page With Image +--- + +Image: + +![Drag Racing](/images/Dragster.jpg "image title") + + +`, "blog/p5.md", `--- +title: Cool Page With Markdownify +--- + +{{< myshortcode4 >}} +Inner Link: [Inner Link](https://www.google.com "Google's Homepage") +{{< /myshortcode4 >}} + +`, "blog/p6.md", `--- +title: With RenderString +--- + +{{< myshortcode5 >}}Inner Link: [Inner Link](https://www.gohugo.io "Hugo's Homepage"){{< /myshortcode5 >}} + +`) + b.Build(BuildCfg{}) + b.AssertFileContent("public/blog/p1/index.html", ` +

Cool Page|https://www.google.com|Title: Google's Homepage|Text: First Link|END

+Text: Second +SHORT3| +

IMAGE: Cool Page||/images/Dragster.jpg|Title: image title|Text: Drag Racing|END

+`) + + b.AssertFileContent("public/customview/p1/index.html", `.Render: myrender: Custom View|P4: PARTIAL4`) + b.AssertFileContent("public/blog/p2/index.html", `PARTIAL`) + b.AssertFileContent("public/blog/p3/index.html", `PARTIAL3`) + // We may add type template support later, keep this for then. b.AssertFileContent("public/docs/docs1/index.html", `Link docs section: Docs 1|END`) + b.AssertFileContent("public/blog/p4/index.html", `

IMAGE: Cool Page With Image||/images/Dragster.jpg|Title: image title|Text: Drag Racing|END

`) + // The regular markdownify func currently gets regular links. + b.AssertFileContent("public/blog/p5/index.html", "Inner Link: Inner Link\n") + + b.AssertFileContent("public/blog/p6/index.html", + "Inner Inline: Inner Link: With RenderString|https://www.gohugo.io|Title: Hugo's Homepage|Text: Inner Link|END", + "Inner Block:

Inner Link: With RenderString|https://www.gohugo.io|Title: Hugo's Homepage|Text: Inner Link|END

", + ) + + b.EditFiles( + "layouts/_default/_markup/render-link.html", `EDITED: {{ .Destination | safeURL }}|`, + "layouts/_default/_markup/render-image.html", `IMAGE EDITED: {{ .Destination | safeURL }}|`, + "layouts/docs/_markup/render-link.html", `DOCS EDITED: {{ .Destination | safeURL }}|`, + "layouts/partials/mypartial1.html", `PARTIAL1_EDITED`, + "layouts/partials/mypartial3.html", `PARTIAL3_EDITED`, + "layouts/partials/mypartial4.html", `PARTIAL4_EDITED`, + "layouts/shortcodes/myshortcode3.html", `SHORT3_EDITED|`, + ) + + b.Build(BuildCfg{}) + b.AssertFileContent("public/customview/p1/index.html", `.Render: myrender: Custom View|P4: PARTIAL4_EDITED`) + b.AssertFileContent("public/blog/p1/index.html", `

EDITED: https://www.google.com|

`, "SHORT3_EDITED|") + b.AssertFileContent("public/blog/p2/index.html", `PARTIAL1_EDITED`) + b.AssertFileContent("public/blog/p3/index.html", `PARTIAL3_EDITED`) + // We may add type template support later, keep this for then. b.AssertFileContent("public/docs/docs1/index.html", `DOCS EDITED: https://www.google.com|

`) + b.AssertFileContent("public/blog/p4/index.html", `IMAGE EDITED: /images/Dragster.jpg|`) + b.AssertFileContent("public/blog/p6/index.html", "

Inner Link: EDITED: https://www.gohugo.io|

") + +} + +func TestRenderHooksRSS(t *testing.T) { + + b := newTestSitesBuilder(t) + + b.WithTemplates("index.html", ` +{{ $p := site.GetPage "p1.md" }} + +P1: {{ $p.Content }} + + `, "index.xml", ` + +{{ $p2 := site.GetPage "p2.md" }} +{{ $p3 := site.GetPage "p3.md" }} + +P2: {{ $p2.Content }} +P3: {{ $p3.Content }} + + + `, + "_default/_markup/render-link.html", `html-link: {{ .Destination | safeURL }}|`, + "_default/_markup/render-link.rss.xml", `xml-link: {{ .Destination | safeURL }}|`, + ) + + b.WithContent("p1.md", `--- +title: "p1" +--- +P1. [I'm an inline-style link](https://www.gohugo.io) + + +`, "p2.md", `--- +title: "p2" +--- +P1. [I'm an inline-style link](https://www.bep.is) + + +`, + "p3.md", `--- +title: "p2" +outputs: ["rss"] +--- +P3. [I'm an inline-style link](https://www.example.org) + +`, + ) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", "P1:

P1. html-link: https://www.gohugo.io|

") + b.AssertFileContent("public/index.xml", ` +P2:

P1. xml-link: https://www.bep.is|

+P3:

P3. xml-link: https://www.example.org|

+`) + +} + +func TestRenderString(t *testing.T) { + + b := newTestSitesBuilder(t) + + b.WithTemplates("index.html", ` +{{ $p := site.GetPage "p1.md" }} +{{ $optBlock := dict "display" "block" }} +{{ $optOrg := dict "markup" "org" }} +RSTART:{{ "**Bold Markdown**" | $p.RenderString }}:REND +RSTART:{{ "**Bold Block Markdown**" | $p.RenderString $optBlock }}:REND +RSTART:{{ "/italic org mode/" | $p.RenderString $optOrg }}:REND + +`) + + b.WithContent("p1.md", `--- +title: "p1" +--- +`, + ) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", ` +RSTART:Bold Markdown:REND +RSTART:

Bold Block Markdown

+RSTART:italic org mode:REND +`) + +} diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go index de6baa130..cdc39ce61 100644 --- a/hugolib/filesystems/basefs.go +++ b/hugolib/filesystems/basefs.go @@ -126,10 +126,28 @@ type SourceFilesystems struct { StaticDirs []hugofs.FileMetaInfo } +// FileSystems returns the FileSystems relevant for the change detection +// in server mode. +// Note: This does currently not return any static fs. +func (s *SourceFilesystems) FileSystems() []*SourceFilesystem { + return []*SourceFilesystem{ + s.Content, + s.Data, + s.I18n, + s.Layouts, + s.Archetypes, + // TODO(bep) static + } + +} + // A SourceFilesystem holds the filesystem for a given source type in Hugo (data, // i18n, layouts, static) and additional metadata to be able to use that filesystem // in server mode. type SourceFilesystem struct { + // Name matches one in files.ComponentFolders + Name string + // This is a virtual composite filesystem. It expects path relative to a context. Fs afero.Fs @@ -275,6 +293,19 @@ func (d *SourceFilesystem) Contains(filename string) bool { return false } +// Path returns the relative path to the given filename if it is a member of +// of the current filesystem, an empty string if not. +func (d *SourceFilesystem) Path(filename string) string { + for _, dir := range d.Dirs { + meta := dir.Meta() + if strings.HasPrefix(filename, meta.Filename()) { + p := strings.TrimPrefix(strings.TrimPrefix(filename, meta.Filename()), filePathSeparator) + return p + } + } + return "" +} + // RealDirs gets a list of absolute paths to directories starting from the given // path. func (d *SourceFilesystem) RealDirs(from string) []string { @@ -349,12 +380,14 @@ func newSourceFilesystemsBuilder(p *paths.Paths, logger *loggers.Logger, b *Base return &sourceFilesystemsBuilder{p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs, result: &SourceFilesystems{}} } -func (b *sourceFilesystemsBuilder) newSourceFilesystem(fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem { +func (b *sourceFilesystemsBuilder) newSourceFilesystem(name string, fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem { return &SourceFilesystem{ + Name: name, Fs: fs, Dirs: dirs, } } + func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) { if b.theBigFs == nil { @@ -369,12 +402,12 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) { createView := func(componentID string) *SourceFilesystem { if b.theBigFs == nil || b.theBigFs.overlayMounts == nil { - return b.newSourceFilesystem(hugofs.NoOpFs, nil) + return b.newSourceFilesystem(componentID, hugofs.NoOpFs, nil) } dirs := b.theBigFs.overlayDirs[componentID] - return b.newSourceFilesystem(afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs) + return b.newSourceFilesystem(componentID, afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs) } @@ -392,14 +425,14 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) { return nil, err } - b.result.Data = b.newSourceFilesystem(dataFs, dataDirs) + b.result.Data = b.newSourceFilesystem(files.ComponentFolderData, dataFs, dataDirs) i18nDirs := b.theBigFs.overlayDirs[files.ComponentFolderI18n] i18nFs, err := hugofs.NewSliceFs(i18nDirs...) if err != nil { return nil, err } - b.result.I18n = b.newSourceFilesystem(i18nFs, i18nDirs) + b.result.I18n = b.newSourceFilesystem(files.ComponentFolderI18n, i18nFs, i18nDirs) contentDirs := b.theBigFs.overlayDirs[files.ComponentFolderContent] contentBfs := afero.NewBasePathFs(b.theBigFs.overlayMountsContent, files.ComponentFolderContent) @@ -409,7 +442,7 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) { return nil, errors.Wrap(err, "create content filesystem") } - b.result.Content = b.newSourceFilesystem(contentFs, contentDirs) + b.result.Content = b.newSourceFilesystem(files.ComponentFolderContent, contentFs, contentDirs) b.result.Work = afero.NewReadOnlyFs(b.theBigFs.overlayFull) @@ -421,13 +454,13 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) { if b.theBigFs.staticPerLanguage != nil { // Multihost mode for k, v := range b.theBigFs.staticPerLanguage { - sfs := b.newSourceFilesystem(v, b.result.StaticDirs) + sfs := b.newSourceFilesystem(files.ComponentFolderStatic, v, b.result.StaticDirs) sfs.PublishFolder = k ms[k] = sfs } } else { bfs := afero.NewBasePathFs(b.theBigFs.overlayMountsStatic, files.ComponentFolderStatic) - ms[""] = b.newSourceFilesystem(bfs, b.result.StaticDirs) + ms[""] = b.newSourceFilesystem(files.ComponentFolderStatic, bfs, b.result.StaticDirs) } return b.result, nil diff --git a/hugolib/hugo_modules_test.go b/hugolib/hugo_modules_test.go index 40185e051..900443275 100644 --- a/hugolib/hugo_modules_test.go +++ b/hugolib/hugo_modules_test.go @@ -40,6 +40,9 @@ import ( // TODO(bep) this fails when testmodBuilder is also building ... func TestHugoModules(t *testing.T) { + if !isCI() { + t.Skip("skip (relative) long running modules test when running locally") + } t.Parallel() if !isCI() || hugo.GoMinorVersion() < 12 { diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index c71dcaa59..526f39fca 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -20,6 +20,8 @@ import ( "strings" "sync" + "github.com/gohugoio/hugo/identity" + radix "github.com/armon/go-radix" "github.com/gohugoio/hugo/output" @@ -411,7 +413,6 @@ func applyDeps(cfg deps.DepsCfg, sites ...*Site) error { } d.OutputFormatsConfig = s.outputFormatsConfig } - } return nil @@ -806,12 +807,40 @@ func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Page return h.Sites[0].findPagesByKindIn(kind, inPages) } -func (h *HugoSites) findPagesByShortcode(shortcode string) page.Pages { - var pages page.Pages +func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) { + for _, s := range h.Sites { - pages = append(pages, s.findPagesByShortcode(shortcode)...) + PAGES: + for _, p := range s.rawAllPages { + OUTPUTS: + for _, po := range p.pageOutputs { + if po.cp == nil { + continue + } + for id, _ := range idset { + if po.cp.dependencyTracker.Search(id) != nil { + po.cp.Reset() + p.forceRender = true + continue OUTPUTS + } + } + } + + for _, s := range p.shortcodeState.shortcodes { + for id, _ := range idset { + if idm, ok := s.info.(identity.Manager); ok && idm.Search(id) != nil { + for _, po := range p.pageOutputs { + if po.cp != nil { + po.cp.Reset() + } + } + p.forceRender = true + continue PAGES + } + } + } + } } - return pages } // Used in partial reloading to determine if the change is in a bundle. diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index a70a19e7c..d749ff581 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -71,7 +71,7 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { if conf.whatChanged == nil { // Assume everything has changed - conf.whatChanged = &whatChanged{source: true, other: true} + conf.whatChanged = &whatChanged{source: true} } var prepareErr error diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go index feee85910..d62d6d519 100644 --- a/hugolib/hugo_sites_build_test.go +++ b/hugolib/hugo_sites_build_test.go @@ -1459,3 +1459,19 @@ other = %q return &multiSiteTestBuilder{sitesBuilder: b, configFormat: configFormat, config: config, configData: configData} } + +func TestRebuildOnAssetChange(t *testing.T) { + b := newTestSitesBuilder(t).Running() + b.WithTemplatesAdded("index.html", ` +{{ (resources.Get "data.json").Content }} +`) + b.WithSourceFile("assets/data.json", "orig data") + + b.Build(BuildCfg{}) + b.AssertFileContent("public/index.html", `orig data`) + + b.EditFiles("assets/data.json", "changed data") + + b.Build(BuildCfg{}) + b.AssertFileContent("public/index.html", `changed data`) +} diff --git a/hugolib/page.go b/hugolib/page.go index 56202f5e0..fb3b597be 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -23,6 +23,12 @@ import ( "sort" "strings" + "github.com/mitchellh/mapstructure" + + "github.com/gohugoio/hugo/tpl" + + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/common/maps" @@ -43,9 +49,11 @@ import ( "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/source" + "github.com/spf13/cast" "github.com/gohugoio/hugo/common/collections" "github.com/gohugoio/hugo/common/text" + "github.com/gohugoio/hugo/markup/converter/hooks" "github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" @@ -59,7 +67,11 @@ var ( var ( pageTypesProvider = resource.NewResourceTypesProvider(media.OctetType, pageResourceType) - nopPageOutput = &pageOutput{pagePerOutputProviders: nopPagePerOutput} + nopPageOutput = &pageOutput{ + pagePerOutputProviders: nopPagePerOutput, + ContentProvider: page.NopPage, + TableOfContentsProvider: page.NopPage, + } ) // pageContext provides contextual information about this page, for error @@ -317,6 +329,54 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error { return nil } +func (p *pageState) createRenderHooks(f output.Format) (*hooks.Render, error) { + + layoutDescriptor := p.getLayoutDescriptor() + layoutDescriptor.RenderingHook = true + layoutDescriptor.LayoutOverride = false + layoutDescriptor.Layout = "" + + layoutDescriptor.Kind = "render-link" + linkLayouts, err := p.s.layoutHandler.For(layoutDescriptor, f) + if err != nil { + return nil, err + } + + layoutDescriptor.Kind = "render-image" + imageLayouts, err := p.s.layoutHandler.For(layoutDescriptor, f) + if err != nil { + return nil, err + } + + if linkLayouts == nil && imageLayouts == nil { + return nil, nil + } + + var linkRenderer hooks.LinkRenderer + var imageRenderer hooks.LinkRenderer + + if templ, found := p.s.lookupTemplate(linkLayouts...); found { + linkRenderer = contentLinkRenderer{ + templateHandler: p.s.Tmpl, + Provider: templ.(tpl.Info), + templ: templ, + } + } + + if templ, found := p.s.lookupTemplate(imageLayouts...); found { + imageRenderer = contentLinkRenderer{ + templateHandler: p.s.Tmpl, + Provider: templ.(tpl.Info), + templ: templ, + } + } + + return &hooks.Render{ + LinkRenderer: linkRenderer, + ImageRenderer: imageRenderer, + }, nil +} + func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor { p.layoutDescriptorInit.Do(func() { var section string @@ -464,11 +524,86 @@ func (p *pageState) AlternativeOutputFormats() page.OutputFormats { return o } -func (p *pageState) Render(layout ...string) template.HTML { +type renderStringOpts struct { + Display string + Markup string +} + +var defualtRenderStringOpts = renderStringOpts{ + Display: "inline", + Markup: "", // Will inherit the page's value when not set. +} + +func (p *pageState) RenderString(args ...interface{}) (template.HTML, error) { + if len(args) < 1 || len(args) > 2 { + return "", errors.New("want 1 or 2 arguments") + } + + var s string + opts := defualtRenderStringOpts + sidx := 1 + + if len(args) == 1 { + sidx = 0 + } else { + m, ok := args[0].(map[string]interface{}) + if !ok { + return "", errors.New("first argument must be a map") + } + + if err := mapstructure.WeakDecode(m, &opts); err != nil { + return "", errors.WithMessage(err, "failed to decode options") + } + } + + var err error + s, err = cast.ToStringE(args[sidx]) + if err != nil { + return "", err + } + + conv := p.getContentConverter() + if opts.Markup != "" && opts.Markup != p.m.markup { + var err error + // TODO(bep) consider cache + conv, err = p.m.newContentConverter(p, opts.Markup, nil) + if err != nil { + return "", p.wrapError(err) + } + } + + c, err := p.pageOutput.cp.renderContentWithConverter(conv, []byte(s), false) + if err != nil { + return "", p.wrapError(err) + } + + b := c.Bytes() + + if opts.Display == "inline" { + // We may have to rethink this in the future when we get other + // renderers. + b = p.s.ContentSpec.TrimShortHTML(b) + } + + return template.HTML(string(b)), nil +} + +func (p *pageState) addDependency(dep identity.Provider) { + if !p.s.running() || p.pageOutput.cp == nil { + return + } + p.pageOutput.cp.dependencyTracker.Add(dep) +} + +func (p *pageState) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) { + p.addDependency(info) + return p.Render(layout...) +} + +func (p *pageState) Render(layout ...string) (template.HTML, error) { l, err := p.getLayouts(layout...) if err != nil { - p.s.SendError(p.wrapError(errors.Errorf(".Render: failed to resolve layout %v", layout))) - return "" + return "", p.wrapError(errors.Errorf("failed to resolve layout %v", layout)) } for _, layout := range l { @@ -479,17 +614,18 @@ func (p *pageState) Render(layout ...string) template.HTML { // We default to good old HTML. templ, _ = p.s.Tmpl.Lookup(layout + ".html") } + if templ != nil { + p.addDependency(templ.(tpl.Info)) 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 "" + return "", p.wrapError(errors.Wrapf(err, "failed to execute template %q v", layout)) } - return template.HTML(res) + return template.HTML(res), nil } } - return "" + return "", nil } @@ -745,15 +881,33 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error { p.pageOutput.paginator.reset() } - if idx > 0 { - // Check if we can reuse content from one of the previous formats. - for i := idx - 1; i >= 0; i-- { - po := p.pageOutputs[i] - if po.cp != nil && po.cp.reuse { - p.pageOutput.cp = po.cp - break + if isRenderingSite { + cp := p.pageOutput.cp + if cp == nil { + + // Look for content to reuse. + for i := 0; i < len(p.pageOutputs); i++ { + if i == idx { + continue + } + po := p.pageOutputs[i] + + if po.cp != nil && po.cp.reuse { + cp = po.cp + break + } } } + + if cp == nil { + var err error + cp, err = newPageContentOutput(p, p.pageOutput) + if err != nil { + return err + } + } + p.pageOutput.initContentProvider(cp) + p.pageOutput.cp = cp } for _, r := range p.Resources().ByType(pageResourceType) { diff --git a/hugolib/page__content.go b/hugolib/page__content.go index 1919fb171..013ab3072 100644 --- a/hugolib/page__content.go +++ b/hugolib/page__content.go @@ -30,8 +30,7 @@ var ( type pageContent struct { renderable bool selfLayout string - - truncated bool + truncated bool cmap *pageContentMap diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index 1fc69c218..9f3e1687a 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -592,7 +592,7 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte return nil } -func (p *pageMeta) applyDefaultValues() error { +func (p *pageMeta) applyDefaultValues(ps *pageState) error { if p.markup == "" { if !p.File().IsZero() { // Fall back to file extension @@ -651,27 +651,39 @@ func (p *pageMeta) applyDefaultValues() error { markup = "markdown" } - cp := p.s.ContentSpec.Converters.Get(markup) - if cp == nil { - return errors.Errorf("no content renderer found for markup %q", p.markup) - } - - cpp, err := cp.New(converter.DocumentContext{ - DocumentID: p.f.UniqueID(), - DocumentName: p.f.Path(), - ConfigOverrides: renderingConfigOverrides, - }) - + cp, err := p.newContentConverter(ps, markup, renderingConfigOverrides) if err != nil { return err } - p.contentConverter = cpp + p.contentConverter = cp } return nil } +func (p *pageMeta) newContentConverter(ps *pageState, markup string, renderingConfigOverrides map[string]interface{}) (converter.Converter, error) { + cp := p.s.ContentSpec.Converters.Get(markup) + if cp == nil { + return nil, errors.Errorf("no content renderer found for markup %q", p.markup) + } + + cpp, err := cp.New( + converter.DocumentContext{ + Document: newPageForRenderHook(ps), + DocumentID: p.f.UniqueID(), + DocumentName: p.f.Path(), + ConfigOverrides: renderingConfigOverrides, + }, + ) + + if err != nil { + return nil, err + } + + return cpp, nil +} + // The output formats this page will be rendered to. func (m *pageMeta) outputFormats() output.Formats { if len(m.configuredOutputFormats) > 0 { diff --git a/hugolib/page__new.go b/hugolib/page__new.go index 99bf305aa..d810c8df6 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -112,7 +112,7 @@ func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*page } } - if err := metaProvider.applyDefaultValues(); err != nil { + if err := metaProvider.applyDefaultValues(ps); err != nil { return err } @@ -134,7 +134,7 @@ func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*page } makeOut := func(f output.Format, render bool) *pageOutput { - return newPageOutput(nil, ps, pp, f, render) + return newPageOutput(ps, pp, f, render) } if ps.m.standalone { @@ -234,7 +234,7 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope return ps.wrapError(err) } - if err := metaProvider.applyDefaultValues(); err != nil { + if err := metaProvider.applyDefaultValues(ps); err != nil { return err } @@ -242,10 +242,6 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope } ps.init.Add(func() (interface{}, error) { - reuseContent := ps.renderable && !ps.shortcodeState.hasShortcodes() - - // Creates what's needed for each output format. - contentPerOutput := newPageContentOutput(ps) pp, err := newPagePaths(s, ps, metaProvider) if err != nil { @@ -264,18 +260,18 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope } _, render := outputFormatsForPage.GetByName(f.Name) - var contentProvider *pageContentOutput - if reuseContent && i > 0 { - contentProvider = ps.pageOutputs[0].cp - } else { - var err error - contentProvider, err = contentPerOutput(f) + po := newPageOutput(ps, pp, f, render) + + // Create a content provider for the first, + // we may be able to reuse it. + if i == 0 { + contentProvider, err := newPageContentOutput(ps, po) if err != nil { return nil, err } + po.initContentProvider(contentProvider) } - po := newPageOutput(contentProvider, ps, pp, f, render) ps.pageOutputs[i] = po created[f.Name] = po } diff --git a/hugolib/page__output.go b/hugolib/page__output.go index 764c46a93..183bf010d 100644 --- a/hugolib/page__output.go +++ b/hugolib/page__output.go @@ -14,13 +14,13 @@ package hugolib import ( + "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" ) func newPageOutput( - cp *pageContentOutput, // may be nil ps *pageState, pp pagePaths, f output.Format, @@ -45,36 +45,23 @@ func newPageOutput( paginatorProvider = pag } - var ( - contentProvider page.ContentProvider = page.NopPage - tableOfContentsProvider page.TableOfContentsProvider = page.NopPage - ) - - if cp != nil { - contentProvider = cp - tableOfContentsProvider = cp - } - providers := struct { - page.ContentProvider - page.TableOfContentsProvider page.PaginatorProvider resource.ResourceLinksProvider targetPather }{ - contentProvider, - tableOfContentsProvider, paginatorProvider, linksProvider, targetPathsProvider, } po := &pageOutput{ - f: f, - cp: cp, - pagePerOutputProviders: providers, - render: render, - paginator: pag, + f: f, + pagePerOutputProviders: providers, + ContentProvider: page.NopPage, + TableOfContentsProvider: page.NopPage, + render: render, + paginator: pag, } return po @@ -94,16 +81,54 @@ type pageOutput struct { // used in template(s). paginator *pagePaginator - // This interface provides the functionality that is specific for this + // These interface provides the functionality that is specific for this // output format. pagePerOutputProviders + page.ContentProvider + page.TableOfContentsProvider - // This may be nil. + // May be nil. cp *pageContentOutput } +func (o *pageOutput) initRenderHooks() error { + if o.cp == nil { + return nil + } + + ps := o.cp.p + + c := ps.getContentConverter() + if c == nil || !c.Supports(converter.FeatureRenderHooks) { + return nil + } + + h, err := ps.createRenderHooks(o.f) + if err != nil { + return err + } + if h == nil { + return nil + } + + o.cp.renderHooks = h + + return nil + +} + +func (p *pageOutput) initContentProvider(cp *pageContentOutput) { + if cp == nil { + return + } + p.ContentProvider = cp + p.TableOfContentsProvider = cp + p.cp = cp +} + func (p *pageOutput) enablePlaceholders() { if p.cp != nil { p.cp.enablePlaceholders() } + } diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index d3a32e15c..03448ba80 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -23,6 +23,10 @@ import ( "sync" "unicode/utf8" + "github.com/gohugoio/hugo/identity" + + "github.com/gohugoio/hugo/markup/converter/hooks" + "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/lazy" @@ -58,160 +62,182 @@ var ( } ) -func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutput, error) { +var pageContentOutputDependenciesID = identity.KeyValueIdentity{Key: "pageOutput", Value: "dependencies"} + +func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, error) { parent := p.init - return func(f output.Format) (*pageContentOutput, error) { - cp := &pageContentOutput{ - p: p, - f: f, - } + var dependencyTracker identity.Manager + if p.s.running() { + dependencyTracker = identity.NewManager(pageContentOutputDependenciesID) + } - initContent := func() (err error) { - if p.cmap == nil { - // Nothing to do. - return nil - } - defer func() { - // See https://github.com/gohugoio/hugo/issues/6210 - if r := recover(); r != nil { - err = fmt.Errorf("%s", r) - p.s.Log.ERROR.Printf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack())) - } - }() - - var hasVariants bool - - cp.contentPlaceholders, hasVariants, err = p.shortcodeState.renderShortcodesForPage(p, f) - if err != nil { - return err - } - - if p.render && !hasVariants { - // We can reuse this for the other output formats - cp.enableReuse() - } - - cp.workContent = p.contentToRender(cp.contentPlaceholders) - - isHTML := cp.p.m.markup == "html" - - if p.renderable { - if !isHTML { - r, err := cp.renderContent(cp.workContent) - if err != nil { - return err - } - cp.convertedResult = r - cp.workContent = r.Bytes() - - if _, ok := r.(converter.TableOfContentsProvider); !ok { - tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent) - cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents) - cp.workContent = tmpContent - } - } - - if cp.placeholdersEnabled { - // ToC was accessed via .Page.TableOfContents in the shortcode, - // at a time when the ToC wasn't ready. - cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents) - } - - if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled { - // There are one or more replacement tokens to be replaced. - cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders) - if err != nil { - return err - } - } - - if cp.p.source.hasSummaryDivider { - if isHTML { - src := p.source.parsed.Input() - - // Use the summary sections as they are provided by the user. - if p.source.posSummaryEnd != -1 { - cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd]) - } - - if cp.p.source.posBodyStart != -1 { - cp.workContent = src[cp.p.source.posBodyStart:] - } - - } else { - summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent) - if err != nil { - cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err) - } else { - cp.workContent = content - cp.summary = helpers.BytesToHTML(summary) - } - } - } else if cp.p.m.summary != "" { - b, err := cp.p.getContentConverter().Convert( - converter.RenderContext{ - Src: []byte(cp.p.m.summary), - }, - ) - - if err != nil { - return err - } - html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes()) - cp.summary = helpers.BytesToHTML(html) - } - } - - cp.content = helpers.BytesToHTML(cp.workContent) - - if !p.renderable { - err := cp.addSelfTemplate() - return err - } + cp := &pageContentOutput{ + dependencyTracker: dependencyTracker, + p: p, + f: po.f, + } + initContent := func() (err error) { + if p.cmap == nil { + // Nothing to do. return nil + } + defer func() { + // See https://github.com/gohugoio/hugo/issues/6210 + if r := recover(); r != nil { + err = fmt.Errorf("%s", r) + p.s.Log.ERROR.Printf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack())) + } + }() + if err := po.initRenderHooks(); err != nil { + return err } - // Recursive loops can only happen in content files with template code (shortcodes etc.) - // Avoid creating new goroutines if we don't have to. - needTimeout := !p.renderable || p.shortcodeState.hasShortcodes() + var hasShortcodeVariants bool - if needTimeout { - cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) { - return nil, initContent() - }) - } else { - cp.initMain = parent.Branch(func() (interface{}, error) { - return nil, initContent() - }) + f := po.f + cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f) + if err != nil { + return err } - cp.initPlain = cp.initMain.Branch(func() (interface{}, error) { - cp.plain = helpers.StripHTML(string(cp.content)) - cp.plainWords = strings.Fields(cp.plain) - cp.setWordCounts(p.m.isCJKLanguage) + enableReuse := !(hasShortcodeVariants || cp.renderHooksHaveVariants) - if err := cp.setAutoSummary(); err != nil { - return err, nil + if enableReuse { + // Reuse this for the other output formats. + // We may improve on this, but we really want to avoid re-rendering the content + // to all output formats. + // The current rule is that if you need output format-aware shortcodes or + // content rendering hooks, create a output format-specific template, e.g. + // myshortcode.amp.html. + cp.enableReuse() + } + + cp.workContent = p.contentToRender(cp.contentPlaceholders) + + isHTML := cp.p.m.markup == "html" + + if p.renderable { + if !isHTML { + r, err := cp.renderContent(cp.workContent, true) + if err != nil { + return err + } + + cp.workContent = r.Bytes() + + if tocProvider, ok := r.(converter.TableOfContentsProvider); ok { + cfg := p.s.ContentSpec.Converters.GetMarkupConfig() + cp.tableOfContents = template.HTML( + tocProvider.TableOfContents().ToHTML( + cfg.TableOfContents.StartLevel, + cfg.TableOfContents.EndLevel, + cfg.TableOfContents.Ordered, + ), + ) + } else { + tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent) + cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents) + cp.workContent = tmpContent + } } - return nil, nil - }) + if cp.placeholdersEnabled { + // ToC was accessed via .Page.TableOfContents in the shortcode, + // at a time when the ToC wasn't ready. + cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents) + } - return cp, nil + if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled { + // There are one or more replacement tokens to be replaced. + cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders) + if err != nil { + return err + } + } + + if cp.p.source.hasSummaryDivider { + if isHTML { + src := p.source.parsed.Input() + + // Use the summary sections as they are provided by the user. + if p.source.posSummaryEnd != -1 { + cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd]) + } + + if cp.p.source.posBodyStart != -1 { + cp.workContent = src[cp.p.source.posBodyStart:] + } + + } else { + summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent) + if err != nil { + cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err) + } else { + cp.workContent = content + cp.summary = helpers.BytesToHTML(summary) + } + } + } else if cp.p.m.summary != "" { + b, err := cp.renderContent([]byte(cp.p.m.summary), false) + if err != nil { + return err + } + html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes()) + cp.summary = helpers.BytesToHTML(html) + } + } + + cp.content = helpers.BytesToHTML(cp.workContent) + + if !p.renderable { + err := cp.addSelfTemplate() + return err + } + + return nil } + // Recursive loops can only happen in content files with template code (shortcodes etc.) + // Avoid creating new goroutines if we don't have to. + needTimeout := !p.renderable || p.shortcodeState.hasShortcodes() + needTimeout = needTimeout || cp.renderHooks != nil + + if needTimeout { + cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) { + return nil, initContent() + }) + } else { + cp.initMain = parent.Branch(func() (interface{}, error) { + return nil, initContent() + }) + } + + cp.initPlain = cp.initMain.Branch(func() (interface{}, error) { + cp.plain = helpers.StripHTML(string(cp.content)) + cp.plainWords = strings.Fields(cp.plain) + cp.setWordCounts(p.m.isCJKLanguage) + + if err := cp.setAutoSummary(); err != nil { + return err, nil + } + + return nil, nil + }) + + return cp, nil + } // pageContentOutput represents the Page content for a given output format. type pageContentOutput struct { f output.Format - // If we can safely reuse this for other output formats. + // If we can reuse this for other output formats. reuse bool reuseInit sync.Once @@ -224,10 +250,15 @@ type pageContentOutput struct { placeholdersEnabled bool placeholdersEnabledInit sync.Once + // May be nil. + renderHooks *hooks.Render + // Set if there are more than one output format variant + renderHooksHaveVariants bool // TODO(bep) reimplement this in another way, consolidate with shortcodes + // Content state - workContent []byte - convertedResult converter.Result + workContent []byte + dependencyTracker identity.Manager // Set in server mode. // Temporary storage of placeholders mapped to their content. // These are shortcodes etc. Some of these will need to be replaced @@ -248,6 +279,20 @@ type pageContentOutput struct { readingTime int } +func (p *pageContentOutput) trackDependency(id identity.Provider) { + if p.dependencyTracker != nil { + p.dependencyTracker.Add(id) + } +} + +func (p *pageContentOutput) Reset() { + if p.dependencyTracker != nil { + p.dependencyTracker.Reset() + } + p.initMain.Reset() + p.initPlain.Reset() +} + func (p *pageContentOutput) Content() (interface{}, error) { if p.p.s.initInit(p.initMain, p.p) { return p.content, nil @@ -290,10 +335,6 @@ func (p *pageContentOutput) Summary() template.HTML { func (p *pageContentOutput) TableOfContents() template.HTML { p.p.s.initInit(p.initMain, p.p) - if tocProvider, ok := p.convertedResult.(converter.TableOfContentsProvider); ok { - cfg := p.p.s.ContentSpec.Converters.GetMarkupConfig() - return template.HTML(tocProvider.TableOfContents().ToHTML(cfg.TableOfContents.StartLevel, cfg.TableOfContents.EndLevel, cfg.TableOfContents.Ordered)) - } return p.tableOfContents } @@ -331,12 +372,30 @@ func (p *pageContentOutput) setAutoSummary() error { } -func (cp *pageContentOutput) renderContent(content []byte) (converter.Result, error) { - return cp.p.getContentConverter().Convert( +func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) { + c := cp.p.getContentConverter() + return cp.renderContentWithConverter(c, content, renderTOC) +} + +func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, content []byte, renderTOC bool) (converter.Result, error) { + + r, err := c.Convert( converter.RenderContext{ - Src: content, - RenderTOC: true, + Src: content, + RenderTOC: renderTOC, + RenderHooks: cp.renderHooks, }) + + if err == nil { + if ids, ok := r.(identity.IdentitiesProvider); ok { + for _, v := range ids.GetIdentities() { + cp.trackDependency(v) + } + } + } + + return r, err + } func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) { @@ -392,9 +451,7 @@ func (p *pageContentOutput) enableReuse() { // these will be shifted out when rendering a given output format. type pagePerOutputProviders interface { targetPather - page.ContentProvider page.PaginatorProvider - page.TableOfContentsProvider resource.ResourceLinksProvider } diff --git a/hugolib/page_test.go b/hugolib/page_test.go index ff037a3cc..7f8d3cf49 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -93,12 +93,6 @@ Summary Next Line. {{
}}. More text here. Some more text -` - - simplePageWithEmbeddedScript = `--- -title: Simple ---- - ` simplePageWithSummaryDelimiterSameLine = `--- @@ -325,6 +319,7 @@ func normalizeContent(c string) string { } func checkPageTOC(t *testing.T, page page.Page, toc string) { + t.Helper() if page.TableOfContents() != template.HTML(toc) { t.Fatalf("Page TableOfContents is:\n%q.\nExpected %q", page.TableOfContents(), toc) } diff --git a/hugolib/page_unwrap_test.go b/hugolib/page_unwrap_test.go index 20888166a..bcc1b769a 100644 --- a/hugolib/page_unwrap_test.go +++ b/hugolib/page_unwrap_test.go @@ -26,6 +26,7 @@ func TestUnwrapPage(t *testing.T) { p := &pageState{} c.Assert(mustUnwrap(newPageForShortcode(p)), qt.Equals, p) + c.Assert(mustUnwrap(newPageForRenderHook(p)), qt.Equals, p) } func mustUnwrap(v interface{}) page.Page { diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go index da7427d7e..eeed51b91 100644 --- a/hugolib/pagebundler_test.go +++ b/hugolib/pagebundler_test.go @@ -811,6 +811,7 @@ Short Thumb Width: {{ $thumb.Width }} writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), singleLayout) writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), listLayout) writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.html"), myShort) + writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.customo"), myShort) writeSource(t, fs, filepath.Join(workDir, "base", "_index.md"), pageContent) writeSource(t, fs, filepath.Join(workDir, "base", "_1.md"), pageContent) diff --git a/hugolib/pagecollections.go b/hugolib/pagecollections.go index 7e9682e90..adcbbccef 100644 --- a/hugolib/pagecollections.go +++ b/hugolib/pagecollections.go @@ -358,16 +358,6 @@ func (c *PageCollections) removePage(page *pageState) { } } -func (c *PageCollections) findPagesByShortcode(shortcode string) page.Pages { - var pages page.Pages - for _, p := range c.rawAllPages { - if p.HasShortcode(shortcode) { - pages = append(pages, p) - } - } - return pages -} - func (c *PageCollections) replacePage(page *pageState) { // will find existing page that matches filepath and remove it c.removePage(page) diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index 69bcb6d4f..a4d635a55 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -23,8 +23,6 @@ import ( "html/template" "path" - "github.com/gohugoio/hugo/markup/converter" - "github.com/gohugoio/hugo/common/herrors" "github.com/pkg/errors" @@ -198,7 +196,7 @@ type shortcode struct { } func (s shortcode) insertPlaceholder() bool { - return !s.doMarkup || s.info.Config.Version == 1 + return !s.doMarkup || s.info.ParseInfo().Config.Version == 1 } func (s shortcode) innerString() string { @@ -349,14 +347,9 @@ func renderShortcode( // Pre Hugo 0.55 this was the behaviour even for the outer-most // shortcode. - if sc.doMarkup && (level > 0 || sc.info.Config.Version == 1) { + if sc.doMarkup && (level > 0 || sc.info.ParseInfo().Config.Version == 1) { var err error - - b, err := p.getContentConverter().Convert( - converter.RenderContext{ - Src: []byte(inner), - }, - ) + b, err := p.pageOutput.cp.renderContent([]byte(inner), false) if err != nil { return "", false, err @@ -494,13 +487,13 @@ Loop: case currItem.IsRightShortcodeDelim(): // we trust the template on this: // if there's no inner, we're done - if !sc.isInline && !sc.info.IsInner { + if !sc.isInline && !sc.info.ParseInfo().IsInner { return sc, nil } case currItem.IsShortcodeClose(): next := pt.Peek() - if !sc.isInline && !sc.info.IsInner { + if !sc.isInline && !sc.info.ParseInfo().IsInner { if next.IsError() { // return that error, more specific continue @@ -540,7 +533,7 @@ Loop: return nil, _errors.Errorf("template for shortcode %q not found", sc.name) } - sc.info = tmpl.(tpl.TemplateInfoProvider).TemplateInfo() + sc.info = tmpl.(tpl.Info) case currItem.IsInlineShortcodeName(): sc.name = currItem.ValStr() sc.isInline = true diff --git a/hugolib/shortcode_page.go b/hugolib/shortcode_page.go index e8a3a37e1..5a56e434f 100644 --- a/hugolib/shortcode_page.go +++ b/hugolib/shortcode_page.go @@ -54,3 +54,22 @@ func (p *pageForShortcode) TableOfContents() template.HTML { p.p.enablePlaceholders() return p.toc } + +// This is what is sent into the content render hooks (link, image). +type pageForRenderHooks struct { + page.PageWithoutContent + page.TableOfContentsProvider + page.ContentProvider +} + +func newPageForRenderHook(p *pageState) page.Page { + return &pageForRenderHooks{ + PageWithoutContent: p, + ContentProvider: page.NopPage, + TableOfContentsProvider: page.NopPage, + } +} + +func (p *pageForRenderHooks) page() page.Page { + return p.PageWithoutContent.(page.Page) +} diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index 5e71db501..9d948c807 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -379,8 +379,13 @@ title: "Shortcodes Galore!" if s == nil { return "" } + + var version int + if s.info != nil { + version = s.info.ParseInfo().Config.Version + } return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d", - s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, s.info.Config.Version, s.pos)) + s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos)) } regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) { diff --git a/hugolib/site.go b/hugolib/site.go index 67ddff4d9..866ff5624 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -28,6 +28,12 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/resources" + + "github.com/gohugoio/hugo/identity" + + "github.com/gohugoio/hugo/markup/converter/hooks" + "github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/markup/converter" @@ -60,7 +66,6 @@ import ( "github.com/gohugoio/hugo/navigation" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/related" - "github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources/page/pagemeta" "github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/tpl" @@ -801,7 +806,6 @@ func (s *Site) multilingual() *Multilingual { type whatChanged struct { source bool - other bool files map[string]bool } @@ -888,10 +892,11 @@ func (s *Site) translateFileEvents(events []fsnotify.Event) []fsnotify.Event { // It returns whetever the content source was changed. // TODO(bep) clean up/rewrite this method. func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error { - events = s.filterFileEvents(events) events = s.translateFileEvents(events) + changeIdentities := make(identity.Identities) + s.Log.DEBUG.Printf("Rebuild for events %q", events) h := s.h @@ -902,11 +907,12 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro sourceChanged = []fsnotify.Event{} sourceReallyChanged = []fsnotify.Event{} contentFilesChanged []string - tmplChanged = []fsnotify.Event{} - dataChanged = []fsnotify.Event{} - i18nChanged = []fsnotify.Event{} - shortcodesChanged = make(map[string]bool) - sourceFilesChanged = make(map[string]bool) + + tmplChanged bool + dataChanged bool + i18nChanged bool + + sourceFilesChanged = make(map[string]bool) // prevent spamming the log on changes logger = helpers.NewDistinctFeedbackLogger() @@ -919,33 +925,30 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...) } - if s.isContentDirEvent(ev) { - logger.Println("Source changed", ev) - sourceChanged = append(sourceChanged, ev) - } - if s.isLayoutDirEvent(ev) { - logger.Println("Template changed", ev) - tmplChanged = append(tmplChanged, ev) + id, found := s.eventToIdentity(ev) + if found { + changeIdentities[id] = id + + switch id.Type { + case files.ComponentFolderContent: + logger.Println("Source changed", ev) + sourceChanged = append(sourceChanged, ev) + case files.ComponentFolderLayouts: + logger.Println("Template changed", ev) + tmplChanged = true + case files.ComponentFolderData: + logger.Println("Data changed", ev) + dataChanged = true + case files.ComponentFolderI18n: + logger.Println("i18n changed", ev) + i18nChanged = true - if strings.Contains(ev.Name, "shortcodes") { - shortcode := filepath.Base(ev.Name) - shortcode = strings.TrimSuffix(shortcode, filepath.Ext(shortcode)) - shortcodesChanged[shortcode] = true } } - if s.isDataDirEvent(ev) { - logger.Println("Data changed", ev) - dataChanged = append(dataChanged, ev) - } - if s.isI18nEvent(ev) { - logger.Println("i18n changed", ev) - i18nChanged = append(dataChanged, ev) - } } changed := &whatChanged{ - source: len(sourceChanged) > 0 || len(shortcodesChanged) > 0, - other: len(tmplChanged) > 0 || len(i18nChanged) > 0 || len(dataChanged) > 0, + source: len(sourceChanged) > 0, files: sourceFilesChanged, } @@ -960,7 +963,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...) } - if len(tmplChanged) > 0 || len(i18nChanged) > 0 { + if tmplChanged || i18nChanged { sites := s.h.Sites first := sites[0] @@ -989,7 +992,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro } } - if len(dataChanged) > 0 { + if dataChanged { s.h.init.data.Reset() } @@ -1018,18 +1021,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro sourceFilesChanged[ev.Name] = true } - for shortcode := range shortcodesChanged { - // There are certain scenarios that, when a shortcode changes, - // it isn't sufficient to just rerender the already parsed shortcode. - // One example is if the user adds a new shortcode to the content file first, - // and then creates the shortcode on the file system. - // To handle these scenarios, we must do a full reprocessing of the - // pages that keeps a reference to the changed shortcode. - pagesWithShortcode := h.findPagesByShortcode(shortcode) - for _, p := range pagesWithShortcode { - contentFilesChanged = append(contentFilesChanged, p.File().Filename()) - } - } + h.resetPageStateFromEvents(changeIdentities) if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 { var filenamesChanged []string @@ -1218,20 +1210,14 @@ func (s *Site) initializeSiteInfo() error { return nil } -func (s *Site) isI18nEvent(e fsnotify.Event) bool { - return s.BaseFs.SourceFilesystems.IsI18n(e.Name) -} +func (s *Site) eventToIdentity(e fsnotify.Event) (identity.PathIdentity, bool) { + for _, fs := range s.BaseFs.SourceFilesystems.FileSystems() { + if p := fs.Path(e.Name); p != "" { + return identity.NewPathIdentity(fs.Name, p), true + } + } -func (s *Site) isDataDirEvent(e fsnotify.Event) bool { - return s.BaseFs.SourceFilesystems.IsData(e.Name) -} - -func (s *Site) isLayoutDirEvent(e fsnotify.Event) bool { - return s.BaseFs.SourceFilesystems.IsLayout(e.Name) -} - -func (s *Site) isContentDirEvent(e fsnotify.Event) bool { - return s.BaseFs.IsContent(e.Name) + return identity.PathIdentity{}, false } func (s *Site) readAndProcessContent(filenames ...string) error { @@ -1562,6 +1548,26 @@ var infoOnMissingLayout = map[string]bool{ "404": true, } +type contentLinkRenderer struct { + templateHandler tpl.TemplateHandler + identity.Provider + templ tpl.Template +} + +func (r contentLinkRenderer) Render(w io.Writer, ctx hooks.LinkContext) error { + return r.templateHandler.Execute(r.templ, w, ctx) +} + +func (s *Site) lookupTemplate(layouts ...string) (tpl.Template, bool) { + for _, l := range layouts { + if templ, found := s.Tmpl.Lookup(l); found { + return templ, true + } + } + + return nil, false +} + func (s *Site) renderForLayouts(name, outputFormat string, d interface{}, w io.Writer, layouts ...string) (err error) { templ := s.findFirstTemplate(layouts...) if templ == nil { diff --git a/hugolib/site_benchmark_new_test.go b/hugolib/site_benchmark_new_test.go index 646124b09..13302300e 100644 --- a/hugolib/site_benchmark_new_test.go +++ b/hugolib/site_benchmark_new_test.go @@ -127,6 +127,36 @@ title = "What is Markdown" baseURL = "https://example.com" `) + + data, err := ioutil.ReadFile(filepath.FromSlash("testdata/what-is-markdown.md")) + sb.Assert(err, qt.IsNil) + datastr := string(data) + getContent := func(i int) string { + return fmt.Sprintf(`--- +title: "Page %d" +--- + +`, i) + datastr + + } + for i := 1; i <= 100; i++ { + sb.WithContent(fmt.Sprintf("content/page%d.md", i), getContent(i)) + } + + return sb + }, + func(s *sitesBuilder) { + s.Assert(s.CheckExists("public/page8/index.html"), qt.Equals, true) + }, + }, + {"Markdown with custom link handler", func(b testing.TB) *sitesBuilder { + sb := newTestSitesBuilder(b).WithConfigFile("toml", ` +title = "What is Markdown" +baseURL = "https://example.com" + +`) + + sb.WithTemplatesAdded("_default/_markup/render-link.html", `CUSTOM LINK`) data, err := ioutil.ReadFile(filepath.FromSlash("testdata/what-is-markdown.md")) sb.Assert(err, qt.IsNil) datastr := string(data) diff --git a/hugolib/template_test.go b/hugolib/template_test.go index 71b4b46c0..4c41894ca 100644 --- a/hugolib/template_test.go +++ b/hugolib/template_test.go @@ -18,8 +18,12 @@ import ( "path/filepath" "testing" + "github.com/gohugoio/hugo/identity" + + qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/tpl" "github.com/spf13/viper" ) @@ -320,6 +324,7 @@ Partial cached1: {{ partialCached "p1" "input1" $key1 }} Partial cached2: {{ partialCached "p1" "input2" $key1 }} Partial cached3: {{ partialCached "p1" "input3" $key2 }} `, + "partials/p1.html", `partial: {{ . }}`, ) @@ -331,3 +336,85 @@ Partial cached3: {{ partialCached "p1" "input3" $key2 }} Partial cached3: partial: input3 `) } + +func TestTemplateDependencies(t *testing.T) { + b := newTestSitesBuilder(t).Running() + + b.WithTemplates("index.html", ` +{{ $p := site.GetPage "p1" }} +{{ partial "p1.html" $p }} +{{ partialCached "p2.html" "foo" }} +{{ partials.Include "p3.html" "data" }} +{{ partials.IncludeCached "p4.html" "foo" }} +{{ $p := partial "p5" }} +{{ partial "sub/p6.html" }} +{{ partial "P7.html" }} +{{ template "_default/foo.html" }} +Partial nested: {{ partial "p10" }} + +`, + "partials/p1.html", `ps: {{ .Render "li" }}`, + "partials/p2.html", `p2`, + "partials/p3.html", `p3`, + "partials/p4.html", `p4`, + "partials/p5.html", `p5`, + "partials/sub/p6.html", `p6`, + "partials/P7.html", `p7`, + "partials/p8.html", `p8 {{ partial "p9.html" }}`, + "partials/p9.html", `p9`, + "partials/p10.html", `p10 {{ partial "p11.html" }}`, + "partials/p11.html", `p11`, + "_default/foo.html", `foo`, + "_default/li.html", `li {{ partial "p8.html" }}`, + ) + + b.WithContent("p1.md", `--- +title: P1 +--- + + +`) + + b.Build(BuildCfg{}) + + s := b.H.Sites[0] + + templ, found := s.lookupTemplate("index.html") + b.Assert(found, qt.Equals, true) + + idset := make(map[identity.Identity]bool) + collectIdentities(idset, templ.(tpl.Info)) + b.Assert(idset, qt.HasLen, 10) + +} + +func collectIdentities(set map[identity.Identity]bool, provider identity.Provider) { + if ids, ok := provider.(identity.IdentitiesProvider); ok { + for _, id := range ids.GetIdentities() { + collectIdentities(set, id) + } + } else { + set[provider.GetIdentity()] = true + } +} + +func printRecursiveIdentities(level int, id identity.Provider) { + if level == 0 { + fmt.Println(id.GetIdentity(), "===>") + } + if ids, ok := id.(identity.IdentitiesProvider); ok { + level++ + for _, id := range ids.GetIdentities() { + printRecursiveIdentities(level, id) + } + } else { + ident(level) + fmt.Println("ID", id) + } +} + +func ident(n int) { + for i := 0; i < n; i++ { + fmt.Print(" ") + } +} diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index ea1ee9674..80aafe052 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -389,8 +389,9 @@ func (s *sitesBuilder) EditFiles(filenameContent ...string) *sitesBuilder { var changedFiles []string for i := 0; i < len(filenameContent); i += 2 { filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1] - changedFiles = append(changedFiles, filename) - writeSource(s.T, s.Fs, s.absFilename(filename), content) + absFilename := s.absFilename(filename) + changedFiles = append(changedFiles, absFilename) + writeSource(s.T, s.Fs, absFilename, content) } s.changedFiles = changedFiles @@ -963,10 +964,6 @@ func isCI() bool { return os.Getenv("CI") != "" } -func isGo111() bool { - return strings.Contains(runtime.Version(), "1.11") -} - // See https://github.com/golang/go/issues/19280 // Not in use. var parallelEnabled = true diff --git a/identity/identity.go b/identity/identity.go new file mode 100644 index 000000000..d06710efe --- /dev/null +++ b/identity/identity.go @@ -0,0 +1,131 @@ +package identity + +import ( + "path/filepath" + "strings" + "sync" +) + +// NewIdentityManager creates a new Manager starting at id. +func NewManager(id Provider) Manager { + return &identityManager{ + Provider: id, + ids: Identities{id.GetIdentity(): id}, + } +} + +// NewPathIdentity creates a new Identity with the two identifiers +// type and path. +func NewPathIdentity(typ, pat string) PathIdentity { + pat = strings.ToLower(strings.TrimPrefix(filepath.ToSlash(pat), "/")) + return PathIdentity{Type: typ, Path: pat} +} + +// Identities stores identity providers. +type Identities map[Identity]Provider + +func (ids Identities) search(id Identity) Provider { + if v, found := ids[id]; found { + return v + } + for _, v := range ids { + switch t := v.(type) { + case IdentitiesProvider: + if nested := t.GetIdentities().search(id); nested != nil { + return nested + } + } + } + return nil +} + +// IdentitiesProvider provides all Identities. +type IdentitiesProvider interface { + GetIdentities() Identities +} + +// Identity represents an thing that can provide an identify. This can be +// any Go type, but the Identity returned by GetIdentify must be hashable. +type Identity interface { + Provider + Name() string +} + +// Manager manages identities, and is itself a Provider of Identity. +type Manager interface { + IdentitiesProvider + Provider + Add(ids ...Provider) + Search(id Identity) Provider + Reset() +} + +// A PathIdentity is a common identity identified by a type and a path, e.g. "layouts" and "_default/single.html". +type PathIdentity struct { + Type string + Path string +} + +// GetIdentity returns itself. +func (id PathIdentity) GetIdentity() Identity { + return id +} + +// Name returns the Path. +func (id PathIdentity) Name() string { + return id.Path +} + +// A KeyValueIdentity a general purpose identity. +type KeyValueIdentity struct { + Key string + Value string +} + +// GetIdentity returns itself. +func (id KeyValueIdentity) GetIdentity() Identity { + return id +} + +// Name returns the Key. +func (id KeyValueIdentity) Name() string { + return id.Key +} + +// Provider provides the hashable Identity. +type Provider interface { + GetIdentity() Identity +} + +type identityManager struct { + sync.Mutex + Provider + ids Identities +} + +func (im *identityManager) Add(ids ...Provider) { + im.Lock() + for _, id := range ids { + im.ids[id.GetIdentity()] = id + } + im.Unlock() +} + +func (im *identityManager) Reset() { + im.Lock() + id := im.GetIdentity() + im.ids = Identities{id.GetIdentity(): id} + im.Unlock() +} + +func (im *identityManager) GetIdentities() Identities { + im.Lock() + defer im.Unlock() + return im.ids +} + +func (im *identityManager) Search(id Identity) Provider { + im.Lock() + defer im.Unlock() + return im.ids.search(id.GetIdentity()) +} diff --git a/identity/identity_test.go b/identity/identity_test.go new file mode 100644 index 000000000..adebcad91 --- /dev/null +++ b/identity/identity_test.go @@ -0,0 +1,42 @@ +// 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 identity + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestIdentityManager(t *testing.T) { + c := qt.New(t) + + id1 := testIdentity{name: "id1"} + im := NewManager(id1) + + c.Assert(im.Search(id1).GetIdentity(), qt.Equals, id1) + c.Assert(im.Search(testIdentity{name: "notfound"}), qt.Equals, nil) +} + +type testIdentity struct { + name string +} + +func (id testIdentity) GetIdentity() Identity { + return id +} + +func (id testIdentity) Name() string { + return id.name +} diff --git a/markup/asciidoc/convert.go b/markup/asciidoc/convert.go index 65fdde0f5..a72aac391 100644 --- a/markup/asciidoc/convert.go +++ b/markup/asciidoc/convert.go @@ -18,6 +18,7 @@ package asciidoc import ( "os/exec" + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/internal" "github.com/gohugoio/hugo/markup/converter" @@ -47,6 +48,10 @@ func (a *asciidocConverter) Convert(ctx converter.RenderContext) (converter.Resu return converter.Bytes(a.getAsciidocContent(ctx.Src, a.ctx)), nil } +func (c *asciidocConverter) Supports(feature identity.Identity) bool { + return false +} + // getAsciidocContent calls asciidoctor or asciidoc as an external helper // to convert AsciiDoc content to HTML. func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) []byte { diff --git a/markup/blackfriday/convert.go b/markup/blackfriday/convert.go index 350defcb6..3df23c7ae 100644 --- a/markup/blackfriday/convert.go +++ b/markup/blackfriday/convert.go @@ -15,6 +15,7 @@ package blackfriday import ( + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config" "github.com/gohugoio/hugo/markup/converter" "github.com/russross/blackfriday" @@ -72,6 +73,10 @@ func (c *blackfridayConverter) Convert(ctx converter.RenderContext) (converter.R return converter.Bytes(blackfriday.Markdown(ctx.Src, r, c.extensions)), nil } +func (c *blackfridayConverter) Supports(feature identity.Identity) bool { + return false +} + func (c *blackfridayConverter) getHTMLRenderer(renderTOC bool) blackfriday.Renderer { flags := getFlags(renderTOC, c.bf) diff --git a/markup/converter/converter.go b/markup/converter/converter.go index a1141f65c..a4585bd03 100644 --- a/markup/converter/converter.go +++ b/markup/converter/converter.go @@ -16,6 +16,8 @@ package converter import ( "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/markup/converter/hooks" "github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/spf13/afero" @@ -67,6 +69,7 @@ func (n newConverter) Name() string { // another format, e.g. Markdown to HTML. type Converter interface { Convert(ctx RenderContext) (Result, error) + Supports(feature identity.Identity) bool } // Result represents the minimum returned from Convert. @@ -94,6 +97,7 @@ func (b Bytes) Bytes() []byte { // DocumentContext holds contextual information about the document to convert. type DocumentContext struct { + Document interface{} // May be nil. Usually a page.Page DocumentID string DocumentName string ConfigOverrides map[string]interface{} @@ -101,6 +105,11 @@ type DocumentContext struct { // RenderContext holds contextual information about the content to render. type RenderContext struct { - Src []byte - RenderTOC bool + Src []byte + RenderTOC bool + RenderHooks *hooks.Render } + +var ( + FeatureRenderHooks = identity.NewPathIdentity("markup", "renderingHooks") +) diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go new file mode 100644 index 000000000..63beacc37 --- /dev/null +++ b/markup/converter/hooks/hooks.go @@ -0,0 +1,57 @@ +// 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 hooks + +import ( + "io" + + "github.com/gohugoio/hugo/identity" +) + +type LinkContext interface { + Page() interface{} + Destination() string + Title() string + Text() string +} + +type Render struct { + LinkRenderer LinkRenderer + ImageRenderer LinkRenderer +} + +func (r *Render) Eq(other interface{}) bool { + ro, ok := other.(*Render) + if !ok { + return false + } + if r == nil || ro == nil { + return r == nil + } + + if r.ImageRenderer.GetIdentity() != ro.ImageRenderer.GetIdentity() { + return false + } + + if r.LinkRenderer.GetIdentity() != ro.LinkRenderer.GetIdentity() { + return false + } + + return true +} + +type LinkRenderer interface { + Render(w io.Writer, ctx LinkContext) error + identity.Provider +} diff --git a/markup/goldmark/convert.go b/markup/goldmark/convert.go index 15b0f0d77..130f02a2f 100644 --- a/markup/goldmark/convert.go +++ b/markup/goldmark/convert.go @@ -15,21 +15,22 @@ package goldmark import ( + "bufio" "bytes" "fmt" "path/filepath" "runtime/debug" + "github.com/gohugoio/hugo/identity" + "github.com/pkg/errors" "github.com/spf13/afero" "github.com/gohugoio/hugo/hugofs" - "github.com/alecthomas/chroma/styles" "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/highlight" - "github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/yuin/goldmark" hl "github.com/yuin/goldmark-highlighting" @@ -48,7 +49,7 @@ type provide struct { } func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) { - md := newMarkdown(cfg.MarkupConfig) + md := newMarkdown(cfg) return converter.NewProvider("goldmark", func(ctx converter.DocumentContext) (converter.Converter, error) { return &goldmarkConverter{ ctx: ctx, @@ -64,11 +65,13 @@ type goldmarkConverter struct { cfg converter.ProviderConfig } -func newMarkdown(mcfg markup_config.Config) goldmark.Markdown { - cfg := mcfg.Goldmark +func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown { + mcfg := pcfg.MarkupConfig + cfg := pcfg.MarkupConfig.Goldmark var ( extensions = []goldmark.Extender{ + newLinks(), newTocExtension(), } rendererOptions []renderer.Option @@ -143,15 +146,53 @@ func newMarkdown(mcfg markup_config.Config) goldmark.Markdown { } +var _ identity.IdentitiesProvider = (*converterResult)(nil) + type converterResult struct { converter.Result toc tableofcontents.Root + ids identity.Identities } func (c converterResult) TableOfContents() tableofcontents.Root { return c.toc } +func (c converterResult) GetIdentities() identity.Identities { + return c.ids +} + +type renderContext struct { + util.BufWriter + renderContextData +} + +type renderContextData interface { + RenderContext() converter.RenderContext + DocumentContext() converter.DocumentContext + AddIdentity(id identity.Identity) +} + +type renderContextDataHolder struct { + rctx converter.RenderContext + dctx converter.DocumentContext + ids identity.Manager +} + +func (ctx *renderContextDataHolder) RenderContext() converter.RenderContext { + return ctx.rctx +} + +func (ctx *renderContextDataHolder) DocumentContext() converter.DocumentContext { + return ctx.dctx +} + +func (ctx *renderContextDataHolder) AddIdentity(id identity.Identity) { + ctx.ids.Add(id) +} + +var converterIdentity = identity.KeyValueIdentity{Key: "goldmark", Value: "converter"} + func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result converter.Result, err error) { defer func() { if r := recover(); r != nil { @@ -166,9 +207,7 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert buf := &bytes.Buffer{} result = buf - pctx := parser.NewContext() - pctx.Set(tocEnableKey, ctx.RenderTOC) - + pctx := newParserContext(ctx) reader := text.NewReader(ctx.Src) doc := c.md.Parser().Parse( @@ -176,27 +215,58 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert parser.WithContext(pctx), ) - if err := c.md.Renderer().Render(buf, ctx.Src, doc); err != nil { + rcx := &renderContextDataHolder{ + rctx: ctx, + dctx: c.ctx, + ids: identity.NewManager(converterIdentity), + } + + w := renderContext{ + BufWriter: bufio.NewWriter(buf), + renderContextData: rcx, + } + + if err := c.md.Renderer().Render(w, ctx.Src, doc); err != nil { return nil, err } - if toc, ok := pctx.Get(tocResultKey).(tableofcontents.Root); ok { - return converterResult{ - Result: buf, - toc: toc, - }, nil - } + return converterResult{ + Result: buf, + ids: rcx.ids.GetIdentities(), + toc: pctx.TableOfContents(), + }, nil - return buf, nil +} + +var featureSet = map[identity.Identity]bool{ + converter.FeatureRenderHooks: true, +} + +func (c *goldmarkConverter) Supports(feature identity.Identity) bool { + return featureSet[feature.GetIdentity()] +} + +func newParserContext(rctx converter.RenderContext) *parserContext { + ctx := parser.NewContext() + ctx.Set(tocEnableKey, rctx.RenderTOC) + return &parserContext{ + Context: ctx, + } +} + +type parserContext struct { + parser.Context +} + +func (p *parserContext) TableOfContents() tableofcontents.Root { + if v := p.Get(tocResultKey); v != nil { + return v.(tableofcontents.Root) + } + return tableofcontents.Root{} } func newHighlighting(cfg highlight.Config) goldmark.Extender { - style := styles.Get(cfg.Style) - if style == nil { - style = styles.Fallback - } - - e := hl.NewHighlighting( + return hl.NewHighlighting( hl.WithStyle(cfg.Style), hl.WithGuessLanguage(cfg.GuessSyntax), hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()), @@ -230,6 +300,4 @@ func newHighlighting(cfg highlight.Config) goldmark.Extender { }), ) - - return e } diff --git a/markup/goldmark/convert_test.go b/markup/goldmark/convert_test.go index b6816d2e5..2a9727606 100644 --- a/markup/goldmark/convert_test.go +++ b/markup/goldmark/convert_test.go @@ -38,6 +38,9 @@ func TestConvert(t *testing.T) { https://github.com/gohugoio/hugo/issues/6528 [Live Demo here!](https://docuapi.netlify.com/) +[I'm an inline-style link with title](https://www.google.com "Google's Homepage") + + ## Code Fences §§§bash @@ -98,6 +101,7 @@ description mconf := markup_config.Default mconf.Highlight.NoClasses = false + mconf.Goldmark.Renderer.Unsafe = true p, err := Provider.New( converter.ProviderConfig{ @@ -106,15 +110,15 @@ description }, ) c.Assert(err, qt.IsNil) - conv, err := p.New(converter.DocumentContext{}) + conv, err := p.New(converter.DocumentContext{DocumentID: "thedoc"}) c.Assert(err, qt.IsNil) - b, err := conv.Convert(converter.RenderContext{Src: []byte(content)}) + b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content)}) c.Assert(err, qt.IsNil) got := string(b.Bytes()) // Links - c.Assert(got, qt.Contains, `Live Demo here!`) + // c.Assert(got, qt.Contains, `Live Demo here!`) // Header IDs c.Assert(got, qt.Contains, `

Custom ID

`, qt.Commentf(got)) @@ -137,6 +141,11 @@ description c.Assert(got, qt.Contains, `
`) c.Assert(got, qt.Contains, `
date
`) + toc, ok := b.(converter.TableOfContentsProvider) + c.Assert(ok, qt.Equals, true) + tocHTML := toc.TableOfContents().ToHTML(1, 2, false) + c.Assert(tocHTML, qt.Contains, "TableOfContents") + } func TestCodeFence(t *testing.T) { diff --git a/markup/goldmark/render_link.go b/markup/goldmark/render_link.go new file mode 100644 index 000000000..17ba5bada --- /dev/null +++ b/markup/goldmark/render_link.go @@ -0,0 +1,208 @@ +// 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 goldmark + +import ( + "github.com/gohugoio/hugo/markup/converter/hooks" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/util" +) + +var _ renderer.SetOptioner = (*linkRenderer)(nil) + +func newLinkRenderer() renderer.NodeRenderer { + r := &linkRenderer{ + Config: html.Config{ + Writer: html.DefaultWriter, + }, + } + return r +} + +func newLinks() goldmark.Extender { + return &links{} +} + +type linkContext struct { + page interface{} + destination string + title string + text string +} + +func (ctx linkContext) Destination() string { + return ctx.destination +} + +func (ctx linkContext) Resolved() bool { + return false +} + +func (ctx linkContext) Page() interface{} { + return ctx.page +} + +func (ctx linkContext) Text() string { + return ctx.text +} + +func (ctx linkContext) Title() string { + return ctx.title +} + +type linkRenderer struct { + html.Config +} + +func (r *linkRenderer) SetOption(name renderer.OptionName, value interface{}) { + r.Config.SetOption(name, value) +} + +// RegisterFuncs implements NodeRenderer.RegisterFuncs. +func (r *linkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(ast.KindLink, r.renderLink) + reg.Register(ast.KindImage, r.renderImage) +} + +// Fall back to the default Goldmark render funcs. Method below borrowed from: +// https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404 +func (r *linkRenderer) renderDefaultImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + n := node.(*ast.Image) + _, _ = w.WriteString("`)
+	_, _ = w.Write(n.Text(source))
+	_ = w.WriteByte('") + } else { + _, _ = w.WriteString(">") + } + return ast.WalkSkipChildren, nil +} + +// Fall back to the default Goldmark render funcs. Method below borrowed from: +// https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404 +func (r *linkRenderer) renderDefaultLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.Link) + if entering { + _, _ = w.WriteString("') + } else { + _, _ = w.WriteString("") + } + return ast.WalkContinue, nil +} + +func (r *linkRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.Image) + var h *hooks.Render + + ctx, ok := w.(renderContextData) + if ok { + h = ctx.RenderContext().RenderHooks + ok = h != nil && h.ImageRenderer != nil + } + + if !ok { + return r.renderDefaultImage(w, source, node, entering) + } + + if !entering { + return ast.WalkContinue, nil + } + + err := h.ImageRenderer.Render( + w, + linkContext{ + page: ctx.DocumentContext().Document, + destination: string(n.Destination), + title: string(n.Title), + text: string(n.Text(source)), + }, + ) + + ctx.AddIdentity(h.ImageRenderer.GetIdentity()) + + return ast.WalkSkipChildren, err + +} + +func (r *linkRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.Link) + var h *hooks.Render + + ctx, ok := w.(renderContextData) + if ok { + h = ctx.RenderContext().RenderHooks + ok = h != nil && h.LinkRenderer != nil + } + + if !ok { + return r.renderDefaultLink(w, source, node, entering) + } + + if !entering { + return ast.WalkContinue, nil + } + + err := h.LinkRenderer.Render( + w, + linkContext{ + page: ctx.DocumentContext().Document, + destination: string(n.Destination), + title: string(n.Title), + text: string(n.Text(source)), + }, + ) + + ctx.AddIdentity(h.LinkRenderer.GetIdentity()) + + // Do not render the inner text. + return ast.WalkSkipChildren, err + +} + +type links struct { +} + +// Extend implements goldmark.Extender. +func (e *links) Extend(m goldmark.Markdown) { + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(newLinkRenderer(), 100), + )) +} diff --git a/markup/mmark/convert.go b/markup/mmark/convert.go index 07b2a6f81..0682ad276 100644 --- a/markup/mmark/convert.go +++ b/markup/mmark/convert.go @@ -15,6 +15,7 @@ package mmark import ( + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config" "github.com/gohugoio/hugo/markup/converter" "github.com/miekg/mmark" @@ -65,6 +66,10 @@ func (c *mmarkConverter) Convert(ctx converter.RenderContext) (converter.Result, return mmark.Parse(ctx.Src, r, c.extensions), nil } +func (c *mmarkConverter) Supports(feature identity.Identity) bool { + return false +} + func getHTMLRenderer( ctx converter.DocumentContext, cfg blackfriday_config.Config, diff --git a/markup/org/convert.go b/markup/org/convert.go index 4d6e5e2fa..2b1fbb73c 100644 --- a/markup/org/convert.go +++ b/markup/org/convert.go @@ -17,6 +17,8 @@ package org import ( "bytes" + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/markup/converter" "github.com/niklasfasching/go-org/org" "github.com/spf13/afero" @@ -66,3 +68,7 @@ func (c *orgConverter) Convert(ctx converter.RenderContext) (converter.Result, e } return converter.Bytes([]byte(html)), nil } + +func (c *orgConverter) Supports(feature identity.Identity) bool { + return false +} diff --git a/markup/pandoc/convert.go b/markup/pandoc/convert.go index d538d4a52..d6d5ab18c 100644 --- a/markup/pandoc/convert.go +++ b/markup/pandoc/convert.go @@ -17,6 +17,7 @@ package pandoc import ( "os/exec" + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/internal" "github.com/gohugoio/hugo/markup/converter" @@ -47,6 +48,10 @@ func (c *pandocConverter) Convert(ctx converter.RenderContext) (converter.Result return converter.Bytes(c.getPandocContent(ctx.Src, c.ctx)), nil } +func (c *pandocConverter) Supports(feature identity.Identity) bool { + return false +} + // getPandocContent calls pandoc as an external helper to convert pandoc markdown to HTML. func (c *pandocConverter) getPandocContent(src []byte, ctx converter.DocumentContext) []byte { logger := c.cfg.Logger diff --git a/markup/rst/convert.go b/markup/rst/convert.go index 040b40d79..64cc8b511 100644 --- a/markup/rst/convert.go +++ b/markup/rst/convert.go @@ -19,6 +19,7 @@ import ( "os/exec" "runtime" + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/internal" "github.com/gohugoio/hugo/markup/converter" @@ -48,6 +49,10 @@ func (c *rstConverter) Convert(ctx converter.RenderContext) (converter.Result, e return converter.Bytes(c.getRstContent(ctx.Src, c.ctx)), nil } +func (c *rstConverter) Supports(feature identity.Identity) bool { + return false +} + // getRstContent calls the Python script rst2html as an external helper // to convert reStructuredText content to HTML. func (c *rstConverter) getRstContent(src []byte, ctx converter.DocumentContext) []byte { diff --git a/output/layout.go b/output/layout.go index 055d742b1..091684bee 100644 --- a/output/layout.go +++ b/output/layout.go @@ -37,6 +37,12 @@ type LayoutDescriptor struct { Layout string // LayoutOverride indicates what we should only look for the above layout. LayoutOverride bool + + RenderingHook bool +} + +func (d LayoutDescriptor) isList() bool { + return !d.RenderingHook && d.Kind != "page" } // LayoutHandler calculates the layout template to use to render a given output type. @@ -89,7 +95,7 @@ type layoutBuilder struct { func (l *layoutBuilder) addLayoutVariations(vars ...string) { for _, layoutVar := range vars { - if l.d.LayoutOverride && layoutVar != l.d.Layout { + if !l.d.RenderingHook && l.d.LayoutOverride && layoutVar != l.d.Layout { continue } l.layoutVariations = append(l.layoutVariations, layoutVar) @@ -99,6 +105,9 @@ func (l *layoutBuilder) addLayoutVariations(vars ...string) { func (l *layoutBuilder) addTypeVariations(vars ...string) { for _, typeVar := range vars { if !reservedSections[typeVar] { + if l.d.RenderingHook { + typeVar = typeVar + renderingHookRoot + } l.typeVariations = append(l.typeVariations, typeVar) } } @@ -115,16 +124,21 @@ func (l *layoutBuilder) addKind() { l.addTypeVariations(l.d.Kind) } +const renderingHookRoot = "/_markup" + func resolvePageTemplate(d LayoutDescriptor, f Format) []string { b := &layoutBuilder{d: d, f: f} - if d.Layout != "" { - b.addLayoutVariations(d.Layout) - } - - if d.Type != "" { - b.addTypeVariations(d.Type) + if d.RenderingHook { + b.addLayoutVariations(d.Kind) + } else { + if d.Layout != "" { + b.addLayoutVariations(d.Layout) + } + if d.Type != "" { + b.addTypeVariations(d.Type) + } } switch d.Kind { @@ -159,7 +173,7 @@ func resolvePageTemplate(d LayoutDescriptor, f Format) []string { } isRSS := f.Name == RSSFormat.Name - if isRSS { + if !d.RenderingHook && isRSS { // The historic and common rss.xml case b.addLayoutVariations("") } @@ -167,14 +181,14 @@ func resolvePageTemplate(d LayoutDescriptor, f Format) []string { // All have _default in their lookup path b.addTypeVariations("_default") - if d.Kind != "page" { + if d.isList() { // Add the common list type b.addLayoutVariations("list") } layouts := b.resolveVariations() - if isRSS { + if !d.RenderingHook && isRSS { layouts = append(layouts, "_internal/_default/rss.xml") } diff --git a/output/layout_test.go b/output/layout_test.go index c6267b274..cff275929 100644 --- a/output/layout_test.go +++ b/output/layout_test.go @@ -111,6 +111,9 @@ func TestLayout(t *testing.T) { []string{"section/shortcodes.amp.html"}, 12}, {"Reserved section, partials", LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials"}, "", ampType, []string{"section/partials.amp.html"}, 12}, + // We may add type support ... later. + {"Content hook", LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog"}, "", ampType, + []string{"_default/_markup/render-link.amp.html", "_default/_markup/render-link.html"}, 2}, } { c.Run(this.name, func(c *qt.C) { l := NewLayoutHandler() diff --git a/resources/page/page.go b/resources/page/page.go index 3b43b0af3..28094a4a9 100644 --- a/resources/page/page.go +++ b/resources/page/page.go @@ -201,9 +201,10 @@ type PageMetaProvider interface { Weight() int } -// PageRenderProvider provides a way for a Page to render itself. +// PageRenderProvider provides a way for a Page to render content. type PageRenderProvider interface { - Render(layout ...string) template.HTML + Render(layout ...string) (template.HTML, error) + RenderString(args ...interface{}) (template.HTML, error) } // PageWithoutContent is the Page without any of the content methods. diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go index 09ac136fc..19c7068e0 100644 --- a/resources/page/page_nop.go +++ b/resources/page/page_nop.go @@ -371,8 +371,12 @@ func (p *nopPage) RelRef(argsm map[string]interface{}) (string, error) { return "", nil } -func (p *nopPage) Render(layout ...string) template.HTML { - return "" +func (p *nopPage) Render(layout ...string) (template.HTML, error) { + return "", nil +} + +func (p *nopPage) RenderString(args ...interface{}) (template.HTML, error) { + return "", nil } func (p *nopPage) ResourceType() string { diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index cc6a74f06..0d21faa51 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -446,7 +446,11 @@ func (p *testPage) RelRefFrom(argsm map[string]interface{}, source interface{}) return "", nil } -func (p *testPage) Render(layout ...string) template.HTML { +func (p *testPage) Render(layout ...string) (template.HTML, error) { + panic("not implemented") +} + +func (p *testPage) RenderString(args ...interface{}) (template.HTML, error) { panic("not implemented") } diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go index 1cae78a43..127d7ce03 100644 --- a/scripts/fork_go_templates/main.go +++ b/scripts/fork_go_templates/main.go @@ -59,6 +59,7 @@ var ( "type state struct", "type stateOld struct", "func (s *state) evalFunction", "func (s *state) evalFunctionOld", "func (s *state) evalField(", "func (s *state) evalFieldOld(", + "func (s *state) evalCall(", "func (s *state) evalCallOld(", ) htmlTemplateReplacers = strings.NewReplacer( diff --git a/tpl/internal/go_templates/texttemplate/exec.go b/tpl/internal/go_templates/texttemplate/exec.go index db64edcb2..078bcf643 100644 --- a/tpl/internal/go_templates/texttemplate/exec.go +++ b/tpl/internal/go_templates/texttemplate/exec.go @@ -658,7 +658,7 @@ var ( // evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so // it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0] // as the function itself. -func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value { +func (s *state) evalCallOld(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value { if args != nil { args = args[1:] // Zeroth arg is function name/node; not passed to function. } diff --git a/tpl/internal/go_templates/texttemplate/hugo_template.go b/tpl/internal/go_templates/texttemplate/hugo_template.go index be8a5558f..a39f027fb 100644 --- a/tpl/internal/go_templates/texttemplate/hugo_template.go +++ b/tpl/internal/go_templates/texttemplate/hugo_template.go @@ -34,8 +34,9 @@ type Preparer interface { // ExecHelper allows some custom eval hooks. type ExecHelper interface { - GetFunc(name string) (reflect.Value, bool) - GetMapValue(receiver, key reflect.Value) (reflect.Value, bool) + GetFunc(tmpl Preparer, name string) (reflect.Value, bool) + GetMethod(tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) + GetMapValue(tmpl Preparer, receiver, key reflect.Value) (reflect.Value, bool) } // Executer executes a given template. @@ -64,6 +65,7 @@ func (t *executer) Execute(p Preparer, wr io.Writer, data interface{}) error { state := &state{ helper: t.helper, + prep: p, tmpl: tmpl, wr: wr, vars: []variable{{"$", value}}, @@ -75,7 +77,6 @@ func (t *executer) Execute(p Preparer, wr io.Writer, data interface{}) error { // Prepare returns a template ready for execution. func (t *Template) Prepare() (*Template, error) { - return t, nil } @@ -95,6 +96,7 @@ func (t *Template) executeWithState(state *state, value reflect.Value) (err erro // can execute in parallel. type state struct { tmpl *Template + prep Preparer // Added for Hugo. helper ExecHelper // Added for Hugo. wr io.Writer node parse.Node // current node, for errors @@ -110,7 +112,7 @@ func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd var ok bool if s.helper != nil { // Added for Hugo. - function, ok = s.helper.GetFunc(name) + function, ok = s.helper.GetFunc(s.prep, name) } if !ok { @@ -148,9 +150,23 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() { ptr = ptr.Addr() } - if method := ptr.MethodByName(fieldName); method.IsValid() { + // Added for Hugo. + var first reflect.Value + var method reflect.Value + if s.helper != nil { + method, first = s.helper.GetMethod(s.prep, ptr, fieldName) + } else { + method = ptr.MethodByName(fieldName) + } + + if method.IsValid() { + if first != zero { + return s.evalCall(dot, method, node, fieldName, args, final, first) + } + 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() { @@ -177,7 +193,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, var result reflect.Value if s.helper != nil { // Added for Hugo. - result, _ = s.helper.GetMapValue(receiver, nameVal) + result, _ = s.helper.GetMapValue(s.prep, receiver, nameVal) } else { result = receiver.MapIndex(nameVal) } @@ -209,3 +225,79 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, s.errorf("can't evaluate field %s in type %s", fieldName, typ) panic("not reached") } + +// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so +// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0] +// as the function itself. +func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value, first ...reflect.Value) reflect.Value { + if args != nil { + args = args[1:] // Zeroth arg is function name/node; not passed to function. + } + typ := fun.Type() + numFirst := len(first) + numIn := len(args) + numFirst // // Added for Hugo + if final != missingVal { + numIn++ + } + numFixed := len(args) + len(first) + if typ.IsVariadic() { + numFixed = typ.NumIn() - 1 // last arg is the variadic one. + if numIn < numFixed { + s.errorf("wrong number of args for %s: want at least %d got %d", name, typ.NumIn()-1, len(args)) + } + } else if numIn != typ.NumIn() { + s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn) + } + if !goodFunc(typ) { + // TODO: This could still be a confusing error; maybe goodFunc should provide info. + s.errorf("can't call method/function %q with %d results", name, typ.NumOut()) + } + // Build the arg list. + argv := make([]reflect.Value, numIn) + // Args must be evaluated. Fixed args first. + i := len(first) + for ; i < numFixed && i < len(args)+numFirst; i++ { + argv[i] = s.evalArg(dot, typ.In(i), args[i-numFirst]) + } + // Now the ... args. + if typ.IsVariadic() { + argType := typ.In(typ.NumIn() - 1).Elem() // Argument is a slice. + for ; i < len(args)+numFirst; i++ { + argv[i] = s.evalArg(dot, argType, args[i-numFirst]) + } + + } + // Add final value if necessary. + if final != missingVal { + t := typ.In(typ.NumIn() - 1) + if typ.IsVariadic() { + if numIn-1 < numFixed { + // The added final argument corresponds to a fixed parameter of the function. + // Validate against the type of the actual parameter. + t = typ.In(numIn - 1) + } else { + // The added final argument corresponds to the variadic part. + // Validate against the type of the elements of the variadic slice. + t = t.Elem() + } + } + argv[i] = s.validateType(final, t) + } + + // Added for Hugo + for i := 0; i < len(first); i++ { + argv[i] = s.validateType(first[i], typ.In(i)) + } + + v, err := safeCall(fun, argv) + // If we have an error that is not nil, stop execution and return that + // error to the caller. + if err != nil { + s.at(node) + s.errorf("error calling %s: %v", name, err) + } + if v.Type() == reflectValueType { + v = v.Interface().(reflect.Value) + } + return v +} diff --git a/tpl/internal/go_templates/texttemplate/hugo_template_test.go b/tpl/internal/go_templates/texttemplate/hugo_template_test.go index 2424a0a48..98a2575eb 100644 --- a/tpl/internal/go_templates/texttemplate/hugo_template_test.go +++ b/tpl/internal/go_templates/texttemplate/hugo_template_test.go @@ -27,10 +27,18 @@ type TestStruct struct { M map[string]string } +func (t TestStruct) Hello1(arg string) string { + return arg +} + +func (t TestStruct) Hello2(arg1, arg2 string) string { + return arg1 + " " + arg2 +} + type execHelper struct { } -func (e *execHelper) GetFunc(name string) (reflect.Value, bool) { +func (e *execHelper) GetFunc(tmpl Preparer, name string) (reflect.Value, bool) { if name == "print" { return zero, false } @@ -39,11 +47,19 @@ func (e *execHelper) GetFunc(name string) (reflect.Value, bool) { }), true } -func (e *execHelper) GetMapValue(m, key reflect.Value) (reflect.Value, bool) { +func (e *execHelper) GetMapValue(tmpl Preparer, m, key reflect.Value) (reflect.Value, bool) { key = reflect.ValueOf(strings.ToLower(key.String())) return m.MapIndex(key), true } +func (e *execHelper) GetMethod(tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) { + if name != "Hello1" { + return zero, zero + } + m := receiver.MethodByName("Hello2") + return m, reflect.ValueOf("v2") +} + func TestTemplateExecutor(t *testing.T) { c := qt.New(t) @@ -51,6 +67,7 @@ func TestTemplateExecutor(t *testing.T) { {{ print "foo" }} {{ printf "hugo" }} Map: {{ .M.A }} +Method: {{ .Hello1 "v1" }} `) @@ -67,5 +84,6 @@ Map: {{ .M.A }} c.Assert(got, qt.Contains, "foo") c.Assert(got, qt.Contains, "hello hugo") c.Assert(got, qt.Contains, "Map: av") + c.Assert(got, qt.Contains, "Method: v2 v1") } diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go index bfc3a82d3..6f3ba2d13 100644 --- a/tpl/partials/partials.go +++ b/tpl/partials/partials.go @@ -116,9 +116,9 @@ func (ns *Namespace) Include(name string, contextList ...interface{}) (interface return "", fmt.Errorf("partial %q not found", name) } - var info tpl.Info - if ip, ok := templ.(tpl.TemplateInfoProvider); ok { - info = ip.TemplateInfo() + var info tpl.ParseInfo + if ip, ok := templ.(tpl.Info); ok { + info = ip.ParseInfo() } var w io.Writer diff --git a/tpl/template.go b/tpl/template.go index db715c306..0841236de 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -24,8 +24,6 @@ import ( texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" ) -var _ TemplateInfoProvider = (*TemplateInfo)(nil) - // TemplateManager manages the collection of templates. type TemplateManager interface { TemplateHandler @@ -34,7 +32,6 @@ type TemplateManager interface { AddLateTemplate(name, tpl string) error LoadTemplates(prefix string) error - MarkReady() error RebuildClone() } @@ -80,11 +77,6 @@ type Template interface { Prepare() (*texttemplate.Template, error) } -// TemplateInfoProvider provides some contextual information about a template. -type TemplateInfoProvider interface { - TemplateInfo() Info -} - // TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain. type TemplateParser interface { Parse(name, tpl string) (Template, error) @@ -101,10 +93,31 @@ type TemplateDebugger interface { Debug() } -// TemplateInfo wraps a Template with some additional information. -type TemplateInfo struct { +// templateInfo wraps a Template with some additional information. +type templateInfo struct { Template - Info Info + Info +} + +// templateInfo wraps a Template with some additional information. +type templateInfoManager struct { + Template + InfoManager +} + +// WithInfo wraps the info in a template. +func WithInfo(templ Template, info Info) Template { + if manager, ok := info.(InfoManager); ok { + return &templateInfoManager{ + Template: templ, + InfoManager: manager, + } + } + + return &templateInfo{ + Template: templ, + Info: info, + } } var baseOfRe = regexp.MustCompile("template: (.*?):") @@ -117,10 +130,6 @@ func extractBaseOf(err string) string { return "" } -func (t *TemplateInfo) TemplateInfo() Info { - return t.Info -} - // TemplateFuncGetter allows to find a template func by name. type TemplateFuncGetter interface { GetFunc(name string) (reflect.Value, bool) diff --git a/tpl/template_info.go b/tpl/template_info.go index be0566958..d9b438138 100644 --- a/tpl/template_info.go +++ b/tpl/template_info.go @@ -13,12 +13,44 @@ package tpl +import ( + "github.com/gohugoio/hugo/identity" +) + // Increments on breaking changes. const TemplateVersion = 2 -// Info holds some info extracted from a parsed template. -type Info struct { +type Info interface { + ParseInfo() ParseInfo + // Identifies this template and its dependencies. + identity.Provider +} + +type InfoManager interface { + ParseInfo() ParseInfo + + // Identifies and manages this template and its dependencies. + identity.Manager +} + +type defaultInfo struct { + identity.Manager + parseInfo ParseInfo +} + +func NewInfo(id identity.Manager, parseInfo ParseInfo) Info { + return &defaultInfo{ + Manager: id, + parseInfo: parseInfo, + } +} + +func (info *defaultInfo) ParseInfo() ParseInfo { + return info.parseInfo +} + +type ParseInfo struct { // Set for shortcode templates with any {{ .Inner }} IsInner bool @@ -26,17 +58,25 @@ type Info struct { HasReturn bool // Config extracted from template. - Config Config + Config ParseConfig } -func (info Info) IsZero() bool { +func (info ParseInfo) IsZero() bool { return info.Config.Version == 0 } -type Config struct { +// Info holds some info extracted from a parsed template. +type Info1 struct { +} + +type ParseConfig struct { Version int } -var DefaultConfig = Config{ +var DefaultParseConfig = ParseConfig{ Version: TemplateVersion, } + +var DefaultParseInfo = ParseInfo{ + Config: DefaultParseConfig, +} diff --git a/tpl/tplimpl/shortcodes.go b/tpl/tplimpl/shortcodes.go index e5dbabdd8..abef11e1e 100644 --- a/tpl/tplimpl/shortcodes.go +++ b/tpl/tplimpl/shortcodes.go @@ -83,10 +83,12 @@ func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVari func (s *shortcodeTemplates) compareVariants(a, b []string) int { weight := 0 + k := len(a) for i, av := range a { bv := b[i] if av == bv { - weight++ + // Add more weight to the left side (language...). + weight = weight + k - i } else { weight-- } diff --git a/tpl/tplimpl/shortcodes_test.go b/tpl/tplimpl/shortcodes_test.go index 08200444d..4ef8c5cd7 100644 --- a/tpl/tplimpl/shortcodes_test.go +++ b/tpl/tplimpl/shortcodes_test.go @@ -53,10 +53,10 @@ func TestShortcodesTemplate(t *testing.T) { name2 string expected int }{ - {"Same suffix", "figure.html", "figure.html", 3}, - {"Same suffix and output format", "figure.html.html", "figure.html.html", 3}, - {"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 3}, - {"No suffix", "figure", "figure", 3}, + {"Same suffix", "figure.html", "figure.html", 6}, + {"Same suffix and output format", "figure.html.html", "figure.html.html", 6}, + {"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 6}, + {"No suffix", "figure", "figure", 6}, {"Different output format", "figure.amp.html", "figure.html.html", -1}, {"One with output format, one without", "figure.amp.html", "figure.html", -1}, } diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go index dd8de9067..2d2a63cf9 100644 --- a/tpl/tplimpl/template.go +++ b/tpl/tplimpl/template.go @@ -20,6 +20,10 @@ import ( "regexp" "time" + "github.com/gohugoio/hugo/hugofs/files" + + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/common/herrors" "strings" @@ -27,7 +31,6 @@ import ( 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" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/tpl/tplimpl/embedded" @@ -81,6 +84,7 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler { common := &templatesCommon{ nameBaseTemplateName: make(map[string]string), transformNotFound: make(map[string]bool), + identityNotFound: make(map[string][]identity.Manager), } htmlT := &htmlTemplates{ @@ -100,13 +104,16 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler { Deps: deps, layoutsFs: deps.BaseFs.Layouts.Fs, templateHandlerCommon: &templateHandlerCommon{ - shortcodes: make(map[string]*shortcodeTemplates), - templateInfo: make(map[string]tpl.Info), - html: htmlT, - text: textT, + shortcodes: make(map[string]*shortcodeTemplates), + templateInfo: make(map[string]tpl.Info), + templateInfoTree: make(map[string]*templateInfoTree), + html: htmlT, + text: textT, }, } + textT.textTemplate.templates = textT + textT.standalone.templates = textT common.handler = h return h @@ -152,27 +159,26 @@ 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) { - templ, err := tt.New(name).Parse(tpl) +func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, templstr string) (*templateContext, error) { + templ, err := tt.New(name).Parse(templstr) if err != nil { return nil, err } typ := resolveTemplateType(name) - c, err := applyTemplateTransformersToHMLTTemplate(typ, templ) + c, err := t.handler.applyTemplateTransformersToHMLTTemplate(typ, templ) if err != nil { return nil, err } - for k := range c.notFound { + for k := range c.templateNotFound { t.transformNotFound[k] = true + t.identityNotFound[k] = append(t.identityNotFound[k], c.id) } - if typ == templateShortcode { - t.handler.addShortcodeVariant(name, c.Info, templ) - } else { - t.handler.templateInfo[name] = c.Info + for k := range c.identityNotFound { + t.identityNotFound[k] = append(t.identityNotFound[k], c.id) } return c, nil @@ -208,7 +214,7 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin // * https://github.com/golang/go/issues/16101 // * https://github.com/gohugoio/hugo/issues/2549 overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - if _, err := applyTemplateTransformersToHMLTTemplate(templateUndefined, overlayTpl); err != nil { + if _, err := t.handler.applyTemplateTransformersToHMLTTemplate(templateUndefined, overlayTpl); err != nil { return err } @@ -253,6 +259,8 @@ func (l nopLookupVariant) LookupVariant(name string, variants tpl.TemplateVarian // It implements the templateLoader and tpl.TemplateHandler interfaces. // There is one templateHandler created per Site. type templateHandler struct { + ready bool + executor texttemplate.Executer funcs map[string]reflect.Value @@ -324,6 +332,7 @@ func (t *templateHandler) LoadTemplates(prefix string) error { // 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. @@ -345,6 +354,9 @@ func (t *templateHandler) Lookup(name string) (tpl.Template, bool) { // 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) { + if !t.ready { + panic("handler not ready") + } name = templateBaseName(templateShortcode, name) s, found := t.shortcodes[name] if !found { @@ -358,18 +370,17 @@ func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVarian more := len(s.variants) > 1 - return &tpl.TemplateInfo{ - Template: sv.templ, - Info: sv.info, - }, true, more + return tpl.WithInfo(sv.templ, sv.info), true, more } -// MarkReady marks the templates as "ready for execution". No changes allowed +// 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 { +func (t *templateHandler) markReady() error { + defer func() { + t.ready = true + }() + if err := t.postTransform(); err != nil { return err } @@ -483,6 +494,7 @@ func (t *templateHandler) addInternalTemplate(name, tpl string) error { } func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) { + base := templateBaseName(templateShortcode, name) shortcodename, variants := templateNameAndVariants(base) @@ -561,18 +573,9 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e } 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 templ != nil { if info, found := t.templateInfo[templ.Name()]; found { - return &tpl.TemplateInfo{ - Template: templ, - Info: info, - }, true + return tpl.WithInfo(templ, info), true } } @@ -586,7 +589,11 @@ func (t *templateHandler) checkState() { } func (t *templateHandler) clone(d *deps.Deps) *templateHandler { + if !t.ready { + panic("invalid state") + } c := &templateHandler{ + ready: true, Deps: d, layoutsFs: d.BaseFs.Layouts.Fs, } @@ -703,36 +710,69 @@ func (t *templateHandler) loadTemplates(prefix string) error { } -func (t *templateHandler) postTransform() error { - if len(t.html.transformNotFound) == 0 && len(t.text.transformNotFound) == 0 { - return nil +func (t *templateHandler) getOrCreateTemplateInfo(name string) (identity.Manager, tpl.ParseInfo) { + info, found := t.templateInfo[name] + if found { + return info.(identity.Manager), info.ParseInfo() + } + return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), tpl.DefaultParseInfo +} + +func (t *templateHandler) createTemplateInfo(name string) (identity.Manager, tpl.ParseInfo) { + _, found := t.templateInfo[name] + if found { + panic("already created: " + name) } - defer func() { - t.text.transformNotFound = make(map[string]bool) - t.html.transformNotFound = make(map[string]bool) - }() + return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), tpl.DefaultParseInfo +} + +func (t *templateHandler) postTransform() error { + for k, v := range t.templateInfoTree { + if v.id != nil { + info := tpl.NewInfo( + v.id, + v.info, + ) + t.templateInfo[k] = info + + if v.typ == templateShortcode { + t.addShortcodeVariant(k, info, v.templ) + } + } + } for _, s := range []struct { - lookup func(name string) *parse.Tree + lookup func(name string) *templateInfoTree transformNotFound map[string]bool + identityNotFound map[string][]identity.Manager }{ // html templates - {func(name string) *parse.Tree { + {func(name string) *templateInfoTree { templ := t.html.lookup(name) if templ == nil { return nil } - return templ.Tree - }, t.html.transformNotFound}, + id, info := t.getOrCreateTemplateInfo(name) + return &templateInfoTree{ + id: id, + info: info, + tree: templ.Tree, + } + }, t.html.transformNotFound, t.html.identityNotFound}, // text templates - {func(name string) *parse.Tree { + {func(name string) *templateInfoTree { templT := t.text.lookup(name) if templT == nil { return nil } - return templT.Tree - }, t.text.transformNotFound}, + id, info := t.getOrCreateTemplateInfo(name) + return &templateInfoTree{ + id: id, + info: info, + tree: templT.Tree, + } + }, t.text.transformNotFound, t.text.identityNotFound}, } { for name := range s.transformNotFound { templ := s.lookup(name) @@ -743,6 +783,15 @@ func (t *templateHandler) postTransform() error { } } } + + for k, v := range s.identityNotFound { + tmpl := s.lookup(k) + if tmpl != nil { + for _, im := range v { + im.Add(tmpl.id) + } + } + } } return nil @@ -758,7 +807,6 @@ func (t *templateHandler) wrapTextTemplate(tt *textTemplate) tpl.TemplateParseFi tt, new(nopLookupVariant), } - } type templateHandlerCommon struct { @@ -771,6 +819,9 @@ type templateHandlerCommon struct { // shortcodeTemplates type. templateInfo map[string]tpl.Info + // Used to track templates during the AST transformations. + templateInfoTree map[string]*templateInfoTree + // text holds all the pure text templates. text *textTemplates html *htmlTemplates @@ -795,9 +846,12 @@ type templatesCommon struct { // Used to get proper filenames in errors nameBaseTemplateName map[string]string - // Holds names of the templates not found during the first AST transformation + // Holds names of the template definitions not found during the first AST transformation // pass. transformNotFound map[string]bool + + // Holds identities of templates not found during first pass. + identityNotFound map[string][]identity.Manager } func (t templatesCommon) withNewHandler(h *templateHandler) *templatesCommon { @@ -806,8 +860,9 @@ func (t templatesCommon) withNewHandler(h *templateHandler) *templatesCommon { } type textTemplate struct { - mu sync.RWMutex - t *texttemplate.Template + mu sync.RWMutex + t *texttemplate.Template + templates *textTemplates } func (t *textTemplate) Lookup(name string) (tpl.Template, bool) { @@ -831,7 +886,7 @@ func (t *textTemplate) parseIn(tt *texttemplate.Template, name, tpl string) (*te return nil, err } - if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil { + if _, err := t.templates.handler.applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil { return nil, err } return templ, nil @@ -868,30 +923,24 @@ 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) { +func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tplstr string) (*templateContext, error) { name = strings.TrimPrefix(name, textTmplNamePrefix) - templ, err := t.parseIn(tt, name, tpl) + templ, err := t.parseIn(tt, name, tplstr) if err != nil { return nil, err } typ := resolveTemplateType(name) - c, err := applyTemplateTransformersToTextTemplate(typ, templ) + c, err := t.handler.applyTemplateTransformersToTextTemplate(typ, templ) if err != nil { return nil, err } - for k := range c.notFound { + for k := range c.templateNotFound { t.transformNotFound[k] = true } - if typ == templateShortcode { - t.handler.addShortcodeVariant(name, c.Info, templ) - } else { - t.handler.templateInfo[name] = c.Info - } - return c, nil } @@ -924,7 +973,7 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin } overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil { + if _, err := t.handler.applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil { return err } t.overlays[name] = overlayTpl diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go index 910c0be89..68de00561 100644 --- a/tpl/tplimpl/templateProvider.go +++ b/tpl/tplimpl/templateProvider.go @@ -44,16 +44,13 @@ func (*TemplateProvider) Update(deps *deps.Deps) error { } - return newTmpl.MarkReady() + return newTmpl.markReady() } // Clone clones. func (*TemplateProvider) Clone(d *deps.Deps) error { - t := d.Tmpl.(*templateHandler) - clone := t.clone(d) - - return clone.MarkReady() - + t.clone(d) + return nil } diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go index 31d24b71d..997126a32 100644 --- a/tpl/tplimpl/template_ast_transformers.go +++ b/tpl/tplimpl/template_ast_transformers.go @@ -14,8 +14,12 @@ package tplimpl import ( - template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" + "regexp" + "strings" + "github.com/gohugoio/hugo/identity" + + 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" @@ -34,9 +38,10 @@ const ( ) type templateContext struct { - visited map[string]bool - notFound map[string]bool - lookupFn func(name string) *parse.Tree + visited map[string]bool + templateNotFound map[string]bool + identityNotFound map[string]bool + lookupFn func(name string) *templateInfoTree // The last error encountered. err error @@ -47,13 +52,14 @@ type templateContext struct { configChecked bool // Contains some info about the template - tpl.Info + parseInfo *tpl.ParseInfo + id identity.Manager // Store away the return node in partials. returnNode *parse.CommandNode } -func (c templateContext) getIfNotVisited(name string) *parse.Tree { +func (c templateContext) getIfNotVisited(name string) *templateInfoTree { if c.visited[name] { return nil } @@ -63,59 +69,95 @@ func (c templateContext) getIfNotVisited(name string) *parse.Tree { // This may be a inline template defined outside of this file // and not yet parsed. Unusual, but it happens. // Store the name to try again later. - c.notFound[name] = true + c.templateNotFound[name] = true } return templ } -func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext { - return &templateContext{ - Info: tpl.Info{Config: tpl.DefaultConfig}, - lookupFn: lookupFn, - visited: make(map[string]bool), - notFound: make(map[string]bool)} -} +func newTemplateContext( + id identity.Manager, + info *tpl.ParseInfo, + lookupFn func(name string) *templateInfoTree) *templateContext { -func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree { - return func(nn string) *parse.Tree { - tt := templ.Lookup(nn) - if tt != nil { - return tt.Tree - } - return nil + return &templateContext{ + id: id, + parseInfo: info, + lookupFn: lookupFn, + visited: make(map[string]bool), + templateNotFound: make(map[string]bool), + identityNotFound: make(map[string]bool), } } -func applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) { - return applyTemplateTransformers(typ, templ.Tree, createParseTreeLookup(templ)) +func createGetTemplateInfoTreeFor(getID func(name string) *templateInfoTree) func(nn string) *templateInfoTree { + return func(nn string) *templateInfoTree { + return getID(nn) + } } -func applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) { - return applyTemplateTransformers(typ, templ.Tree, - func(nn string) *parse.Tree { - tt := templ.Lookup(nn) - if tt != nil { - return tt.Tree - } - return nil - }) +func (t *templateHandler) applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) { + id, info := t.createTemplateInfo(templ.Name()) + ti := &templateInfoTree{ + tree: templ.Tree, + templ: templ, + typ: typ, + id: id, + info: info, + } + t.templateInfoTree[templ.Name()] = ti + getTemplateInfoTree := createGetTemplateInfoTreeFor(func(name string) *templateInfoTree { + return t.templateInfoTree[name] + }) + + return applyTemplateTransformers(typ, ti, getTemplateInfoTree) } -func applyTemplateTransformers(typ templateType, templ *parse.Tree, lookupFn func(name string) *parse.Tree) (*templateContext, error) { +func (t *templateHandler) applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) { + id, info := t.createTemplateInfo(templ.Name()) + ti := &templateInfoTree{ + tree: templ.Tree, + templ: templ, + typ: typ, + id: id, + info: info, + } + + t.templateInfoTree[templ.Name()] = ti + getTemplateInfoTree := createGetTemplateInfoTreeFor(func(name string) *templateInfoTree { + return t.templateInfoTree[name] + }) + + return applyTemplateTransformers(typ, ti, getTemplateInfoTree) + +} + +type templateInfoTree struct { + info tpl.ParseInfo + typ templateType + id identity.Manager + templ tpl.Template + tree *parse.Tree +} + +func applyTemplateTransformers( + typ templateType, + templ *templateInfoTree, + lookupFn func(name string) *templateInfoTree) (*templateContext, error) { + if templ == nil { return nil, errors.New("expected template, but none provided") } - c := newTemplateContext(lookupFn) + c := newTemplateContext(templ.id, &templ.info, lookupFn) c.typ = typ - _, err := c.applyTransformations(templ.Root) + _, err := c.applyTransformations(templ.tree.Root) if err == nil && c.returnNode != nil { // This is a partial with a return statement. - c.Info.HasReturn = true - templ.Root = c.wrapInPartialReturnWrapper(templ.Root) + c.parseInfo.HasReturn = true + templ.tree.Root = c.wrapInPartialReturnWrapper(templ.tree.Root) } return c, err @@ -125,7 +167,9 @@ const ( partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ with .Arg }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}` ) -var partialReturnWrapper *parse.ListNode +var ( + partialReturnWrapper *parse.ListNode +) func init() { templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl) @@ -133,6 +177,7 @@ func init() { panic(err) } partialReturnWrapper = templ.Tree.Root + } func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode { @@ -156,6 +201,7 @@ func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.L // getif works slightly different than the Go built-in in that it also // considers any IsZero methods on the values (as in time.Time). // See https://github.com/gohugoio/hugo/issues/5738 +// TODO(bep) get rid of this. func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) { if len(p.Cmds) == 0 { return @@ -176,9 +222,9 @@ func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) { } // applyTransformations do 3 things: -// 1) Make all .Params.CamelCase and similar into lowercase. -// 2) Wraps every with and if pipe in getif -// 3) Collects some information about the template content. +// 1) Wraps every with and if pipe in getif +// 2) Parses partial return statement. +// 3) Tracks template (partial) dependencies and some other info. func (c *templateContext) applyTransformations(n parse.Node) (bool, error) { switch x := n.(type) { case *parse.ListNode: @@ -198,7 +244,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) { case *parse.TemplateNode: subTempl := c.getIfNotVisited(x.Name) if subTempl != nil { - c.applyTransformationsToNodes(subTempl.Root) + c.applyTransformationsToNodes(subTempl.tree.Root) } case *parse.PipeNode: c.collectConfig(x) @@ -210,6 +256,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) { } case *parse.CommandNode: + c.collectPartialInfo(x) c.collectInner(x) keep := c.collectReturnNode(x) @@ -277,11 +324,10 @@ func (c *templateContext) collectConfig(n *parse.PipeNode) { c.err = errors.Wrap(err, errMsg) return } - if err := mapstructure.WeakDecode(m, &c.Info.Config); err != nil { + if err := mapstructure.WeakDecode(m, &c.parseInfo.Config); err != nil { c.err = errors.Wrap(err, errMsg) } } - } // collectInner determines if the given CommandNode represents a @@ -290,7 +336,7 @@ func (c *templateContext) collectInner(n *parse.CommandNode) { if c.typ != templateShortcode { return } - if c.Info.IsInner || len(n.Args) == 0 { + if c.parseInfo.IsInner || len(n.Args) == 0 { return } @@ -304,13 +350,45 @@ func (c *templateContext) collectInner(n *parse.CommandNode) { } if c.hasIdent(idents, "Inner") { - c.Info.IsInner = true + c.parseInfo.IsInner = true break } } } +var partialRe = regexp.MustCompile(`^partial(Cached)?$|^partials\.Include(Cached)?$`) + +func (c *templateContext) collectPartialInfo(x *parse.CommandNode) { + if len(x.Args) < 2 { + return + } + + first := x.Args[0] + var id string + switch v := first.(type) { + case *parse.IdentifierNode: + id = v.Ident + case *parse.ChainNode: + id = v.String() + } + + if partialRe.MatchString(id) { + partialName := strings.Trim(x.Args[1].String(), "\"") + if !strings.Contains(partialName, ".") { + partialName += ".html" + } + partialName = "partials/" + partialName + info := c.lookupFn(partialName) + if info != nil { + c.id.Add(info.id) + } else { + // Delay for later + c.identityNotFound[partialName] = true + } + } +} + func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool { if c.typ != templatePartial || c.returnNode != nil { return true diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go index 0dc91ac32..1e2ff2124 100644 --- a/tpl/tplimpl/template_ast_transformers_test.go +++ b/tpl/tplimpl/template_ast_transformers_test.go @@ -15,14 +15,17 @@ package tplimpl import ( "strings" - template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" + "github.com/gohugoio/hugo/hugofs/files" "testing" "time" - "github.com/gohugoio/hugo/tpl" + template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" + "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/tpl" ) // Issue #2927 @@ -33,7 +36,7 @@ func TestTransformRecursiveTemplate(t *testing.T) { {{ define "menu-nodes" }} {{ template "menu-node" }} {{ end }} -{{ define "menu-node" }} +{{ define "menu-nßode" }} {{ template "menu-node" }} {{ end }} {{ template "menu-nodes" }} @@ -41,12 +44,25 @@ func TestTransformRecursiveTemplate(t *testing.T) { templ, err := template.New("foo").Parse(recursive) c.Assert(err, qt.IsNil) + parseInfo := tpl.DefaultParseInfo - ctx := newTemplateContext(createParseTreeLookup(templ)) + ctx := newTemplateContext( + newTemplateInfo("test").(identity.Manager), + &parseInfo, + createGetTemplateInfoTree(templ.Tree), + ) ctx.applyTransformations(templ.Tree.Root) } +func createGetTemplateInfoTree(tree *parse.Tree) func(name string) *templateInfoTree { + return func(name string) *templateInfoTree { + return &templateInfoTree{ + tree: tree, + } + } +} + type I interface { Method0() } @@ -80,13 +96,10 @@ func TestInsertIsZeroFunc(t *testing.T) { {{ with .TimeZero }}.TimeZero1 with: {{ . }}{{ else }}.TimeZero1 with: FALSE{{ end }} {{ template "mytemplate" . }} {{ if .T.NonEmptyInterfaceTypedNil }}.NonEmptyInterfaceTypedNil: TRUE{{ else }}.NonEmptyInterfaceTypedNil: FALSE{{ end }} - {{ template "other-file-template" . }} - {{ define "mytemplate" }} {{ if .TimeZero }}.TimeZero1: mytemplate: TRUE{{ else }}.TimeZero1: mytemplate: FALSE{{ end }} {{ end }} - ` // https://github.com/gohugoio/hugo/issues/5865 @@ -97,7 +110,7 @@ func TestInsertIsZeroFunc(t *testing.T) { ) d := newD(c) - h := d.Tmpl.(tpl.TemplateManager) + h := d.Tmpl.(*templateHandler) // HTML templates c.Assert(h.AddTemplate("mytemplate.html", templ1), qt.IsNil) @@ -107,15 +120,13 @@ func TestInsertIsZeroFunc(t *testing.T) { c.Assert(h.AddTemplate("_text/mytexttemplate.txt", templ1), qt.IsNil) c.Assert(h.AddTemplate("_text/myothertexttemplate.txt", templ2), qt.IsNil) - c.Assert(h.MarkReady(), qt.IsNil) + c.Assert(h.markReady(), qt.IsNil) for _, name := range []string{"mytemplate.html", "mytexttemplate.txt"} { + var sb strings.Builder tt, _ := d.Tmpl.Lookup(name) - sb := &strings.Builder{} - - err := d.Tmpl.Execute(tt, sb, ctx) + err := h.Execute(tt, &sb, ctx) c.Assert(err, qt.IsNil) - result := sb.String() c.Assert(result, qt.Contains, ".True: TRUE") @@ -138,14 +149,10 @@ func TestCollectInfo(t *testing.T) { tests := []struct { name string tplString string - expected tpl.Info + expected tpl.ParseInfo }{ - {"Basic Inner", `{{ .Inner }}`, tpl.Info{IsInner: true, Config: tpl.DefaultConfig}}, - {"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.Info{ - Config: tpl.Config{ - Version: 42, - }, - }}, + {"Basic Inner", `{{ .Inner }}`, tpl.ParseInfo{IsInner: true, Config: tpl.DefaultParseConfig}}, + {"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.ParseInfo{Config: tpl.ParseConfig{Version: 42}}}, } echo := func(in interface{}) interface{} { @@ -162,12 +169,13 @@ func TestCollectInfo(t *testing.T) { templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString) c.Assert(err, qt.IsNil) + parseInfo := tpl.DefaultParseInfo - ctx := newTemplateContext(createParseTreeLookup(templ)) + ctx := newTemplateContext( + newTemplateInfo("test").(identity.Manager), &parseInfo, createGetTemplateInfoTree(templ.Tree)) ctx.typ = templateShortcode ctx.applyTransformations(templ.Tree.Root) - - c.Assert(ctx.Info, qt.Equals, test.expected) + c.Assert(ctx.parseInfo, qt.DeepEquals, &test.expected) }) } @@ -205,7 +213,10 @@ func TestPartialReturn(t *testing.T) { templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString) c.Assert(err, qt.IsNil) - _, err = applyTemplateTransformers(templatePartial, templ.Tree, createParseTreeLookup(templ)) + _, err = applyTemplateTransformers( + templatePartial, + &templateInfoTree{tree: templ.Tree, info: tpl.DefaultParseInfo}, + createGetTemplateInfoTree(templ.Tree)) // Just check that it doesn't fail in this test. We have functional tests // in hugoblib. @@ -215,3 +226,10 @@ func TestPartialReturn(t *testing.T) { } } + +func newTemplateInfo(name string) tpl.Info { + return tpl.NewInfo( + identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), + tpl.DefaultParseInfo, + ) +} diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go index 2098732f6..6be8aa8b7 100644 --- a/tpl/tplimpl/template_funcs.go +++ b/tpl/tplimpl/template_funcs.go @@ -19,6 +19,8 @@ import ( "reflect" "strings" + "github.com/gohugoio/hugo/tpl" + "github.com/gohugoio/hugo/common/maps" template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" @@ -62,14 +64,14 @@ type templateExecHelper struct { funcs map[string]reflect.Value } -func (t *templateExecHelper) GetFunc(name string) (reflect.Value, bool) { +func (t *templateExecHelper) GetFunc(tmpl texttemplate.Preparer, 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) { +func (t *templateExecHelper) GetMapValue(tmpl texttemplate.Preparer, receiver, key reflect.Value) (reflect.Value, bool) { if params, ok := receiver.Interface().(maps.Params); ok { // Case insensitive. keystr := strings.ToLower(key.String()) @@ -85,6 +87,22 @@ func (t *templateExecHelper) GetMapValue(receiver, key reflect.Value) (reflect.V return v, v.IsValid() } +func (t *templateExecHelper) GetMethod(tmpl texttemplate.Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) { + // This is a hot path and receiver.MethodByName really shows up in the benchmarks. + // Page.Render is the only method with a WithTemplateInfo as of now, so let's just + // check that for now. + // TODO(bep) find a more flexible, but still fast, way. + if name == "Render" { + if info, ok := tmpl.(tpl.Info); ok { + if m := receiver.MethodByName(name + "WithTemplateInfo"); m.IsValid() { + return m, reflect.ValueOf(info) + } + } + } + + return receiver.MethodByName(name), zero +} + func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) { funcs := createFuncMap(d) funcsv := make(map[string]reflect.Value) @@ -120,9 +138,7 @@ func createFuncMap(d *deps.Deps) map[string]interface{} { } funcMap[alias] = mm.Method } - } - } if d.OverloadedTemplateFuncs != nil { diff --git a/tpl/tplimpl/template_info_test.go b/tpl/tplimpl/template_info_test.go index 6841b4c47..e72e859ed 100644 --- a/tpl/tplimpl/template_info_test.go +++ b/tpl/tplimpl/template_info_test.go @@ -24,18 +24,19 @@ import ( func TestTemplateInfoShortcode(t *testing.T) { c := qt.New(t) d := newD(c) - h := d.Tmpl.(tpl.TemplateManager) + h := d.Tmpl.(*templateHandler) c.Assert(h.AddTemplate("shortcodes/mytemplate.html", ` {{ .Inner }} `), qt.IsNil) + c.Assert(h.markReady(), qt.IsNil) tt, found, _ := d.Tmpl.LookupVariant("mytemplate", tpl.TemplateVariants{}) c.Assert(found, qt.Equals, true) - tti, ok := tt.(tpl.TemplateInfoProvider) + tti, ok := tt.(tpl.Info) c.Assert(ok, qt.Equals, true) - c.Assert(tti.TemplateInfo().IsInner, qt.Equals, true) + c.Assert(tti.ParseInfo().IsInner, qt.Equals, true) }