mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
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:
parent
67f3aa72cf
commit
e625088ef5
59 changed files with 2234 additions and 542 deletions
37
docs/content/en/functions/RenderString.md
Normal file
37
docs/content/en/functions/RenderString.md
Normal 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.
|
|
@ -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 >}}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
244
hugolib/content_render_hooks_test.go
Normal file
244
hugolib/content_render_hooks_test.go
Normal 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
|
||||
`)
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`)
|
||||
}
|
||||
|
|
184
hugolib/page.go
184
hugolib/page.go
|
@ -23,6 +23,12 @@ import (
|
|||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
|
||||
"github.com/gohugoio/hugo/markup/converter"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
@ -43,9 +49,11 @@ import (
|
|||
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/source"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/hugo/common/collections"
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
|
@ -59,7 +67,11 @@ var (
|
|||
|
||||
var (
|
||||
pageTypesProvider = resource.NewResourceTypesProvider(media.OctetType, pageResourceType)
|
||||
nopPageOutput = &pageOutput{pagePerOutputProviders: nopPagePerOutput}
|
||||
nopPageOutput = &pageOutput{
|
||||
pagePerOutputProviders: nopPagePerOutput,
|
||||
ContentProvider: page.NopPage,
|
||||
TableOfContentsProvider: page.NopPage,
|
||||
}
|
||||
)
|
||||
|
||||
// pageContext provides contextual information about this page, for error
|
||||
|
@ -317,6 +329,54 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *pageState) createRenderHooks(f output.Format) (*hooks.Render, error) {
|
||||
|
||||
layoutDescriptor := p.getLayoutDescriptor()
|
||||
layoutDescriptor.RenderingHook = true
|
||||
layoutDescriptor.LayoutOverride = false
|
||||
layoutDescriptor.Layout = ""
|
||||
|
||||
layoutDescriptor.Kind = "render-link"
|
||||
linkLayouts, err := p.s.layoutHandler.For(layoutDescriptor, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
layoutDescriptor.Kind = "render-image"
|
||||
imageLayouts, err := p.s.layoutHandler.For(layoutDescriptor, f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if linkLayouts == nil && imageLayouts == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var linkRenderer hooks.LinkRenderer
|
||||
var imageRenderer hooks.LinkRenderer
|
||||
|
||||
if templ, found := p.s.lookupTemplate(linkLayouts...); found {
|
||||
linkRenderer = contentLinkRenderer{
|
||||
templateHandler: p.s.Tmpl,
|
||||
Provider: templ.(tpl.Info),
|
||||
templ: templ,
|
||||
}
|
||||
}
|
||||
|
||||
if templ, found := p.s.lookupTemplate(imageLayouts...); found {
|
||||
imageRenderer = contentLinkRenderer{
|
||||
templateHandler: p.s.Tmpl,
|
||||
Provider: templ.(tpl.Info),
|
||||
templ: templ,
|
||||
}
|
||||
}
|
||||
|
||||
return &hooks.Render{
|
||||
LinkRenderer: linkRenderer,
|
||||
ImageRenderer: imageRenderer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
|
||||
p.layoutDescriptorInit.Do(func() {
|
||||
var section string
|
||||
|
@ -464,11 +524,86 @@ func (p *pageState) AlternativeOutputFormats() page.OutputFormats {
|
|||
return o
|
||||
}
|
||||
|
||||
func (p *pageState) Render(layout ...string) template.HTML {
|
||||
type renderStringOpts struct {
|
||||
Display string
|
||||
Markup string
|
||||
}
|
||||
|
||||
var defualtRenderStringOpts = renderStringOpts{
|
||||
Display: "inline",
|
||||
Markup: "", // Will inherit the page's value when not set.
|
||||
}
|
||||
|
||||
func (p *pageState) RenderString(args ...interface{}) (template.HTML, error) {
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
return "", errors.New("want 1 or 2 arguments")
|
||||
}
|
||||
|
||||
var s string
|
||||
opts := defualtRenderStringOpts
|
||||
sidx := 1
|
||||
|
||||
if len(args) == 1 {
|
||||
sidx = 0
|
||||
} else {
|
||||
m, ok := args[0].(map[string]interface{})
|
||||
if !ok {
|
||||
return "", errors.New("first argument must be a map")
|
||||
}
|
||||
|
||||
if err := mapstructure.WeakDecode(m, &opts); err != nil {
|
||||
return "", errors.WithMessage(err, "failed to decode options")
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
s, err = cast.ToStringE(args[sidx])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
conv := p.getContentConverter()
|
||||
if opts.Markup != "" && opts.Markup != p.m.markup {
|
||||
var err error
|
||||
// TODO(bep) consider cache
|
||||
conv, err = p.m.newContentConverter(p, opts.Markup, nil)
|
||||
if err != nil {
|
||||
return "", p.wrapError(err)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := p.pageOutput.cp.renderContentWithConverter(conv, []byte(s), false)
|
||||
if err != nil {
|
||||
return "", p.wrapError(err)
|
||||
}
|
||||
|
||||
b := c.Bytes()
|
||||
|
||||
if opts.Display == "inline" {
|
||||
// We may have to rethink this in the future when we get other
|
||||
// renderers.
|
||||
b = p.s.ContentSpec.TrimShortHTML(b)
|
||||
}
|
||||
|
||||
return template.HTML(string(b)), nil
|
||||
}
|
||||
|
||||
func (p *pageState) addDependency(dep identity.Provider) {
|
||||
if !p.s.running() || p.pageOutput.cp == nil {
|
||||
return
|
||||
}
|
||||
p.pageOutput.cp.dependencyTracker.Add(dep)
|
||||
}
|
||||
|
||||
func (p *pageState) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) {
|
||||
p.addDependency(info)
|
||||
return p.Render(layout...)
|
||||
}
|
||||
|
||||
func (p *pageState) Render(layout ...string) (template.HTML, error) {
|
||||
l, err := p.getLayouts(layout...)
|
||||
if err != nil {
|
||||
p.s.SendError(p.wrapError(errors.Errorf(".Render: failed to resolve layout %v", layout)))
|
||||
return ""
|
||||
return "", p.wrapError(errors.Errorf("failed to resolve layout %v", layout))
|
||||
}
|
||||
|
||||
for _, layout := range l {
|
||||
|
@ -479,17 +614,18 @@ func (p *pageState) Render(layout ...string) template.HTML {
|
|||
// We default to good old HTML.
|
||||
templ, _ = p.s.Tmpl.Lookup(layout + ".html")
|
||||
}
|
||||
|
||||
if templ != nil {
|
||||
p.addDependency(templ.(tpl.Info))
|
||||
res, err := executeToString(p.s.Tmpl, templ, p)
|
||||
if err != nil {
|
||||
p.s.SendError(p.wrapError(errors.Wrapf(err, ".Render: failed to execute template %q v", layout)))
|
||||
return ""
|
||||
return "", p.wrapError(errors.Wrapf(err, "failed to execute template %q v", layout))
|
||||
}
|
||||
return template.HTML(res)
|
||||
return template.HTML(res), nil
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
return "", nil
|
||||
|
||||
}
|
||||
|
||||
|
@ -745,15 +881,33 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
|
|||
p.pageOutput.paginator.reset()
|
||||
}
|
||||
|
||||
if idx > 0 {
|
||||
// Check if we can reuse content from one of the previous formats.
|
||||
for i := idx - 1; i >= 0; i-- {
|
||||
po := p.pageOutputs[i]
|
||||
if po.cp != nil && po.cp.reuse {
|
||||
p.pageOutput.cp = po.cp
|
||||
break
|
||||
if isRenderingSite {
|
||||
cp := p.pageOutput.cp
|
||||
if cp == nil {
|
||||
|
||||
// Look for content to reuse.
|
||||
for i := 0; i < len(p.pageOutputs); i++ {
|
||||
if i == idx {
|
||||
continue
|
||||
}
|
||||
po := p.pageOutputs[i]
|
||||
|
||||
if po.cp != nil && po.cp.reuse {
|
||||
cp = po.cp
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cp == nil {
|
||||
var err error
|
||||
cp, err = newPageContentOutput(p, p.pageOutput)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
p.pageOutput.initContentProvider(cp)
|
||||
p.pageOutput.cp = cp
|
||||
}
|
||||
|
||||
for _, r := range p.Resources().ByType(pageResourceType) {
|
||||
|
|
|
@ -30,8 +30,7 @@ var (
|
|||
type pageContent struct {
|
||||
renderable bool
|
||||
selfLayout string
|
||||
|
||||
truncated bool
|
||||
truncated bool
|
||||
|
||||
cmap *pageContentMap
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
118
hugolib/site.go
118
hugolib/site.go
|
@ -28,6 +28,12 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
|
||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
|
||||
"github.com/gohugoio/hugo/markup/converter"
|
||||
|
@ -60,7 +66,6 @@ import (
|
|||
"github.com/gohugoio/hugo/navigation"
|
||||
"github.com/gohugoio/hugo/output"
|
||||
"github.com/gohugoio/hugo/related"
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
"github.com/gohugoio/hugo/resources/page/pagemeta"
|
||||
"github.com/gohugoio/hugo/source"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
@ -801,7 +806,6 @@ func (s *Site) multilingual() *Multilingual {
|
|||
|
||||
type whatChanged struct {
|
||||
source bool
|
||||
other bool
|
||||
files map[string]bool
|
||||
}
|
||||
|
||||
|
@ -888,10 +892,11 @@ func (s *Site) translateFileEvents(events []fsnotify.Event) []fsnotify.Event {
|
|||
// It returns whetever the content source was changed.
|
||||
// TODO(bep) clean up/rewrite this method.
|
||||
func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error {
|
||||
|
||||
events = s.filterFileEvents(events)
|
||||
events = s.translateFileEvents(events)
|
||||
|
||||
changeIdentities := make(identity.Identities)
|
||||
|
||||
s.Log.DEBUG.Printf("Rebuild for events %q", events)
|
||||
|
||||
h := s.h
|
||||
|
@ -902,11 +907,12 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
|
|||
sourceChanged = []fsnotify.Event{}
|
||||
sourceReallyChanged = []fsnotify.Event{}
|
||||
contentFilesChanged []string
|
||||
tmplChanged = []fsnotify.Event{}
|
||||
dataChanged = []fsnotify.Event{}
|
||||
i18nChanged = []fsnotify.Event{}
|
||||
shortcodesChanged = make(map[string]bool)
|
||||
sourceFilesChanged = make(map[string]bool)
|
||||
|
||||
tmplChanged bool
|
||||
dataChanged bool
|
||||
i18nChanged bool
|
||||
|
||||
sourceFilesChanged = make(map[string]bool)
|
||||
|
||||
// prevent spamming the log on changes
|
||||
logger = helpers.NewDistinctFeedbackLogger()
|
||||
|
@ -919,33 +925,30 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
|
|||
cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...)
|
||||
}
|
||||
|
||||
if s.isContentDirEvent(ev) {
|
||||
logger.Println("Source changed", ev)
|
||||
sourceChanged = append(sourceChanged, ev)
|
||||
}
|
||||
if s.isLayoutDirEvent(ev) {
|
||||
logger.Println("Template changed", ev)
|
||||
tmplChanged = append(tmplChanged, ev)
|
||||
id, found := s.eventToIdentity(ev)
|
||||
if found {
|
||||
changeIdentities[id] = id
|
||||
|
||||
switch id.Type {
|
||||
case files.ComponentFolderContent:
|
||||
logger.Println("Source changed", ev)
|
||||
sourceChanged = append(sourceChanged, ev)
|
||||
case files.ComponentFolderLayouts:
|
||||
logger.Println("Template changed", ev)
|
||||
tmplChanged = true
|
||||
case files.ComponentFolderData:
|
||||
logger.Println("Data changed", ev)
|
||||
dataChanged = true
|
||||
case files.ComponentFolderI18n:
|
||||
logger.Println("i18n changed", ev)
|
||||
i18nChanged = true
|
||||
|
||||
if strings.Contains(ev.Name, "shortcodes") {
|
||||
shortcode := filepath.Base(ev.Name)
|
||||
shortcode = strings.TrimSuffix(shortcode, filepath.Ext(shortcode))
|
||||
shortcodesChanged[shortcode] = true
|
||||
}
|
||||
}
|
||||
if s.isDataDirEvent(ev) {
|
||||
logger.Println("Data changed", ev)
|
||||
dataChanged = append(dataChanged, ev)
|
||||
}
|
||||
if s.isI18nEvent(ev) {
|
||||
logger.Println("i18n changed", ev)
|
||||
i18nChanged = append(dataChanged, ev)
|
||||
}
|
||||
}
|
||||
|
||||
changed := &whatChanged{
|
||||
source: len(sourceChanged) > 0 || len(shortcodesChanged) > 0,
|
||||
other: len(tmplChanged) > 0 || len(i18nChanged) > 0 || len(dataChanged) > 0,
|
||||
source: len(sourceChanged) > 0,
|
||||
files: sourceFilesChanged,
|
||||
}
|
||||
|
||||
|
@ -960,7 +963,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
|
|||
s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...)
|
||||
}
|
||||
|
||||
if len(tmplChanged) > 0 || len(i18nChanged) > 0 {
|
||||
if tmplChanged || i18nChanged {
|
||||
sites := s.h.Sites
|
||||
first := sites[0]
|
||||
|
||||
|
@ -989,7 +992,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
|
|||
}
|
||||
}
|
||||
|
||||
if len(dataChanged) > 0 {
|
||||
if dataChanged {
|
||||
s.h.init.data.Reset()
|
||||
}
|
||||
|
||||
|
@ -1018,18 +1021,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
|
|||
sourceFilesChanged[ev.Name] = true
|
||||
}
|
||||
|
||||
for shortcode := range shortcodesChanged {
|
||||
// There are certain scenarios that, when a shortcode changes,
|
||||
// it isn't sufficient to just rerender the already parsed shortcode.
|
||||
// One example is if the user adds a new shortcode to the content file first,
|
||||
// and then creates the shortcode on the file system.
|
||||
// To handle these scenarios, we must do a full reprocessing of the
|
||||
// pages that keeps a reference to the changed shortcode.
|
||||
pagesWithShortcode := h.findPagesByShortcode(shortcode)
|
||||
for _, p := range pagesWithShortcode {
|
||||
contentFilesChanged = append(contentFilesChanged, p.File().Filename())
|
||||
}
|
||||
}
|
||||
h.resetPageStateFromEvents(changeIdentities)
|
||||
|
||||
if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 {
|
||||
var filenamesChanged []string
|
||||
|
@ -1218,20 +1210,14 @@ func (s *Site) initializeSiteInfo() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Site) isI18nEvent(e fsnotify.Event) bool {
|
||||
return s.BaseFs.SourceFilesystems.IsI18n(e.Name)
|
||||
}
|
||||
func (s *Site) eventToIdentity(e fsnotify.Event) (identity.PathIdentity, bool) {
|
||||
for _, fs := range s.BaseFs.SourceFilesystems.FileSystems() {
|
||||
if p := fs.Path(e.Name); p != "" {
|
||||
return identity.NewPathIdentity(fs.Name, p), true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Site) isDataDirEvent(e fsnotify.Event) bool {
|
||||
return s.BaseFs.SourceFilesystems.IsData(e.Name)
|
||||
}
|
||||
|
||||
func (s *Site) isLayoutDirEvent(e fsnotify.Event) bool {
|
||||
return s.BaseFs.SourceFilesystems.IsLayout(e.Name)
|
||||
}
|
||||
|
||||
func (s *Site) isContentDirEvent(e fsnotify.Event) bool {
|
||||
return s.BaseFs.IsContent(e.Name)
|
||||
return identity.PathIdentity{}, false
|
||||
}
|
||||
|
||||
func (s *Site) readAndProcessContent(filenames ...string) error {
|
||||
|
@ -1562,6 +1548,26 @@ var infoOnMissingLayout = map[string]bool{
|
|||
"404": true,
|
||||
}
|
||||
|
||||
type contentLinkRenderer struct {
|
||||
templateHandler tpl.TemplateHandler
|
||||
identity.Provider
|
||||
templ tpl.Template
|
||||
}
|
||||
|
||||
func (r contentLinkRenderer) Render(w io.Writer, ctx hooks.LinkContext) error {
|
||||
return r.templateHandler.Execute(r.templ, w, ctx)
|
||||
}
|
||||
|
||||
func (s *Site) lookupTemplate(layouts ...string) (tpl.Template, bool) {
|
||||
for _, l := range layouts {
|
||||
if templ, found := s.Tmpl.Lookup(l); found {
|
||||
return templ, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (s *Site) renderForLayouts(name, outputFormat string, d interface{}, w io.Writer, layouts ...string) (err error) {
|
||||
templ := s.findFirstTemplate(layouts...)
|
||||
if templ == nil {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(" ")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
131
identity/identity.go
Normal 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
42
identity/identity_test.go
Normal 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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
)
|
||||
|
|
57
markup/converter/hooks/hooks.go
Normal file
57
markup/converter/hooks/hooks.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
208
markup/goldmark/render_link.go
Normal file
208
markup/goldmark/render_link.go
Normal 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),
|
||||
))
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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--
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue