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

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
}
// FileSystems returns the FileSystems relevant for the change detection
// in server mode.
// Note: This does currently not return any static fs.
func (s *SourceFilesystems) FileSystems() []*SourceFilesystem {
return []*SourceFilesystem{
s.Content,
s.Data,
s.I18n,
s.Layouts,
s.Archetypes,
// TODO(bep) static
}
}
// A SourceFilesystem holds the filesystem for a given source type in Hugo (data,
// i18n, layouts, static) and additional metadata to be able to use that filesystem
// in server mode.
type SourceFilesystem struct {
// Name matches one in files.ComponentFolders
Name string
// This is a virtual composite filesystem. It expects path relative to a context.
Fs afero.Fs
@ -275,6 +293,19 @@ func (d *SourceFilesystem) Contains(filename string) bool {
return false
}
// Path returns the relative path to the given filename if it is a member of
// of the current filesystem, an empty string if not.
func (d *SourceFilesystem) Path(filename string) string {
for _, dir := range d.Dirs {
meta := dir.Meta()
if strings.HasPrefix(filename, meta.Filename()) {
p := strings.TrimPrefix(strings.TrimPrefix(filename, meta.Filename()), filePathSeparator)
return p
}
}
return ""
}
// RealDirs gets a list of absolute paths to directories starting from the given
// path.
func (d *SourceFilesystem) RealDirs(from string) []string {
@ -349,12 +380,14 @@ func newSourceFilesystemsBuilder(p *paths.Paths, logger *loggers.Logger, b *Base
return &sourceFilesystemsBuilder{p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs, result: &SourceFilesystems{}}
}
func (b *sourceFilesystemsBuilder) newSourceFilesystem(fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem {
func (b *sourceFilesystemsBuilder) newSourceFilesystem(name string, fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem {
return &SourceFilesystem{
Name: name,
Fs: fs,
Dirs: dirs,
}
}
func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
if b.theBigFs == nil {
@ -369,12 +402,12 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
createView := func(componentID string) *SourceFilesystem {
if b.theBigFs == nil || b.theBigFs.overlayMounts == nil {
return b.newSourceFilesystem(hugofs.NoOpFs, nil)
return b.newSourceFilesystem(componentID, hugofs.NoOpFs, nil)
}
dirs := b.theBigFs.overlayDirs[componentID]
return b.newSourceFilesystem(afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs)
return b.newSourceFilesystem(componentID, afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs)
}
@ -392,14 +425,14 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
return nil, err
}
b.result.Data = b.newSourceFilesystem(dataFs, dataDirs)
b.result.Data = b.newSourceFilesystem(files.ComponentFolderData, dataFs, dataDirs)
i18nDirs := b.theBigFs.overlayDirs[files.ComponentFolderI18n]
i18nFs, err := hugofs.NewSliceFs(i18nDirs...)
if err != nil {
return nil, err
}
b.result.I18n = b.newSourceFilesystem(i18nFs, i18nDirs)
b.result.I18n = b.newSourceFilesystem(files.ComponentFolderI18n, i18nFs, i18nDirs)
contentDirs := b.theBigFs.overlayDirs[files.ComponentFolderContent]
contentBfs := afero.NewBasePathFs(b.theBigFs.overlayMountsContent, files.ComponentFolderContent)
@ -409,7 +442,7 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
return nil, errors.Wrap(err, "create content filesystem")
}
b.result.Content = b.newSourceFilesystem(contentFs, contentDirs)
b.result.Content = b.newSourceFilesystem(files.ComponentFolderContent, contentFs, contentDirs)
b.result.Work = afero.NewReadOnlyFs(b.theBigFs.overlayFull)
@ -421,13 +454,13 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
if b.theBigFs.staticPerLanguage != nil {
// Multihost mode
for k, v := range b.theBigFs.staticPerLanguage {
sfs := b.newSourceFilesystem(v, b.result.StaticDirs)
sfs := b.newSourceFilesystem(files.ComponentFolderStatic, v, b.result.StaticDirs)
sfs.PublishFolder = k
ms[k] = sfs
}
} else {
bfs := afero.NewBasePathFs(b.theBigFs.overlayMountsStatic, files.ComponentFolderStatic)
ms[""] = b.newSourceFilesystem(bfs, b.result.StaticDirs)
ms[""] = b.newSourceFilesystem(files.ComponentFolderStatic, bfs, b.result.StaticDirs)
}
return b.result, nil

View File

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

View File

@ -20,6 +20,8 @@ import (
"strings"
"sync"
"github.com/gohugoio/hugo/identity"
radix "github.com/armon/go-radix"
"github.com/gohugoio/hugo/output"
@ -411,7 +413,6 @@ func applyDeps(cfg deps.DepsCfg, sites ...*Site) error {
}
d.OutputFormatsConfig = s.outputFormatsConfig
}
}
return nil
@ -806,12 +807,40 @@ func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Page
return h.Sites[0].findPagesByKindIn(kind, inPages)
}
func (h *HugoSites) findPagesByShortcode(shortcode string) page.Pages {
var pages page.Pages
func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) {
for _, s := range h.Sites {
pages = append(pages, s.findPagesByShortcode(shortcode)...)
PAGES:
for _, p := range s.rawAllPages {
OUTPUTS:
for _, po := range p.pageOutputs {
if po.cp == nil {
continue
}
for id, _ := range idset {
if po.cp.dependencyTracker.Search(id) != nil {
po.cp.Reset()
p.forceRender = true
continue OUTPUTS
}
}
}
for _, s := range p.shortcodeState.shortcodes {
for id, _ := range idset {
if idm, ok := s.info.(identity.Manager); ok && idm.Search(id) != nil {
for _, po := range p.pageOutputs {
if po.cp != nil {
po.cp.Reset()
}
}
p.forceRender = true
continue PAGES
}
}
}
}
}
return pages
}
// Used in partial reloading to determine if the change is in a bundle.

View File

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

View File

@ -1459,3 +1459,19 @@ other = %q
return &multiSiteTestBuilder{sitesBuilder: b, configFormat: configFormat, config: config, configData: configData}
}
func TestRebuildOnAssetChange(t *testing.T) {
b := newTestSitesBuilder(t).Running()
b.WithTemplatesAdded("index.html", `
{{ (resources.Get "data.json").Content }}
`)
b.WithSourceFile("assets/data.json", "orig data")
b.Build(BuildCfg{})
b.AssertFileContent("public/index.html", `orig data`)
b.EditFiles("assets/data.json", "changed data")
b.Build(BuildCfg{})
b.AssertFileContent("public/index.html", `changed data`)
}

View File

@ -23,6 +23,12 @@ import (
"sort"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/common/maps"
@ -43,9 +49,11 @@ import (
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/source"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
@ -59,7 +67,11 @@ var (
var (
pageTypesProvider = resource.NewResourceTypesProvider(media.OctetType, pageResourceType)
nopPageOutput = &pageOutput{pagePerOutputProviders: nopPagePerOutput}
nopPageOutput = &pageOutput{
pagePerOutputProviders: nopPagePerOutput,
ContentProvider: page.NopPage,
TableOfContentsProvider: page.NopPage,
}
)
// pageContext provides contextual information about this page, for error
@ -317,6 +329,54 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
return nil
}
func (p *pageState) createRenderHooks(f output.Format) (*hooks.Render, error) {
layoutDescriptor := p.getLayoutDescriptor()
layoutDescriptor.RenderingHook = true
layoutDescriptor.LayoutOverride = false
layoutDescriptor.Layout = ""
layoutDescriptor.Kind = "render-link"
linkLayouts, err := p.s.layoutHandler.For(layoutDescriptor, f)
if err != nil {
return nil, err
}
layoutDescriptor.Kind = "render-image"
imageLayouts, err := p.s.layoutHandler.For(layoutDescriptor, f)
if err != nil {
return nil, err
}
if linkLayouts == nil && imageLayouts == nil {
return nil, nil
}
var linkRenderer hooks.LinkRenderer
var imageRenderer hooks.LinkRenderer
if templ, found := p.s.lookupTemplate(linkLayouts...); found {
linkRenderer = contentLinkRenderer{
templateHandler: p.s.Tmpl,
Provider: templ.(tpl.Info),
templ: templ,
}
}
if templ, found := p.s.lookupTemplate(imageLayouts...); found {
imageRenderer = contentLinkRenderer{
templateHandler: p.s.Tmpl,
Provider: templ.(tpl.Info),
templ: templ,
}
}
return &hooks.Render{
LinkRenderer: linkRenderer,
ImageRenderer: imageRenderer,
}, nil
}
func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
p.layoutDescriptorInit.Do(func() {
var section string
@ -464,11 +524,86 @@ func (p *pageState) AlternativeOutputFormats() page.OutputFormats {
return o
}
func (p *pageState) Render(layout ...string) template.HTML {
type renderStringOpts struct {
Display string
Markup string
}
var defualtRenderStringOpts = renderStringOpts{
Display: "inline",
Markup: "", // Will inherit the page's value when not set.
}
func (p *pageState) RenderString(args ...interface{}) (template.HTML, error) {
if len(args) < 1 || len(args) > 2 {
return "", errors.New("want 1 or 2 arguments")
}
var s string
opts := defualtRenderStringOpts
sidx := 1
if len(args) == 1 {
sidx = 0
} else {
m, ok := args[0].(map[string]interface{})
if !ok {
return "", errors.New("first argument must be a map")
}
if err := mapstructure.WeakDecode(m, &opts); err != nil {
return "", errors.WithMessage(err, "failed to decode options")
}
}
var err error
s, err = cast.ToStringE(args[sidx])
if err != nil {
return "", err
}
conv := p.getContentConverter()
if opts.Markup != "" && opts.Markup != p.m.markup {
var err error
// TODO(bep) consider cache
conv, err = p.m.newContentConverter(p, opts.Markup, nil)
if err != nil {
return "", p.wrapError(err)
}
}
c, err := p.pageOutput.cp.renderContentWithConverter(conv, []byte(s), false)
if err != nil {
return "", p.wrapError(err)
}
b := c.Bytes()
if opts.Display == "inline" {
// We may have to rethink this in the future when we get other
// renderers.
b = p.s.ContentSpec.TrimShortHTML(b)
}
return template.HTML(string(b)), nil
}
func (p *pageState) addDependency(dep identity.Provider) {
if !p.s.running() || p.pageOutput.cp == nil {
return
}
p.pageOutput.cp.dependencyTracker.Add(dep)
}
func (p *pageState) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) {
p.addDependency(info)
return p.Render(layout...)
}
func (p *pageState) Render(layout ...string) (template.HTML, error) {
l, err := p.getLayouts(layout...)
if err != nil {
p.s.SendError(p.wrapError(errors.Errorf(".Render: failed to resolve layout %v", layout)))
return ""
return "", p.wrapError(errors.Errorf("failed to resolve layout %v", layout))
}
for _, layout := range l {
@ -479,17 +614,18 @@ func (p *pageState) Render(layout ...string) template.HTML {
// We default to good old HTML.
templ, _ = p.s.Tmpl.Lookup(layout + ".html")
}
if templ != nil {
p.addDependency(templ.(tpl.Info))
res, err := executeToString(p.s.Tmpl, templ, p)
if err != nil {
p.s.SendError(p.wrapError(errors.Wrapf(err, ".Render: failed to execute template %q v", layout)))
return ""
return "", p.wrapError(errors.Wrapf(err, "failed to execute template %q v", layout))
}
return template.HTML(res)
return template.HTML(res), nil
}
}
return ""
return "", nil
}
@ -745,15 +881,33 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
p.pageOutput.paginator.reset()
}
if idx > 0 {
// Check if we can reuse content from one of the previous formats.
for i := idx - 1; i >= 0; i-- {
po := p.pageOutputs[i]
if po.cp != nil && po.cp.reuse {
p.pageOutput.cp = po.cp
break
if isRenderingSite {
cp := p.pageOutput.cp
if cp == nil {
// Look for content to reuse.
for i := 0; i < len(p.pageOutputs); i++ {
if i == idx {
continue
}
po := p.pageOutputs[i]
if po.cp != nil && po.cp.reuse {
cp = po.cp
break
}
}
}
if cp == nil {
var err error
cp, err = newPageContentOutput(p, p.pageOutput)
if err != nil {
return err
}
}
p.pageOutput.initContentProvider(cp)
p.pageOutput.cp = cp
}
for _, r := range p.Resources().ByType(pageResourceType) {

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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", "list.html"), listLayout)
writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.html"), myShort)
writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.customo"), myShort)
writeSource(t, fs, filepath.Join(workDir, "base", "_index.md"), pageContent)
writeSource(t, fs, filepath.Join(workDir, "base", "_1.md"), pageContent)

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) {
// will find existing page that matches filepath and remove it
c.removePage(page)

View File

@ -23,8 +23,6 @@ import (
"html/template"
"path"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/common/herrors"
"github.com/pkg/errors"
@ -198,7 +196,7 @@ type shortcode struct {
}
func (s shortcode) insertPlaceholder() bool {
return !s.doMarkup || s.info.Config.Version == 1
return !s.doMarkup || s.info.ParseInfo().Config.Version == 1
}
func (s shortcode) innerString() string {
@ -349,14 +347,9 @@ func renderShortcode(
// Pre Hugo 0.55 this was the behaviour even for the outer-most
// shortcode.
if sc.doMarkup && (level > 0 || sc.info.Config.Version == 1) {
if sc.doMarkup && (level > 0 || sc.info.ParseInfo().Config.Version == 1) {
var err error
b, err := p.getContentConverter().Convert(
converter.RenderContext{
Src: []byte(inner),
},
)
b, err := p.pageOutput.cp.renderContent([]byte(inner), false)
if err != nil {
return "", false, err
@ -494,13 +487,13 @@ Loop:
case currItem.IsRightShortcodeDelim():
// we trust the template on this:
// if there's no inner, we're done
if !sc.isInline && !sc.info.IsInner {
if !sc.isInline && !sc.info.ParseInfo().IsInner {
return sc, nil
}
case currItem.IsShortcodeClose():
next := pt.Peek()
if !sc.isInline && !sc.info.IsInner {
if !sc.isInline && !sc.info.ParseInfo().IsInner {
if next.IsError() {
// return that error, more specific
continue
@ -540,7 +533,7 @@ Loop:
return nil, _errors.Errorf("template for shortcode %q not found", sc.name)
}
sc.info = tmpl.(tpl.TemplateInfoProvider).TemplateInfo()
sc.info = tmpl.(tpl.Info)
case currItem.IsInlineShortcodeName():
sc.name = currItem.ValStr()
sc.isInline = true

View File

@ -54,3 +54,22 @@ func (p *pageForShortcode) TableOfContents() template.HTML {
p.p.enablePlaceholders()
return p.toc
}
// This is what is sent into the content render hooks (link, image).
type pageForRenderHooks struct {
page.PageWithoutContent
page.TableOfContentsProvider
page.ContentProvider
}
func newPageForRenderHook(p *pageState) page.Page {
return &pageForRenderHooks{
PageWithoutContent: p,
ContentProvider: page.NopPage,
TableOfContentsProvider: page.NopPage,
}
}
func (p *pageForRenderHooks) page() page.Page {
return p.PageWithoutContent.(page.Page)
}

View File

@ -379,8 +379,13 @@ title: "Shortcodes Galore!"
if s == nil {
return "<nil>"
}
var version int
if s.info != nil {
version = s.info.ParseInfo().Config.Version
}
return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d",
s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, s.info.Config.Version, s.pos))
s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos))
}
regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {

View File

@ -28,6 +28,12 @@ import (
"strings"
"time"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/markup/converter"
@ -60,7 +66,6 @@ import (
"github.com/gohugoio/hugo/navigation"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/related"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/source"
"github.com/gohugoio/hugo/tpl"
@ -801,7 +806,6 @@ func (s *Site) multilingual() *Multilingual {
type whatChanged struct {
source bool
other bool
files map[string]bool
}
@ -888,10 +892,11 @@ func (s *Site) translateFileEvents(events []fsnotify.Event) []fsnotify.Event {
// It returns whetever the content source was changed.
// TODO(bep) clean up/rewrite this method.
func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error {
events = s.filterFileEvents(events)
events = s.translateFileEvents(events)
changeIdentities := make(identity.Identities)
s.Log.DEBUG.Printf("Rebuild for events %q", events)
h := s.h
@ -902,11 +907,12 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
sourceChanged = []fsnotify.Event{}
sourceReallyChanged = []fsnotify.Event{}
contentFilesChanged []string
tmplChanged = []fsnotify.Event{}
dataChanged = []fsnotify.Event{}
i18nChanged = []fsnotify.Event{}
shortcodesChanged = make(map[string]bool)
sourceFilesChanged = make(map[string]bool)
tmplChanged bool
dataChanged bool
i18nChanged bool
sourceFilesChanged = make(map[string]bool)
// prevent spamming the log on changes
logger = helpers.NewDistinctFeedbackLogger()
@ -919,33 +925,30 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...)
}
if s.isContentDirEvent(ev) {
logger.Println("Source changed", ev)
sourceChanged = append(sourceChanged, ev)
}
if s.isLayoutDirEvent(ev) {
logger.Println("Template changed", ev)
tmplChanged = append(tmplChanged, ev)
id, found := s.eventToIdentity(ev)
if found {
changeIdentities[id] = id
switch id.Type {
case files.ComponentFolderContent:
logger.Println("Source changed", ev)
sourceChanged = append(sourceChanged, ev)
case files.ComponentFolderLayouts:
logger.Println("Template changed", ev)
tmplChanged = true
case files.ComponentFolderData:
logger.Println("Data changed", ev)
dataChanged = true
case files.ComponentFolderI18n:
logger.Println("i18n changed", ev)
i18nChanged = true
if strings.Contains(ev.Name, "shortcodes") {
shortcode := filepath.Base(ev.Name)
shortcode = strings.TrimSuffix(shortcode, filepath.Ext(shortcode))
shortcodesChanged[shortcode] = true
}
}
if s.isDataDirEvent(ev) {
logger.Println("Data changed", ev)
dataChanged = append(dataChanged, ev)
}
if s.isI18nEvent(ev) {
logger.Println("i18n changed", ev)
i18nChanged = append(dataChanged, ev)
}
}
changed := &whatChanged{
source: len(sourceChanged) > 0 || len(shortcodesChanged) > 0,
other: len(tmplChanged) > 0 || len(i18nChanged) > 0 || len(dataChanged) > 0,
source: len(sourceChanged) > 0,
files: sourceFilesChanged,
}
@ -960,7 +963,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...)
}
if len(tmplChanged) > 0 || len(i18nChanged) > 0 {
if tmplChanged || i18nChanged {
sites := s.h.Sites
first := sites[0]
@ -989,7 +992,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
}
}
if len(dataChanged) > 0 {
if dataChanged {
s.h.init.data.Reset()
}
@ -1018,18 +1021,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
sourceFilesChanged[ev.Name] = true
}
for shortcode := range shortcodesChanged {
// There are certain scenarios that, when a shortcode changes,
// it isn't sufficient to just rerender the already parsed shortcode.
// One example is if the user adds a new shortcode to the content file first,
// and then creates the shortcode on the file system.
// To handle these scenarios, we must do a full reprocessing of the
// pages that keeps a reference to the changed shortcode.
pagesWithShortcode := h.findPagesByShortcode(shortcode)
for _, p := range pagesWithShortcode {
contentFilesChanged = append(contentFilesChanged, p.File().Filename())
}
}
h.resetPageStateFromEvents(changeIdentities)
if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 {
var filenamesChanged []string
@ -1218,20 +1210,14 @@ func (s *Site) initializeSiteInfo() error {
return nil
}
func (s *Site) isI18nEvent(e fsnotify.Event) bool {
return s.BaseFs.SourceFilesystems.IsI18n(e.Name)
}
func (s *Site) eventToIdentity(e fsnotify.Event) (identity.PathIdentity, bool) {
for _, fs := range s.BaseFs.SourceFilesystems.FileSystems() {
if p := fs.Path(e.Name); p != "" {
return identity.NewPathIdentity(fs.Name, p), true
}
}
func (s *Site) isDataDirEvent(e fsnotify.Event) bool {
return s.BaseFs.SourceFilesystems.IsData(e.Name)
}
func (s *Site) isLayoutDirEvent(e fsnotify.Event) bool {
return s.BaseFs.SourceFilesystems.IsLayout(e.Name)
}
func (s *Site) isContentDirEvent(e fsnotify.Event) bool {
return s.BaseFs.IsContent(e.Name)
return identity.PathIdentity{}, false
}
func (s *Site) readAndProcessContent(filenames ...string) error {
@ -1562,6 +1548,26 @@ var infoOnMissingLayout = map[string]bool{
"404": true,
}
type contentLinkRenderer struct {
templateHandler tpl.TemplateHandler
identity.Provider
templ tpl.Template
}
func (r contentLinkRenderer) Render(w io.Writer, ctx hooks.LinkContext) error {
return r.templateHandler.Execute(r.templ, w, ctx)
}
func (s *Site) lookupTemplate(layouts ...string) (tpl.Template, bool) {
for _, l := range layouts {
if templ, found := s.Tmpl.Lookup(l); found {
return templ, true
}
}
return nil, false
}
func (s *Site) renderForLayouts(name, outputFormat string, d interface{}, w io.Writer, layouts ...string) (err error) {
templ := s.findFirstTemplate(layouts...)
if templ == nil {

View File

@ -127,6 +127,36 @@ title = "What is Markdown"
baseURL = "https://example.com"
`)
data, err := ioutil.ReadFile(filepath.FromSlash("testdata/what-is-markdown.md"))
sb.Assert(err, qt.IsNil)
datastr := string(data)
getContent := func(i int) string {
return fmt.Sprintf(`---
title: "Page %d"
---
`, i) + datastr
}
for i := 1; i <= 100; i++ {
sb.WithContent(fmt.Sprintf("content/page%d.md", i), getContent(i))
}
return sb
},
func(s *sitesBuilder) {
s.Assert(s.CheckExists("public/page8/index.html"), qt.Equals, true)
},
},
{"Markdown with custom link handler", func(b testing.TB) *sitesBuilder {
sb := newTestSitesBuilder(b).WithConfigFile("toml", `
title = "What is Markdown"
baseURL = "https://example.com"
`)
sb.WithTemplatesAdded("_default/_markup/render-link.html", `<a href="{{ .Destination | safeURL }}#custom">CUSTOM LINK</a>`)
data, err := ioutil.ReadFile(filepath.FromSlash("testdata/what-is-markdown.md"))
sb.Assert(err, qt.IsNil)
datastr := string(data)

View File

@ -18,8 +18,12 @@ import (
"path/filepath"
"testing"
"github.com/gohugoio/hugo/identity"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/tpl"
"github.com/spf13/viper"
)
@ -320,6 +324,7 @@ Partial cached1: {{ partialCached "p1" "input1" $key1 }}
Partial cached2: {{ partialCached "p1" "input2" $key1 }}
Partial cached3: {{ partialCached "p1" "input3" $key2 }}
`,
"partials/p1.html", `partial: {{ . }}`,
)
@ -331,3 +336,85 @@ Partial cached3: {{ partialCached "p1" "input3" $key2 }}
Partial cached3: partial: input3
`)
}
func TestTemplateDependencies(t *testing.T) {
b := newTestSitesBuilder(t).Running()
b.WithTemplates("index.html", `
{{ $p := site.GetPage "p1" }}
{{ partial "p1.html" $p }}
{{ partialCached "p2.html" "foo" }}
{{ partials.Include "p3.html" "data" }}
{{ partials.IncludeCached "p4.html" "foo" }}
{{ $p := partial "p5" }}
{{ partial "sub/p6.html" }}
{{ partial "P7.html" }}
{{ template "_default/foo.html" }}
Partial nested: {{ partial "p10" }}
`,
"partials/p1.html", `ps: {{ .Render "li" }}`,
"partials/p2.html", `p2`,
"partials/p3.html", `p3`,
"partials/p4.html", `p4`,
"partials/p5.html", `p5`,
"partials/sub/p6.html", `p6`,
"partials/P7.html", `p7`,
"partials/p8.html", `p8 {{ partial "p9.html" }}`,
"partials/p9.html", `p9`,
"partials/p10.html", `p10 {{ partial "p11.html" }}`,
"partials/p11.html", `p11`,
"_default/foo.html", `foo`,
"_default/li.html", `li {{ partial "p8.html" }}`,
)
b.WithContent("p1.md", `---
title: P1
---
`)
b.Build(BuildCfg{})
s := b.H.Sites[0]
templ, found := s.lookupTemplate("index.html")
b.Assert(found, qt.Equals, true)
idset := make(map[identity.Identity]bool)
collectIdentities(idset, templ.(tpl.Info))
b.Assert(idset, qt.HasLen, 10)
}
func collectIdentities(set map[identity.Identity]bool, provider identity.Provider) {
if ids, ok := provider.(identity.IdentitiesProvider); ok {
for _, id := range ids.GetIdentities() {
collectIdentities(set, id)
}
} else {
set[provider.GetIdentity()] = true
}
}
func printRecursiveIdentities(level int, id identity.Provider) {
if level == 0 {
fmt.Println(id.GetIdentity(), "===>")
}
if ids, ok := id.(identity.IdentitiesProvider); ok {
level++
for _, id := range ids.GetIdentities() {
printRecursiveIdentities(level, id)
}
} else {
ident(level)
fmt.Println("ID", id)
}
}
func ident(n int) {
for i := 0; i < n; i++ {
fmt.Print(" ")
}
}

View File

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

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

View File

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

View File

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

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

View File

@ -38,6 +38,9 @@ func TestConvert(t *testing.T) {
https://github.com/gohugoio/hugo/issues/6528
[Live Demo here!](https://docuapi.netlify.com/)
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
## Code Fences
§§§bash
@ -98,6 +101,7 @@ description
mconf := markup_config.Default
mconf.Highlight.NoClasses = false
mconf.Goldmark.Renderer.Unsafe = true
p, err := Provider.New(
converter.ProviderConfig{
@ -106,15 +110,15 @@ description
},
)
c.Assert(err, qt.IsNil)
conv, err := p.New(converter.DocumentContext{})
conv, err := p.New(converter.DocumentContext{DocumentID: "thedoc"})
c.Assert(err, qt.IsNil)
b, err := conv.Convert(converter.RenderContext{Src: []byte(content)})
b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content)})
c.Assert(err, qt.IsNil)
got := string(b.Bytes())
// Links
c.Assert(got, qt.Contains, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`)
// c.Assert(got, qt.Contains, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`)
// Header IDs
c.Assert(got, qt.Contains, `<h2 id="custom">Custom ID</h2>`, qt.Commentf(got))
@ -137,6 +141,11 @@ description
c.Assert(got, qt.Contains, `<section class="footnotes" role="doc-endnotes">`)
c.Assert(got, qt.Contains, `<dt>date</dt>`)
toc, ok := b.(converter.TableOfContentsProvider)
c.Assert(ok, qt.Equals, true)
tocHTML := toc.TableOfContents().ToHTML(1, 2, false)
c.Assert(tocHTML, qt.Contains, "TableOfContents")
}
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
import (
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
"github.com/gohugoio/hugo/markup/converter"
"github.com/miekg/mmark"
@ -65,6 +66,10 @@ func (c *mmarkConverter) Convert(ctx converter.RenderContext) (converter.Result,
return mmark.Parse(ctx.Src, r, c.extensions), nil
}
func (c *mmarkConverter) Supports(feature identity.Identity) bool {
return false
}
func getHTMLRenderer(
ctx converter.DocumentContext,
cfg blackfriday_config.Config,

View File

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

View File

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

View File

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

View File

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

View File

@ -111,6 +111,9 @@ func TestLayout(t *testing.T) {
[]string{"section/shortcodes.amp.html"}, 12},
{"Reserved section, partials", LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials"}, "", ampType,
[]string{"section/partials.amp.html"}, 12},
// We may add type support ... later.
{"Content hook", LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog"}, "", ampType,
[]string{"_default/_markup/render-link.amp.html", "_default/_markup/render-link.html"}, 2},
} {
c.Run(this.name, func(c *qt.C) {
l := NewLayoutHandler()

View File

@ -201,9 +201,10 @@ type PageMetaProvider interface {
Weight() int
}
// PageRenderProvider provides a way for a Page to render itself.
// PageRenderProvider provides a way for a Page to render content.
type PageRenderProvider interface {
Render(layout ...string) template.HTML
Render(layout ...string) (template.HTML, error)
RenderString(args ...interface{}) (template.HTML, error)
}
// PageWithoutContent is the Page without any of the content methods.

View File

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

View File

@ -446,7 +446,11 @@ func (p *testPage) RelRefFrom(argsm map[string]interface{}, source interface{})
return "", nil
}
func (p *testPage) Render(layout ...string) template.HTML {
func (p *testPage) Render(layout ...string) (template.HTML, error) {
panic("not implemented")
}
func (p *testPage) RenderString(args ...interface{}) (template.HTML, error) {
panic("not implemented")
}

View File

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

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
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
// as the function itself.
func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value {
func (s *state) evalCallOld(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value {
if args != nil {
args = args[1:] // Zeroth arg is function name/node; not passed to function.
}

View File

@ -34,8 +34,9 @@ type Preparer interface {
// ExecHelper allows some custom eval hooks.
type ExecHelper interface {
GetFunc(name string) (reflect.Value, bool)
GetMapValue(receiver, key reflect.Value) (reflect.Value, bool)
GetFunc(tmpl Preparer, name string) (reflect.Value, bool)
GetMethod(tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value)
GetMapValue(tmpl Preparer, receiver, key reflect.Value) (reflect.Value, bool)
}
// Executer executes a given template.
@ -64,6 +65,7 @@ func (t *executer) Execute(p Preparer, wr io.Writer, data interface{}) error {
state := &state{
helper: t.helper,
prep: p,
tmpl: tmpl,
wr: wr,
vars: []variable{{"$", value}},
@ -75,7 +77,6 @@ func (t *executer) Execute(p Preparer, wr io.Writer, data interface{}) error {
// Prepare returns a template ready for execution.
func (t *Template) Prepare() (*Template, error) {
return t, nil
}
@ -95,6 +96,7 @@ func (t *Template) executeWithState(state *state, value reflect.Value) (err erro
// can execute in parallel.
type state struct {
tmpl *Template
prep Preparer // Added for Hugo.
helper ExecHelper // Added for Hugo.
wr io.Writer
node parse.Node // current node, for errors
@ -110,7 +112,7 @@ func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd
var ok bool
if s.helper != nil {
// Added for Hugo.
function, ok = s.helper.GetFunc(name)
function, ok = s.helper.GetFunc(s.prep, name)
}
if !ok {
@ -148,9 +150,23 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() {
ptr = ptr.Addr()
}
if method := ptr.MethodByName(fieldName); method.IsValid() {
// Added for Hugo.
var first reflect.Value
var method reflect.Value
if s.helper != nil {
method, first = s.helper.GetMethod(s.prep, ptr, fieldName)
} else {
method = ptr.MethodByName(fieldName)
}
if method.IsValid() {
if first != zero {
return s.evalCall(dot, method, node, fieldName, args, final, first)
}
return s.evalCall(dot, method, node, fieldName, args, final)
}
hasArgs := len(args) > 1 || final != missingVal
// It's not a method; must be a field of a struct or an element of a map.
switch receiver.Kind() {
@ -177,7 +193,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
var result reflect.Value
if s.helper != nil {
// Added for Hugo.
result, _ = s.helper.GetMapValue(receiver, nameVal)
result, _ = s.helper.GetMapValue(s.prep, receiver, nameVal)
} else {
result = receiver.MapIndex(nameVal)
}
@ -209,3 +225,79 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
s.errorf("can't evaluate field %s in type %s", fieldName, typ)
panic("not reached")
}
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
// as the function itself.
func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value, first ...reflect.Value) reflect.Value {
if args != nil {
args = args[1:] // Zeroth arg is function name/node; not passed to function.
}
typ := fun.Type()
numFirst := len(first)
numIn := len(args) + numFirst // // Added for Hugo
if final != missingVal {
numIn++
}
numFixed := len(args) + len(first)
if typ.IsVariadic() {
numFixed = typ.NumIn() - 1 // last arg is the variadic one.
if numIn < numFixed {
s.errorf("wrong number of args for %s: want at least %d got %d", name, typ.NumIn()-1, len(args))
}
} else if numIn != typ.NumIn() {
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
}
if !goodFunc(typ) {
// TODO: This could still be a confusing error; maybe goodFunc should provide info.
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
}
// Build the arg list.
argv := make([]reflect.Value, numIn)
// Args must be evaluated. Fixed args first.
i := len(first)
for ; i < numFixed && i < len(args)+numFirst; i++ {
argv[i] = s.evalArg(dot, typ.In(i), args[i-numFirst])
}
// Now the ... args.
if typ.IsVariadic() {
argType := typ.In(typ.NumIn() - 1).Elem() // Argument is a slice.
for ; i < len(args)+numFirst; i++ {
argv[i] = s.evalArg(dot, argType, args[i-numFirst])
}
}
// Add final value if necessary.
if final != missingVal {
t := typ.In(typ.NumIn() - 1)
if typ.IsVariadic() {
if numIn-1 < numFixed {
// The added final argument corresponds to a fixed parameter of the function.
// Validate against the type of the actual parameter.
t = typ.In(numIn - 1)
} else {
// The added final argument corresponds to the variadic part.
// Validate against the type of the elements of the variadic slice.
t = t.Elem()
}
}
argv[i] = s.validateType(final, t)
}
// Added for Hugo
for i := 0; i < len(first); i++ {
argv[i] = s.validateType(first[i], typ.In(i))
}
v, err := safeCall(fun, argv)
// If we have an error that is not nil, stop execution and return that
// error to the caller.
if err != nil {
s.at(node)
s.errorf("error calling %s: %v", name, err)
}
if v.Type() == reflectValueType {
v = v.Interface().(reflect.Value)
}
return v
}

View File

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

View File

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

View File

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

View File

@ -13,12 +13,44 @@
package tpl
import (
"github.com/gohugoio/hugo/identity"
)
// Increments on breaking changes.
const TemplateVersion = 2
// Info holds some info extracted from a parsed template.
type Info struct {
type Info interface {
ParseInfo() ParseInfo
// Identifies this template and its dependencies.
identity.Provider
}
type InfoManager interface {
ParseInfo() ParseInfo
// Identifies and manages this template and its dependencies.
identity.Manager
}
type defaultInfo struct {
identity.Manager
parseInfo ParseInfo
}
func NewInfo(id identity.Manager, parseInfo ParseInfo) Info {
return &defaultInfo{
Manager: id,
parseInfo: parseInfo,
}
}
func (info *defaultInfo) ParseInfo() ParseInfo {
return info.parseInfo
}
type ParseInfo struct {
// Set for shortcode templates with any {{ .Inner }}
IsInner bool
@ -26,17 +58,25 @@ type Info struct {
HasReturn bool
// Config extracted from template.
Config Config
Config ParseConfig
}
func (info Info) IsZero() bool {
func (info ParseInfo) IsZero() bool {
return info.Config.Version == 0
}
type Config struct {
// Info holds some info extracted from a parsed template.
type Info1 struct {
}
type ParseConfig struct {
Version int
}
var DefaultConfig = Config{
var DefaultParseConfig = ParseConfig{
Version: TemplateVersion,
}
var DefaultParseInfo = ParseInfo{
Config: DefaultParseConfig,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,8 @@ import (
"reflect"
"strings"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/common/maps"
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
@ -62,14 +64,14 @@ type templateExecHelper struct {
funcs map[string]reflect.Value
}
func (t *templateExecHelper) GetFunc(name string) (reflect.Value, bool) {
func (t *templateExecHelper) GetFunc(tmpl texttemplate.Preparer, name string) (reflect.Value, bool) {
if fn, found := t.funcs[name]; found {
return fn, true
}
return zero, false
}
func (t *templateExecHelper) GetMapValue(receiver, key reflect.Value) (reflect.Value, bool) {
func (t *templateExecHelper) GetMapValue(tmpl texttemplate.Preparer, receiver, key reflect.Value) (reflect.Value, bool) {
if params, ok := receiver.Interface().(maps.Params); ok {
// Case insensitive.
keystr := strings.ToLower(key.String())
@ -85,6 +87,22 @@ func (t *templateExecHelper) GetMapValue(receiver, key reflect.Value) (reflect.V
return v, v.IsValid()
}
func (t *templateExecHelper) GetMethod(tmpl texttemplate.Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) {
// This is a hot path and receiver.MethodByName really shows up in the benchmarks.
// Page.Render is the only method with a WithTemplateInfo as of now, so let's just
// check that for now.
// TODO(bep) find a more flexible, but still fast, way.
if name == "Render" {
if info, ok := tmpl.(tpl.Info); ok {
if m := receiver.MethodByName(name + "WithTemplateInfo"); m.IsValid() {
return m, reflect.ValueOf(info)
}
}
}
return receiver.MethodByName(name), zero
}
func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
funcs := createFuncMap(d)
funcsv := make(map[string]reflect.Value)
@ -120,9 +138,7 @@ func createFuncMap(d *deps.Deps) map[string]interface{} {
}
funcMap[alias] = mm.Method
}
}
}
if d.OverloadedTemplateFuncs != nil {

View File

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