ref: e625088ef5a970388ad50e464e87db56b358dac4
parent: 67f3aa72cf9aaf3d6e447fa6bc12de704d46adf7
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Wed Nov 27 08:42:36 EST 2019
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
--- /dev/null
+++ b/docs/content/en/functions/RenderString.md
@@ -1,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 ´<p></p>` 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
--- a/docs/content/en/getting-started/configuration-markup.md
+++ b/docs/content/en/getting-started/configuration-markup.md
@@ -74,3 +74,62 @@
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" >}}
+<a href="{{ .Destination | safeURL }}"{{ with .Title}}title="{{ . }}"{{ end }}>{{ .Text }}{{ with .Page }} (in page {{ .Title }}){{ end }}"</a>
+{{< /code >}}
--- a/helpers/content.go
+++ b/helpers/content.go
@@ -25,6 +25,8 @@
"github.com/gohugoio/hugo/common/loggers"
+ "github.com/spf13/afero"
+
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup"
@@ -31,7 +33,6 @@
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/config"
- "github.com/spf13/afero"
"strings"
)
@@ -78,6 +79,7 @@
ContentFs: contentFs,
Logger: logger,
})
+
if err != nil {
return nil, err
}
--- /dev/null
+++ b/hugolib/content_render_hooks_test.go
@@ -1,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", `
+<div class="foo">
+{{ .Inner | markdownify }}
+</div>
+`)
+ 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", `
+<p>Cool Page|https://www.google.com|Title: Google's Homepage|Text: First Link|END</p>
+Text: Second
+SHORT3|
+<p>IMAGE: Cool Page||/images/Dragster.jpg|Title: image title|Text: Drag Racing|END</p>
+`)
+
+ 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", `<p>IMAGE: Cool Page With Image||/images/Dragster.jpg|Title: image title|Text: Drag Racing|END</p>`)
+ // The regular markdownify func currently gets regular links.
+ b.AssertFileContent("public/blog/p5/index.html", "Inner Link: <a href=\"https://www.google.com\" title=\"Google's Homepage\">Inner Link</a>\n</div>")
+
+ 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: <p>Inner Link: With RenderString|https://www.gohugo.io|Title: Hugo's Homepage|Text: Inner Link|END</p>",
+ )
+
+ 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", `<p>EDITED: https://www.google.com|</p>`, "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|</p>`)
+ b.AssertFileContent("public/blog/p4/index.html", `IMAGE EDITED: /images/Dragster.jpg|`)
+ b.AssertFileContent("public/blog/p6/index.html", "<p>Inner Link: EDITED: https://www.gohugo.io|</p>")
+
+}
+
+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: <p>P1. html-link: https://www.gohugo.io|</p>")
+ b.AssertFileContent("public/index.xml", `
+P2: <p>P1. xml-link: https://www.bep.is|</p>
+P3: <p>P3. xml-link: https://www.example.org|</p>
+`)
+
+}
+
+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:<strong>Bold Markdown</strong>:REND
+RSTART:<p><strong>Bold Block Markdown</strong></p>
+RSTART:<em>italic org mode</em>:REND
+`)
+
+}
--- a/hugolib/filesystems/basefs.go
+++ b/hugolib/filesystems/basefs.go
@@ -126,10 +126,28 @@
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 @@
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 @@
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 @@
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,7 +425,7 @@
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...)
@@ -399,7 +432,7 @@
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 @@
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 @@
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
--- a/hugolib/hugo_modules_test.go
+++ b/hugolib/hugo_modules_test.go
@@ -40,6 +40,9 @@
// 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 {
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -20,6 +20,8 @@
"strings"
"sync"
+ "github.com/gohugoio/hugo/identity"
+
radix "github.com/armon/go-radix"
"github.com/gohugoio/hugo/output"
@@ -411,7 +413,6 @@
}
d.OutputFormatsConfig = s.outputFormatsConfig
}
-
}
return nil
@@ -806,12 +807,40 @@
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.
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -71,7 +71,7 @@
if conf.whatChanged == nil {
// Assume everything has changed
- conf.whatChanged = &whatChanged{source: true, other: true}
+ conf.whatChanged = &whatChanged{source: true}
}
var prepareErr error
--- a/hugolib/hugo_sites_build_test.go
+++ b/hugolib/hugo_sites_build_test.go
@@ -1459,3 +1459,19 @@
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`)
+}
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -23,6 +23,12 @@
"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 @@
"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 (
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 @@
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 @@
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 @@
// 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 @@
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) {
--- a/hugolib/page__content.go
+++ b/hugolib/page__content.go
@@ -30,8 +30,7 @@
type pageContent struct {
renderable bool
selfLayout string
-
- truncated bool
+ truncated bool
cmap *pageContentMap
--- a/hugolib/page__meta.go
+++ b/hugolib/page__meta.go
@@ -592,7 +592,7 @@
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,25 +651,37 @@
markup = "markdown"
}
- cp := p.s.ContentSpec.Converters.Get(markup)
- if cp == nil {
- return errors.Errorf("no content renderer found for markup %q", p.markup)
+ cp, err := p.newContentConverter(ps, markup, renderingConfigOverrides)
+ if err != nil {
+ return err
}
+ p.contentConverter = cp
+ }
- cpp, err := cp.New(converter.DocumentContext{
+ 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 err
- }
- p.contentConverter = cpp
+ if err != nil {
+ return nil, err
}
- return nil
-
+ return cpp, nil
}
// The output formats this page will be rendered to.
--- a/hugolib/page__new.go
+++ b/hugolib/page__new.go
@@ -112,7 +112,7 @@
}
}
- if err := metaProvider.applyDefaultValues(); err != nil {
+ if err := metaProvider.applyDefaultValues(ps); err != nil {
return err
}
@@ -134,7 +134,7 @@
}
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 @@
return ps.wrapError(err)
}
- if err := metaProvider.applyDefaultValues(); err != nil {
+ if err := metaProvider.applyDefaultValues(ps); err != nil {
return err
}
@@ -242,11 +242,7 @@
}
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 {
return nil, err
@@ -264,18 +260,18 @@
}
_, 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
}
--- a/hugolib/page__output.go
+++ b/hugolib/page__output.go
@@ -14,6 +14,7 @@
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"
@@ -20,7 +21,6 @@
)
func newPageOutput(
- cp *pageContentOutput, // may be nil
ps *pageState,
pp pagePaths,
f output.Format,
@@ -45,25 +45,11 @@
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,
@@ -70,11 +56,12 @@
}
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 @@
// 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()
}
+
}
--- a/hugolib/page__per_output.go
+++ b/hugolib/page__per_output.go
@@ -23,6 +23,10 @@
"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,153 +62,175 @@
}
)
-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
+ 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()))
}
- 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
+ if err := po.initRenderHooks(); err != nil {
+ return err
+ }
- cp.contentPlaceholders, hasVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
- if err != nil {
- return err
- }
+ var hasShortcodeVariants bool
- if p.render && !hasVariants {
- // We can reuse this for the other output formats
- cp.enableReuse()
- }
+ f := po.f
+ cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
+ if err != nil {
+ return err
+ }
- cp.workContent = p.contentToRender(cp.contentPlaceholders)
+ enableReuse := !(hasShortcodeVariants || cp.renderHooksHaveVariants)
- isHTML := cp.p.m.markup == "html"
+ 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()
+ }
- if p.renderable {
- if !isHTML {
- r, err := cp.renderContent(cp.workContent)
- if err != nil {
- return err
- }
- cp.convertedResult = r
- cp.workContent = r.Bytes()
+ cp.workContent = p.contentToRender(cp.contentPlaceholders)
- if _, ok := r.(converter.TableOfContentsProvider); !ok {
- tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
- cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
- cp.workContent = tmpContent
- }
- }
+ isHTML := cp.p.m.markup == "html"
- 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.renderable {
+ if !isHTML {
+ r, err := cp.renderContent(cp.workContent, true)
+ if err != nil {
+ return err
}
- 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
- }
+ 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
}
+ }
- if cp.p.source.hasSummaryDivider {
- if isHTML {
- src := p.source.parsed.Input()
+ 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)
+ }
- // 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 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.posBodyStart != -1 {
- cp.workContent = src[cp.p.source.posBodyStart:]
- }
+ if cp.p.source.hasSummaryDivider {
+ if isHTML {
+ src := p.source.parsed.Input()
- } 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)
- }
+ // 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])
}
- } else if cp.p.m.summary != "" {
- b, err := cp.p.getContentConverter().Convert(
- converter.RenderContext{
- Src: []byte(cp.p.m.summary),
- },
- )
+ 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 {
- return err
+ 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)
}
- html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
- cp.summary = helpers.BytesToHTML(html)
}
+ } 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()
+ cp.content = helpers.BytesToHTML(cp.workContent)
- 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()
- })
+ if !p.renderable {
+ err := cp.addSelfTemplate()
+ 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)
+ return nil
- if err := cp.setAutoSummary(); err != nil {
- return err, nil
- }
+ }
- return nil, 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()
+ })
+ }
- return cp, nil
+ 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.
@@ -211,7 +237,7 @@
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 @@
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 @@
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) 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 (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 @@
// these will be shifted out when rendering a given output format.
type pagePerOutputProviders interface {
targetPather
- page.ContentProvider
page.PaginatorProvider
- page.TableOfContentsProvider
resource.ResourceLinksProvider
}
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -95,12 +95,6 @@
Some more text
`
- simplePageWithEmbeddedScript = `---
-title: Simple
----
-<script type='text/javascript'>alert('the script tags are still there, right?');</script>
-`
-
simplePageWithSummaryDelimiterSameLine = `---
title: Simple
---
@@ -325,6 +319,7 @@
}
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)
}
--- a/hugolib/page_unwrap_test.go
+++ b/hugolib/page_unwrap_test.go
@@ -26,6 +26,7 @@
p := &pageState{}
c.Assert(mustUnwrap(newPageForShortcode(p)), qt.Equals, p)
+ c.Assert(mustUnwrap(newPageForRenderHook(p)), qt.Equals, p)
}
func mustUnwrap(v interface{}) page.Page {
--- a/hugolib/pagebundler_test.go
+++ b/hugolib/pagebundler_test.go
@@ -811,6 +811,7 @@
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)
--- a/hugolib/pagecollections.go
+++ b/hugolib/pagecollections.go
@@ -358,16 +358,6 @@
}
}
-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)
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -23,8 +23,6 @@
"html/template"
"path"
- "github.com/gohugoio/hugo/markup/converter"
-
"github.com/gohugoio/hugo/common/herrors"
"github.com/pkg/errors"
@@ -198,7 +196,7 @@
}
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,15 +347,10 @@
// 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.pageOutput.cp.renderContent([]byte(inner), false)
- b, err := p.getContentConverter().Convert(
- converter.RenderContext{
- Src: []byte(inner),
- },
- )
-
if err != nil {
return "", false, err
}
@@ -494,13 +487,13 @@
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 @@
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
--- a/hugolib/shortcode_page.go
+++ b/hugolib/shortcode_page.go
@@ -54,3 +54,22 @@
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)
+}
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -379,8 +379,13 @@
if s == nil {
return "<nil>"
}
+
+ 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) {
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -28,6 +28,12 @@
"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 @@
"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 @@
type whatChanged struct {
source bool
- other bool
files map[string]bool
}
@@ -888,10 +892,11 @@
// 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,12 +907,13 @@
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 @@
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
- if strings.Contains(ev.Name, "shortcodes") {
- shortcode := filepath.Base(ev.Name)
- shortcode = strings.TrimSuffix(shortcode, filepath.Ext(shortcode))
- shortcodesChanged[shortcode] = true
+ 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 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 @@
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 @@
}
}
- if len(dataChanged) > 0 {
+ if dataChanged {
s.h.init.data.Reset()
}
@@ -1018,18 +1021,7 @@
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,22 +1210,16 @@
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)
+ return identity.PathIdentity{}, false
}
-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)
-}
-
func (s *Site) readAndProcessContent(filenames ...string) error {
sourceSpec := source.NewSourceSpec(s.PathSpec, s.BaseFs.Content.Fs)
@@ -1560,6 +1546,26 @@
var infoOnMissingLayout = map[string]bool{
// The 404 layout is very much optional in Hugo, but we do look for it.
"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) {
--- a/hugolib/site_benchmark_new_test.go
+++ b/hugolib/site_benchmark_new_test.go
@@ -127,6 +127,36 @@
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", `<a href="{{ .Destination | safeURL }}#custom">CUSTOM LINK</a>`)
data, err := ioutil.ReadFile(filepath.FromSlash("testdata/what-is-markdown.md"))
sb.Assert(err, qt.IsNil)
datastr := string(data)
--- a/hugolib/template_test.go
+++ b/hugolib/template_test.go
@@ -18,8 +18,12 @@
"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 cached2: {{ partialCached "p1" "input2" $key1 }}
Partial cached3: {{ partialCached "p1" "input3" $key2 }}
`,
+
"partials/p1.html", `partial: {{ . }}`,
)
@@ -330,4 +335,86 @@
Partial cached2: partial: input1
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(" ")
+ }
}
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -389,8 +389,9 @@
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
@@ -961,10 +962,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
--- /dev/null
+++ b/identity/identity.go
@@ -1,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())
+}
--- /dev/null
+++ b/identity/identity_test.go
@@ -1,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
+}
--- a/markup/asciidoc/convert.go
+++ b/markup/asciidoc/convert.go
@@ -18,6 +18,7 @@
import (
"os/exec"
+ "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/internal"
"github.com/gohugoio/hugo/markup/converter"
@@ -45,6 +46,10 @@
func (a *asciidocConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
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
--- 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"
@@ -70,6 +71,10 @@
r := c.getHTMLRenderer(ctx.RenderTOC)
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 {
--- a/markup/converter/converter.go
+++ b/markup/converter/converter.go
@@ -16,6 +16,8 @@
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 @@
// 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 @@
// 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 @@
// 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")
+)
--- /dev/null
+++ b/markup/converter/hooks/hooks.go
@@ -1,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
+}
--- a/markup/goldmark/convert.go
+++ b/markup/goldmark/convert.go
@@ -15,11 +15,14 @@
package goldmark
import (
+ "bufio"
"bytes"
"fmt"
"path/filepath"
"runtime/debug"
+ "github.com/gohugoio/hugo/identity"
+
"github.com/pkg/errors"
"github.com/spf13/afero"
@@ -26,10 +29,8 @@
"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 @@
}
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 @@
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,9 +146,12 @@
}
+var _ identity.IdentitiesProvider = (*converterResult)(nil)
+
type converterResult struct {
converter.Result
toc tableofcontents.Root
+ ids identity.Identities
}
func (c converterResult) TableOfContents() tableofcontents.Root {
@@ -152,6 +158,41 @@
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 @@
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 @@
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
+
+}
+
+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,
}
+}
- return buf, nil
+type parserContext struct {
+ parser.Context
}
-func newHighlighting(cfg highlight.Config) goldmark.Extender {
- style := styles.Get(cfg.Style)
- if style == nil {
- style = styles.Fallback
+func (p *parserContext) TableOfContents() tableofcontents.Root {
+ if v := p.Get(tocResultKey); v != nil {
+ return v.(tableofcontents.Root)
}
+ return tableofcontents.Root{}
+}
- e := hl.NewHighlighting(
+func newHighlighting(cfg highlight.Config) goldmark.Extender {
+ return hl.NewHighlighting(
hl.WithStyle(cfg.Style),
hl.WithGuessLanguage(cfg.GuessSyntax),
hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()),
@@ -230,6 +300,4 @@
}),
)
-
- return e
}
--- a/markup/goldmark/convert_test.go
+++ b/markup/goldmark/convert_test.go
@@ -38,6 +38,9 @@
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 @@
mconf := markup_config.Default
mconf.Highlight.NoClasses = false
+ mconf.Goldmark.Renderer.Unsafe = true
p, err := Provider.New(
converter.ProviderConfig{
@@ -106,15 +110,15 @@
},
)
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, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`)
+ // c.Assert(got, qt.Contains, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`)
// Header IDs
c.Assert(got, qt.Contains, `<h2 id="custom">Custom ID</h2>`, qt.Commentf(got))
@@ -136,6 +140,11 @@
c.Assert(got, qt.Contains, `footnote.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>`)
c.Assert(got, qt.Contains, `<section class="footnotes" role="doc-endnotes">`)
c.Assert(got, qt.Contains, `<dt>date</dt>`)
+
+ toc, ok := b.(converter.TableOfContentsProvider)
+ c.Assert(ok, qt.Equals, true)
+ tocHTML := toc.TableOfContents().ToHTML(1, 2, false)
+ c.Assert(tocHTML, qt.Contains, "TableOfContents")
}
--- /dev/null
+++ b/markup/goldmark/render_link.go
@@ -1,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("<img src=\"")
+ if r.Unsafe || !html.IsDangerousURL(n.Destination) {
+ _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
+ }
+ _, _ = w.WriteString(`" alt="`)
+ _, _ = w.Write(n.Text(source))
+ _ = w.WriteByte('"')
+ if n.Title != nil {
+ _, _ = w.WriteString(` title="`)
+ r.Writer.Write(w, n.Title)
+ _ = w.WriteByte('"')
+ }
+ if r.XHTML {
+ _, _ = w.WriteString(" />")
+ } 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("<a href=\"")
+ if r.Unsafe || !html.IsDangerousURL(n.Destination) {
+ _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
+ }
+ _ = w.WriteByte('"')
+ if n.Title != nil {
+ _, _ = w.WriteString(` title="`)
+ r.Writer.Write(w, n.Title)
+ _ = w.WriteByte('"')
+ }
+ _ = w.WriteByte('>')
+ } else {
+ _, _ = w.WriteString("</a>")
+ }
+ 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),
+ ))
+}
--- 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"
@@ -63,6 +64,10 @@
func (c *mmarkConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
r := getHTMLRenderer(c.ctx, c.b, c.cfg)
return mmark.Parse(ctx.Src, r, c.extensions), nil
+}
+
+func (c *mmarkConverter) Supports(feature identity.Identity) bool {
+ return false
}
func getHTMLRenderer(
--- a/markup/org/convert.go
+++ b/markup/org/convert.go
@@ -17,6 +17,8 @@
import (
"bytes"
+ "github.com/gohugoio/hugo/identity"
+
"github.com/gohugoio/hugo/markup/converter"
"github.com/niklasfasching/go-org/org"
"github.com/spf13/afero"
@@ -65,4 +67,8 @@
return converter.Bytes(ctx.Src), nil
}
return converter.Bytes([]byte(html)), nil
+}
+
+func (c *orgConverter) Supports(feature identity.Identity) bool {
+ return false
}
--- a/markup/pandoc/convert.go
+++ b/markup/pandoc/convert.go
@@ -17,6 +17,7 @@
import (
"os/exec"
+ "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/internal"
"github.com/gohugoio/hugo/markup/converter"
@@ -45,6 +46,10 @@
func (c *pandocConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
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.
--- a/markup/rst/convert.go
+++ b/markup/rst/convert.go
@@ -19,6 +19,7 @@
"os/exec"
"runtime"
+ "github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/internal"
"github.com/gohugoio/hugo/markup/converter"
@@ -46,6 +47,10 @@
func (c *rstConverter) Convert(ctx converter.RenderContext) (converter.Result, error) {
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
--- a/output/layout.go
+++ b/output/layout.go
@@ -37,8 +37,14 @@
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.
type LayoutHandler struct {
mu sync.RWMutex
@@ -89,7 +95,7 @@
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) addTypeVariations(vars ...string) {
for _, typeVar := range vars {
if !reservedSections[typeVar] {
+ if l.d.RenderingHook {
+ typeVar = typeVar + renderingHookRoot
+ }
l.typeVariations = append(l.typeVariations, typeVar)
}
}
@@ -115,18 +124,23 @@
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.RenderingHook {
+ b.addLayoutVariations(d.Kind)
+ } else {
+ if d.Layout != "" {
+ b.addLayoutVariations(d.Layout)
+ }
+ if d.Type != "" {
+ b.addTypeVariations(d.Type)
+ }
}
- if d.Type != "" {
- b.addTypeVariations(d.Type)
- }
-
switch d.Kind {
case "page":
b.addLayoutVariations("single")
@@ -159,7 +173,7 @@
}
isRSS := f.Name == RSSFormat.Name
- if isRSS {
+ if !d.RenderingHook && isRSS {
// The historic and common rss.xml case
b.addLayoutVariations("")
}
@@ -167,7 +181,7 @@
// 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")
}
@@ -174,7 +188,7 @@
layouts := b.resolveVariations()
- if isRSS {
+ if !d.RenderingHook && isRSS {
layouts = append(layouts, "_internal/_default/rss.xml")
}
--- a/output/layout_test.go
+++ b/output/layout_test.go
@@ -111,6 +111,9 @@
[]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()
--- a/resources/page/page.go
+++ b/resources/page/page.go
@@ -201,9 +201,10 @@
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.
--- a/resources/page/page_nop.go
+++ b/resources/page/page_nop.go
@@ -371,8 +371,12 @@
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 {
--- a/resources/page/testhelpers_test.go
+++ b/resources/page/testhelpers_test.go
@@ -446,7 +446,11 @@
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")
}
--- a/scripts/fork_go_templates/main.go
+++ b/scripts/fork_go_templates/main.go
@@ -59,6 +59,7 @@
"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(
--- a/tpl/internal/go_templates/texttemplate/exec.go
+++ b/tpl/internal/go_templates/texttemplate/exec.go
@@ -658,7 +658,7 @@
// 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.
}
--- a/tpl/internal/go_templates/texttemplate/hugo_template.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template.go
@@ -34,8 +34,9 @@
// 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 @@
state := &state{
helper: t.helper,
+ prep: p,
tmpl: tmpl,
wr: wr,
vars: []variable{{"$", value}},
@@ -75,7 +77,6 @@
// Prepare returns a template ready for execution.
func (t *Template) Prepare() (*Template, error) {
-
return t, nil
}
@@ -95,6 +96,7 @@
// 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 @@
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 @@
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 @@
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)
}
@@ -208,4 +224,80 @@
}
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
}
--- a/tpl/internal/go_templates/texttemplate/hugo_template_test.go
+++ b/tpl/internal/go_templates/texttemplate/hugo_template_test.go
@@ -27,10 +27,18 @@
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 @@
}), 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 @@
{{ print "foo" }}
{{ printf "hugo" }}
Map: {{ .M.A }}
+Method: {{ .Hello1 "v1" }}
`)
@@ -67,5 +84,6 @@
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")
}
--- a/tpl/partials/partials.go
+++ b/tpl/partials/partials.go
@@ -116,9 +116,9 @@
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
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -24,8 +24,6 @@
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 @@
AddLateTemplate(name, tpl string) error
LoadTemplates(prefix string) error
- MarkReady() error
RebuildClone()
}
@@ -80,11 +77,6 @@
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,12 +93,33 @@
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: (.*?):")
func extractBaseOf(err string) string {
@@ -115,10 +128,6 @@
return m[1]
}
return ""
-}
-
-func (t *TemplateInfo) TemplateInfo() Info {
- return t.Info
}
// TemplateFuncGetter allows to find a template func by name.
--- 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 @@
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,
}
--- a/tpl/tplimpl/shortcodes.go
+++ b/tpl/tplimpl/shortcodes.go
@@ -83,10 +83,12 @@
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--
}
--- a/tpl/tplimpl/shortcodes_test.go
+++ b/tpl/tplimpl/shortcodes_test.go
@@ -53,10 +53,10 @@
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},
}
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -20,6 +20,10 @@
"regexp"
"time"
+ "github.com/gohugoio/hugo/hugofs/files"
+
+ "github.com/gohugoio/hugo/identity"
+
"github.com/gohugoio/hugo/common/herrors"
"strings"
@@ -27,7 +31,6 @@
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 @@
common := &templatesCommon{
nameBaseTemplateName: make(map[string]string),
transformNotFound: make(map[string]bool),
+ identityNotFound: make(map[string][]identity.Manager),
}
htmlT := &htmlTemplates{
@@ -100,13 +104,16 @@
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,8 +159,8 @@
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
}
@@ -160,19 +167,18 @@
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 @@
// * 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 @@
// 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 @@
// 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 @@
// 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 @@
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) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) {
+
base := templateBaseName(templateShortcode, name)
shortcodename, variants := templateNameAndVariants(base)
@@ -561,18 +573,9 @@
}
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) 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) 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
+}
- defer func() {
- t.text.transformNotFound = make(map[string]bool)
- t.html.transformNotFound = make(map[string]bool)
- }()
+func (t *templateHandler) createTemplateInfo(name string) (identity.Manager, tpl.ParseInfo) {
+ _, found := t.templateInfo[name]
+ if found {
+ panic("already created: " + name)
+ }
+ 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 @@
}
}
}
+
+ 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 @@
tt,
new(nopLookupVariant),
}
-
}
type templateHandlerCommon struct {
@@ -771,6 +819,9 @@
// 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 @@
// 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 @@
}
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 @@
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,9 +923,9 @@
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
}
@@ -877,21 +932,15 @@
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 @@
}
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
--- a/tpl/tplimpl/templateProvider.go
+++ b/tpl/tplimpl/templateProvider.go
@@ -44,16 +44,13 @@
}
- 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
}
--- 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 @@
)
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 @@
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 @@
// 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 {
+func newTemplateContext(
+ id identity.Manager,
+ info *tpl.ParseInfo,
+ lookupFn func(name string) *templateInfoTree) *templateContext {
+
return &templateContext{
- Info: tpl.Info{Config: tpl.DefaultConfig},
- lookupFn: lookupFn,
- visited: make(map[string]bool),
- notFound: make(map[string]bool)}
+ id: id,
+ parseInfo: info,
+ lookupFn: lookupFn,
+ visited: make(map[string]bool),
+ templateNotFound: make(map[string]bool),
+ identityNotFound: make(map[string]bool),
+ }
}
-func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree {
- return func(nn string) *parse.Tree {
- tt := templ.Lookup(nn)
- if tt != nil {
- return tt.Tree
- }
- return nil
+func createGetTemplateInfoTreeFor(getID func(name string) *templateInfoTree) func(nn string) *templateInfoTree {
+ return func(nn string) *templateInfoTree {
+ return getID(nn)
}
}
-func applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) {
- return applyTemplateTransformers(typ, templ.Tree, createParseTreeLookup(templ))
+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 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) 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)
+
}
-func applyTemplateTransformers(typ templateType, templ *parse.Tree, lookupFn func(name string) *parse.Tree) (*templateContext, error) {
+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 @@
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 @@
panic(err)
}
partialReturnWrapper = templ.Tree.Root
+
}
func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
@@ -156,6 +201,7 @@
// 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 @@
}
// 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 @@
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 @@
}
case *parse.CommandNode:
+ c.collectPartialInfo(x)
c.collectInner(x)
keep := c.collectReturnNode(x)
@@ -277,11 +324,10 @@
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 @@
if c.typ != templateShortcode {
return
}
- if c.Info.IsInner || len(n.Args) == 0 {
+ if c.parseInfo.IsInner || len(n.Args) == 0 {
return
}
@@ -304,11 +350,43 @@
}
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 {
--- a/tpl/tplimpl/template_ast_transformers_test.go
+++ b/tpl/tplimpl/template_ast_transformers_test.go
@@ -15,14 +15,17 @@
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 @@
{{ 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 @@
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 @@
{{ 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 @@
)
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 @@
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 @@
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 @@
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 @@
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.
@@ -214,4 +225,11 @@
})
}
+}
+
+func newTemplateInfo(name string) tpl.Info {
+ return tpl.NewInfo(
+ identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)),
+ tpl.DefaultParseInfo,
+ )
}
--- a/tpl/tplimpl/template_funcs.go
+++ b/tpl/tplimpl/template_funcs.go
@@ -19,6 +19,8 @@
"reflect"
"strings"
+ "github.com/gohugoio/hugo/tpl"
+
"github.com/gohugoio/hugo/common/maps"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
@@ -62,7 +64,7 @@
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
}
@@ -69,7 +71,7 @@
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 @@
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 @@
}
funcMap[alias] = mm.Method
}
-
}
-
}
if d.OverloadedTemplateFuncs != nil {
--- a/tpl/tplimpl/template_info_test.go
+++ b/tpl/tplimpl/template_info_test.go
@@ -24,18 +24,19 @@
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)
}