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
This commit is contained in:
Bjørn Erik Pedersen 2019-11-27 13:42:36 +01:00
parent 67f3aa72cf
commit e625088ef5
No known key found for this signature in database
GPG key ID: 330E6E2BD4859D8F
59 changed files with 2234 additions and 542 deletions

View file

@ -0,0 +1,37 @@
---
title: .RenderString
description: "Renders markup to HTML."
godocref:
date: 2019-12-18
categories: [functions]
menu:
docs:
parent: "functions"
keywords: [markdown,goldmark,render]
signature: [".RenderString MARKUP"]
---
{{< new-in "0.62.0" >}}
`.RenderString` is a method on `Page` that renders some markup to HTML using the content renderer defined for that page (if not set in the options).
The method takes an optional map argument with these options:
display ("inline")
: `inline` or `block`. If `inline` (default), surrounding ´<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.

View file

@ -74,3 +74,62 @@ endLevel
ordered ordered
: Whether or not to generate an ordered list instead of an unordered list. : 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 >}}

View file

@ -25,13 +25,14 @@ import (
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup" "github.com/gohugoio/hugo/markup"
bp "github.com/gohugoio/hugo/bufferpool" bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/spf13/afero"
"strings" "strings"
) )
@ -78,6 +79,7 @@ func NewContentSpec(cfg config.Provider, logger *loggers.Logger, contentFs afero
ContentFs: contentFs, ContentFs: contentFs,
Logger: logger, Logger: logger,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -0,0 +1,244 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless requiredF by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import "testing"
func TestRenderHooks(t *testing.T) {
config := `
baseURL="https://example.org"
workingDir="/mywork"
`
b := newTestSitesBuilder(t).WithWorkingDir("/mywork").WithConfigFile("toml", config).Running()
b.WithTemplatesAdded("_default/single.html", `{{ .Content }}`)
b.WithTemplatesAdded("shortcodes/myshortcode1.html", `{{ partial "mypartial1" }}`)
b.WithTemplatesAdded("shortcodes/myshortcode2.html", `{{ partial "mypartial2" }}`)
b.WithTemplatesAdded("shortcodes/myshortcode3.html", `SHORT3|`)
b.WithTemplatesAdded("shortcodes/myshortcode4.html", `
<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
`)
}

View file

@ -126,10 +126,28 @@ type SourceFilesystems struct {
StaticDirs []hugofs.FileMetaInfo 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, // 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 // i18n, layouts, static) and additional metadata to be able to use that filesystem
// in server mode. // in server mode.
type SourceFilesystem struct { type SourceFilesystem struct {
// Name matches one in files.ComponentFolders
Name string
// This is a virtual composite filesystem. It expects path relative to a context. // This is a virtual composite filesystem. It expects path relative to a context.
Fs afero.Fs Fs afero.Fs
@ -275,6 +293,19 @@ func (d *SourceFilesystem) Contains(filename string) bool {
return false 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 // RealDirs gets a list of absolute paths to directories starting from the given
// path. // path.
func (d *SourceFilesystem) RealDirs(from string) []string { func (d *SourceFilesystem) RealDirs(from string) []string {
@ -349,12 +380,14 @@ func newSourceFilesystemsBuilder(p *paths.Paths, logger *loggers.Logger, b *Base
return &sourceFilesystemsBuilder{p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs, result: &SourceFilesystems{}} 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{ return &SourceFilesystem{
Name: name,
Fs: fs, Fs: fs,
Dirs: dirs, Dirs: dirs,
} }
} }
func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) { func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
if b.theBigFs == nil { if b.theBigFs == nil {
@ -369,12 +402,12 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
createView := func(componentID string) *SourceFilesystem { createView := func(componentID string) *SourceFilesystem {
if b.theBigFs == nil || b.theBigFs.overlayMounts == nil { 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] dirs := b.theBigFs.overlayDirs[componentID]
return b.newSourceFilesystem(afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs) return b.newSourceFilesystem(componentID, afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs)
} }
@ -392,14 +425,14 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
return nil, err 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] i18nDirs := b.theBigFs.overlayDirs[files.ComponentFolderI18n]
i18nFs, err := hugofs.NewSliceFs(i18nDirs...) i18nFs, err := hugofs.NewSliceFs(i18nDirs...)
if err != nil { if err != nil {
return nil, err 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] contentDirs := b.theBigFs.overlayDirs[files.ComponentFolderContent]
contentBfs := afero.NewBasePathFs(b.theBigFs.overlayMountsContent, files.ComponentFolderContent) contentBfs := afero.NewBasePathFs(b.theBigFs.overlayMountsContent, files.ComponentFolderContent)
@ -409,7 +442,7 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
return nil, errors.Wrap(err, "create content filesystem") 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) b.result.Work = afero.NewReadOnlyFs(b.theBigFs.overlayFull)
@ -421,13 +454,13 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
if b.theBigFs.staticPerLanguage != nil { if b.theBigFs.staticPerLanguage != nil {
// Multihost mode // Multihost mode
for k, v := range b.theBigFs.staticPerLanguage { 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 sfs.PublishFolder = k
ms[k] = sfs ms[k] = sfs
} }
} else { } else {
bfs := afero.NewBasePathFs(b.theBigFs.overlayMountsStatic, files.ComponentFolderStatic) 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 return b.result, nil

View file

@ -40,6 +40,9 @@ import (
// TODO(bep) this fails when testmodBuilder is also building ... // TODO(bep) this fails when testmodBuilder is also building ...
func TestHugoModules(t *testing.T) { func TestHugoModules(t *testing.T) {
if !isCI() {
t.Skip("skip (relative) long running modules test when running locally")
}
t.Parallel() t.Parallel()
if !isCI() || hugo.GoMinorVersion() < 12 { if !isCI() || hugo.GoMinorVersion() < 12 {

View file

@ -20,6 +20,8 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/gohugoio/hugo/identity"
radix "github.com/armon/go-radix" radix "github.com/armon/go-radix"
"github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output"
@ -411,7 +413,6 @@ func applyDeps(cfg deps.DepsCfg, sites ...*Site) error {
} }
d.OutputFormatsConfig = s.outputFormatsConfig d.OutputFormatsConfig = s.outputFormatsConfig
} }
} }
return nil return nil
@ -806,12 +807,40 @@ func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Page
return h.Sites[0].findPagesByKindIn(kind, inPages) return h.Sites[0].findPagesByKindIn(kind, inPages)
} }
func (h *HugoSites) findPagesByShortcode(shortcode string) page.Pages { func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) {
var pages page.Pages
for _, s := range h.Sites { 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. // Used in partial reloading to determine if the change is in a bundle.

View file

@ -71,7 +71,7 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
if conf.whatChanged == nil { if conf.whatChanged == nil {
// Assume everything has changed // Assume everything has changed
conf.whatChanged = &whatChanged{source: true, other: true} conf.whatChanged = &whatChanged{source: true}
} }
var prepareErr error var prepareErr error

View file

@ -1459,3 +1459,19 @@ other = %q
return &multiSiteTestBuilder{sitesBuilder: b, configFormat: configFormat, config: config, configData: configData} 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`)
}

View file

@ -23,6 +23,12 @@ import (
"sort" "sort"
"strings" "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/markup/converter"
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
@ -43,9 +49,11 @@ import (
"github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/source"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/common/collections" "github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/resources/resource"
@ -59,7 +67,11 @@ var (
var ( var (
pageTypesProvider = resource.NewResourceTypesProvider(media.OctetType, pageResourceType) 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 // pageContext provides contextual information about this page, for error
@ -317,6 +329,54 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
return nil 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 { func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
p.layoutDescriptorInit.Do(func() { p.layoutDescriptorInit.Do(func() {
var section string var section string
@ -464,11 +524,86 @@ func (p *pageState) AlternativeOutputFormats() page.OutputFormats {
return o 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...) l, err := p.getLayouts(layout...)
if err != nil { if err != nil {
p.s.SendError(p.wrapError(errors.Errorf(".Render: failed to resolve layout %v", layout))) return "", p.wrapError(errors.Errorf("failed to resolve layout %v", layout))
return ""
} }
for _, layout := range l { for _, layout := range l {
@ -479,17 +614,18 @@ func (p *pageState) Render(layout ...string) template.HTML {
// We default to good old HTML. // We default to good old HTML.
templ, _ = p.s.Tmpl.Lookup(layout + ".html") templ, _ = p.s.Tmpl.Lookup(layout + ".html")
} }
if templ != nil { if templ != nil {
p.addDependency(templ.(tpl.Info))
res, err := executeToString(p.s.Tmpl, templ, p) res, err := executeToString(p.s.Tmpl, templ, p)
if err != nil { if err != nil {
p.s.SendError(p.wrapError(errors.Wrapf(err, ".Render: failed to execute template %q v", layout))) return "", p.wrapError(errors.Wrapf(err, "failed to execute template %q v", layout))
return ""
} }
return template.HTML(res) return template.HTML(res), nil
} }
} }
return "" return "", nil
} }
@ -745,15 +881,33 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
p.pageOutput.paginator.reset() p.pageOutput.paginator.reset()
} }
if idx > 0 { if isRenderingSite {
// Check if we can reuse content from one of the previous formats. cp := p.pageOutput.cp
for i := idx - 1; i >= 0; i-- { if cp == nil {
po := p.pageOutputs[i]
if po.cp != nil && po.cp.reuse { // Look for content to reuse.
p.pageOutput.cp = po.cp for i := 0; i < len(p.pageOutputs); i++ {
break 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) { for _, r := range p.Resources().ByType(pageResourceType) {

View file

@ -30,8 +30,7 @@ var (
type pageContent struct { type pageContent struct {
renderable bool renderable bool
selfLayout string selfLayout string
truncated bool
truncated bool
cmap *pageContentMap cmap *pageContentMap

View file

@ -592,7 +592,7 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte
return nil return nil
} }
func (p *pageMeta) applyDefaultValues() error { func (p *pageMeta) applyDefaultValues(ps *pageState) error {
if p.markup == "" { if p.markup == "" {
if !p.File().IsZero() { if !p.File().IsZero() {
// Fall back to file extension // Fall back to file extension
@ -651,27 +651,39 @@ func (p *pageMeta) applyDefaultValues() error {
markup = "markdown" markup = "markdown"
} }
cp := p.s.ContentSpec.Converters.Get(markup) cp, err := p.newContentConverter(ps, markup, renderingConfigOverrides)
if cp == nil {
return errors.Errorf("no content renderer found for markup %q", p.markup)
}
cpp, err := cp.New(converter.DocumentContext{
DocumentID: p.f.UniqueID(),
DocumentName: p.f.Path(),
ConfigOverrides: renderingConfigOverrides,
})
if err != nil { if err != nil {
return err return err
} }
p.contentConverter = cpp p.contentConverter = cp
} }
return nil return nil
} }
func (p *pageMeta) newContentConverter(ps *pageState, markup string, renderingConfigOverrides map[string]interface{}) (converter.Converter, error) {
cp := p.s.ContentSpec.Converters.Get(markup)
if cp == nil {
return nil, errors.Errorf("no content renderer found for markup %q", p.markup)
}
cpp, err := cp.New(
converter.DocumentContext{
Document: newPageForRenderHook(ps),
DocumentID: p.f.UniqueID(),
DocumentName: p.f.Path(),
ConfigOverrides: renderingConfigOverrides,
},
)
if err != nil {
return nil, err
}
return cpp, nil
}
// The output formats this page will be rendered to. // The output formats this page will be rendered to.
func (m *pageMeta) outputFormats() output.Formats { func (m *pageMeta) outputFormats() output.Formats {
if len(m.configuredOutputFormats) > 0 { if len(m.configuredOutputFormats) > 0 {

View file

@ -112,7 +112,7 @@ func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*page
} }
} }
if err := metaProvider.applyDefaultValues(); err != nil { if err := metaProvider.applyDefaultValues(ps); err != nil {
return err return err
} }
@ -134,7 +134,7 @@ func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*page
} }
makeOut := func(f output.Format, render bool) *pageOutput { 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 { if ps.m.standalone {
@ -234,7 +234,7 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope
return ps.wrapError(err) return ps.wrapError(err)
} }
if err := metaProvider.applyDefaultValues(); err != nil { if err := metaProvider.applyDefaultValues(ps); err != nil {
return err return err
} }
@ -242,10 +242,6 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope
} }
ps.init.Add(func() (interface{}, error) { 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) pp, err := newPagePaths(s, ps, metaProvider)
if err != nil { if err != nil {
@ -264,18 +260,18 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope
} }
_, render := outputFormatsForPage.GetByName(f.Name) _, render := outputFormatsForPage.GetByName(f.Name)
var contentProvider *pageContentOutput po := newPageOutput(ps, pp, f, render)
if reuseContent && i > 0 {
contentProvider = ps.pageOutputs[0].cp // Create a content provider for the first,
} else { // we may be able to reuse it.
var err error if i == 0 {
contentProvider, err = contentPerOutput(f) contentProvider, err := newPageContentOutput(ps, po)
if err != nil { if err != nil {
return nil, err return nil, err
} }
po.initContentProvider(contentProvider)
} }
po := newPageOutput(contentProvider, ps, pp, f, render)
ps.pageOutputs[i] = po ps.pageOutputs[i] = po
created[f.Name] = po created[f.Name] = po
} }

View file

@ -14,13 +14,13 @@
package hugolib package hugolib
import ( import (
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/resources/resource"
) )
func newPageOutput( func newPageOutput(
cp *pageContentOutput, // may be nil
ps *pageState, ps *pageState,
pp pagePaths, pp pagePaths,
f output.Format, f output.Format,
@ -45,36 +45,23 @@ func newPageOutput(
paginatorProvider = pag paginatorProvider = pag
} }
var (
contentProvider page.ContentProvider = page.NopPage
tableOfContentsProvider page.TableOfContentsProvider = page.NopPage
)
if cp != nil {
contentProvider = cp
tableOfContentsProvider = cp
}
providers := struct { providers := struct {
page.ContentProvider
page.TableOfContentsProvider
page.PaginatorProvider page.PaginatorProvider
resource.ResourceLinksProvider resource.ResourceLinksProvider
targetPather targetPather
}{ }{
contentProvider,
tableOfContentsProvider,
paginatorProvider, paginatorProvider,
linksProvider, linksProvider,
targetPathsProvider, targetPathsProvider,
} }
po := &pageOutput{ po := &pageOutput{
f: f, f: f,
cp: cp, pagePerOutputProviders: providers,
pagePerOutputProviders: providers, ContentProvider: page.NopPage,
render: render, TableOfContentsProvider: page.NopPage,
paginator: pag, render: render,
paginator: pag,
} }
return po return po
@ -94,16 +81,54 @@ type pageOutput struct {
// used in template(s). // used in template(s).
paginator *pagePaginator paginator *pagePaginator
// This interface provides the functionality that is specific for this // These interface provides the functionality that is specific for this
// output format. // output format.
pagePerOutputProviders pagePerOutputProviders
page.ContentProvider
page.TableOfContentsProvider
// This may be nil. // May be nil.
cp *pageContentOutput 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() { func (p *pageOutput) enablePlaceholders() {
if p.cp != nil { if p.cp != nil {
p.cp.enablePlaceholders() p.cp.enablePlaceholders()
} }
} }

View file

@ -23,6 +23,10 @@ import (
"sync" "sync"
"unicode/utf8" "unicode/utf8"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/lazy" "github.com/gohugoio/hugo/lazy"
@ -58,160 +62,182 @@ var (
} }
) )
func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutput, error) { var pageContentOutputDependenciesID = identity.KeyValueIdentity{Key: "pageOutput", Value: "dependencies"}
func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, error) {
parent := p.init parent := p.init
return func(f output.Format) (*pageContentOutput, error) { var dependencyTracker identity.Manager
cp := &pageContentOutput{ if p.s.running() {
p: p, dependencyTracker = identity.NewManager(pageContentOutputDependenciesID)
f: f, }
}
initContent := func() (err error) { cp := &pageContentOutput{
if p.cmap == nil { dependencyTracker: dependencyTracker,
// Nothing to do. p: p,
return nil f: po.f,
} }
defer func() {
// See https://github.com/gohugoio/hugo/issues/6210
if r := recover(); r != nil {
err = fmt.Errorf("%s", r)
p.s.Log.ERROR.Printf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
}
}()
var hasVariants bool
cp.contentPlaceholders, hasVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
if err != nil {
return err
}
if p.render && !hasVariants {
// We can reuse this for the other output formats
cp.enableReuse()
}
cp.workContent = p.contentToRender(cp.contentPlaceholders)
isHTML := cp.p.m.markup == "html"
if p.renderable {
if !isHTML {
r, err := cp.renderContent(cp.workContent)
if err != nil {
return err
}
cp.convertedResult = r
cp.workContent = r.Bytes()
if _, ok := r.(converter.TableOfContentsProvider); !ok {
tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
cp.workContent = tmpContent
}
}
if cp.placeholdersEnabled {
// ToC was accessed via .Page.TableOfContents in the shortcode,
// at a time when the ToC wasn't ready.
cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
}
if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
// There are one or more replacement tokens to be replaced.
cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
if err != nil {
return err
}
}
if cp.p.source.hasSummaryDivider {
if isHTML {
src := p.source.parsed.Input()
// Use the summary sections as they are provided by the user.
if p.source.posSummaryEnd != -1 {
cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
}
if cp.p.source.posBodyStart != -1 {
cp.workContent = src[cp.p.source.posBodyStart:]
}
} else {
summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
if err != nil {
cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
} else {
cp.workContent = content
cp.summary = helpers.BytesToHTML(summary)
}
}
} else if cp.p.m.summary != "" {
b, err := cp.p.getContentConverter().Convert(
converter.RenderContext{
Src: []byte(cp.p.m.summary),
},
)
if err != nil {
return err
}
html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
cp.summary = helpers.BytesToHTML(html)
}
}
cp.content = helpers.BytesToHTML(cp.workContent)
if !p.renderable {
err := cp.addSelfTemplate()
return err
}
initContent := func() (err error) {
if p.cmap == nil {
// Nothing to do.
return nil return nil
}
defer func() {
// See https://github.com/gohugoio/hugo/issues/6210
if r := recover(); r != nil {
err = fmt.Errorf("%s", r)
p.s.Log.ERROR.Printf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
}
}()
if err := po.initRenderHooks(); err != nil {
return err
} }
// Recursive loops can only happen in content files with template code (shortcodes etc.) var hasShortcodeVariants bool
// Avoid creating new goroutines if we don't have to.
needTimeout := !p.renderable || p.shortcodeState.hasShortcodes()
if needTimeout { f := po.f
cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) { cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
return nil, initContent() if err != nil {
}) return err
} else {
cp.initMain = parent.Branch(func() (interface{}, error) {
return nil, initContent()
})
} }
cp.initPlain = cp.initMain.Branch(func() (interface{}, error) { enableReuse := !(hasShortcodeVariants || cp.renderHooksHaveVariants)
cp.plain = helpers.StripHTML(string(cp.content))
cp.plainWords = strings.Fields(cp.plain)
cp.setWordCounts(p.m.isCJKLanguage)
if err := cp.setAutoSummary(); err != nil { if enableReuse {
return err, nil // Reuse this for the other output formats.
// We may improve on this, but we really want to avoid re-rendering the content
// to all output formats.
// The current rule is that if you need output format-aware shortcodes or
// content rendering hooks, create a output format-specific template, e.g.
// myshortcode.amp.html.
cp.enableReuse()
}
cp.workContent = p.contentToRender(cp.contentPlaceholders)
isHTML := cp.p.m.markup == "html"
if p.renderable {
if !isHTML {
r, err := cp.renderContent(cp.workContent, true)
if err != nil {
return err
}
cp.workContent = r.Bytes()
if tocProvider, ok := r.(converter.TableOfContentsProvider); ok {
cfg := p.s.ContentSpec.Converters.GetMarkupConfig()
cp.tableOfContents = template.HTML(
tocProvider.TableOfContents().ToHTML(
cfg.TableOfContents.StartLevel,
cfg.TableOfContents.EndLevel,
cfg.TableOfContents.Ordered,
),
)
} else {
tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
cp.workContent = tmpContent
}
} }
return nil, nil if cp.placeholdersEnabled {
}) // ToC was accessed via .Page.TableOfContents in the shortcode,
// at a time when the ToC wasn't ready.
cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
}
return cp, nil if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
// There are one or more replacement tokens to be replaced.
cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
if err != nil {
return err
}
}
if cp.p.source.hasSummaryDivider {
if isHTML {
src := p.source.parsed.Input()
// Use the summary sections as they are provided by the user.
if p.source.posSummaryEnd != -1 {
cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
}
if cp.p.source.posBodyStart != -1 {
cp.workContent = src[cp.p.source.posBodyStart:]
}
} else {
summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
if err != nil {
cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
} else {
cp.workContent = content
cp.summary = helpers.BytesToHTML(summary)
}
}
} else if cp.p.m.summary != "" {
b, err := cp.renderContent([]byte(cp.p.m.summary), false)
if err != nil {
return err
}
html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
cp.summary = helpers.BytesToHTML(html)
}
}
cp.content = helpers.BytesToHTML(cp.workContent)
if !p.renderable {
err := cp.addSelfTemplate()
return err
}
return nil
} }
// Recursive loops can only happen in content files with template code (shortcodes etc.)
// Avoid creating new goroutines if we don't have to.
needTimeout := !p.renderable || p.shortcodeState.hasShortcodes()
needTimeout = needTimeout || cp.renderHooks != nil
if needTimeout {
cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
return nil, initContent()
})
} else {
cp.initMain = parent.Branch(func() (interface{}, error) {
return nil, initContent()
})
}
cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
cp.plain = helpers.StripHTML(string(cp.content))
cp.plainWords = strings.Fields(cp.plain)
cp.setWordCounts(p.m.isCJKLanguage)
if err := cp.setAutoSummary(); err != nil {
return err, nil
}
return nil, nil
})
return cp, nil
} }
// pageContentOutput represents the Page content for a given output format. // pageContentOutput represents the Page content for a given output format.
type pageContentOutput struct { type pageContentOutput struct {
f output.Format f output.Format
// If we can safely reuse this for other output formats. // If we can reuse this for other output formats.
reuse bool reuse bool
reuseInit sync.Once reuseInit sync.Once
@ -224,10 +250,15 @@ type pageContentOutput struct {
placeholdersEnabled bool placeholdersEnabled bool
placeholdersEnabledInit sync.Once 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 // Content state
workContent []byte workContent []byte
convertedResult converter.Result dependencyTracker identity.Manager // Set in server mode.
// Temporary storage of placeholders mapped to their content. // Temporary storage of placeholders mapped to their content.
// These are shortcodes etc. Some of these will need to be replaced // These are shortcodes etc. Some of these will need to be replaced
@ -248,6 +279,20 @@ type pageContentOutput struct {
readingTime int 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) { func (p *pageContentOutput) Content() (interface{}, error) {
if p.p.s.initInit(p.initMain, p.p) { if p.p.s.initInit(p.initMain, p.p) {
return p.content, nil return p.content, nil
@ -290,10 +335,6 @@ func (p *pageContentOutput) Summary() template.HTML {
func (p *pageContentOutput) TableOfContents() template.HTML { func (p *pageContentOutput) TableOfContents() template.HTML {
p.p.s.initInit(p.initMain, p.p) 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 return p.tableOfContents
} }
@ -331,12 +372,30 @@ func (p *pageContentOutput) setAutoSummary() error {
} }
func (cp *pageContentOutput) renderContent(content []byte) (converter.Result, error) { func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) {
return cp.p.getContentConverter().Convert( 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{ converter.RenderContext{
Src: content, Src: content,
RenderTOC: true, 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) { func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
@ -392,9 +451,7 @@ func (p *pageContentOutput) enableReuse() {
// these will be shifted out when rendering a given output format. // these will be shifted out when rendering a given output format.
type pagePerOutputProviders interface { type pagePerOutputProviders interface {
targetPather targetPather
page.ContentProvider
page.PaginatorProvider page.PaginatorProvider
page.TableOfContentsProvider
resource.ResourceLinksProvider resource.ResourceLinksProvider
} }

View file

@ -93,12 +93,6 @@ Summary Next Line. {{<figure src="/not/real" >}}.
More text here. More text here.
Some more text Some more text
`
simplePageWithEmbeddedScript = `---
title: Simple
---
<script type='text/javascript'>alert('the script tags are still there, right?');</script>
` `
simplePageWithSummaryDelimiterSameLine = `--- simplePageWithSummaryDelimiterSameLine = `---
@ -325,6 +319,7 @@ func normalizeContent(c string) string {
} }
func checkPageTOC(t *testing.T, page page.Page, toc string) { func checkPageTOC(t *testing.T, page page.Page, toc string) {
t.Helper()
if page.TableOfContents() != template.HTML(toc) { if page.TableOfContents() != template.HTML(toc) {
t.Fatalf("Page TableOfContents is:\n%q.\nExpected %q", page.TableOfContents(), toc) t.Fatalf("Page TableOfContents is:\n%q.\nExpected %q", page.TableOfContents(), toc)
} }

View file

@ -26,6 +26,7 @@ func TestUnwrapPage(t *testing.T) {
p := &pageState{} p := &pageState{}
c.Assert(mustUnwrap(newPageForShortcode(p)), qt.Equals, p) c.Assert(mustUnwrap(newPageForShortcode(p)), qt.Equals, p)
c.Assert(mustUnwrap(newPageForRenderHook(p)), qt.Equals, p)
} }
func mustUnwrap(v interface{}) page.Page { func mustUnwrap(v interface{}) page.Page {

View file

@ -811,6 +811,7 @@ Short Thumb Width: {{ $thumb.Width }}
writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), singleLayout) writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), singleLayout)
writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), listLayout) 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.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", "_index.md"), pageContent)
writeSource(t, fs, filepath.Join(workDir, "base", "_1.md"), pageContent) writeSource(t, fs, filepath.Join(workDir, "base", "_1.md"), pageContent)

View file

@ -358,16 +358,6 @@ func (c *PageCollections) removePage(page *pageState) {
} }
} }
func (c *PageCollections) findPagesByShortcode(shortcode string) page.Pages {
var pages page.Pages
for _, p := range c.rawAllPages {
if p.HasShortcode(shortcode) {
pages = append(pages, p)
}
}
return pages
}
func (c *PageCollections) replacePage(page *pageState) { func (c *PageCollections) replacePage(page *pageState) {
// will find existing page that matches filepath and remove it // will find existing page that matches filepath and remove it
c.removePage(page) c.removePage(page)

View file

@ -23,8 +23,6 @@ import (
"html/template" "html/template"
"path" "path"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/herrors"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -198,7 +196,7 @@ type shortcode struct {
} }
func (s shortcode) insertPlaceholder() bool { 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 { func (s shortcode) innerString() string {
@ -349,14 +347,9 @@ func renderShortcode(
// Pre Hugo 0.55 this was the behaviour even for the outer-most // Pre Hugo 0.55 this was the behaviour even for the outer-most
// shortcode. // 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 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 { if err != nil {
return "", false, err return "", false, err
@ -494,13 +487,13 @@ Loop:
case currItem.IsRightShortcodeDelim(): case currItem.IsRightShortcodeDelim():
// we trust the template on this: // we trust the template on this:
// if there's no inner, we're done // if there's no inner, we're done
if !sc.isInline && !sc.info.IsInner { if !sc.isInline && !sc.info.ParseInfo().IsInner {
return sc, nil return sc, nil
} }
case currItem.IsShortcodeClose(): case currItem.IsShortcodeClose():
next := pt.Peek() next := pt.Peek()
if !sc.isInline && !sc.info.IsInner { if !sc.isInline && !sc.info.ParseInfo().IsInner {
if next.IsError() { if next.IsError() {
// return that error, more specific // return that error, more specific
continue continue
@ -540,7 +533,7 @@ Loop:
return nil, _errors.Errorf("template for shortcode %q not found", sc.name) 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(): case currItem.IsInlineShortcodeName():
sc.name = currItem.ValStr() sc.name = currItem.ValStr()
sc.isInline = true sc.isInline = true

View file

@ -54,3 +54,22 @@ func (p *pageForShortcode) TableOfContents() template.HTML {
p.p.enablePlaceholders() p.p.enablePlaceholders()
return p.toc 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)
}

View file

@ -379,8 +379,13 @@ title: "Shortcodes Galore!"
if s == nil { if s == nil {
return "<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", 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) { regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {

View file

@ -28,6 +28,12 @@ import (
"strings" "strings"
"time" "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/resources/resource"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
@ -60,7 +66,6 @@ import (
"github.com/gohugoio/hugo/navigation" "github.com/gohugoio/hugo/navigation"
"github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/related" "github.com/gohugoio/hugo/related"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/page/pagemeta" "github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/source"
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
@ -801,7 +806,6 @@ func (s *Site) multilingual() *Multilingual {
type whatChanged struct { type whatChanged struct {
source bool source bool
other bool
files map[string]bool files map[string]bool
} }
@ -888,10 +892,11 @@ func (s *Site) translateFileEvents(events []fsnotify.Event) []fsnotify.Event {
// It returns whetever the content source was changed. // It returns whetever the content source was changed.
// TODO(bep) clean up/rewrite this method. // TODO(bep) clean up/rewrite this method.
func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error { func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error {
events = s.filterFileEvents(events) events = s.filterFileEvents(events)
events = s.translateFileEvents(events) events = s.translateFileEvents(events)
changeIdentities := make(identity.Identities)
s.Log.DEBUG.Printf("Rebuild for events %q", events) s.Log.DEBUG.Printf("Rebuild for events %q", events)
h := s.h h := s.h
@ -902,11 +907,12 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
sourceChanged = []fsnotify.Event{} sourceChanged = []fsnotify.Event{}
sourceReallyChanged = []fsnotify.Event{} sourceReallyChanged = []fsnotify.Event{}
contentFilesChanged []string contentFilesChanged []string
tmplChanged = []fsnotify.Event{}
dataChanged = []fsnotify.Event{} tmplChanged bool
i18nChanged = []fsnotify.Event{} dataChanged bool
shortcodesChanged = make(map[string]bool) i18nChanged bool
sourceFilesChanged = make(map[string]bool)
sourceFilesChanged = make(map[string]bool)
// prevent spamming the log on changes // prevent spamming the log on changes
logger = helpers.NewDistinctFeedbackLogger() logger = helpers.NewDistinctFeedbackLogger()
@ -919,33 +925,30 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...) cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...)
} }
if s.isContentDirEvent(ev) { id, found := s.eventToIdentity(ev)
logger.Println("Source changed", ev) if found {
sourceChanged = append(sourceChanged, ev) changeIdentities[id] = id
}
if s.isLayoutDirEvent(ev) { switch id.Type {
logger.Println("Template changed", ev) case files.ComponentFolderContent:
tmplChanged = append(tmplChanged, ev) logger.Println("Source changed", ev)
sourceChanged = append(sourceChanged, ev)
case files.ComponentFolderLayouts:
logger.Println("Template changed", ev)
tmplChanged = true
case files.ComponentFolderData:
logger.Println("Data changed", ev)
dataChanged = true
case files.ComponentFolderI18n:
logger.Println("i18n changed", ev)
i18nChanged = true
if strings.Contains(ev.Name, "shortcodes") {
shortcode := filepath.Base(ev.Name)
shortcode = strings.TrimSuffix(shortcode, filepath.Ext(shortcode))
shortcodesChanged[shortcode] = true
} }
} }
if s.isDataDirEvent(ev) {
logger.Println("Data changed", ev)
dataChanged = append(dataChanged, ev)
}
if s.isI18nEvent(ev) {
logger.Println("i18n changed", ev)
i18nChanged = append(dataChanged, ev)
}
} }
changed := &whatChanged{ changed := &whatChanged{
source: len(sourceChanged) > 0 || len(shortcodesChanged) > 0, source: len(sourceChanged) > 0,
other: len(tmplChanged) > 0 || len(i18nChanged) > 0 || len(dataChanged) > 0,
files: sourceFilesChanged, files: sourceFilesChanged,
} }
@ -960,7 +963,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...) s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...)
} }
if len(tmplChanged) > 0 || len(i18nChanged) > 0 { if tmplChanged || i18nChanged {
sites := s.h.Sites sites := s.h.Sites
first := sites[0] first := sites[0]
@ -989,7 +992,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
} }
} }
if len(dataChanged) > 0 { if dataChanged {
s.h.init.data.Reset() s.h.init.data.Reset()
} }
@ -1018,18 +1021,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
sourceFilesChanged[ev.Name] = true sourceFilesChanged[ev.Name] = true
} }
for shortcode := range shortcodesChanged { h.resetPageStateFromEvents(changeIdentities)
// 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())
}
}
if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 { if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 {
var filenamesChanged []string var filenamesChanged []string
@ -1218,20 +1210,14 @@ func (s *Site) initializeSiteInfo() error {
return nil return nil
} }
func (s *Site) isI18nEvent(e fsnotify.Event) bool { func (s *Site) eventToIdentity(e fsnotify.Event) (identity.PathIdentity, bool) {
return s.BaseFs.SourceFilesystems.IsI18n(e.Name) 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 identity.PathIdentity{}, false
return s.BaseFs.SourceFilesystems.IsData(e.Name)
}
func (s *Site) isLayoutDirEvent(e fsnotify.Event) bool {
return s.BaseFs.SourceFilesystems.IsLayout(e.Name)
}
func (s *Site) isContentDirEvent(e fsnotify.Event) bool {
return s.BaseFs.IsContent(e.Name)
} }
func (s *Site) readAndProcessContent(filenames ...string) error { func (s *Site) readAndProcessContent(filenames ...string) error {
@ -1562,6 +1548,26 @@ var infoOnMissingLayout = map[string]bool{
"404": true, "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) { func (s *Site) renderForLayouts(name, outputFormat string, d interface{}, w io.Writer, layouts ...string) (err error) {
templ := s.findFirstTemplate(layouts...) templ := s.findFirstTemplate(layouts...)
if templ == nil { if templ == nil {

View file

@ -127,6 +127,36 @@ title = "What is Markdown"
baseURL = "https://example.com" 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")) data, err := ioutil.ReadFile(filepath.FromSlash("testdata/what-is-markdown.md"))
sb.Assert(err, qt.IsNil) sb.Assert(err, qt.IsNil)
datastr := string(data) datastr := string(data)

View file

@ -18,8 +18,12 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/gohugoio/hugo/identity"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/tpl"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@ -320,6 +324,7 @@ Partial cached1: {{ partialCached "p1" "input1" $key1 }}
Partial cached2: {{ partialCached "p1" "input2" $key1 }} Partial cached2: {{ partialCached "p1" "input2" $key1 }}
Partial cached3: {{ partialCached "p1" "input3" $key2 }} Partial cached3: {{ partialCached "p1" "input3" $key2 }}
`, `,
"partials/p1.html", `partial: {{ . }}`, "partials/p1.html", `partial: {{ . }}`,
) )
@ -331,3 +336,85 @@ Partial cached3: {{ partialCached "p1" "input3" $key2 }}
Partial cached3: partial: input3 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(" ")
}
}

View file

@ -389,8 +389,9 @@ func (s *sitesBuilder) EditFiles(filenameContent ...string) *sitesBuilder {
var changedFiles []string var changedFiles []string
for i := 0; i < len(filenameContent); i += 2 { for i := 0; i < len(filenameContent); i += 2 {
filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1] filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1]
changedFiles = append(changedFiles, filename) absFilename := s.absFilename(filename)
writeSource(s.T, s.Fs, s.absFilename(filename), content) changedFiles = append(changedFiles, absFilename)
writeSource(s.T, s.Fs, absFilename, content)
} }
s.changedFiles = changedFiles s.changedFiles = changedFiles
@ -963,10 +964,6 @@ func isCI() bool {
return os.Getenv("CI") != "" return os.Getenv("CI") != ""
} }
func isGo111() bool {
return strings.Contains(runtime.Version(), "1.11")
}
// See https://github.com/golang/go/issues/19280 // See https://github.com/golang/go/issues/19280
// Not in use. // Not in use.
var parallelEnabled = true var parallelEnabled = true

131
identity/identity.go Normal file
View file

@ -0,0 +1,131 @@
package identity
import (
"path/filepath"
"strings"
"sync"
)
// NewIdentityManager creates a new Manager starting at id.
func NewManager(id Provider) Manager {
return &identityManager{
Provider: id,
ids: Identities{id.GetIdentity(): id},
}
}
// NewPathIdentity creates a new Identity with the two identifiers
// type and path.
func NewPathIdentity(typ, pat string) PathIdentity {
pat = strings.ToLower(strings.TrimPrefix(filepath.ToSlash(pat), "/"))
return PathIdentity{Type: typ, Path: pat}
}
// Identities stores identity providers.
type Identities map[Identity]Provider
func (ids Identities) search(id Identity) Provider {
if v, found := ids[id]; found {
return v
}
for _, v := range ids {
switch t := v.(type) {
case IdentitiesProvider:
if nested := t.GetIdentities().search(id); nested != nil {
return nested
}
}
}
return nil
}
// IdentitiesProvider provides all Identities.
type IdentitiesProvider interface {
GetIdentities() Identities
}
// Identity represents an thing that can provide an identify. This can be
// any Go type, but the Identity returned by GetIdentify must be hashable.
type Identity interface {
Provider
Name() string
}
// Manager manages identities, and is itself a Provider of Identity.
type Manager interface {
IdentitiesProvider
Provider
Add(ids ...Provider)
Search(id Identity) Provider
Reset()
}
// A PathIdentity is a common identity identified by a type and a path, e.g. "layouts" and "_default/single.html".
type PathIdentity struct {
Type string
Path string
}
// GetIdentity returns itself.
func (id PathIdentity) GetIdentity() Identity {
return id
}
// Name returns the Path.
func (id PathIdentity) Name() string {
return id.Path
}
// A KeyValueIdentity a general purpose identity.
type KeyValueIdentity struct {
Key string
Value string
}
// GetIdentity returns itself.
func (id KeyValueIdentity) GetIdentity() Identity {
return id
}
// Name returns the Key.
func (id KeyValueIdentity) Name() string {
return id.Key
}
// Provider provides the hashable Identity.
type Provider interface {
GetIdentity() Identity
}
type identityManager struct {
sync.Mutex
Provider
ids Identities
}
func (im *identityManager) Add(ids ...Provider) {
im.Lock()
for _, id := range ids {
im.ids[id.GetIdentity()] = id
}
im.Unlock()
}
func (im *identityManager) Reset() {
im.Lock()
id := im.GetIdentity()
im.ids = Identities{id.GetIdentity(): id}
im.Unlock()
}
func (im *identityManager) GetIdentities() Identities {
im.Lock()
defer im.Unlock()
return im.ids
}
func (im *identityManager) Search(id Identity) Provider {
im.Lock()
defer im.Unlock()
return im.ids.search(id.GetIdentity())
}

42
identity/identity_test.go Normal file
View file

@ -0,0 +1,42 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package identity
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestIdentityManager(t *testing.T) {
c := qt.New(t)
id1 := testIdentity{name: "id1"}
im := NewManager(id1)
c.Assert(im.Search(id1).GetIdentity(), qt.Equals, id1)
c.Assert(im.Search(testIdentity{name: "notfound"}), qt.Equals, nil)
}
type testIdentity struct {
name string
}
func (id testIdentity) GetIdentity() Identity {
return id
}
func (id testIdentity) Name() string {
return id.name
}

View file

@ -18,6 +18,7 @@ package asciidoc
import ( import (
"os/exec" "os/exec"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/internal" "github.com/gohugoio/hugo/markup/internal"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
@ -47,6 +48,10 @@ func (a *asciidocConverter) Convert(ctx converter.RenderContext) (converter.Resu
return converter.Bytes(a.getAsciidocContent(ctx.Src, a.ctx)), nil 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 // getAsciidocContent calls asciidoctor or asciidoc as an external helper
// to convert AsciiDoc content to HTML. // to convert AsciiDoc content to HTML.
func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) []byte { func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) []byte {

View file

@ -15,6 +15,7 @@
package blackfriday package blackfriday
import ( import (
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config" "github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/russross/blackfriday" "github.com/russross/blackfriday"
@ -72,6 +73,10 @@ func (c *blackfridayConverter) Convert(ctx converter.RenderContext) (converter.R
return converter.Bytes(blackfriday.Markdown(ctx.Src, r, c.extensions)), nil 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 { func (c *blackfridayConverter) getHTMLRenderer(renderTOC bool) blackfriday.Renderer {
flags := getFlags(renderTOC, c.bf) flags := getFlags(renderTOC, c.bf)

View file

@ -16,6 +16,8 @@ package converter
import ( import (
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config" "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/markup_config"
"github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -67,6 +69,7 @@ func (n newConverter) Name() string {
// another format, e.g. Markdown to HTML. // another format, e.g. Markdown to HTML.
type Converter interface { type Converter interface {
Convert(ctx RenderContext) (Result, error) Convert(ctx RenderContext) (Result, error)
Supports(feature identity.Identity) bool
} }
// Result represents the minimum returned from Convert. // Result represents the minimum returned from Convert.
@ -94,6 +97,7 @@ func (b Bytes) Bytes() []byte {
// DocumentContext holds contextual information about the document to convert. // DocumentContext holds contextual information about the document to convert.
type DocumentContext struct { type DocumentContext struct {
Document interface{} // May be nil. Usually a page.Page
DocumentID string DocumentID string
DocumentName string DocumentName string
ConfigOverrides map[string]interface{} ConfigOverrides map[string]interface{}
@ -101,6 +105,11 @@ type DocumentContext struct {
// RenderContext holds contextual information about the content to render. // RenderContext holds contextual information about the content to render.
type RenderContext struct { type RenderContext struct {
Src []byte Src []byte
RenderTOC bool RenderTOC bool
RenderHooks *hooks.Render
} }
var (
FeatureRenderHooks = identity.NewPathIdentity("markup", "renderingHooks")
)

View file

@ -0,0 +1,57 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hooks
import (
"io"
"github.com/gohugoio/hugo/identity"
)
type LinkContext interface {
Page() interface{}
Destination() string
Title() string
Text() string
}
type Render struct {
LinkRenderer LinkRenderer
ImageRenderer LinkRenderer
}
func (r *Render) Eq(other interface{}) bool {
ro, ok := other.(*Render)
if !ok {
return false
}
if r == nil || ro == nil {
return r == nil
}
if r.ImageRenderer.GetIdentity() != ro.ImageRenderer.GetIdentity() {
return false
}
if r.LinkRenderer.GetIdentity() != ro.LinkRenderer.GetIdentity() {
return false
}
return true
}
type LinkRenderer interface {
Render(w io.Writer, ctx LinkContext) error
identity.Provider
}

View file

@ -15,21 +15,22 @@
package goldmark package goldmark
import ( import (
"bufio"
"bytes" "bytes"
"fmt" "fmt"
"path/filepath" "path/filepath"
"runtime/debug" "runtime/debug"
"github.com/gohugoio/hugo/identity"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/alecthomas/chroma/styles"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/highlight" "github.com/gohugoio/hugo/markup/highlight"
"github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
hl "github.com/yuin/goldmark-highlighting" hl "github.com/yuin/goldmark-highlighting"
@ -48,7 +49,7 @@ type provide struct {
} }
func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) { 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 converter.NewProvider("goldmark", func(ctx converter.DocumentContext) (converter.Converter, error) {
return &goldmarkConverter{ return &goldmarkConverter{
ctx: ctx, ctx: ctx,
@ -64,11 +65,13 @@ type goldmarkConverter struct {
cfg converter.ProviderConfig cfg converter.ProviderConfig
} }
func newMarkdown(mcfg markup_config.Config) goldmark.Markdown { func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
cfg := mcfg.Goldmark mcfg := pcfg.MarkupConfig
cfg := pcfg.MarkupConfig.Goldmark
var ( var (
extensions = []goldmark.Extender{ extensions = []goldmark.Extender{
newLinks(),
newTocExtension(), newTocExtension(),
} }
rendererOptions []renderer.Option rendererOptions []renderer.Option
@ -143,15 +146,53 @@ func newMarkdown(mcfg markup_config.Config) goldmark.Markdown {
} }
var _ identity.IdentitiesProvider = (*converterResult)(nil)
type converterResult struct { type converterResult struct {
converter.Result converter.Result
toc tableofcontents.Root toc tableofcontents.Root
ids identity.Identities
} }
func (c converterResult) TableOfContents() tableofcontents.Root { func (c converterResult) TableOfContents() tableofcontents.Root {
return c.toc 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) { func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result converter.Result, err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@ -166,9 +207,7 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
result = buf result = buf
pctx := parser.NewContext() pctx := newParserContext(ctx)
pctx.Set(tocEnableKey, ctx.RenderTOC)
reader := text.NewReader(ctx.Src) reader := text.NewReader(ctx.Src)
doc := c.md.Parser().Parse( doc := c.md.Parser().Parse(
@ -176,27 +215,58 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert
parser.WithContext(pctx), 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 return nil, err
} }
if toc, ok := pctx.Get(tocResultKey).(tableofcontents.Root); ok { return converterResult{
return converterResult{ Result: buf,
Result: buf, ids: rcx.ids.GetIdentities(),
toc: toc, toc: pctx.TableOfContents(),
}, nil }, nil
}
return buf, nil }
var featureSet = map[identity.Identity]bool{
converter.FeatureRenderHooks: true,
}
func (c *goldmarkConverter) Supports(feature identity.Identity) bool {
return featureSet[feature.GetIdentity()]
}
func newParserContext(rctx converter.RenderContext) *parserContext {
ctx := parser.NewContext()
ctx.Set(tocEnableKey, rctx.RenderTOC)
return &parserContext{
Context: ctx,
}
}
type parserContext struct {
parser.Context
}
func (p *parserContext) TableOfContents() tableofcontents.Root {
if v := p.Get(tocResultKey); v != nil {
return v.(tableofcontents.Root)
}
return tableofcontents.Root{}
} }
func newHighlighting(cfg highlight.Config) goldmark.Extender { func newHighlighting(cfg highlight.Config) goldmark.Extender {
style := styles.Get(cfg.Style) return hl.NewHighlighting(
if style == nil {
style = styles.Fallback
}
e := hl.NewHighlighting(
hl.WithStyle(cfg.Style), hl.WithStyle(cfg.Style),
hl.WithGuessLanguage(cfg.GuessSyntax), hl.WithGuessLanguage(cfg.GuessSyntax),
hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()), hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()),
@ -230,6 +300,4 @@ func newHighlighting(cfg highlight.Config) goldmark.Extender {
}), }),
) )
return e
} }

View file

@ -38,6 +38,9 @@ func TestConvert(t *testing.T) {
https://github.com/gohugoio/hugo/issues/6528 https://github.com/gohugoio/hugo/issues/6528
[Live Demo here!](https://docuapi.netlify.com/) [Live Demo here!](https://docuapi.netlify.com/)
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
## Code Fences ## Code Fences
§§§bash §§§bash
@ -98,6 +101,7 @@ description
mconf := markup_config.Default mconf := markup_config.Default
mconf.Highlight.NoClasses = false mconf.Highlight.NoClasses = false
mconf.Goldmark.Renderer.Unsafe = true
p, err := Provider.New( p, err := Provider.New(
converter.ProviderConfig{ converter.ProviderConfig{
@ -106,15 +110,15 @@ description
}, },
) )
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
conv, err := p.New(converter.DocumentContext{}) conv, err := p.New(converter.DocumentContext{DocumentID: "thedoc"})
c.Assert(err, qt.IsNil) 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) c.Assert(err, qt.IsNil)
got := string(b.Bytes()) got := string(b.Bytes())
// Links // 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 // Header IDs
c.Assert(got, qt.Contains, `<h2 id="custom">Custom ID</h2>`, qt.Commentf(got)) c.Assert(got, qt.Contains, `<h2 id="custom">Custom ID</h2>`, qt.Commentf(got))
@ -137,6 +141,11 @@ description
c.Assert(got, qt.Contains, `<section class="footnotes" role="doc-endnotes">`) c.Assert(got, qt.Contains, `<section class="footnotes" role="doc-endnotes">`)
c.Assert(got, qt.Contains, `<dt>date</dt>`) 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")
} }
func TestCodeFence(t *testing.T) { func TestCodeFence(t *testing.T) {

View file

@ -0,0 +1,208 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package goldmark
import (
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
var _ renderer.SetOptioner = (*linkRenderer)(nil)
func newLinkRenderer() renderer.NodeRenderer {
r := &linkRenderer{
Config: html.Config{
Writer: html.DefaultWriter,
},
}
return r
}
func newLinks() goldmark.Extender {
return &links{}
}
type linkContext struct {
page interface{}
destination string
title string
text string
}
func (ctx linkContext) Destination() string {
return ctx.destination
}
func (ctx linkContext) Resolved() bool {
return false
}
func (ctx linkContext) Page() interface{} {
return ctx.page
}
func (ctx linkContext) Text() string {
return ctx.text
}
func (ctx linkContext) Title() string {
return ctx.title
}
type linkRenderer struct {
html.Config
}
func (r *linkRenderer) SetOption(name renderer.OptionName, value interface{}) {
r.Config.SetOption(name, value)
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs.
func (r *linkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindLink, r.renderLink)
reg.Register(ast.KindImage, r.renderImage)
}
// Fall back to the default Goldmark render funcs. Method below borrowed from:
// https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
func (r *linkRenderer) renderDefaultImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Image)
_, _ = w.WriteString("<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),
))
}

View file

@ -15,6 +15,7 @@
package mmark package mmark
import ( import (
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config" "github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/miekg/mmark" "github.com/miekg/mmark"
@ -65,6 +66,10 @@ func (c *mmarkConverter) Convert(ctx converter.RenderContext) (converter.Result,
return mmark.Parse(ctx.Src, r, c.extensions), nil return mmark.Parse(ctx.Src, r, c.extensions), nil
} }
func (c *mmarkConverter) Supports(feature identity.Identity) bool {
return false
}
func getHTMLRenderer( func getHTMLRenderer(
ctx converter.DocumentContext, ctx converter.DocumentContext,
cfg blackfriday_config.Config, cfg blackfriday_config.Config,

View file

@ -17,6 +17,8 @@ package org
import ( import (
"bytes" "bytes"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
"github.com/niklasfasching/go-org/org" "github.com/niklasfasching/go-org/org"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -66,3 +68,7 @@ func (c *orgConverter) Convert(ctx converter.RenderContext) (converter.Result, e
} }
return converter.Bytes([]byte(html)), nil return converter.Bytes([]byte(html)), nil
} }
func (c *orgConverter) Supports(feature identity.Identity) bool {
return false
}

View file

@ -17,6 +17,7 @@ package pandoc
import ( import (
"os/exec" "os/exec"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/internal" "github.com/gohugoio/hugo/markup/internal"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
@ -47,6 +48,10 @@ func (c *pandocConverter) Convert(ctx converter.RenderContext) (converter.Result
return converter.Bytes(c.getPandocContent(ctx.Src, c.ctx)), nil 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. // getPandocContent calls pandoc as an external helper to convert pandoc markdown to HTML.
func (c *pandocConverter) getPandocContent(src []byte, ctx converter.DocumentContext) []byte { func (c *pandocConverter) getPandocContent(src []byte, ctx converter.DocumentContext) []byte {
logger := c.cfg.Logger logger := c.cfg.Logger

View file

@ -19,6 +19,7 @@ import (
"os/exec" "os/exec"
"runtime" "runtime"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/internal" "github.com/gohugoio/hugo/markup/internal"
"github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/converter"
@ -48,6 +49,10 @@ func (c *rstConverter) Convert(ctx converter.RenderContext) (converter.Result, e
return converter.Bytes(c.getRstContent(ctx.Src, c.ctx)), nil 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 // getRstContent calls the Python script rst2html as an external helper
// to convert reStructuredText content to HTML. // to convert reStructuredText content to HTML.
func (c *rstConverter) getRstContent(src []byte, ctx converter.DocumentContext) []byte { func (c *rstConverter) getRstContent(src []byte, ctx converter.DocumentContext) []byte {

View file

@ -37,6 +37,12 @@ type LayoutDescriptor struct {
Layout string Layout string
// LayoutOverride indicates what we should only look for the above layout. // LayoutOverride indicates what we should only look for the above layout.
LayoutOverride bool 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. // LayoutHandler calculates the layout template to use to render a given output type.
@ -89,7 +95,7 @@ type layoutBuilder struct {
func (l *layoutBuilder) addLayoutVariations(vars ...string) { func (l *layoutBuilder) addLayoutVariations(vars ...string) {
for _, layoutVar := range vars { for _, layoutVar := range vars {
if l.d.LayoutOverride && layoutVar != l.d.Layout { if !l.d.RenderingHook && l.d.LayoutOverride && layoutVar != l.d.Layout {
continue continue
} }
l.layoutVariations = append(l.layoutVariations, layoutVar) l.layoutVariations = append(l.layoutVariations, layoutVar)
@ -99,6 +105,9 @@ func (l *layoutBuilder) addLayoutVariations(vars ...string) {
func (l *layoutBuilder) addTypeVariations(vars ...string) { func (l *layoutBuilder) addTypeVariations(vars ...string) {
for _, typeVar := range vars { for _, typeVar := range vars {
if !reservedSections[typeVar] { if !reservedSections[typeVar] {
if l.d.RenderingHook {
typeVar = typeVar + renderingHookRoot
}
l.typeVariations = append(l.typeVariations, typeVar) l.typeVariations = append(l.typeVariations, typeVar)
} }
} }
@ -115,16 +124,21 @@ func (l *layoutBuilder) addKind() {
l.addTypeVariations(l.d.Kind) l.addTypeVariations(l.d.Kind)
} }
const renderingHookRoot = "/_markup"
func resolvePageTemplate(d LayoutDescriptor, f Format) []string { func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
b := &layoutBuilder{d: d, f: f} b := &layoutBuilder{d: d, f: f}
if d.Layout != "" { if d.RenderingHook {
b.addLayoutVariations(d.Layout) b.addLayoutVariations(d.Kind)
} } else {
if d.Layout != "" {
if d.Type != "" { b.addLayoutVariations(d.Layout)
b.addTypeVariations(d.Type) }
if d.Type != "" {
b.addTypeVariations(d.Type)
}
} }
switch d.Kind { switch d.Kind {
@ -159,7 +173,7 @@ func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
} }
isRSS := f.Name == RSSFormat.Name isRSS := f.Name == RSSFormat.Name
if isRSS { if !d.RenderingHook && isRSS {
// The historic and common rss.xml case // The historic and common rss.xml case
b.addLayoutVariations("") b.addLayoutVariations("")
} }
@ -167,14 +181,14 @@ func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
// All have _default in their lookup path // All have _default in their lookup path
b.addTypeVariations("_default") b.addTypeVariations("_default")
if d.Kind != "page" { if d.isList() {
// Add the common list type // Add the common list type
b.addLayoutVariations("list") b.addLayoutVariations("list")
} }
layouts := b.resolveVariations() layouts := b.resolveVariations()
if isRSS { if !d.RenderingHook && isRSS {
layouts = append(layouts, "_internal/_default/rss.xml") layouts = append(layouts, "_internal/_default/rss.xml")
} }

View file

@ -111,6 +111,9 @@ func TestLayout(t *testing.T) {
[]string{"section/shortcodes.amp.html"}, 12}, []string{"section/shortcodes.amp.html"}, 12},
{"Reserved section, partials", LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials"}, "", ampType, {"Reserved section, partials", LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials"}, "", ampType,
[]string{"section/partials.amp.html"}, 12}, []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) { c.Run(this.name, func(c *qt.C) {
l := NewLayoutHandler() l := NewLayoutHandler()

View file

@ -201,9 +201,10 @@ type PageMetaProvider interface {
Weight() int 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 { 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. // PageWithoutContent is the Page without any of the content methods.

View file

@ -371,8 +371,12 @@ func (p *nopPage) RelRef(argsm map[string]interface{}) (string, error) {
return "", nil return "", nil
} }
func (p *nopPage) Render(layout ...string) template.HTML { func (p *nopPage) Render(layout ...string) (template.HTML, error) {
return "" return "", nil
}
func (p *nopPage) RenderString(args ...interface{}) (template.HTML, error) {
return "", nil
} }
func (p *nopPage) ResourceType() string { func (p *nopPage) ResourceType() string {

View file

@ -446,7 +446,11 @@ func (p *testPage) RelRefFrom(argsm map[string]interface{}, source interface{})
return "", nil 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") panic("not implemented")
} }

View file

@ -59,6 +59,7 @@ var (
"type state struct", "type stateOld struct", "type state struct", "type stateOld struct",
"func (s *state) evalFunction", "func (s *state) evalFunctionOld", "func (s *state) evalFunction", "func (s *state) evalFunctionOld",
"func (s *state) evalField(", "func (s *state) evalFieldOld(", "func (s *state) evalField(", "func (s *state) evalFieldOld(",
"func (s *state) evalCall(", "func (s *state) evalCallOld(",
) )
htmlTemplateReplacers = strings.NewReplacer( htmlTemplateReplacers = strings.NewReplacer(

View file

@ -658,7 +658,7 @@ var (
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so // 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] // 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. // 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 { if args != nil {
args = args[1:] // Zeroth arg is function name/node; not passed to function. args = args[1:] // Zeroth arg is function name/node; not passed to function.
} }

View file

@ -34,8 +34,9 @@ type Preparer interface {
// ExecHelper allows some custom eval hooks. // ExecHelper allows some custom eval hooks.
type ExecHelper interface { type ExecHelper interface {
GetFunc(name string) (reflect.Value, bool) GetFunc(tmpl Preparer, name string) (reflect.Value, bool)
GetMapValue(receiver, key reflect.Value) (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. // Executer executes a given template.
@ -64,6 +65,7 @@ func (t *executer) Execute(p Preparer, wr io.Writer, data interface{}) error {
state := &state{ state := &state{
helper: t.helper, helper: t.helper,
prep: p,
tmpl: tmpl, tmpl: tmpl,
wr: wr, wr: wr,
vars: []variable{{"$", value}}, vars: []variable{{"$", value}},
@ -75,7 +77,6 @@ func (t *executer) Execute(p Preparer, wr io.Writer, data interface{}) error {
// Prepare returns a template ready for execution. // Prepare returns a template ready for execution.
func (t *Template) Prepare() (*Template, error) { func (t *Template) Prepare() (*Template, error) {
return t, nil return t, nil
} }
@ -95,6 +96,7 @@ func (t *Template) executeWithState(state *state, value reflect.Value) (err erro
// can execute in parallel. // can execute in parallel.
type state struct { type state struct {
tmpl *Template tmpl *Template
prep Preparer // Added for Hugo.
helper ExecHelper // Added for Hugo. helper ExecHelper // Added for Hugo.
wr io.Writer wr io.Writer
node parse.Node // current node, for errors node parse.Node // current node, for errors
@ -110,7 +112,7 @@ func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd
var ok bool var ok bool
if s.helper != nil { if s.helper != nil {
// Added for Hugo. // Added for Hugo.
function, ok = s.helper.GetFunc(name) function, ok = s.helper.GetFunc(s.prep, name)
} }
if !ok { if !ok {
@ -148,9 +150,23 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() { if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() {
ptr = ptr.Addr() 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) return s.evalCall(dot, method, node, fieldName, args, final)
} }
hasArgs := len(args) > 1 || final != missingVal hasArgs := len(args) > 1 || final != missingVal
// It's not a method; must be a field of a struct or an element of a map. // It's not a method; must be a field of a struct or an element of a map.
switch receiver.Kind() { switch receiver.Kind() {
@ -177,7 +193,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
var result reflect.Value var result reflect.Value
if s.helper != nil { if s.helper != nil {
// Added for Hugo. // Added for Hugo.
result, _ = s.helper.GetMapValue(receiver, nameVal) result, _ = s.helper.GetMapValue(s.prep, receiver, nameVal)
} else { } else {
result = receiver.MapIndex(nameVal) result = receiver.MapIndex(nameVal)
} }
@ -209,3 +225,79 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
s.errorf("can't evaluate field %s in type %s", fieldName, typ) s.errorf("can't evaluate field %s in type %s", fieldName, typ)
panic("not reached") 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
}

View file

@ -27,10 +27,18 @@ type TestStruct struct {
M map[string]string 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 { 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" { if name == "print" {
return zero, false return zero, false
} }
@ -39,11 +47,19 @@ func (e *execHelper) GetFunc(name string) (reflect.Value, bool) {
}), true }), 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())) key = reflect.ValueOf(strings.ToLower(key.String()))
return m.MapIndex(key), true 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) { func TestTemplateExecutor(t *testing.T) {
c := qt.New(t) c := qt.New(t)
@ -51,6 +67,7 @@ func TestTemplateExecutor(t *testing.T) {
{{ print "foo" }} {{ print "foo" }}
{{ printf "hugo" }} {{ printf "hugo" }}
Map: {{ .M.A }} Map: {{ .M.A }}
Method: {{ .Hello1 "v1" }}
`) `)
@ -67,5 +84,6 @@ Map: {{ .M.A }}
c.Assert(got, qt.Contains, "foo") c.Assert(got, qt.Contains, "foo")
c.Assert(got, qt.Contains, "hello hugo") c.Assert(got, qt.Contains, "hello hugo")
c.Assert(got, qt.Contains, "Map: av") c.Assert(got, qt.Contains, "Map: av")
c.Assert(got, qt.Contains, "Method: v2 v1")
} }

View file

@ -116,9 +116,9 @@ func (ns *Namespace) Include(name string, contextList ...interface{}) (interface
return "", fmt.Errorf("partial %q not found", name) return "", fmt.Errorf("partial %q not found", name)
} }
var info tpl.Info var info tpl.ParseInfo
if ip, ok := templ.(tpl.TemplateInfoProvider); ok { if ip, ok := templ.(tpl.Info); ok {
info = ip.TemplateInfo() info = ip.ParseInfo()
} }
var w io.Writer var w io.Writer

View file

@ -24,8 +24,6 @@ import (
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
) )
var _ TemplateInfoProvider = (*TemplateInfo)(nil)
// TemplateManager manages the collection of templates. // TemplateManager manages the collection of templates.
type TemplateManager interface { type TemplateManager interface {
TemplateHandler TemplateHandler
@ -34,7 +32,6 @@ type TemplateManager interface {
AddLateTemplate(name, tpl string) error AddLateTemplate(name, tpl string) error
LoadTemplates(prefix string) error LoadTemplates(prefix string) error
MarkReady() error
RebuildClone() RebuildClone()
} }
@ -80,11 +77,6 @@ type Template interface {
Prepare() (*texttemplate.Template, error) 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. // TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
type TemplateParser interface { type TemplateParser interface {
Parse(name, tpl string) (Template, error) Parse(name, tpl string) (Template, error)
@ -101,10 +93,31 @@ type TemplateDebugger interface {
Debug() Debug()
} }
// TemplateInfo wraps a Template with some additional information. // templateInfo wraps a Template with some additional information.
type TemplateInfo struct { type templateInfo struct {
Template 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: (.*?):") var baseOfRe = regexp.MustCompile("template: (.*?):")
@ -117,10 +130,6 @@ func extractBaseOf(err string) string {
return "" return ""
} }
func (t *TemplateInfo) TemplateInfo() Info {
return t.Info
}
// TemplateFuncGetter allows to find a template func by name. // TemplateFuncGetter allows to find a template func by name.
type TemplateFuncGetter interface { type TemplateFuncGetter interface {
GetFunc(name string) (reflect.Value, bool) GetFunc(name string) (reflect.Value, bool)

View file

@ -13,12 +13,44 @@
package tpl package tpl
import (
"github.com/gohugoio/hugo/identity"
)
// Increments on breaking changes. // Increments on breaking changes.
const TemplateVersion = 2 const TemplateVersion = 2
// Info holds some info extracted from a parsed template. type Info interface {
type Info struct { 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 }} // Set for shortcode templates with any {{ .Inner }}
IsInner bool IsInner bool
@ -26,17 +58,25 @@ type Info struct {
HasReturn bool HasReturn bool
// Config extracted from template. // Config extracted from template.
Config Config Config ParseConfig
} }
func (info Info) IsZero() bool { func (info ParseInfo) IsZero() bool {
return info.Config.Version == 0 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 Version int
} }
var DefaultConfig = Config{ var DefaultParseConfig = ParseConfig{
Version: TemplateVersion, Version: TemplateVersion,
} }
var DefaultParseInfo = ParseInfo{
Config: DefaultParseConfig,
}

View file

@ -83,10 +83,12 @@ func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVari
func (s *shortcodeTemplates) compareVariants(a, b []string) int { func (s *shortcodeTemplates) compareVariants(a, b []string) int {
weight := 0 weight := 0
k := len(a)
for i, av := range a { for i, av := range a {
bv := b[i] bv := b[i]
if av == bv { if av == bv {
weight++ // Add more weight to the left side (language...).
weight = weight + k - i
} else { } else {
weight-- weight--
} }

View file

@ -53,10 +53,10 @@ func TestShortcodesTemplate(t *testing.T) {
name2 string name2 string
expected int expected int
}{ }{
{"Same suffix", "figure.html", "figure.html", 3}, {"Same suffix", "figure.html", "figure.html", 6},
{"Same suffix and output format", "figure.html.html", "figure.html.html", 3}, {"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", 3}, {"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 6},
{"No suffix", "figure", "figure", 3}, {"No suffix", "figure", "figure", 6},
{"Different output format", "figure.amp.html", "figure.html.html", -1}, {"Different output format", "figure.amp.html", "figure.html.html", -1},
{"One with output format, one without", "figure.amp.html", "figure.html", -1}, {"One with output format, one without", "figure.amp.html", "figure.html", -1},
} }

View file

@ -20,6 +20,10 @@ import (
"regexp" "regexp"
"time" "time"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/herrors"
"strings" "strings"
@ -27,7 +31,6 @@ import (
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" 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/hugofs"
"github.com/gohugoio/hugo/tpl/tplimpl/embedded" "github.com/gohugoio/hugo/tpl/tplimpl/embedded"
@ -81,6 +84,7 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler {
common := &templatesCommon{ common := &templatesCommon{
nameBaseTemplateName: make(map[string]string), nameBaseTemplateName: make(map[string]string),
transformNotFound: make(map[string]bool), transformNotFound: make(map[string]bool),
identityNotFound: make(map[string][]identity.Manager),
} }
htmlT := &htmlTemplates{ htmlT := &htmlTemplates{
@ -100,13 +104,16 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler {
Deps: deps, Deps: deps,
layoutsFs: deps.BaseFs.Layouts.Fs, layoutsFs: deps.BaseFs.Layouts.Fs,
templateHandlerCommon: &templateHandlerCommon{ templateHandlerCommon: &templateHandlerCommon{
shortcodes: make(map[string]*shortcodeTemplates), shortcodes: make(map[string]*shortcodeTemplates),
templateInfo: make(map[string]tpl.Info), templateInfo: make(map[string]tpl.Info),
html: htmlT, templateInfoTree: make(map[string]*templateInfoTree),
text: textT, html: htmlT,
text: textT,
}, },
} }
textT.textTemplate.templates = textT
textT.standalone.templates = textT
common.handler = h common.handler = h
return h return h
@ -152,27 +159,26 @@ func (t *htmlTemplates) addTemplate(name, tpl string) (*templateContext, error)
return t.addTemplateIn(t.t, name, tpl) return t.addTemplateIn(t.t, name, tpl)
} }
func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) (*templateContext, error) { func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, templstr string) (*templateContext, error) {
templ, err := tt.New(name).Parse(tpl) templ, err := tt.New(name).Parse(templstr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
typ := resolveTemplateType(name) typ := resolveTemplateType(name)
c, err := applyTemplateTransformersToHMLTTemplate(typ, templ) c, err := t.handler.applyTemplateTransformersToHMLTTemplate(typ, templ)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for k := range c.notFound { for k := range c.templateNotFound {
t.transformNotFound[k] = true t.transformNotFound[k] = true
t.identityNotFound[k] = append(t.identityNotFound[k], c.id)
} }
if typ == templateShortcode { for k := range c.identityNotFound {
t.handler.addShortcodeVariant(name, c.Info, templ) t.identityNotFound[k] = append(t.identityNotFound[k], c.id)
} else {
t.handler.templateInfo[name] = c.Info
} }
return c, nil return c, nil
@ -208,7 +214,7 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin
// * https://github.com/golang/go/issues/16101 // * https://github.com/golang/go/issues/16101
// * https://github.com/gohugoio/hugo/issues/2549 // * https://github.com/gohugoio/hugo/issues/2549
overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
if _, err := applyTemplateTransformersToHMLTTemplate(templateUndefined, overlayTpl); err != nil { if _, err := t.handler.applyTemplateTransformersToHMLTTemplate(templateUndefined, overlayTpl); err != nil {
return err return err
} }
@ -253,6 +259,8 @@ func (l nopLookupVariant) LookupVariant(name string, variants tpl.TemplateVarian
// It implements the templateLoader and tpl.TemplateHandler interfaces. // It implements the templateLoader and tpl.TemplateHandler interfaces.
// There is one templateHandler created per Site. // There is one templateHandler created per Site.
type templateHandler struct { type templateHandler struct {
ready bool
executor texttemplate.Executer executor texttemplate.Executer
funcs map[string]reflect.Value funcs map[string]reflect.Value
@ -324,6 +332,7 @@ func (t *templateHandler) LoadTemplates(prefix string) error {
// Lookup tries to find a template with the given name in both template // Lookup tries to find a template with the given name in both template
// collections: First HTML, then the plain text template collection. // collections: First HTML, then the plain text template collection.
func (t *templateHandler) Lookup(name string) (tpl.Template, bool) { func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
if strings.HasPrefix(name, textTmplNamePrefix) { if strings.HasPrefix(name, textTmplNamePrefix) {
// The caller has explicitly asked for a text template, so only look // The caller has explicitly asked for a text template, so only look
// in the text template collection. // in the text template collection.
@ -345,6 +354,9 @@ func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
// This currently only applies to shortcodes and what we get here is the // This currently only applies to shortcodes and what we get here is the
// shortcode name. // shortcode name.
func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) { func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
if !t.ready {
panic("handler not ready")
}
name = templateBaseName(templateShortcode, name) name = templateBaseName(templateShortcode, name)
s, found := t.shortcodes[name] s, found := t.shortcodes[name]
if !found { if !found {
@ -358,18 +370,17 @@ func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVarian
more := len(s.variants) > 1 more := len(s.variants) > 1
return &tpl.TemplateInfo{ return tpl.WithInfo(sv.templ, sv.info), true, more
Template: sv.templ,
Info: 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. // after this is set.
// TODO(bep) if this proves to be resource heavy, we could detect func (t *templateHandler) markReady() error {
// earlier if we really need this, or make it lazy. defer func() {
func (t *templateHandler) MarkReady() error { t.ready = true
}()
if err := t.postTransform(); err != nil { if err := t.postTransform(); err != nil {
return err return err
} }
@ -483,6 +494,7 @@ func (t *templateHandler) addInternalTemplate(name, tpl string) error {
} }
func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) { func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) {
base := templateBaseName(templateShortcode, name) base := templateBaseName(templateShortcode, name)
shortcodename, variants := templateNameAndVariants(base) shortcodename, variants := templateNameAndVariants(base)
@ -561,18 +573,9 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e
} }
func (t *templateHandler) applyTemplateInfo(templ tpl.Template, found bool) (tpl.Template, bool) { func (t *templateHandler) applyTemplateInfo(templ tpl.Template, found bool) (tpl.Template, bool) {
if adapter, ok := templ.(*tpl.TemplateInfo); ok { if templ != nil {
if adapter.Info.IsZero() {
if info, found := t.templateInfo[templ.Name()]; found {
adapter.Info = info
}
}
} else if templ != nil {
if info, found := t.templateInfo[templ.Name()]; found { if info, found := t.templateInfo[templ.Name()]; found {
return &tpl.TemplateInfo{ return tpl.WithInfo(templ, info), true
Template: templ,
Info: info,
}, true
} }
} }
@ -586,7 +589,11 @@ func (t *templateHandler) checkState() {
} }
func (t *templateHandler) clone(d *deps.Deps) *templateHandler { func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
if !t.ready {
panic("invalid state")
}
c := &templateHandler{ c := &templateHandler{
ready: true,
Deps: d, Deps: d,
layoutsFs: d.BaseFs.Layouts.Fs, layoutsFs: d.BaseFs.Layouts.Fs,
} }
@ -703,36 +710,69 @@ func (t *templateHandler) loadTemplates(prefix string) error {
} }
func (t *templateHandler) postTransform() error { func (t *templateHandler) getOrCreateTemplateInfo(name string) (identity.Manager, tpl.ParseInfo) {
if len(t.html.transformNotFound) == 0 && len(t.text.transformNotFound) == 0 { info, found := t.templateInfo[name]
return nil if found {
return info.(identity.Manager), info.ParseInfo()
}
return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), tpl.DefaultParseInfo
}
func (t *templateHandler) createTemplateInfo(name string) (identity.Manager, tpl.ParseInfo) {
_, found := t.templateInfo[name]
if found {
panic("already created: " + name)
} }
defer func() { return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), tpl.DefaultParseInfo
t.text.transformNotFound = make(map[string]bool) }
t.html.transformNotFound = make(map[string]bool)
}() 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 { for _, s := range []struct {
lookup func(name string) *parse.Tree lookup func(name string) *templateInfoTree
transformNotFound map[string]bool transformNotFound map[string]bool
identityNotFound map[string][]identity.Manager
}{ }{
// html templates // html templates
{func(name string) *parse.Tree { {func(name string) *templateInfoTree {
templ := t.html.lookup(name) templ := t.html.lookup(name)
if templ == nil { if templ == nil {
return nil return nil
} }
return templ.Tree id, info := t.getOrCreateTemplateInfo(name)
}, t.html.transformNotFound}, return &templateInfoTree{
id: id,
info: info,
tree: templ.Tree,
}
}, t.html.transformNotFound, t.html.identityNotFound},
// text templates // text templates
{func(name string) *parse.Tree { {func(name string) *templateInfoTree {
templT := t.text.lookup(name) templT := t.text.lookup(name)
if templT == nil { if templT == nil {
return nil return nil
} }
return templT.Tree id, info := t.getOrCreateTemplateInfo(name)
}, t.text.transformNotFound}, return &templateInfoTree{
id: id,
info: info,
tree: templT.Tree,
}
}, t.text.transformNotFound, t.text.identityNotFound},
} { } {
for name := range s.transformNotFound { for name := range s.transformNotFound {
templ := s.lookup(name) templ := s.lookup(name)
@ -743,6 +783,15 @@ func (t *templateHandler) postTransform() error {
} }
} }
} }
for k, v := range s.identityNotFound {
tmpl := s.lookup(k)
if tmpl != nil {
for _, im := range v {
im.Add(tmpl.id)
}
}
}
} }
return nil return nil
@ -758,7 +807,6 @@ func (t *templateHandler) wrapTextTemplate(tt *textTemplate) tpl.TemplateParseFi
tt, tt,
new(nopLookupVariant), new(nopLookupVariant),
} }
} }
type templateHandlerCommon struct { type templateHandlerCommon struct {
@ -771,6 +819,9 @@ type templateHandlerCommon struct {
// shortcodeTemplates type. // shortcodeTemplates type.
templateInfo map[string]tpl.Info 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 holds all the pure text templates.
text *textTemplates text *textTemplates
html *htmlTemplates html *htmlTemplates
@ -795,9 +846,12 @@ type templatesCommon struct {
// Used to get proper filenames in errors // Used to get proper filenames in errors
nameBaseTemplateName map[string]string 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. // pass.
transformNotFound map[string]bool 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 { func (t templatesCommon) withNewHandler(h *templateHandler) *templatesCommon {
@ -806,8 +860,9 @@ func (t templatesCommon) withNewHandler(h *templateHandler) *templatesCommon {
} }
type textTemplate struct { type textTemplate struct {
mu sync.RWMutex mu sync.RWMutex
t *texttemplate.Template t *texttemplate.Template
templates *textTemplates
} }
func (t *textTemplate) Lookup(name string) (tpl.Template, bool) { func (t *textTemplate) Lookup(name string) (tpl.Template, bool) {
@ -831,7 +886,7 @@ func (t *textTemplate) parseIn(tt *texttemplate.Template, name, tpl string) (*te
return nil, err return nil, err
} }
if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil { if _, err := t.templates.handler.applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil {
return nil, err return nil, err
} }
return templ, nil return templ, nil
@ -868,30 +923,24 @@ func (t *textTemplates) addTemplate(name, tpl string) (*templateContext, error)
return t.addTemplateIn(t.t, name, tpl) 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) name = strings.TrimPrefix(name, textTmplNamePrefix)
templ, err := t.parseIn(tt, name, tpl) templ, err := t.parseIn(tt, name, tplstr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
typ := resolveTemplateType(name) typ := resolveTemplateType(name)
c, err := applyTemplateTransformersToTextTemplate(typ, templ) c, err := t.handler.applyTemplateTransformersToTextTemplate(typ, templ)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for k := range c.notFound { for k := range c.templateNotFound {
t.transformNotFound[k] = true t.transformNotFound[k] = true
} }
if typ == templateShortcode {
t.handler.addShortcodeVariant(name, c.Info, templ)
} else {
t.handler.templateInfo[name] = c.Info
}
return c, nil return c, nil
} }
@ -924,7 +973,7 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin
} }
overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil { if _, err := t.handler.applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil {
return err return err
} }
t.overlays[name] = overlayTpl t.overlays[name] = overlayTpl

View file

@ -44,16 +44,13 @@ func (*TemplateProvider) Update(deps *deps.Deps) error {
} }
return newTmpl.MarkReady() return newTmpl.markReady()
} }
// Clone clones. // Clone clones.
func (*TemplateProvider) Clone(d *deps.Deps) error { func (*TemplateProvider) Clone(d *deps.Deps) error {
t := d.Tmpl.(*templateHandler) t := d.Tmpl.(*templateHandler)
clone := t.clone(d) t.clone(d)
return nil
return clone.MarkReady()
} }

View file

@ -14,8 +14,12 @@
package tplimpl package tplimpl
import ( 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" texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse" "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
@ -34,9 +38,10 @@ const (
) )
type templateContext struct { type templateContext struct {
visited map[string]bool visited map[string]bool
notFound map[string]bool templateNotFound map[string]bool
lookupFn func(name string) *parse.Tree identityNotFound map[string]bool
lookupFn func(name string) *templateInfoTree
// The last error encountered. // The last error encountered.
err error err error
@ -47,13 +52,14 @@ type templateContext struct {
configChecked bool configChecked bool
// Contains some info about the template // Contains some info about the template
tpl.Info parseInfo *tpl.ParseInfo
id identity.Manager
// Store away the return node in partials. // Store away the return node in partials.
returnNode *parse.CommandNode returnNode *parse.CommandNode
} }
func (c templateContext) getIfNotVisited(name string) *parse.Tree { func (c templateContext) getIfNotVisited(name string) *templateInfoTree {
if c.visited[name] { if c.visited[name] {
return nil return nil
} }
@ -63,59 +69,95 @@ func (c templateContext) getIfNotVisited(name string) *parse.Tree {
// This may be a inline template defined outside of this file // This may be a inline template defined outside of this file
// and not yet parsed. Unusual, but it happens. // and not yet parsed. Unusual, but it happens.
// Store the name to try again later. // Store the name to try again later.
c.notFound[name] = true c.templateNotFound[name] = true
} }
return templ return templ
} }
func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext { func newTemplateContext(
return &templateContext{ id identity.Manager,
Info: tpl.Info{Config: tpl.DefaultConfig}, info *tpl.ParseInfo,
lookupFn: lookupFn, lookupFn func(name string) *templateInfoTree) *templateContext {
visited: make(map[string]bool),
notFound: make(map[string]bool)}
}
func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree { return &templateContext{
return func(nn string) *parse.Tree { id: id,
tt := templ.Lookup(nn) parseInfo: info,
if tt != nil { lookupFn: lookupFn,
return tt.Tree visited: make(map[string]bool),
} templateNotFound: make(map[string]bool),
return nil identityNotFound: make(map[string]bool),
} }
} }
func applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) { func createGetTemplateInfoTreeFor(getID func(name string) *templateInfoTree) func(nn string) *templateInfoTree {
return applyTemplateTransformers(typ, templ.Tree, createParseTreeLookup(templ)) return func(nn string) *templateInfoTree {
return getID(nn)
}
} }
func applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) { func (t *templateHandler) applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) {
return applyTemplateTransformers(typ, templ.Tree, id, info := t.createTemplateInfo(templ.Name())
func(nn string) *parse.Tree { ti := &templateInfoTree{
tt := templ.Lookup(nn) tree: templ.Tree,
if tt != nil { templ: templ,
return tt.Tree typ: typ,
} id: id,
return nil info: info,
}) }
t.templateInfoTree[templ.Name()] = ti
getTemplateInfoTree := createGetTemplateInfoTreeFor(func(name string) *templateInfoTree {
return t.templateInfoTree[name]
})
return applyTemplateTransformers(typ, ti, getTemplateInfoTree)
} }
func applyTemplateTransformers(typ templateType, templ *parse.Tree, lookupFn func(name string) *parse.Tree) (*templateContext, error) { func (t *templateHandler) applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) {
id, info := t.createTemplateInfo(templ.Name())
ti := &templateInfoTree{
tree: templ.Tree,
templ: templ,
typ: typ,
id: id,
info: info,
}
t.templateInfoTree[templ.Name()] = ti
getTemplateInfoTree := createGetTemplateInfoTreeFor(func(name string) *templateInfoTree {
return t.templateInfoTree[name]
})
return applyTemplateTransformers(typ, ti, getTemplateInfoTree)
}
type templateInfoTree struct {
info tpl.ParseInfo
typ templateType
id identity.Manager
templ tpl.Template
tree *parse.Tree
}
func applyTemplateTransformers(
typ templateType,
templ *templateInfoTree,
lookupFn func(name string) *templateInfoTree) (*templateContext, error) {
if templ == nil { if templ == nil {
return nil, errors.New("expected template, but none provided") return nil, errors.New("expected template, but none provided")
} }
c := newTemplateContext(lookupFn) c := newTemplateContext(templ.id, &templ.info, lookupFn)
c.typ = typ c.typ = typ
_, err := c.applyTransformations(templ.Root) _, err := c.applyTransformations(templ.tree.Root)
if err == nil && c.returnNode != nil { if err == nil && c.returnNode != nil {
// This is a partial with a return statement. // This is a partial with a return statement.
c.Info.HasReturn = true c.parseInfo.HasReturn = true
templ.Root = c.wrapInPartialReturnWrapper(templ.Root) templ.tree.Root = c.wrapInPartialReturnWrapper(templ.tree.Root)
} }
return c, err return c, err
@ -125,7 +167,9 @@ const (
partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ with .Arg }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}` partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ with .Arg }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
) )
var partialReturnWrapper *parse.ListNode var (
partialReturnWrapper *parse.ListNode
)
func init() { func init() {
templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl) templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
@ -133,6 +177,7 @@ func init() {
panic(err) panic(err)
} }
partialReturnWrapper = templ.Tree.Root partialReturnWrapper = templ.Tree.Root
} }
func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode { func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
@ -156,6 +201,7 @@ func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.L
// getif works slightly different than the Go built-in in that it also // getif works slightly different than the Go built-in in that it also
// considers any IsZero methods on the values (as in time.Time). // considers any IsZero methods on the values (as in time.Time).
// See https://github.com/gohugoio/hugo/issues/5738 // See https://github.com/gohugoio/hugo/issues/5738
// TODO(bep) get rid of this.
func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) { func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) {
if len(p.Cmds) == 0 { if len(p.Cmds) == 0 {
return return
@ -176,9 +222,9 @@ func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) {
} }
// applyTransformations do 3 things: // applyTransformations do 3 things:
// 1) Make all .Params.CamelCase and similar into lowercase. // 1) Wraps every with and if pipe in getif
// 2) Wraps every with and if pipe in getif // 2) Parses partial return statement.
// 3) Collects some information about the template content. // 3) Tracks template (partial) dependencies and some other info.
func (c *templateContext) applyTransformations(n parse.Node) (bool, error) { func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
switch x := n.(type) { switch x := n.(type) {
case *parse.ListNode: case *parse.ListNode:
@ -198,7 +244,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
case *parse.TemplateNode: case *parse.TemplateNode:
subTempl := c.getIfNotVisited(x.Name) subTempl := c.getIfNotVisited(x.Name)
if subTempl != nil { if subTempl != nil {
c.applyTransformationsToNodes(subTempl.Root) c.applyTransformationsToNodes(subTempl.tree.Root)
} }
case *parse.PipeNode: case *parse.PipeNode:
c.collectConfig(x) c.collectConfig(x)
@ -210,6 +256,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
} }
case *parse.CommandNode: case *parse.CommandNode:
c.collectPartialInfo(x)
c.collectInner(x) c.collectInner(x)
keep := c.collectReturnNode(x) keep := c.collectReturnNode(x)
@ -277,11 +324,10 @@ func (c *templateContext) collectConfig(n *parse.PipeNode) {
c.err = errors.Wrap(err, errMsg) c.err = errors.Wrap(err, errMsg)
return 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) c.err = errors.Wrap(err, errMsg)
} }
} }
} }
// collectInner determines if the given CommandNode represents a // collectInner determines if the given CommandNode represents a
@ -290,7 +336,7 @@ func (c *templateContext) collectInner(n *parse.CommandNode) {
if c.typ != templateShortcode { if c.typ != templateShortcode {
return return
} }
if c.Info.IsInner || len(n.Args) == 0 { if c.parseInfo.IsInner || len(n.Args) == 0 {
return return
} }
@ -304,13 +350,45 @@ func (c *templateContext) collectInner(n *parse.CommandNode) {
} }
if c.hasIdent(idents, "Inner") { if c.hasIdent(idents, "Inner") {
c.Info.IsInner = true c.parseInfo.IsInner = true
break 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 { func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
if c.typ != templatePartial || c.returnNode != nil { if c.typ != templatePartial || c.returnNode != nil {
return true return true

View file

@ -15,14 +15,17 @@ package tplimpl
import ( import (
"strings" "strings"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" "github.com/gohugoio/hugo/hugofs/files"
"testing" "testing"
"time" "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" qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/tpl"
) )
// Issue #2927 // Issue #2927
@ -33,7 +36,7 @@ func TestTransformRecursiveTemplate(t *testing.T) {
{{ define "menu-nodes" }} {{ define "menu-nodes" }}
{{ template "menu-node" }} {{ template "menu-node" }}
{{ end }} {{ end }}
{{ define "menu-node" }} {{ define "menu-nßode" }}
{{ template "menu-node" }} {{ template "menu-node" }}
{{ end }} {{ end }}
{{ template "menu-nodes" }} {{ template "menu-nodes" }}
@ -41,12 +44,25 @@ func TestTransformRecursiveTemplate(t *testing.T) {
templ, err := template.New("foo").Parse(recursive) templ, err := template.New("foo").Parse(recursive)
c.Assert(err, qt.IsNil) 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) 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 { type I interface {
Method0() Method0()
} }
@ -80,13 +96,10 @@ func TestInsertIsZeroFunc(t *testing.T) {
{{ with .TimeZero }}.TimeZero1 with: {{ . }}{{ else }}.TimeZero1 with: FALSE{{ end }} {{ with .TimeZero }}.TimeZero1 with: {{ . }}{{ else }}.TimeZero1 with: FALSE{{ end }}
{{ template "mytemplate" . }} {{ template "mytemplate" . }}
{{ if .T.NonEmptyInterfaceTypedNil }}.NonEmptyInterfaceTypedNil: TRUE{{ else }}.NonEmptyInterfaceTypedNil: FALSE{{ end }} {{ if .T.NonEmptyInterfaceTypedNil }}.NonEmptyInterfaceTypedNil: TRUE{{ else }}.NonEmptyInterfaceTypedNil: FALSE{{ end }}
{{ template "other-file-template" . }} {{ template "other-file-template" . }}
{{ define "mytemplate" }} {{ define "mytemplate" }}
{{ if .TimeZero }}.TimeZero1: mytemplate: TRUE{{ else }}.TimeZero1: mytemplate: FALSE{{ end }} {{ if .TimeZero }}.TimeZero1: mytemplate: TRUE{{ else }}.TimeZero1: mytemplate: FALSE{{ end }}
{{ end }} {{ end }}
` `
// https://github.com/gohugoio/hugo/issues/5865 // https://github.com/gohugoio/hugo/issues/5865
@ -97,7 +110,7 @@ func TestInsertIsZeroFunc(t *testing.T) {
) )
d := newD(c) d := newD(c)
h := d.Tmpl.(tpl.TemplateManager) h := d.Tmpl.(*templateHandler)
// HTML templates // HTML templates
c.Assert(h.AddTemplate("mytemplate.html", templ1), qt.IsNil) c.Assert(h.AddTemplate("mytemplate.html", templ1), qt.IsNil)
@ -107,15 +120,13 @@ func TestInsertIsZeroFunc(t *testing.T) {
c.Assert(h.AddTemplate("_text/mytexttemplate.txt", templ1), qt.IsNil) c.Assert(h.AddTemplate("_text/mytexttemplate.txt", templ1), qt.IsNil)
c.Assert(h.AddTemplate("_text/myothertexttemplate.txt", templ2), 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"} { for _, name := range []string{"mytemplate.html", "mytexttemplate.txt"} {
var sb strings.Builder
tt, _ := d.Tmpl.Lookup(name) tt, _ := d.Tmpl.Lookup(name)
sb := &strings.Builder{} err := h.Execute(tt, &sb, ctx)
err := d.Tmpl.Execute(tt, sb, ctx)
c.Assert(err, qt.IsNil) c.Assert(err, qt.IsNil)
result := sb.String() result := sb.String()
c.Assert(result, qt.Contains, ".True: TRUE") c.Assert(result, qt.Contains, ".True: TRUE")
@ -138,14 +149,10 @@ func TestCollectInfo(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
tplString string tplString string
expected tpl.Info expected tpl.ParseInfo
}{ }{
{"Basic Inner", `{{ .Inner }}`, tpl.Info{IsInner: true, Config: tpl.DefaultConfig}}, {"Basic Inner", `{{ .Inner }}`, tpl.ParseInfo{IsInner: true, Config: tpl.DefaultParseConfig}},
{"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.Info{ {"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.ParseInfo{Config: tpl.ParseConfig{Version: 42}}},
Config: tpl.Config{
Version: 42,
},
}},
} }
echo := func(in interface{}) interface{} { echo := func(in interface{}) interface{} {
@ -162,12 +169,13 @@ func TestCollectInfo(t *testing.T) {
templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString) templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
c.Assert(err, qt.IsNil) 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.typ = templateShortcode
ctx.applyTransformations(templ.Tree.Root) ctx.applyTransformations(templ.Tree.Root)
c.Assert(ctx.parseInfo, qt.DeepEquals, &test.expected)
c.Assert(ctx.Info, qt.Equals, test.expected)
}) })
} }
@ -205,7 +213,10 @@ func TestPartialReturn(t *testing.T) {
templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString) templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
c.Assert(err, qt.IsNil) 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 // Just check that it doesn't fail in this test. We have functional tests
// in hugoblib. // in hugoblib.
@ -215,3 +226,10 @@ func TestPartialReturn(t *testing.T) {
} }
} }
func newTemplateInfo(name string) tpl.Info {
return tpl.NewInfo(
identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)),
tpl.DefaultParseInfo,
)
}

View file

@ -19,6 +19,8 @@ import (
"reflect" "reflect"
"strings" "strings"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate" template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
@ -62,14 +64,14 @@ type templateExecHelper struct {
funcs map[string]reflect.Value 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 { if fn, found := t.funcs[name]; found {
return fn, true return fn, true
} }
return zero, false 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 { if params, ok := receiver.Interface().(maps.Params); ok {
// Case insensitive. // Case insensitive.
keystr := strings.ToLower(key.String()) keystr := strings.ToLower(key.String())
@ -85,6 +87,22 @@ func (t *templateExecHelper) GetMapValue(receiver, key reflect.Value) (reflect.V
return v, v.IsValid() 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) { func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
funcs := createFuncMap(d) funcs := createFuncMap(d)
funcsv := make(map[string]reflect.Value) funcsv := make(map[string]reflect.Value)
@ -120,9 +138,7 @@ func createFuncMap(d *deps.Deps) map[string]interface{} {
} }
funcMap[alias] = mm.Method funcMap[alias] = mm.Method
} }
} }
} }
if d.OverloadedTemplateFuncs != nil { if d.OverloadedTemplateFuncs != nil {

View file

@ -24,18 +24,19 @@ import (
func TestTemplateInfoShortcode(t *testing.T) { func TestTemplateInfoShortcode(t *testing.T) {
c := qt.New(t) c := qt.New(t)
d := newD(c) d := newD(c)
h := d.Tmpl.(tpl.TemplateManager) h := d.Tmpl.(*templateHandler)
c.Assert(h.AddTemplate("shortcodes/mytemplate.html", ` c.Assert(h.AddTemplate("shortcodes/mytemplate.html", `
{{ .Inner }} {{ .Inner }}
`), qt.IsNil) `), qt.IsNil)
c.Assert(h.markReady(), qt.IsNil)
tt, found, _ := d.Tmpl.LookupVariant("mytemplate", tpl.TemplateVariants{}) tt, found, _ := d.Tmpl.LookupVariant("mytemplate", tpl.TemplateVariants{})
c.Assert(found, qt.Equals, true) c.Assert(found, qt.Equals, true)
tti, ok := tt.(tpl.TemplateInfoProvider) tti, ok := tt.(tpl.Info)
c.Assert(ok, qt.Equals, true) c.Assert(ok, qt.Equals, true)
c.Assert(tti.TemplateInfo().IsInner, qt.Equals, true) c.Assert(tti.ParseInfo().IsInner, qt.Equals, true)
} }