mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-29 12:22:02 -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
|
ordered
|
||||||
: Whether or not to generate an ordered list instead of an unordered list.
|
: Whether or not to generate an ordered list instead of an unordered list.
|
||||||
|
|
||||||
|
|
||||||
|
## Markdown Render Hooks
|
||||||
|
|
||||||
|
{{< new-in "0.62.0" >}}
|
||||||
|
|
||||||
|
Note that this is only supported with the [Goldmark](#goldmark) renderer.
|
||||||
|
|
||||||
|
These Render Hooks allow custom templates to render links and images from markdown.
|
||||||
|
|
||||||
|
You can do this by creating templates with base names `render-link` and/or `render-image` inside `layouts/_default`.
|
||||||
|
|
||||||
|
You can define [Output Format](/templates/output-formats) specific templates if needed.[^1] Your `layouts` folder may then look like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
layouts
|
||||||
|
└── _default
|
||||||
|
└── markup
|
||||||
|
├── render-image.html
|
||||||
|
├── render-image.rss.xml
|
||||||
|
└── render-link.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Some use cases for the above:
|
||||||
|
|
||||||
|
* Resolve link references using `.GetPage`. This would make links more portable as you could translate `./my-post.md` (and similar constructs that would work on GitHub) into `/blog/2019/01/01/my-post/` etc.
|
||||||
|
* Add `target=blank` to external links.
|
||||||
|
* Resolve (look in the page bundle, inside `/assets` etc.) and [transform](/content-management/image-processing) images.
|
||||||
|
|
||||||
|
|
||||||
|
[^1]: It's currently only possible to have one set of render hook templates, e.g. not per `Type` or `Section`. We may consider that in a future version.
|
||||||
|
|
||||||
|
### Render Hook Templates
|
||||||
|
|
||||||
|
Both `render-link` and `render-image` templates will receive this context:
|
||||||
|
|
||||||
|
Page
|
||||||
|
: The [Page](/variables/page/) being rendered.
|
||||||
|
|
||||||
|
Destination
|
||||||
|
: The URL.
|
||||||
|
|
||||||
|
Title
|
||||||
|
: The title attribute.
|
||||||
|
|
||||||
|
Text
|
||||||
|
: The link text.
|
||||||
|
|
||||||
|
A Markdown example for a inline-style link with title:
|
||||||
|
|
||||||
|
```md
|
||||||
|
[Text](https://www.gohugo.io "Title")
|
||||||
|
```
|
||||||
|
|
||||||
|
A very simple template example given the above:
|
||||||
|
|
||||||
|
{{< code file="layouts/_default/render-link.html" >}}
|
||||||
|
<a href="{{ .Destination | safeURL }}"{{ with .Title}}title="{{ . }}"{{ end }}>{{ .Text }}{{ with .Page }} (in page {{ .Title }}){{ end }}"</a>
|
||||||
|
{{< /code >}}
|
||||||
|
|
|
@ -25,13 +25,14 @@ import (
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/common/loggers"
|
"github.com/gohugoio/hugo/common/loggers"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup"
|
"github.com/gohugoio/hugo/markup"
|
||||||
|
|
||||||
bp "github.com/gohugoio/hugo/bufferpool"
|
bp "github.com/gohugoio/hugo/bufferpool"
|
||||||
"github.com/gohugoio/hugo/config"
|
"github.com/gohugoio/hugo/config"
|
||||||
"github.com/spf13/afero"
|
|
||||||
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
@ -78,6 +79,7 @@ func NewContentSpec(cfg config.Provider, logger *loggers.Logger, contentFs afero
|
||||||
ContentFs: contentFs,
|
ContentFs: contentFs,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
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
|
StaticDirs []hugofs.FileMetaInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileSystems returns the FileSystems relevant for the change detection
|
||||||
|
// in server mode.
|
||||||
|
// Note: This does currently not return any static fs.
|
||||||
|
func (s *SourceFilesystems) FileSystems() []*SourceFilesystem {
|
||||||
|
return []*SourceFilesystem{
|
||||||
|
s.Content,
|
||||||
|
s.Data,
|
||||||
|
s.I18n,
|
||||||
|
s.Layouts,
|
||||||
|
s.Archetypes,
|
||||||
|
// TODO(bep) static
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// A SourceFilesystem holds the filesystem for a given source type in Hugo (data,
|
// A SourceFilesystem holds the filesystem for a given source type in Hugo (data,
|
||||||
// i18n, layouts, static) and additional metadata to be able to use that filesystem
|
// i18n, layouts, static) and additional metadata to be able to use that filesystem
|
||||||
// in server mode.
|
// in server mode.
|
||||||
type SourceFilesystem struct {
|
type SourceFilesystem struct {
|
||||||
|
// Name matches one in files.ComponentFolders
|
||||||
|
Name string
|
||||||
|
|
||||||
// This is a virtual composite filesystem. It expects path relative to a context.
|
// This is a virtual composite filesystem. It expects path relative to a context.
|
||||||
Fs afero.Fs
|
Fs afero.Fs
|
||||||
|
|
||||||
|
@ -275,6 +293,19 @@ func (d *SourceFilesystem) Contains(filename string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path returns the relative path to the given filename if it is a member of
|
||||||
|
// of the current filesystem, an empty string if not.
|
||||||
|
func (d *SourceFilesystem) Path(filename string) string {
|
||||||
|
for _, dir := range d.Dirs {
|
||||||
|
meta := dir.Meta()
|
||||||
|
if strings.HasPrefix(filename, meta.Filename()) {
|
||||||
|
p := strings.TrimPrefix(strings.TrimPrefix(filename, meta.Filename()), filePathSeparator)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// RealDirs gets a list of absolute paths to directories starting from the given
|
// RealDirs gets a list of absolute paths to directories starting from the given
|
||||||
// path.
|
// path.
|
||||||
func (d *SourceFilesystem) RealDirs(from string) []string {
|
func (d *SourceFilesystem) RealDirs(from string) []string {
|
||||||
|
@ -349,12 +380,14 @@ func newSourceFilesystemsBuilder(p *paths.Paths, logger *loggers.Logger, b *Base
|
||||||
return &sourceFilesystemsBuilder{p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs, result: &SourceFilesystems{}}
|
return &sourceFilesystemsBuilder{p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs, result: &SourceFilesystems{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *sourceFilesystemsBuilder) newSourceFilesystem(fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem {
|
func (b *sourceFilesystemsBuilder) newSourceFilesystem(name string, fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem {
|
||||||
return &SourceFilesystem{
|
return &SourceFilesystem{
|
||||||
|
Name: name,
|
||||||
Fs: fs,
|
Fs: fs,
|
||||||
Dirs: dirs,
|
Dirs: dirs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
|
func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
|
||||||
|
|
||||||
if b.theBigFs == nil {
|
if b.theBigFs == nil {
|
||||||
|
@ -369,12 +402,12 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
|
||||||
|
|
||||||
createView := func(componentID string) *SourceFilesystem {
|
createView := func(componentID string) *SourceFilesystem {
|
||||||
if b.theBigFs == nil || b.theBigFs.overlayMounts == nil {
|
if b.theBigFs == nil || b.theBigFs.overlayMounts == nil {
|
||||||
return b.newSourceFilesystem(hugofs.NoOpFs, nil)
|
return b.newSourceFilesystem(componentID, hugofs.NoOpFs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
dirs := b.theBigFs.overlayDirs[componentID]
|
dirs := b.theBigFs.overlayDirs[componentID]
|
||||||
|
|
||||||
return b.newSourceFilesystem(afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs)
|
return b.newSourceFilesystem(componentID, afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -392,14 +425,14 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.result.Data = b.newSourceFilesystem(dataFs, dataDirs)
|
b.result.Data = b.newSourceFilesystem(files.ComponentFolderData, dataFs, dataDirs)
|
||||||
|
|
||||||
i18nDirs := b.theBigFs.overlayDirs[files.ComponentFolderI18n]
|
i18nDirs := b.theBigFs.overlayDirs[files.ComponentFolderI18n]
|
||||||
i18nFs, err := hugofs.NewSliceFs(i18nDirs...)
|
i18nFs, err := hugofs.NewSliceFs(i18nDirs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
b.result.I18n = b.newSourceFilesystem(i18nFs, i18nDirs)
|
b.result.I18n = b.newSourceFilesystem(files.ComponentFolderI18n, i18nFs, i18nDirs)
|
||||||
|
|
||||||
contentDirs := b.theBigFs.overlayDirs[files.ComponentFolderContent]
|
contentDirs := b.theBigFs.overlayDirs[files.ComponentFolderContent]
|
||||||
contentBfs := afero.NewBasePathFs(b.theBigFs.overlayMountsContent, files.ComponentFolderContent)
|
contentBfs := afero.NewBasePathFs(b.theBigFs.overlayMountsContent, files.ComponentFolderContent)
|
||||||
|
@ -409,7 +442,7 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
|
||||||
return nil, errors.Wrap(err, "create content filesystem")
|
return nil, errors.Wrap(err, "create content filesystem")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.result.Content = b.newSourceFilesystem(contentFs, contentDirs)
|
b.result.Content = b.newSourceFilesystem(files.ComponentFolderContent, contentFs, contentDirs)
|
||||||
|
|
||||||
b.result.Work = afero.NewReadOnlyFs(b.theBigFs.overlayFull)
|
b.result.Work = afero.NewReadOnlyFs(b.theBigFs.overlayFull)
|
||||||
|
|
||||||
|
@ -421,13 +454,13 @@ func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) {
|
||||||
if b.theBigFs.staticPerLanguage != nil {
|
if b.theBigFs.staticPerLanguage != nil {
|
||||||
// Multihost mode
|
// Multihost mode
|
||||||
for k, v := range b.theBigFs.staticPerLanguage {
|
for k, v := range b.theBigFs.staticPerLanguage {
|
||||||
sfs := b.newSourceFilesystem(v, b.result.StaticDirs)
|
sfs := b.newSourceFilesystem(files.ComponentFolderStatic, v, b.result.StaticDirs)
|
||||||
sfs.PublishFolder = k
|
sfs.PublishFolder = k
|
||||||
ms[k] = sfs
|
ms[k] = sfs
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bfs := afero.NewBasePathFs(b.theBigFs.overlayMountsStatic, files.ComponentFolderStatic)
|
bfs := afero.NewBasePathFs(b.theBigFs.overlayMountsStatic, files.ComponentFolderStatic)
|
||||||
ms[""] = b.newSourceFilesystem(bfs, b.result.StaticDirs)
|
ms[""] = b.newSourceFilesystem(files.ComponentFolderStatic, bfs, b.result.StaticDirs)
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.result, nil
|
return b.result, nil
|
||||||
|
|
|
@ -40,6 +40,9 @@ import (
|
||||||
|
|
||||||
// TODO(bep) this fails when testmodBuilder is also building ...
|
// TODO(bep) this fails when testmodBuilder is also building ...
|
||||||
func TestHugoModules(t *testing.T) {
|
func TestHugoModules(t *testing.T) {
|
||||||
|
if !isCI() {
|
||||||
|
t.Skip("skip (relative) long running modules test when running locally")
|
||||||
|
}
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
if !isCI() || hugo.GoMinorVersion() < 12 {
|
if !isCI() || hugo.GoMinorVersion() < 12 {
|
||||||
|
|
|
@ -20,6 +20,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
|
||||||
radix "github.com/armon/go-radix"
|
radix "github.com/armon/go-radix"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/output"
|
"github.com/gohugoio/hugo/output"
|
||||||
|
@ -411,7 +413,6 @@ func applyDeps(cfg deps.DepsCfg, sites ...*Site) error {
|
||||||
}
|
}
|
||||||
d.OutputFormatsConfig = s.outputFormatsConfig
|
d.OutputFormatsConfig = s.outputFormatsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -806,12 +807,40 @@ func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Page
|
||||||
return h.Sites[0].findPagesByKindIn(kind, inPages)
|
return h.Sites[0].findPagesByKindIn(kind, inPages)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HugoSites) findPagesByShortcode(shortcode string) page.Pages {
|
func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) {
|
||||||
var pages page.Pages
|
|
||||||
for _, s := range h.Sites {
|
for _, s := range h.Sites {
|
||||||
pages = append(pages, s.findPagesByShortcode(shortcode)...)
|
PAGES:
|
||||||
|
for _, p := range s.rawAllPages {
|
||||||
|
OUTPUTS:
|
||||||
|
for _, po := range p.pageOutputs {
|
||||||
|
if po.cp == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for id, _ := range idset {
|
||||||
|
if po.cp.dependencyTracker.Search(id) != nil {
|
||||||
|
po.cp.Reset()
|
||||||
|
p.forceRender = true
|
||||||
|
continue OUTPUTS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range p.shortcodeState.shortcodes {
|
||||||
|
for id, _ := range idset {
|
||||||
|
if idm, ok := s.info.(identity.Manager); ok && idm.Search(id) != nil {
|
||||||
|
for _, po := range p.pageOutputs {
|
||||||
|
if po.cp != nil {
|
||||||
|
po.cp.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.forceRender = true
|
||||||
|
continue PAGES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return pages
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used in partial reloading to determine if the change is in a bundle.
|
// Used in partial reloading to determine if the change is in a bundle.
|
||||||
|
|
|
@ -71,7 +71,7 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
|
||||||
|
|
||||||
if conf.whatChanged == nil {
|
if conf.whatChanged == nil {
|
||||||
// Assume everything has changed
|
// Assume everything has changed
|
||||||
conf.whatChanged = &whatChanged{source: true, other: true}
|
conf.whatChanged = &whatChanged{source: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
var prepareErr error
|
var prepareErr error
|
||||||
|
|
|
@ -1459,3 +1459,19 @@ other = %q
|
||||||
|
|
||||||
return &multiSiteTestBuilder{sitesBuilder: b, configFormat: configFormat, config: config, configData: configData}
|
return &multiSiteTestBuilder{sitesBuilder: b, configFormat: configFormat, config: config, configData: configData}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRebuildOnAssetChange(t *testing.T) {
|
||||||
|
b := newTestSitesBuilder(t).Running()
|
||||||
|
b.WithTemplatesAdded("index.html", `
|
||||||
|
{{ (resources.Get "data.json").Content }}
|
||||||
|
`)
|
||||||
|
b.WithSourceFile("assets/data.json", "orig data")
|
||||||
|
|
||||||
|
b.Build(BuildCfg{})
|
||||||
|
b.AssertFileContent("public/index.html", `orig data`)
|
||||||
|
|
||||||
|
b.EditFiles("assets/data.json", "changed data")
|
||||||
|
|
||||||
|
b.Build(BuildCfg{})
|
||||||
|
b.AssertFileContent("public/index.html", `changed data`)
|
||||||
|
}
|
||||||
|
|
184
hugolib/page.go
184
hugolib/page.go
|
@ -23,6 +23,12 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/tpl"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/common/maps"
|
"github.com/gohugoio/hugo/common/maps"
|
||||||
|
@ -43,9 +49,11 @@ import (
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/media"
|
"github.com/gohugoio/hugo/media"
|
||||||
"github.com/gohugoio/hugo/source"
|
"github.com/gohugoio/hugo/source"
|
||||||
|
"github.com/spf13/cast"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/common/collections"
|
"github.com/gohugoio/hugo/common/collections"
|
||||||
"github.com/gohugoio/hugo/common/text"
|
"github.com/gohugoio/hugo/common/text"
|
||||||
|
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||||
"github.com/gohugoio/hugo/resources"
|
"github.com/gohugoio/hugo/resources"
|
||||||
"github.com/gohugoio/hugo/resources/page"
|
"github.com/gohugoio/hugo/resources/page"
|
||||||
"github.com/gohugoio/hugo/resources/resource"
|
"github.com/gohugoio/hugo/resources/resource"
|
||||||
|
@ -59,7 +67,11 @@ var (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pageTypesProvider = resource.NewResourceTypesProvider(media.OctetType, pageResourceType)
|
pageTypesProvider = resource.NewResourceTypesProvider(media.OctetType, pageResourceType)
|
||||||
nopPageOutput = &pageOutput{pagePerOutputProviders: nopPagePerOutput}
|
nopPageOutput = &pageOutput{
|
||||||
|
pagePerOutputProviders: nopPagePerOutput,
|
||||||
|
ContentProvider: page.NopPage,
|
||||||
|
TableOfContentsProvider: page.NopPage,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// pageContext provides contextual information about this page, for error
|
// pageContext provides contextual information about this page, for error
|
||||||
|
@ -317,6 +329,54 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *pageState) createRenderHooks(f output.Format) (*hooks.Render, error) {
|
||||||
|
|
||||||
|
layoutDescriptor := p.getLayoutDescriptor()
|
||||||
|
layoutDescriptor.RenderingHook = true
|
||||||
|
layoutDescriptor.LayoutOverride = false
|
||||||
|
layoutDescriptor.Layout = ""
|
||||||
|
|
||||||
|
layoutDescriptor.Kind = "render-link"
|
||||||
|
linkLayouts, err := p.s.layoutHandler.For(layoutDescriptor, f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutDescriptor.Kind = "render-image"
|
||||||
|
imageLayouts, err := p.s.layoutHandler.For(layoutDescriptor, f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if linkLayouts == nil && imageLayouts == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var linkRenderer hooks.LinkRenderer
|
||||||
|
var imageRenderer hooks.LinkRenderer
|
||||||
|
|
||||||
|
if templ, found := p.s.lookupTemplate(linkLayouts...); found {
|
||||||
|
linkRenderer = contentLinkRenderer{
|
||||||
|
templateHandler: p.s.Tmpl,
|
||||||
|
Provider: templ.(tpl.Info),
|
||||||
|
templ: templ,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if templ, found := p.s.lookupTemplate(imageLayouts...); found {
|
||||||
|
imageRenderer = contentLinkRenderer{
|
||||||
|
templateHandler: p.s.Tmpl,
|
||||||
|
Provider: templ.(tpl.Info),
|
||||||
|
templ: templ,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &hooks.Render{
|
||||||
|
LinkRenderer: linkRenderer,
|
||||||
|
ImageRenderer: imageRenderer,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
|
func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor {
|
||||||
p.layoutDescriptorInit.Do(func() {
|
p.layoutDescriptorInit.Do(func() {
|
||||||
var section string
|
var section string
|
||||||
|
@ -464,11 +524,86 @@ func (p *pageState) AlternativeOutputFormats() page.OutputFormats {
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pageState) Render(layout ...string) template.HTML {
|
type renderStringOpts struct {
|
||||||
|
Display string
|
||||||
|
Markup string
|
||||||
|
}
|
||||||
|
|
||||||
|
var defualtRenderStringOpts = renderStringOpts{
|
||||||
|
Display: "inline",
|
||||||
|
Markup: "", // Will inherit the page's value when not set.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageState) RenderString(args ...interface{}) (template.HTML, error) {
|
||||||
|
if len(args) < 1 || len(args) > 2 {
|
||||||
|
return "", errors.New("want 1 or 2 arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
var s string
|
||||||
|
opts := defualtRenderStringOpts
|
||||||
|
sidx := 1
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
sidx = 0
|
||||||
|
} else {
|
||||||
|
m, ok := args[0].(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("first argument must be a map")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mapstructure.WeakDecode(m, &opts); err != nil {
|
||||||
|
return "", errors.WithMessage(err, "failed to decode options")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
s, err = cast.ToStringE(args[sidx])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
conv := p.getContentConverter()
|
||||||
|
if opts.Markup != "" && opts.Markup != p.m.markup {
|
||||||
|
var err error
|
||||||
|
// TODO(bep) consider cache
|
||||||
|
conv, err = p.m.newContentConverter(p, opts.Markup, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", p.wrapError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := p.pageOutput.cp.renderContentWithConverter(conv, []byte(s), false)
|
||||||
|
if err != nil {
|
||||||
|
return "", p.wrapError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := c.Bytes()
|
||||||
|
|
||||||
|
if opts.Display == "inline" {
|
||||||
|
// We may have to rethink this in the future when we get other
|
||||||
|
// renderers.
|
||||||
|
b = p.s.ContentSpec.TrimShortHTML(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return template.HTML(string(b)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageState) addDependency(dep identity.Provider) {
|
||||||
|
if !p.s.running() || p.pageOutput.cp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.pageOutput.cp.dependencyTracker.Add(dep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageState) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) {
|
||||||
|
p.addDependency(info)
|
||||||
|
return p.Render(layout...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageState) Render(layout ...string) (template.HTML, error) {
|
||||||
l, err := p.getLayouts(layout...)
|
l, err := p.getLayouts(layout...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.s.SendError(p.wrapError(errors.Errorf(".Render: failed to resolve layout %v", layout)))
|
return "", p.wrapError(errors.Errorf("failed to resolve layout %v", layout))
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, layout := range l {
|
for _, layout := range l {
|
||||||
|
@ -479,17 +614,18 @@ func (p *pageState) Render(layout ...string) template.HTML {
|
||||||
// We default to good old HTML.
|
// We default to good old HTML.
|
||||||
templ, _ = p.s.Tmpl.Lookup(layout + ".html")
|
templ, _ = p.s.Tmpl.Lookup(layout + ".html")
|
||||||
}
|
}
|
||||||
|
|
||||||
if templ != nil {
|
if templ != nil {
|
||||||
|
p.addDependency(templ.(tpl.Info))
|
||||||
res, err := executeToString(p.s.Tmpl, templ, p)
|
res, err := executeToString(p.s.Tmpl, templ, p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.s.SendError(p.wrapError(errors.Wrapf(err, ".Render: failed to execute template %q v", layout)))
|
return "", p.wrapError(errors.Wrapf(err, "failed to execute template %q v", layout))
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
return template.HTML(res)
|
return template.HTML(res), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return "", nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -745,15 +881,33 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
|
||||||
p.pageOutput.paginator.reset()
|
p.pageOutput.paginator.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx > 0 {
|
if isRenderingSite {
|
||||||
// Check if we can reuse content from one of the previous formats.
|
cp := p.pageOutput.cp
|
||||||
for i := idx - 1; i >= 0; i-- {
|
if cp == nil {
|
||||||
po := p.pageOutputs[i]
|
|
||||||
if po.cp != nil && po.cp.reuse {
|
// Look for content to reuse.
|
||||||
p.pageOutput.cp = po.cp
|
for i := 0; i < len(p.pageOutputs); i++ {
|
||||||
break
|
if i == idx {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
po := p.pageOutputs[i]
|
||||||
|
|
||||||
|
if po.cp != nil && po.cp.reuse {
|
||||||
|
cp = po.cp
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cp == nil {
|
||||||
|
var err error
|
||||||
|
cp, err = newPageContentOutput(p, p.pageOutput)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.pageOutput.initContentProvider(cp)
|
||||||
|
p.pageOutput.cp = cp
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range p.Resources().ByType(pageResourceType) {
|
for _, r := range p.Resources().ByType(pageResourceType) {
|
||||||
|
|
|
@ -30,8 +30,7 @@ var (
|
||||||
type pageContent struct {
|
type pageContent struct {
|
||||||
renderable bool
|
renderable bool
|
||||||
selfLayout string
|
selfLayout string
|
||||||
|
truncated bool
|
||||||
truncated bool
|
|
||||||
|
|
||||||
cmap *pageContentMap
|
cmap *pageContentMap
|
||||||
|
|
||||||
|
|
|
@ -592,7 +592,7 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pageMeta) applyDefaultValues() error {
|
func (p *pageMeta) applyDefaultValues(ps *pageState) error {
|
||||||
if p.markup == "" {
|
if p.markup == "" {
|
||||||
if !p.File().IsZero() {
|
if !p.File().IsZero() {
|
||||||
// Fall back to file extension
|
// Fall back to file extension
|
||||||
|
@ -651,27 +651,39 @@ func (p *pageMeta) applyDefaultValues() error {
|
||||||
markup = "markdown"
|
markup = "markdown"
|
||||||
}
|
}
|
||||||
|
|
||||||
cp := p.s.ContentSpec.Converters.Get(markup)
|
cp, err := p.newContentConverter(ps, markup, renderingConfigOverrides)
|
||||||
if cp == nil {
|
|
||||||
return errors.Errorf("no content renderer found for markup %q", p.markup)
|
|
||||||
}
|
|
||||||
|
|
||||||
cpp, err := cp.New(converter.DocumentContext{
|
|
||||||
DocumentID: p.f.UniqueID(),
|
|
||||||
DocumentName: p.f.Path(),
|
|
||||||
ConfigOverrides: renderingConfigOverrides,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
p.contentConverter = cpp
|
p.contentConverter = cp
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *pageMeta) newContentConverter(ps *pageState, markup string, renderingConfigOverrides map[string]interface{}) (converter.Converter, error) {
|
||||||
|
cp := p.s.ContentSpec.Converters.Get(markup)
|
||||||
|
if cp == nil {
|
||||||
|
return nil, errors.Errorf("no content renderer found for markup %q", p.markup)
|
||||||
|
}
|
||||||
|
|
||||||
|
cpp, err := cp.New(
|
||||||
|
converter.DocumentContext{
|
||||||
|
Document: newPageForRenderHook(ps),
|
||||||
|
DocumentID: p.f.UniqueID(),
|
||||||
|
DocumentName: p.f.Path(),
|
||||||
|
ConfigOverrides: renderingConfigOverrides,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cpp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// The output formats this page will be rendered to.
|
// The output formats this page will be rendered to.
|
||||||
func (m *pageMeta) outputFormats() output.Formats {
|
func (m *pageMeta) outputFormats() output.Formats {
|
||||||
if len(m.configuredOutputFormats) > 0 {
|
if len(m.configuredOutputFormats) > 0 {
|
||||||
|
|
|
@ -112,7 +112,7 @@ func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*page
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := metaProvider.applyDefaultValues(); err != nil {
|
if err := metaProvider.applyDefaultValues(ps); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*page
|
||||||
}
|
}
|
||||||
|
|
||||||
makeOut := func(f output.Format, render bool) *pageOutput {
|
makeOut := func(f output.Format, render bool) *pageOutput {
|
||||||
return newPageOutput(nil, ps, pp, f, render)
|
return newPageOutput(ps, pp, f, render)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ps.m.standalone {
|
if ps.m.standalone {
|
||||||
|
@ -234,7 +234,7 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope
|
||||||
return ps.wrapError(err)
|
return ps.wrapError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := metaProvider.applyDefaultValues(); err != nil {
|
if err := metaProvider.applyDefaultValues(ps); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,10 +242,6 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope
|
||||||
}
|
}
|
||||||
|
|
||||||
ps.init.Add(func() (interface{}, error) {
|
ps.init.Add(func() (interface{}, error) {
|
||||||
reuseContent := ps.renderable && !ps.shortcodeState.hasShortcodes()
|
|
||||||
|
|
||||||
// Creates what's needed for each output format.
|
|
||||||
contentPerOutput := newPageContentOutput(ps)
|
|
||||||
|
|
||||||
pp, err := newPagePaths(s, ps, metaProvider)
|
pp, err := newPagePaths(s, ps, metaProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -264,18 +260,18 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope
|
||||||
}
|
}
|
||||||
|
|
||||||
_, render := outputFormatsForPage.GetByName(f.Name)
|
_, render := outputFormatsForPage.GetByName(f.Name)
|
||||||
var contentProvider *pageContentOutput
|
po := newPageOutput(ps, pp, f, render)
|
||||||
if reuseContent && i > 0 {
|
|
||||||
contentProvider = ps.pageOutputs[0].cp
|
// Create a content provider for the first,
|
||||||
} else {
|
// we may be able to reuse it.
|
||||||
var err error
|
if i == 0 {
|
||||||
contentProvider, err = contentPerOutput(f)
|
contentProvider, err := newPageContentOutput(ps, po)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
po.initContentProvider(contentProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
po := newPageOutput(contentProvider, ps, pp, f, render)
|
|
||||||
ps.pageOutputs[i] = po
|
ps.pageOutputs[i] = po
|
||||||
created[f.Name] = po
|
created[f.Name] = po
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,13 @@
|
||||||
package hugolib
|
package hugolib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
"github.com/gohugoio/hugo/output"
|
"github.com/gohugoio/hugo/output"
|
||||||
"github.com/gohugoio/hugo/resources/page"
|
"github.com/gohugoio/hugo/resources/page"
|
||||||
"github.com/gohugoio/hugo/resources/resource"
|
"github.com/gohugoio/hugo/resources/resource"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newPageOutput(
|
func newPageOutput(
|
||||||
cp *pageContentOutput, // may be nil
|
|
||||||
ps *pageState,
|
ps *pageState,
|
||||||
pp pagePaths,
|
pp pagePaths,
|
||||||
f output.Format,
|
f output.Format,
|
||||||
|
@ -45,36 +45,23 @@ func newPageOutput(
|
||||||
paginatorProvider = pag
|
paginatorProvider = pag
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
contentProvider page.ContentProvider = page.NopPage
|
|
||||||
tableOfContentsProvider page.TableOfContentsProvider = page.NopPage
|
|
||||||
)
|
|
||||||
|
|
||||||
if cp != nil {
|
|
||||||
contentProvider = cp
|
|
||||||
tableOfContentsProvider = cp
|
|
||||||
}
|
|
||||||
|
|
||||||
providers := struct {
|
providers := struct {
|
||||||
page.ContentProvider
|
|
||||||
page.TableOfContentsProvider
|
|
||||||
page.PaginatorProvider
|
page.PaginatorProvider
|
||||||
resource.ResourceLinksProvider
|
resource.ResourceLinksProvider
|
||||||
targetPather
|
targetPather
|
||||||
}{
|
}{
|
||||||
contentProvider,
|
|
||||||
tableOfContentsProvider,
|
|
||||||
paginatorProvider,
|
paginatorProvider,
|
||||||
linksProvider,
|
linksProvider,
|
||||||
targetPathsProvider,
|
targetPathsProvider,
|
||||||
}
|
}
|
||||||
|
|
||||||
po := &pageOutput{
|
po := &pageOutput{
|
||||||
f: f,
|
f: f,
|
||||||
cp: cp,
|
pagePerOutputProviders: providers,
|
||||||
pagePerOutputProviders: providers,
|
ContentProvider: page.NopPage,
|
||||||
render: render,
|
TableOfContentsProvider: page.NopPage,
|
||||||
paginator: pag,
|
render: render,
|
||||||
|
paginator: pag,
|
||||||
}
|
}
|
||||||
|
|
||||||
return po
|
return po
|
||||||
|
@ -94,16 +81,54 @@ type pageOutput struct {
|
||||||
// used in template(s).
|
// used in template(s).
|
||||||
paginator *pagePaginator
|
paginator *pagePaginator
|
||||||
|
|
||||||
// This interface provides the functionality that is specific for this
|
// These interface provides the functionality that is specific for this
|
||||||
// output format.
|
// output format.
|
||||||
pagePerOutputProviders
|
pagePerOutputProviders
|
||||||
|
page.ContentProvider
|
||||||
|
page.TableOfContentsProvider
|
||||||
|
|
||||||
// This may be nil.
|
// May be nil.
|
||||||
cp *pageContentOutput
|
cp *pageContentOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *pageOutput) initRenderHooks() error {
|
||||||
|
if o.cp == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := o.cp.p
|
||||||
|
|
||||||
|
c := ps.getContentConverter()
|
||||||
|
if c == nil || !c.Supports(converter.FeatureRenderHooks) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err := ps.createRenderHooks(o.f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if h == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
o.cp.renderHooks = h
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageOutput) initContentProvider(cp *pageContentOutput) {
|
||||||
|
if cp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.ContentProvider = cp
|
||||||
|
p.TableOfContentsProvider = cp
|
||||||
|
p.cp = cp
|
||||||
|
}
|
||||||
|
|
||||||
func (p *pageOutput) enablePlaceholders() {
|
func (p *pageOutput) enablePlaceholders() {
|
||||||
if p.cp != nil {
|
if p.cp != nil {
|
||||||
p.cp.enablePlaceholders()
|
p.cp.enablePlaceholders()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,10 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/lazy"
|
"github.com/gohugoio/hugo/lazy"
|
||||||
|
@ -58,160 +62,182 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutput, error) {
|
var pageContentOutputDependenciesID = identity.KeyValueIdentity{Key: "pageOutput", Value: "dependencies"}
|
||||||
|
|
||||||
|
func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, error) {
|
||||||
|
|
||||||
parent := p.init
|
parent := p.init
|
||||||
|
|
||||||
return func(f output.Format) (*pageContentOutput, error) {
|
var dependencyTracker identity.Manager
|
||||||
cp := &pageContentOutput{
|
if p.s.running() {
|
||||||
p: p,
|
dependencyTracker = identity.NewManager(pageContentOutputDependenciesID)
|
||||||
f: f,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
initContent := func() (err error) {
|
cp := &pageContentOutput{
|
||||||
if p.cmap == nil {
|
dependencyTracker: dependencyTracker,
|
||||||
// Nothing to do.
|
p: p,
|
||||||
return nil
|
f: po.f,
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
// See https://github.com/gohugoio/hugo/issues/6210
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
err = fmt.Errorf("%s", r)
|
|
||||||
p.s.Log.ERROR.Printf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var hasVariants bool
|
|
||||||
|
|
||||||
cp.contentPlaceholders, hasVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.render && !hasVariants {
|
|
||||||
// We can reuse this for the other output formats
|
|
||||||
cp.enableReuse()
|
|
||||||
}
|
|
||||||
|
|
||||||
cp.workContent = p.contentToRender(cp.contentPlaceholders)
|
|
||||||
|
|
||||||
isHTML := cp.p.m.markup == "html"
|
|
||||||
|
|
||||||
if p.renderable {
|
|
||||||
if !isHTML {
|
|
||||||
r, err := cp.renderContent(cp.workContent)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
cp.convertedResult = r
|
|
||||||
cp.workContent = r.Bytes()
|
|
||||||
|
|
||||||
if _, ok := r.(converter.TableOfContentsProvider); !ok {
|
|
||||||
tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
|
|
||||||
cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
|
|
||||||
cp.workContent = tmpContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cp.placeholdersEnabled {
|
|
||||||
// ToC was accessed via .Page.TableOfContents in the shortcode,
|
|
||||||
// at a time when the ToC wasn't ready.
|
|
||||||
cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
|
|
||||||
// There are one or more replacement tokens to be replaced.
|
|
||||||
cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cp.p.source.hasSummaryDivider {
|
|
||||||
if isHTML {
|
|
||||||
src := p.source.parsed.Input()
|
|
||||||
|
|
||||||
// Use the summary sections as they are provided by the user.
|
|
||||||
if p.source.posSummaryEnd != -1 {
|
|
||||||
cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
|
|
||||||
}
|
|
||||||
|
|
||||||
if cp.p.source.posBodyStart != -1 {
|
|
||||||
cp.workContent = src[cp.p.source.posBodyStart:]
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
|
|
||||||
if err != nil {
|
|
||||||
cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
|
|
||||||
} else {
|
|
||||||
cp.workContent = content
|
|
||||||
cp.summary = helpers.BytesToHTML(summary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if cp.p.m.summary != "" {
|
|
||||||
b, err := cp.p.getContentConverter().Convert(
|
|
||||||
converter.RenderContext{
|
|
||||||
Src: []byte(cp.p.m.summary),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
|
|
||||||
cp.summary = helpers.BytesToHTML(html)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cp.content = helpers.BytesToHTML(cp.workContent)
|
|
||||||
|
|
||||||
if !p.renderable {
|
|
||||||
err := cp.addSelfTemplate()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
|
initContent := func() (err error) {
|
||||||
|
if p.cmap == nil {
|
||||||
|
// Nothing to do.
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// See https://github.com/gohugoio/hugo/issues/6210
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("%s", r)
|
||||||
|
p.s.Log.ERROR.Printf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := po.initRenderHooks(); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recursive loops can only happen in content files with template code (shortcodes etc.)
|
var hasShortcodeVariants bool
|
||||||
// Avoid creating new goroutines if we don't have to.
|
|
||||||
needTimeout := !p.renderable || p.shortcodeState.hasShortcodes()
|
|
||||||
|
|
||||||
if needTimeout {
|
f := po.f
|
||||||
cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
|
cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
|
||||||
return nil, initContent()
|
if err != nil {
|
||||||
})
|
return err
|
||||||
} else {
|
|
||||||
cp.initMain = parent.Branch(func() (interface{}, error) {
|
|
||||||
return nil, initContent()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
|
enableReuse := !(hasShortcodeVariants || cp.renderHooksHaveVariants)
|
||||||
cp.plain = helpers.StripHTML(string(cp.content))
|
|
||||||
cp.plainWords = strings.Fields(cp.plain)
|
|
||||||
cp.setWordCounts(p.m.isCJKLanguage)
|
|
||||||
|
|
||||||
if err := cp.setAutoSummary(); err != nil {
|
if enableReuse {
|
||||||
return err, nil
|
// Reuse this for the other output formats.
|
||||||
|
// We may improve on this, but we really want to avoid re-rendering the content
|
||||||
|
// to all output formats.
|
||||||
|
// The current rule is that if you need output format-aware shortcodes or
|
||||||
|
// content rendering hooks, create a output format-specific template, e.g.
|
||||||
|
// myshortcode.amp.html.
|
||||||
|
cp.enableReuse()
|
||||||
|
}
|
||||||
|
|
||||||
|
cp.workContent = p.contentToRender(cp.contentPlaceholders)
|
||||||
|
|
||||||
|
isHTML := cp.p.m.markup == "html"
|
||||||
|
|
||||||
|
if p.renderable {
|
||||||
|
if !isHTML {
|
||||||
|
r, err := cp.renderContent(cp.workContent, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cp.workContent = r.Bytes()
|
||||||
|
|
||||||
|
if tocProvider, ok := r.(converter.TableOfContentsProvider); ok {
|
||||||
|
cfg := p.s.ContentSpec.Converters.GetMarkupConfig()
|
||||||
|
cp.tableOfContents = template.HTML(
|
||||||
|
tocProvider.TableOfContents().ToHTML(
|
||||||
|
cfg.TableOfContents.StartLevel,
|
||||||
|
cfg.TableOfContents.EndLevel,
|
||||||
|
cfg.TableOfContents.Ordered,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
|
||||||
|
cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
|
||||||
|
cp.workContent = tmpContent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
if cp.placeholdersEnabled {
|
||||||
})
|
// ToC was accessed via .Page.TableOfContents in the shortcode,
|
||||||
|
// at a time when the ToC wasn't ready.
|
||||||
|
cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
|
||||||
|
}
|
||||||
|
|
||||||
return cp, nil
|
if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
|
||||||
|
// There are one or more replacement tokens to be replaced.
|
||||||
|
cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cp.p.source.hasSummaryDivider {
|
||||||
|
if isHTML {
|
||||||
|
src := p.source.parsed.Input()
|
||||||
|
|
||||||
|
// Use the summary sections as they are provided by the user.
|
||||||
|
if p.source.posSummaryEnd != -1 {
|
||||||
|
cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
|
||||||
|
}
|
||||||
|
|
||||||
|
if cp.p.source.posBodyStart != -1 {
|
||||||
|
cp.workContent = src[cp.p.source.posBodyStart:]
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
|
||||||
|
if err != nil {
|
||||||
|
cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
|
||||||
|
} else {
|
||||||
|
cp.workContent = content
|
||||||
|
cp.summary = helpers.BytesToHTML(summary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if cp.p.m.summary != "" {
|
||||||
|
b, err := cp.renderContent([]byte(cp.p.m.summary), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
|
||||||
|
cp.summary = helpers.BytesToHTML(html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cp.content = helpers.BytesToHTML(cp.workContent)
|
||||||
|
|
||||||
|
if !p.renderable {
|
||||||
|
err := cp.addSelfTemplate()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recursive loops can only happen in content files with template code (shortcodes etc.)
|
||||||
|
// Avoid creating new goroutines if we don't have to.
|
||||||
|
needTimeout := !p.renderable || p.shortcodeState.hasShortcodes()
|
||||||
|
needTimeout = needTimeout || cp.renderHooks != nil
|
||||||
|
|
||||||
|
if needTimeout {
|
||||||
|
cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
|
||||||
|
return nil, initContent()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
cp.initMain = parent.Branch(func() (interface{}, error) {
|
||||||
|
return nil, initContent()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
|
||||||
|
cp.plain = helpers.StripHTML(string(cp.content))
|
||||||
|
cp.plainWords = strings.Fields(cp.plain)
|
||||||
|
cp.setWordCounts(p.m.isCJKLanguage)
|
||||||
|
|
||||||
|
if err := cp.setAutoSummary(); err != nil {
|
||||||
|
return err, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return cp, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pageContentOutput represents the Page content for a given output format.
|
// pageContentOutput represents the Page content for a given output format.
|
||||||
type pageContentOutput struct {
|
type pageContentOutput struct {
|
||||||
f output.Format
|
f output.Format
|
||||||
|
|
||||||
// If we can safely reuse this for other output formats.
|
// If we can reuse this for other output formats.
|
||||||
reuse bool
|
reuse bool
|
||||||
reuseInit sync.Once
|
reuseInit sync.Once
|
||||||
|
|
||||||
|
@ -224,10 +250,15 @@ type pageContentOutput struct {
|
||||||
placeholdersEnabled bool
|
placeholdersEnabled bool
|
||||||
placeholdersEnabledInit sync.Once
|
placeholdersEnabledInit sync.Once
|
||||||
|
|
||||||
|
// May be nil.
|
||||||
|
renderHooks *hooks.Render
|
||||||
|
// Set if there are more than one output format variant
|
||||||
|
renderHooksHaveVariants bool // TODO(bep) reimplement this in another way, consolidate with shortcodes
|
||||||
|
|
||||||
// Content state
|
// Content state
|
||||||
|
|
||||||
workContent []byte
|
workContent []byte
|
||||||
convertedResult converter.Result
|
dependencyTracker identity.Manager // Set in server mode.
|
||||||
|
|
||||||
// Temporary storage of placeholders mapped to their content.
|
// Temporary storage of placeholders mapped to their content.
|
||||||
// These are shortcodes etc. Some of these will need to be replaced
|
// These are shortcodes etc. Some of these will need to be replaced
|
||||||
|
@ -248,6 +279,20 @@ type pageContentOutput struct {
|
||||||
readingTime int
|
readingTime int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *pageContentOutput) trackDependency(id identity.Provider) {
|
||||||
|
if p.dependencyTracker != nil {
|
||||||
|
p.dependencyTracker.Add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageContentOutput) Reset() {
|
||||||
|
if p.dependencyTracker != nil {
|
||||||
|
p.dependencyTracker.Reset()
|
||||||
|
}
|
||||||
|
p.initMain.Reset()
|
||||||
|
p.initPlain.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *pageContentOutput) Content() (interface{}, error) {
|
func (p *pageContentOutput) Content() (interface{}, error) {
|
||||||
if p.p.s.initInit(p.initMain, p.p) {
|
if p.p.s.initInit(p.initMain, p.p) {
|
||||||
return p.content, nil
|
return p.content, nil
|
||||||
|
@ -290,10 +335,6 @@ func (p *pageContentOutput) Summary() template.HTML {
|
||||||
|
|
||||||
func (p *pageContentOutput) TableOfContents() template.HTML {
|
func (p *pageContentOutput) TableOfContents() template.HTML {
|
||||||
p.p.s.initInit(p.initMain, p.p)
|
p.p.s.initInit(p.initMain, p.p)
|
||||||
if tocProvider, ok := p.convertedResult.(converter.TableOfContentsProvider); ok {
|
|
||||||
cfg := p.p.s.ContentSpec.Converters.GetMarkupConfig()
|
|
||||||
return template.HTML(tocProvider.TableOfContents().ToHTML(cfg.TableOfContents.StartLevel, cfg.TableOfContents.EndLevel, cfg.TableOfContents.Ordered))
|
|
||||||
}
|
|
||||||
return p.tableOfContents
|
return p.tableOfContents
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,12 +372,30 @@ func (p *pageContentOutput) setAutoSummary() error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cp *pageContentOutput) renderContent(content []byte) (converter.Result, error) {
|
func (cp *pageContentOutput) renderContent(content []byte, renderTOC bool) (converter.Result, error) {
|
||||||
return cp.p.getContentConverter().Convert(
|
c := cp.p.getContentConverter()
|
||||||
|
return cp.renderContentWithConverter(c, content, renderTOC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, content []byte, renderTOC bool) (converter.Result, error) {
|
||||||
|
|
||||||
|
r, err := c.Convert(
|
||||||
converter.RenderContext{
|
converter.RenderContext{
|
||||||
Src: content,
|
Src: content,
|
||||||
RenderTOC: true,
|
RenderTOC: renderTOC,
|
||||||
|
RenderHooks: cp.renderHooks,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
if ids, ok := r.(identity.IdentitiesProvider); ok {
|
||||||
|
for _, v := range ids.GetIdentities() {
|
||||||
|
cp.trackDependency(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, err
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
|
func (p *pageContentOutput) setWordCounts(isCJKLanguage bool) {
|
||||||
|
@ -392,9 +451,7 @@ func (p *pageContentOutput) enableReuse() {
|
||||||
// these will be shifted out when rendering a given output format.
|
// these will be shifted out when rendering a given output format.
|
||||||
type pagePerOutputProviders interface {
|
type pagePerOutputProviders interface {
|
||||||
targetPather
|
targetPather
|
||||||
page.ContentProvider
|
|
||||||
page.PaginatorProvider
|
page.PaginatorProvider
|
||||||
page.TableOfContentsProvider
|
|
||||||
resource.ResourceLinksProvider
|
resource.ResourceLinksProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,12 +93,6 @@ Summary Next Line. {{<figure src="/not/real" >}}.
|
||||||
More text here.
|
More text here.
|
||||||
|
|
||||||
Some more text
|
Some more text
|
||||||
`
|
|
||||||
|
|
||||||
simplePageWithEmbeddedScript = `---
|
|
||||||
title: Simple
|
|
||||||
---
|
|
||||||
<script type='text/javascript'>alert('the script tags are still there, right?');</script>
|
|
||||||
`
|
`
|
||||||
|
|
||||||
simplePageWithSummaryDelimiterSameLine = `---
|
simplePageWithSummaryDelimiterSameLine = `---
|
||||||
|
@ -325,6 +319,7 @@ func normalizeContent(c string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPageTOC(t *testing.T, page page.Page, toc string) {
|
func checkPageTOC(t *testing.T, page page.Page, toc string) {
|
||||||
|
t.Helper()
|
||||||
if page.TableOfContents() != template.HTML(toc) {
|
if page.TableOfContents() != template.HTML(toc) {
|
||||||
t.Fatalf("Page TableOfContents is:\n%q.\nExpected %q", page.TableOfContents(), toc)
|
t.Fatalf("Page TableOfContents is:\n%q.\nExpected %q", page.TableOfContents(), toc)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ func TestUnwrapPage(t *testing.T) {
|
||||||
p := &pageState{}
|
p := &pageState{}
|
||||||
|
|
||||||
c.Assert(mustUnwrap(newPageForShortcode(p)), qt.Equals, p)
|
c.Assert(mustUnwrap(newPageForShortcode(p)), qt.Equals, p)
|
||||||
|
c.Assert(mustUnwrap(newPageForRenderHook(p)), qt.Equals, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustUnwrap(v interface{}) page.Page {
|
func mustUnwrap(v interface{}) page.Page {
|
||||||
|
|
|
@ -811,6 +811,7 @@ Short Thumb Width: {{ $thumb.Width }}
|
||||||
writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), singleLayout)
|
writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "single.html"), singleLayout)
|
||||||
writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), listLayout)
|
writeSource(t, fs, filepath.Join(workDir, "layouts", "_default", "list.html"), listLayout)
|
||||||
writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.html"), myShort)
|
writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.html"), myShort)
|
||||||
|
writeSource(t, fs, filepath.Join(workDir, "layouts", "shortcodes", "myShort.customo"), myShort)
|
||||||
|
|
||||||
writeSource(t, fs, filepath.Join(workDir, "base", "_index.md"), pageContent)
|
writeSource(t, fs, filepath.Join(workDir, "base", "_index.md"), pageContent)
|
||||||
writeSource(t, fs, filepath.Join(workDir, "base", "_1.md"), pageContent)
|
writeSource(t, fs, filepath.Join(workDir, "base", "_1.md"), pageContent)
|
||||||
|
|
|
@ -358,16 +358,6 @@ func (c *PageCollections) removePage(page *pageState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *PageCollections) findPagesByShortcode(shortcode string) page.Pages {
|
|
||||||
var pages page.Pages
|
|
||||||
for _, p := range c.rawAllPages {
|
|
||||||
if p.HasShortcode(shortcode) {
|
|
||||||
pages = append(pages, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pages
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PageCollections) replacePage(page *pageState) {
|
func (c *PageCollections) replacePage(page *pageState) {
|
||||||
// will find existing page that matches filepath and remove it
|
// will find existing page that matches filepath and remove it
|
||||||
c.removePage(page)
|
c.removePage(page)
|
||||||
|
|
|
@ -23,8 +23,6 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/common/herrors"
|
"github.com/gohugoio/hugo/common/herrors"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
@ -198,7 +196,7 @@ type shortcode struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s shortcode) insertPlaceholder() bool {
|
func (s shortcode) insertPlaceholder() bool {
|
||||||
return !s.doMarkup || s.info.Config.Version == 1
|
return !s.doMarkup || s.info.ParseInfo().Config.Version == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s shortcode) innerString() string {
|
func (s shortcode) innerString() string {
|
||||||
|
@ -349,14 +347,9 @@ func renderShortcode(
|
||||||
|
|
||||||
// Pre Hugo 0.55 this was the behaviour even for the outer-most
|
// Pre Hugo 0.55 this was the behaviour even for the outer-most
|
||||||
// shortcode.
|
// shortcode.
|
||||||
if sc.doMarkup && (level > 0 || sc.info.Config.Version == 1) {
|
if sc.doMarkup && (level > 0 || sc.info.ParseInfo().Config.Version == 1) {
|
||||||
var err error
|
var err error
|
||||||
|
b, err := p.pageOutput.cp.renderContent([]byte(inner), false)
|
||||||
b, err := p.getContentConverter().Convert(
|
|
||||||
converter.RenderContext{
|
|
||||||
Src: []byte(inner),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", false, err
|
return "", false, err
|
||||||
|
@ -494,13 +487,13 @@ Loop:
|
||||||
case currItem.IsRightShortcodeDelim():
|
case currItem.IsRightShortcodeDelim():
|
||||||
// we trust the template on this:
|
// we trust the template on this:
|
||||||
// if there's no inner, we're done
|
// if there's no inner, we're done
|
||||||
if !sc.isInline && !sc.info.IsInner {
|
if !sc.isInline && !sc.info.ParseInfo().IsInner {
|
||||||
return sc, nil
|
return sc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
case currItem.IsShortcodeClose():
|
case currItem.IsShortcodeClose():
|
||||||
next := pt.Peek()
|
next := pt.Peek()
|
||||||
if !sc.isInline && !sc.info.IsInner {
|
if !sc.isInline && !sc.info.ParseInfo().IsInner {
|
||||||
if next.IsError() {
|
if next.IsError() {
|
||||||
// return that error, more specific
|
// return that error, more specific
|
||||||
continue
|
continue
|
||||||
|
@ -540,7 +533,7 @@ Loop:
|
||||||
return nil, _errors.Errorf("template for shortcode %q not found", sc.name)
|
return nil, _errors.Errorf("template for shortcode %q not found", sc.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
sc.info = tmpl.(tpl.TemplateInfoProvider).TemplateInfo()
|
sc.info = tmpl.(tpl.Info)
|
||||||
case currItem.IsInlineShortcodeName():
|
case currItem.IsInlineShortcodeName():
|
||||||
sc.name = currItem.ValStr()
|
sc.name = currItem.ValStr()
|
||||||
sc.isInline = true
|
sc.isInline = true
|
||||||
|
|
|
@ -54,3 +54,22 @@ func (p *pageForShortcode) TableOfContents() template.HTML {
|
||||||
p.p.enablePlaceholders()
|
p.p.enablePlaceholders()
|
||||||
return p.toc
|
return p.toc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is what is sent into the content render hooks (link, image).
|
||||||
|
type pageForRenderHooks struct {
|
||||||
|
page.PageWithoutContent
|
||||||
|
page.TableOfContentsProvider
|
||||||
|
page.ContentProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPageForRenderHook(p *pageState) page.Page {
|
||||||
|
return &pageForRenderHooks{
|
||||||
|
PageWithoutContent: p,
|
||||||
|
ContentProvider: page.NopPage,
|
||||||
|
TableOfContentsProvider: page.NopPage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pageForRenderHooks) page() page.Page {
|
||||||
|
return p.PageWithoutContent.(page.Page)
|
||||||
|
}
|
||||||
|
|
|
@ -379,8 +379,13 @@ title: "Shortcodes Galore!"
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return "<nil>"
|
return "<nil>"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var version int
|
||||||
|
if s.info != nil {
|
||||||
|
version = s.info.ParseInfo().Config.Version
|
||||||
|
}
|
||||||
return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d",
|
return strReplacer.Replace(fmt.Sprintf("%s;inline:%t;closing:%t;inner:%v;params:%v;ordinal:%d;markup:%t;version:%d;pos:%d",
|
||||||
s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, s.info.Config.Version, s.pos))
|
s.name, s.isInline, s.isClosing, s.inner, s.params, s.ordinal, s.doMarkup, version, s.pos))
|
||||||
}
|
}
|
||||||
|
|
||||||
regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {
|
regexpCheck := func(re string) func(c *qt.C, shortcode *shortcode, err error) {
|
||||||
|
|
118
hugolib/site.go
118
hugolib/site.go
|
@ -28,6 +28,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/resources"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/resources/resource"
|
"github.com/gohugoio/hugo/resources/resource"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
|
@ -60,7 +66,6 @@ import (
|
||||||
"github.com/gohugoio/hugo/navigation"
|
"github.com/gohugoio/hugo/navigation"
|
||||||
"github.com/gohugoio/hugo/output"
|
"github.com/gohugoio/hugo/output"
|
||||||
"github.com/gohugoio/hugo/related"
|
"github.com/gohugoio/hugo/related"
|
||||||
"github.com/gohugoio/hugo/resources"
|
|
||||||
"github.com/gohugoio/hugo/resources/page/pagemeta"
|
"github.com/gohugoio/hugo/resources/page/pagemeta"
|
||||||
"github.com/gohugoio/hugo/source"
|
"github.com/gohugoio/hugo/source"
|
||||||
"github.com/gohugoio/hugo/tpl"
|
"github.com/gohugoio/hugo/tpl"
|
||||||
|
@ -801,7 +806,6 @@ func (s *Site) multilingual() *Multilingual {
|
||||||
|
|
||||||
type whatChanged struct {
|
type whatChanged struct {
|
||||||
source bool
|
source bool
|
||||||
other bool
|
|
||||||
files map[string]bool
|
files map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -888,10 +892,11 @@ func (s *Site) translateFileEvents(events []fsnotify.Event) []fsnotify.Event {
|
||||||
// It returns whetever the content source was changed.
|
// It returns whetever the content source was changed.
|
||||||
// TODO(bep) clean up/rewrite this method.
|
// TODO(bep) clean up/rewrite this method.
|
||||||
func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error {
|
func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error {
|
||||||
|
|
||||||
events = s.filterFileEvents(events)
|
events = s.filterFileEvents(events)
|
||||||
events = s.translateFileEvents(events)
|
events = s.translateFileEvents(events)
|
||||||
|
|
||||||
|
changeIdentities := make(identity.Identities)
|
||||||
|
|
||||||
s.Log.DEBUG.Printf("Rebuild for events %q", events)
|
s.Log.DEBUG.Printf("Rebuild for events %q", events)
|
||||||
|
|
||||||
h := s.h
|
h := s.h
|
||||||
|
@ -902,11 +907,12 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
|
||||||
sourceChanged = []fsnotify.Event{}
|
sourceChanged = []fsnotify.Event{}
|
||||||
sourceReallyChanged = []fsnotify.Event{}
|
sourceReallyChanged = []fsnotify.Event{}
|
||||||
contentFilesChanged []string
|
contentFilesChanged []string
|
||||||
tmplChanged = []fsnotify.Event{}
|
|
||||||
dataChanged = []fsnotify.Event{}
|
tmplChanged bool
|
||||||
i18nChanged = []fsnotify.Event{}
|
dataChanged bool
|
||||||
shortcodesChanged = make(map[string]bool)
|
i18nChanged bool
|
||||||
sourceFilesChanged = make(map[string]bool)
|
|
||||||
|
sourceFilesChanged = make(map[string]bool)
|
||||||
|
|
||||||
// prevent spamming the log on changes
|
// prevent spamming the log on changes
|
||||||
logger = helpers.NewDistinctFeedbackLogger()
|
logger = helpers.NewDistinctFeedbackLogger()
|
||||||
|
@ -919,33 +925,30 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
|
||||||
cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...)
|
cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.isContentDirEvent(ev) {
|
id, found := s.eventToIdentity(ev)
|
||||||
logger.Println("Source changed", ev)
|
if found {
|
||||||
sourceChanged = append(sourceChanged, ev)
|
changeIdentities[id] = id
|
||||||
}
|
|
||||||
if s.isLayoutDirEvent(ev) {
|
switch id.Type {
|
||||||
logger.Println("Template changed", ev)
|
case files.ComponentFolderContent:
|
||||||
tmplChanged = append(tmplChanged, ev)
|
logger.Println("Source changed", ev)
|
||||||
|
sourceChanged = append(sourceChanged, ev)
|
||||||
|
case files.ComponentFolderLayouts:
|
||||||
|
logger.Println("Template changed", ev)
|
||||||
|
tmplChanged = true
|
||||||
|
case files.ComponentFolderData:
|
||||||
|
logger.Println("Data changed", ev)
|
||||||
|
dataChanged = true
|
||||||
|
case files.ComponentFolderI18n:
|
||||||
|
logger.Println("i18n changed", ev)
|
||||||
|
i18nChanged = true
|
||||||
|
|
||||||
if strings.Contains(ev.Name, "shortcodes") {
|
|
||||||
shortcode := filepath.Base(ev.Name)
|
|
||||||
shortcode = strings.TrimSuffix(shortcode, filepath.Ext(shortcode))
|
|
||||||
shortcodesChanged[shortcode] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.isDataDirEvent(ev) {
|
|
||||||
logger.Println("Data changed", ev)
|
|
||||||
dataChanged = append(dataChanged, ev)
|
|
||||||
}
|
|
||||||
if s.isI18nEvent(ev) {
|
|
||||||
logger.Println("i18n changed", ev)
|
|
||||||
i18nChanged = append(dataChanged, ev)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changed := &whatChanged{
|
changed := &whatChanged{
|
||||||
source: len(sourceChanged) > 0 || len(shortcodesChanged) > 0,
|
source: len(sourceChanged) > 0,
|
||||||
other: len(tmplChanged) > 0 || len(i18nChanged) > 0 || len(dataChanged) > 0,
|
|
||||||
files: sourceFilesChanged,
|
files: sourceFilesChanged,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -960,7 +963,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
|
||||||
s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...)
|
s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(tmplChanged) > 0 || len(i18nChanged) > 0 {
|
if tmplChanged || i18nChanged {
|
||||||
sites := s.h.Sites
|
sites := s.h.Sites
|
||||||
first := sites[0]
|
first := sites[0]
|
||||||
|
|
||||||
|
@ -989,7 +992,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(dataChanged) > 0 {
|
if dataChanged {
|
||||||
s.h.init.data.Reset()
|
s.h.init.data.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1018,18 +1021,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
|
||||||
sourceFilesChanged[ev.Name] = true
|
sourceFilesChanged[ev.Name] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for shortcode := range shortcodesChanged {
|
h.resetPageStateFromEvents(changeIdentities)
|
||||||
// There are certain scenarios that, when a shortcode changes,
|
|
||||||
// it isn't sufficient to just rerender the already parsed shortcode.
|
|
||||||
// One example is if the user adds a new shortcode to the content file first,
|
|
||||||
// and then creates the shortcode on the file system.
|
|
||||||
// To handle these scenarios, we must do a full reprocessing of the
|
|
||||||
// pages that keeps a reference to the changed shortcode.
|
|
||||||
pagesWithShortcode := h.findPagesByShortcode(shortcode)
|
|
||||||
for _, p := range pagesWithShortcode {
|
|
||||||
contentFilesChanged = append(contentFilesChanged, p.File().Filename())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 {
|
if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 {
|
||||||
var filenamesChanged []string
|
var filenamesChanged []string
|
||||||
|
@ -1218,20 +1210,14 @@ func (s *Site) initializeSiteInfo() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Site) isI18nEvent(e fsnotify.Event) bool {
|
func (s *Site) eventToIdentity(e fsnotify.Event) (identity.PathIdentity, bool) {
|
||||||
return s.BaseFs.SourceFilesystems.IsI18n(e.Name)
|
for _, fs := range s.BaseFs.SourceFilesystems.FileSystems() {
|
||||||
}
|
if p := fs.Path(e.Name); p != "" {
|
||||||
|
return identity.NewPathIdentity(fs.Name, p), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Site) isDataDirEvent(e fsnotify.Event) bool {
|
return identity.PathIdentity{}, false
|
||||||
return s.BaseFs.SourceFilesystems.IsData(e.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Site) isLayoutDirEvent(e fsnotify.Event) bool {
|
|
||||||
return s.BaseFs.SourceFilesystems.IsLayout(e.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Site) isContentDirEvent(e fsnotify.Event) bool {
|
|
||||||
return s.BaseFs.IsContent(e.Name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Site) readAndProcessContent(filenames ...string) error {
|
func (s *Site) readAndProcessContent(filenames ...string) error {
|
||||||
|
@ -1562,6 +1548,26 @@ var infoOnMissingLayout = map[string]bool{
|
||||||
"404": true,
|
"404": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type contentLinkRenderer struct {
|
||||||
|
templateHandler tpl.TemplateHandler
|
||||||
|
identity.Provider
|
||||||
|
templ tpl.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r contentLinkRenderer) Render(w io.Writer, ctx hooks.LinkContext) error {
|
||||||
|
return r.templateHandler.Execute(r.templ, w, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Site) lookupTemplate(layouts ...string) (tpl.Template, bool) {
|
||||||
|
for _, l := range layouts {
|
||||||
|
if templ, found := s.Tmpl.Lookup(l); found {
|
||||||
|
return templ, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Site) renderForLayouts(name, outputFormat string, d interface{}, w io.Writer, layouts ...string) (err error) {
|
func (s *Site) renderForLayouts(name, outputFormat string, d interface{}, w io.Writer, layouts ...string) (err error) {
|
||||||
templ := s.findFirstTemplate(layouts...)
|
templ := s.findFirstTemplate(layouts...)
|
||||||
if templ == nil {
|
if templ == nil {
|
||||||
|
|
|
@ -127,6 +127,36 @@ title = "What is Markdown"
|
||||||
baseURL = "https://example.com"
|
baseURL = "https://example.com"
|
||||||
|
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(filepath.FromSlash("testdata/what-is-markdown.md"))
|
||||||
|
sb.Assert(err, qt.IsNil)
|
||||||
|
datastr := string(data)
|
||||||
|
getContent := func(i int) string {
|
||||||
|
return fmt.Sprintf(`---
|
||||||
|
title: "Page %d"
|
||||||
|
---
|
||||||
|
|
||||||
|
`, i) + datastr
|
||||||
|
|
||||||
|
}
|
||||||
|
for i := 1; i <= 100; i++ {
|
||||||
|
sb.WithContent(fmt.Sprintf("content/page%d.md", i), getContent(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb
|
||||||
|
},
|
||||||
|
func(s *sitesBuilder) {
|
||||||
|
s.Assert(s.CheckExists("public/page8/index.html"), qt.Equals, true)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{"Markdown with custom link handler", func(b testing.TB) *sitesBuilder {
|
||||||
|
sb := newTestSitesBuilder(b).WithConfigFile("toml", `
|
||||||
|
title = "What is Markdown"
|
||||||
|
baseURL = "https://example.com"
|
||||||
|
|
||||||
|
`)
|
||||||
|
|
||||||
|
sb.WithTemplatesAdded("_default/_markup/render-link.html", `<a href="{{ .Destination | safeURL }}#custom">CUSTOM LINK</a>`)
|
||||||
data, err := ioutil.ReadFile(filepath.FromSlash("testdata/what-is-markdown.md"))
|
data, err := ioutil.ReadFile(filepath.FromSlash("testdata/what-is-markdown.md"))
|
||||||
sb.Assert(err, qt.IsNil)
|
sb.Assert(err, qt.IsNil)
|
||||||
datastr := string(data)
|
datastr := string(data)
|
||||||
|
|
|
@ -18,8 +18,12 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
|
||||||
|
qt "github.com/frankban/quicktest"
|
||||||
"github.com/gohugoio/hugo/deps"
|
"github.com/gohugoio/hugo/deps"
|
||||||
"github.com/gohugoio/hugo/hugofs"
|
"github.com/gohugoio/hugo/hugofs"
|
||||||
|
"github.com/gohugoio/hugo/tpl"
|
||||||
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
@ -320,6 +324,7 @@ Partial cached1: {{ partialCached "p1" "input1" $key1 }}
|
||||||
Partial cached2: {{ partialCached "p1" "input2" $key1 }}
|
Partial cached2: {{ partialCached "p1" "input2" $key1 }}
|
||||||
Partial cached3: {{ partialCached "p1" "input3" $key2 }}
|
Partial cached3: {{ partialCached "p1" "input3" $key2 }}
|
||||||
`,
|
`,
|
||||||
|
|
||||||
"partials/p1.html", `partial: {{ . }}`,
|
"partials/p1.html", `partial: {{ . }}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -331,3 +336,85 @@ Partial cached3: {{ partialCached "p1" "input3" $key2 }}
|
||||||
Partial cached3: partial: input3
|
Partial cached3: partial: input3
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTemplateDependencies(t *testing.T) {
|
||||||
|
b := newTestSitesBuilder(t).Running()
|
||||||
|
|
||||||
|
b.WithTemplates("index.html", `
|
||||||
|
{{ $p := site.GetPage "p1" }}
|
||||||
|
{{ partial "p1.html" $p }}
|
||||||
|
{{ partialCached "p2.html" "foo" }}
|
||||||
|
{{ partials.Include "p3.html" "data" }}
|
||||||
|
{{ partials.IncludeCached "p4.html" "foo" }}
|
||||||
|
{{ $p := partial "p5" }}
|
||||||
|
{{ partial "sub/p6.html" }}
|
||||||
|
{{ partial "P7.html" }}
|
||||||
|
{{ template "_default/foo.html" }}
|
||||||
|
Partial nested: {{ partial "p10" }}
|
||||||
|
|
||||||
|
`,
|
||||||
|
"partials/p1.html", `ps: {{ .Render "li" }}`,
|
||||||
|
"partials/p2.html", `p2`,
|
||||||
|
"partials/p3.html", `p3`,
|
||||||
|
"partials/p4.html", `p4`,
|
||||||
|
"partials/p5.html", `p5`,
|
||||||
|
"partials/sub/p6.html", `p6`,
|
||||||
|
"partials/P7.html", `p7`,
|
||||||
|
"partials/p8.html", `p8 {{ partial "p9.html" }}`,
|
||||||
|
"partials/p9.html", `p9`,
|
||||||
|
"partials/p10.html", `p10 {{ partial "p11.html" }}`,
|
||||||
|
"partials/p11.html", `p11`,
|
||||||
|
"_default/foo.html", `foo`,
|
||||||
|
"_default/li.html", `li {{ partial "p8.html" }}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
b.WithContent("p1.md", `---
|
||||||
|
title: P1
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
`)
|
||||||
|
|
||||||
|
b.Build(BuildCfg{})
|
||||||
|
|
||||||
|
s := b.H.Sites[0]
|
||||||
|
|
||||||
|
templ, found := s.lookupTemplate("index.html")
|
||||||
|
b.Assert(found, qt.Equals, true)
|
||||||
|
|
||||||
|
idset := make(map[identity.Identity]bool)
|
||||||
|
collectIdentities(idset, templ.(tpl.Info))
|
||||||
|
b.Assert(idset, qt.HasLen, 10)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectIdentities(set map[identity.Identity]bool, provider identity.Provider) {
|
||||||
|
if ids, ok := provider.(identity.IdentitiesProvider); ok {
|
||||||
|
for _, id := range ids.GetIdentities() {
|
||||||
|
collectIdentities(set, id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
set[provider.GetIdentity()] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRecursiveIdentities(level int, id identity.Provider) {
|
||||||
|
if level == 0 {
|
||||||
|
fmt.Println(id.GetIdentity(), "===>")
|
||||||
|
}
|
||||||
|
if ids, ok := id.(identity.IdentitiesProvider); ok {
|
||||||
|
level++
|
||||||
|
for _, id := range ids.GetIdentities() {
|
||||||
|
printRecursiveIdentities(level, id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ident(level)
|
||||||
|
fmt.Println("ID", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ident(n int) {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
fmt.Print(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -389,8 +389,9 @@ func (s *sitesBuilder) EditFiles(filenameContent ...string) *sitesBuilder {
|
||||||
var changedFiles []string
|
var changedFiles []string
|
||||||
for i := 0; i < len(filenameContent); i += 2 {
|
for i := 0; i < len(filenameContent); i += 2 {
|
||||||
filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1]
|
filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1]
|
||||||
changedFiles = append(changedFiles, filename)
|
absFilename := s.absFilename(filename)
|
||||||
writeSource(s.T, s.Fs, s.absFilename(filename), content)
|
changedFiles = append(changedFiles, absFilename)
|
||||||
|
writeSource(s.T, s.Fs, absFilename, content)
|
||||||
|
|
||||||
}
|
}
|
||||||
s.changedFiles = changedFiles
|
s.changedFiles = changedFiles
|
||||||
|
@ -963,10 +964,6 @@ func isCI() bool {
|
||||||
return os.Getenv("CI") != ""
|
return os.Getenv("CI") != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func isGo111() bool {
|
|
||||||
return strings.Contains(runtime.Version(), "1.11")
|
|
||||||
}
|
|
||||||
|
|
||||||
// See https://github.com/golang/go/issues/19280
|
// See https://github.com/golang/go/issues/19280
|
||||||
// Not in use.
|
// Not in use.
|
||||||
var parallelEnabled = true
|
var parallelEnabled = true
|
||||||
|
|
131
identity/identity.go
Normal file
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 (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
"github.com/gohugoio/hugo/markup/internal"
|
"github.com/gohugoio/hugo/markup/internal"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
|
@ -47,6 +48,10 @@ func (a *asciidocConverter) Convert(ctx converter.RenderContext) (converter.Resu
|
||||||
return converter.Bytes(a.getAsciidocContent(ctx.Src, a.ctx)), nil
|
return converter.Bytes(a.getAsciidocContent(ctx.Src, a.ctx)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *asciidocConverter) Supports(feature identity.Identity) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// getAsciidocContent calls asciidoctor or asciidoc as an external helper
|
// getAsciidocContent calls asciidoctor or asciidoc as an external helper
|
||||||
// to convert AsciiDoc content to HTML.
|
// to convert AsciiDoc content to HTML.
|
||||||
func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) []byte {
|
func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) []byte {
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
package blackfriday
|
package blackfriday
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
"github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
|
"github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
"github.com/russross/blackfriday"
|
"github.com/russross/blackfriday"
|
||||||
|
@ -72,6 +73,10 @@ func (c *blackfridayConverter) Convert(ctx converter.RenderContext) (converter.R
|
||||||
return converter.Bytes(blackfriday.Markdown(ctx.Src, r, c.extensions)), nil
|
return converter.Bytes(blackfriday.Markdown(ctx.Src, r, c.extensions)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *blackfridayConverter) Supports(feature identity.Identity) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (c *blackfridayConverter) getHTMLRenderer(renderTOC bool) blackfriday.Renderer {
|
func (c *blackfridayConverter) getHTMLRenderer(renderTOC bool) blackfriday.Renderer {
|
||||||
flags := getFlags(renderTOC, c.bf)
|
flags := getFlags(renderTOC, c.bf)
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@ package converter
|
||||||
import (
|
import (
|
||||||
"github.com/gohugoio/hugo/common/loggers"
|
"github.com/gohugoio/hugo/common/loggers"
|
||||||
"github.com/gohugoio/hugo/config"
|
"github.com/gohugoio/hugo/config"
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||||
"github.com/gohugoio/hugo/markup/markup_config"
|
"github.com/gohugoio/hugo/markup/markup_config"
|
||||||
"github.com/gohugoio/hugo/markup/tableofcontents"
|
"github.com/gohugoio/hugo/markup/tableofcontents"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
@ -67,6 +69,7 @@ func (n newConverter) Name() string {
|
||||||
// another format, e.g. Markdown to HTML.
|
// another format, e.g. Markdown to HTML.
|
||||||
type Converter interface {
|
type Converter interface {
|
||||||
Convert(ctx RenderContext) (Result, error)
|
Convert(ctx RenderContext) (Result, error)
|
||||||
|
Supports(feature identity.Identity) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result represents the minimum returned from Convert.
|
// Result represents the minimum returned from Convert.
|
||||||
|
@ -94,6 +97,7 @@ func (b Bytes) Bytes() []byte {
|
||||||
|
|
||||||
// DocumentContext holds contextual information about the document to convert.
|
// DocumentContext holds contextual information about the document to convert.
|
||||||
type DocumentContext struct {
|
type DocumentContext struct {
|
||||||
|
Document interface{} // May be nil. Usually a page.Page
|
||||||
DocumentID string
|
DocumentID string
|
||||||
DocumentName string
|
DocumentName string
|
||||||
ConfigOverrides map[string]interface{}
|
ConfigOverrides map[string]interface{}
|
||||||
|
@ -101,6 +105,11 @@ type DocumentContext struct {
|
||||||
|
|
||||||
// RenderContext holds contextual information about the content to render.
|
// RenderContext holds contextual information about the content to render.
|
||||||
type RenderContext struct {
|
type RenderContext struct {
|
||||||
Src []byte
|
Src []byte
|
||||||
RenderTOC bool
|
RenderTOC bool
|
||||||
|
RenderHooks *hooks.Render
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
FeatureRenderHooks = identity.NewPathIdentity("markup", "renderingHooks")
|
||||||
|
)
|
||||||
|
|
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
|
package goldmark
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/hugofs"
|
"github.com/gohugoio/hugo/hugofs"
|
||||||
|
|
||||||
"github.com/alecthomas/chroma/styles"
|
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
"github.com/gohugoio/hugo/markup/highlight"
|
"github.com/gohugoio/hugo/markup/highlight"
|
||||||
"github.com/gohugoio/hugo/markup/markup_config"
|
|
||||||
"github.com/gohugoio/hugo/markup/tableofcontents"
|
"github.com/gohugoio/hugo/markup/tableofcontents"
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
hl "github.com/yuin/goldmark-highlighting"
|
hl "github.com/yuin/goldmark-highlighting"
|
||||||
|
@ -48,7 +49,7 @@ type provide struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) {
|
func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) {
|
||||||
md := newMarkdown(cfg.MarkupConfig)
|
md := newMarkdown(cfg)
|
||||||
return converter.NewProvider("goldmark", func(ctx converter.DocumentContext) (converter.Converter, error) {
|
return converter.NewProvider("goldmark", func(ctx converter.DocumentContext) (converter.Converter, error) {
|
||||||
return &goldmarkConverter{
|
return &goldmarkConverter{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
@ -64,11 +65,13 @@ type goldmarkConverter struct {
|
||||||
cfg converter.ProviderConfig
|
cfg converter.ProviderConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMarkdown(mcfg markup_config.Config) goldmark.Markdown {
|
func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
|
||||||
cfg := mcfg.Goldmark
|
mcfg := pcfg.MarkupConfig
|
||||||
|
cfg := pcfg.MarkupConfig.Goldmark
|
||||||
|
|
||||||
var (
|
var (
|
||||||
extensions = []goldmark.Extender{
|
extensions = []goldmark.Extender{
|
||||||
|
newLinks(),
|
||||||
newTocExtension(),
|
newTocExtension(),
|
||||||
}
|
}
|
||||||
rendererOptions []renderer.Option
|
rendererOptions []renderer.Option
|
||||||
|
@ -143,15 +146,53 @@ func newMarkdown(mcfg markup_config.Config) goldmark.Markdown {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ identity.IdentitiesProvider = (*converterResult)(nil)
|
||||||
|
|
||||||
type converterResult struct {
|
type converterResult struct {
|
||||||
converter.Result
|
converter.Result
|
||||||
toc tableofcontents.Root
|
toc tableofcontents.Root
|
||||||
|
ids identity.Identities
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c converterResult) TableOfContents() tableofcontents.Root {
|
func (c converterResult) TableOfContents() tableofcontents.Root {
|
||||||
return c.toc
|
return c.toc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c converterResult) GetIdentities() identity.Identities {
|
||||||
|
return c.ids
|
||||||
|
}
|
||||||
|
|
||||||
|
type renderContext struct {
|
||||||
|
util.BufWriter
|
||||||
|
renderContextData
|
||||||
|
}
|
||||||
|
|
||||||
|
type renderContextData interface {
|
||||||
|
RenderContext() converter.RenderContext
|
||||||
|
DocumentContext() converter.DocumentContext
|
||||||
|
AddIdentity(id identity.Identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
type renderContextDataHolder struct {
|
||||||
|
rctx converter.RenderContext
|
||||||
|
dctx converter.DocumentContext
|
||||||
|
ids identity.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *renderContextDataHolder) RenderContext() converter.RenderContext {
|
||||||
|
return ctx.rctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *renderContextDataHolder) DocumentContext() converter.DocumentContext {
|
||||||
|
return ctx.dctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *renderContextDataHolder) AddIdentity(id identity.Identity) {
|
||||||
|
ctx.ids.Add(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
var converterIdentity = identity.KeyValueIdentity{Key: "goldmark", Value: "converter"}
|
||||||
|
|
||||||
func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result converter.Result, err error) {
|
func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result converter.Result, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
@ -166,9 +207,7 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
result = buf
|
result = buf
|
||||||
pctx := parser.NewContext()
|
pctx := newParserContext(ctx)
|
||||||
pctx.Set(tocEnableKey, ctx.RenderTOC)
|
|
||||||
|
|
||||||
reader := text.NewReader(ctx.Src)
|
reader := text.NewReader(ctx.Src)
|
||||||
|
|
||||||
doc := c.md.Parser().Parse(
|
doc := c.md.Parser().Parse(
|
||||||
|
@ -176,27 +215,58 @@ func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result convert
|
||||||
parser.WithContext(pctx),
|
parser.WithContext(pctx),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := c.md.Renderer().Render(buf, ctx.Src, doc); err != nil {
|
rcx := &renderContextDataHolder{
|
||||||
|
rctx: ctx,
|
||||||
|
dctx: c.ctx,
|
||||||
|
ids: identity.NewManager(converterIdentity),
|
||||||
|
}
|
||||||
|
|
||||||
|
w := renderContext{
|
||||||
|
BufWriter: bufio.NewWriter(buf),
|
||||||
|
renderContextData: rcx,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.md.Renderer().Render(w, ctx.Src, doc); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if toc, ok := pctx.Get(tocResultKey).(tableofcontents.Root); ok {
|
return converterResult{
|
||||||
return converterResult{
|
Result: buf,
|
||||||
Result: buf,
|
ids: rcx.ids.GetIdentities(),
|
||||||
toc: toc,
|
toc: pctx.TableOfContents(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
|
||||||
|
|
||||||
return buf, nil
|
}
|
||||||
|
|
||||||
|
var featureSet = map[identity.Identity]bool{
|
||||||
|
converter.FeatureRenderHooks: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *goldmarkConverter) Supports(feature identity.Identity) bool {
|
||||||
|
return featureSet[feature.GetIdentity()]
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParserContext(rctx converter.RenderContext) *parserContext {
|
||||||
|
ctx := parser.NewContext()
|
||||||
|
ctx.Set(tocEnableKey, rctx.RenderTOC)
|
||||||
|
return &parserContext{
|
||||||
|
Context: ctx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type parserContext struct {
|
||||||
|
parser.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parserContext) TableOfContents() tableofcontents.Root {
|
||||||
|
if v := p.Get(tocResultKey); v != nil {
|
||||||
|
return v.(tableofcontents.Root)
|
||||||
|
}
|
||||||
|
return tableofcontents.Root{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHighlighting(cfg highlight.Config) goldmark.Extender {
|
func newHighlighting(cfg highlight.Config) goldmark.Extender {
|
||||||
style := styles.Get(cfg.Style)
|
return hl.NewHighlighting(
|
||||||
if style == nil {
|
|
||||||
style = styles.Fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
e := hl.NewHighlighting(
|
|
||||||
hl.WithStyle(cfg.Style),
|
hl.WithStyle(cfg.Style),
|
||||||
hl.WithGuessLanguage(cfg.GuessSyntax),
|
hl.WithGuessLanguage(cfg.GuessSyntax),
|
||||||
hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()),
|
hl.WithCodeBlockOptions(highlight.GetCodeBlockOptions()),
|
||||||
|
@ -230,6 +300,4 @@ func newHighlighting(cfg highlight.Config) goldmark.Extender {
|
||||||
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
return e
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,9 @@ func TestConvert(t *testing.T) {
|
||||||
https://github.com/gohugoio/hugo/issues/6528
|
https://github.com/gohugoio/hugo/issues/6528
|
||||||
[Live Demo here!](https://docuapi.netlify.com/)
|
[Live Demo here!](https://docuapi.netlify.com/)
|
||||||
|
|
||||||
|
[I'm an inline-style link with title](https://www.google.com "Google's Homepage")
|
||||||
|
|
||||||
|
|
||||||
## Code Fences
|
## Code Fences
|
||||||
|
|
||||||
§§§bash
|
§§§bash
|
||||||
|
@ -98,6 +101,7 @@ description
|
||||||
|
|
||||||
mconf := markup_config.Default
|
mconf := markup_config.Default
|
||||||
mconf.Highlight.NoClasses = false
|
mconf.Highlight.NoClasses = false
|
||||||
|
mconf.Goldmark.Renderer.Unsafe = true
|
||||||
|
|
||||||
p, err := Provider.New(
|
p, err := Provider.New(
|
||||||
converter.ProviderConfig{
|
converter.ProviderConfig{
|
||||||
|
@ -106,15 +110,15 @@ description
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
conv, err := p.New(converter.DocumentContext{})
|
conv, err := p.New(converter.DocumentContext{DocumentID: "thedoc"})
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
b, err := conv.Convert(converter.RenderContext{Src: []byte(content)})
|
b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content)})
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
got := string(b.Bytes())
|
got := string(b.Bytes())
|
||||||
|
|
||||||
// Links
|
// Links
|
||||||
c.Assert(got, qt.Contains, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`)
|
// c.Assert(got, qt.Contains, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`)
|
||||||
|
|
||||||
// Header IDs
|
// Header IDs
|
||||||
c.Assert(got, qt.Contains, `<h2 id="custom">Custom ID</h2>`, qt.Commentf(got))
|
c.Assert(got, qt.Contains, `<h2 id="custom">Custom ID</h2>`, qt.Commentf(got))
|
||||||
|
@ -137,6 +141,11 @@ description
|
||||||
c.Assert(got, qt.Contains, `<section class="footnotes" role="doc-endnotes">`)
|
c.Assert(got, qt.Contains, `<section class="footnotes" role="doc-endnotes">`)
|
||||||
c.Assert(got, qt.Contains, `<dt>date</dt>`)
|
c.Assert(got, qt.Contains, `<dt>date</dt>`)
|
||||||
|
|
||||||
|
toc, ok := b.(converter.TableOfContentsProvider)
|
||||||
|
c.Assert(ok, qt.Equals, true)
|
||||||
|
tocHTML := toc.TableOfContents().ToHTML(1, 2, false)
|
||||||
|
c.Assert(tocHTML, qt.Contains, "TableOfContents")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCodeFence(t *testing.T) {
|
func TestCodeFence(t *testing.T) {
|
||||||
|
|
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
|
package mmark
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
"github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
|
"github.com/gohugoio/hugo/markup/blackfriday/blackfriday_config"
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
"github.com/miekg/mmark"
|
"github.com/miekg/mmark"
|
||||||
|
@ -65,6 +66,10 @@ func (c *mmarkConverter) Convert(ctx converter.RenderContext) (converter.Result,
|
||||||
return mmark.Parse(ctx.Src, r, c.extensions), nil
|
return mmark.Parse(ctx.Src, r, c.extensions), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *mmarkConverter) Supports(feature identity.Identity) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func getHTMLRenderer(
|
func getHTMLRenderer(
|
||||||
ctx converter.DocumentContext,
|
ctx converter.DocumentContext,
|
||||||
cfg blackfriday_config.Config,
|
cfg blackfriday_config.Config,
|
||||||
|
|
|
@ -17,6 +17,8 @@ package org
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
"github.com/niklasfasching/go-org/org"
|
"github.com/niklasfasching/go-org/org"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
|
@ -66,3 +68,7 @@ func (c *orgConverter) Convert(ctx converter.RenderContext) (converter.Result, e
|
||||||
}
|
}
|
||||||
return converter.Bytes([]byte(html)), nil
|
return converter.Bytes([]byte(html)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *orgConverter) Supports(feature identity.Identity) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ package pandoc
|
||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
"github.com/gohugoio/hugo/markup/internal"
|
"github.com/gohugoio/hugo/markup/internal"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
|
@ -47,6 +48,10 @@ func (c *pandocConverter) Convert(ctx converter.RenderContext) (converter.Result
|
||||||
return converter.Bytes(c.getPandocContent(ctx.Src, c.ctx)), nil
|
return converter.Bytes(c.getPandocContent(ctx.Src, c.ctx)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *pandocConverter) Supports(feature identity.Identity) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// getPandocContent calls pandoc as an external helper to convert pandoc markdown to HTML.
|
// getPandocContent calls pandoc as an external helper to convert pandoc markdown to HTML.
|
||||||
func (c *pandocConverter) getPandocContent(src []byte, ctx converter.DocumentContext) []byte {
|
func (c *pandocConverter) getPandocContent(src []byte, ctx converter.DocumentContext) []byte {
|
||||||
logger := c.cfg.Logger
|
logger := c.cfg.Logger
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
"github.com/gohugoio/hugo/markup/internal"
|
"github.com/gohugoio/hugo/markup/internal"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/markup/converter"
|
"github.com/gohugoio/hugo/markup/converter"
|
||||||
|
@ -48,6 +49,10 @@ func (c *rstConverter) Convert(ctx converter.RenderContext) (converter.Result, e
|
||||||
return converter.Bytes(c.getRstContent(ctx.Src, c.ctx)), nil
|
return converter.Bytes(c.getRstContent(ctx.Src, c.ctx)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *rstConverter) Supports(feature identity.Identity) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// getRstContent calls the Python script rst2html as an external helper
|
// getRstContent calls the Python script rst2html as an external helper
|
||||||
// to convert reStructuredText content to HTML.
|
// to convert reStructuredText content to HTML.
|
||||||
func (c *rstConverter) getRstContent(src []byte, ctx converter.DocumentContext) []byte {
|
func (c *rstConverter) getRstContent(src []byte, ctx converter.DocumentContext) []byte {
|
||||||
|
|
|
@ -37,6 +37,12 @@ type LayoutDescriptor struct {
|
||||||
Layout string
|
Layout string
|
||||||
// LayoutOverride indicates what we should only look for the above layout.
|
// LayoutOverride indicates what we should only look for the above layout.
|
||||||
LayoutOverride bool
|
LayoutOverride bool
|
||||||
|
|
||||||
|
RenderingHook bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d LayoutDescriptor) isList() bool {
|
||||||
|
return !d.RenderingHook && d.Kind != "page"
|
||||||
}
|
}
|
||||||
|
|
||||||
// LayoutHandler calculates the layout template to use to render a given output type.
|
// LayoutHandler calculates the layout template to use to render a given output type.
|
||||||
|
@ -89,7 +95,7 @@ type layoutBuilder struct {
|
||||||
|
|
||||||
func (l *layoutBuilder) addLayoutVariations(vars ...string) {
|
func (l *layoutBuilder) addLayoutVariations(vars ...string) {
|
||||||
for _, layoutVar := range vars {
|
for _, layoutVar := range vars {
|
||||||
if l.d.LayoutOverride && layoutVar != l.d.Layout {
|
if !l.d.RenderingHook && l.d.LayoutOverride && layoutVar != l.d.Layout {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
l.layoutVariations = append(l.layoutVariations, layoutVar)
|
l.layoutVariations = append(l.layoutVariations, layoutVar)
|
||||||
|
@ -99,6 +105,9 @@ func (l *layoutBuilder) addLayoutVariations(vars ...string) {
|
||||||
func (l *layoutBuilder) addTypeVariations(vars ...string) {
|
func (l *layoutBuilder) addTypeVariations(vars ...string) {
|
||||||
for _, typeVar := range vars {
|
for _, typeVar := range vars {
|
||||||
if !reservedSections[typeVar] {
|
if !reservedSections[typeVar] {
|
||||||
|
if l.d.RenderingHook {
|
||||||
|
typeVar = typeVar + renderingHookRoot
|
||||||
|
}
|
||||||
l.typeVariations = append(l.typeVariations, typeVar)
|
l.typeVariations = append(l.typeVariations, typeVar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,16 +124,21 @@ func (l *layoutBuilder) addKind() {
|
||||||
l.addTypeVariations(l.d.Kind)
|
l.addTypeVariations(l.d.Kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderingHookRoot = "/_markup"
|
||||||
|
|
||||||
func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
|
func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
|
||||||
|
|
||||||
b := &layoutBuilder{d: d, f: f}
|
b := &layoutBuilder{d: d, f: f}
|
||||||
|
|
||||||
if d.Layout != "" {
|
if d.RenderingHook {
|
||||||
b.addLayoutVariations(d.Layout)
|
b.addLayoutVariations(d.Kind)
|
||||||
}
|
} else {
|
||||||
|
if d.Layout != "" {
|
||||||
if d.Type != "" {
|
b.addLayoutVariations(d.Layout)
|
||||||
b.addTypeVariations(d.Type)
|
}
|
||||||
|
if d.Type != "" {
|
||||||
|
b.addTypeVariations(d.Type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch d.Kind {
|
switch d.Kind {
|
||||||
|
@ -159,7 +173,7 @@ func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
isRSS := f.Name == RSSFormat.Name
|
isRSS := f.Name == RSSFormat.Name
|
||||||
if isRSS {
|
if !d.RenderingHook && isRSS {
|
||||||
// The historic and common rss.xml case
|
// The historic and common rss.xml case
|
||||||
b.addLayoutVariations("")
|
b.addLayoutVariations("")
|
||||||
}
|
}
|
||||||
|
@ -167,14 +181,14 @@ func resolvePageTemplate(d LayoutDescriptor, f Format) []string {
|
||||||
// All have _default in their lookup path
|
// All have _default in their lookup path
|
||||||
b.addTypeVariations("_default")
|
b.addTypeVariations("_default")
|
||||||
|
|
||||||
if d.Kind != "page" {
|
if d.isList() {
|
||||||
// Add the common list type
|
// Add the common list type
|
||||||
b.addLayoutVariations("list")
|
b.addLayoutVariations("list")
|
||||||
}
|
}
|
||||||
|
|
||||||
layouts := b.resolveVariations()
|
layouts := b.resolveVariations()
|
||||||
|
|
||||||
if isRSS {
|
if !d.RenderingHook && isRSS {
|
||||||
layouts = append(layouts, "_internal/_default/rss.xml")
|
layouts = append(layouts, "_internal/_default/rss.xml")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,9 @@ func TestLayout(t *testing.T) {
|
||||||
[]string{"section/shortcodes.amp.html"}, 12},
|
[]string{"section/shortcodes.amp.html"}, 12},
|
||||||
{"Reserved section, partials", LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials"}, "", ampType,
|
{"Reserved section, partials", LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials"}, "", ampType,
|
||||||
[]string{"section/partials.amp.html"}, 12},
|
[]string{"section/partials.amp.html"}, 12},
|
||||||
|
// We may add type support ... later.
|
||||||
|
{"Content hook", LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog"}, "", ampType,
|
||||||
|
[]string{"_default/_markup/render-link.amp.html", "_default/_markup/render-link.html"}, 2},
|
||||||
} {
|
} {
|
||||||
c.Run(this.name, func(c *qt.C) {
|
c.Run(this.name, func(c *qt.C) {
|
||||||
l := NewLayoutHandler()
|
l := NewLayoutHandler()
|
||||||
|
|
|
@ -201,9 +201,10 @@ type PageMetaProvider interface {
|
||||||
Weight() int
|
Weight() int
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageRenderProvider provides a way for a Page to render itself.
|
// PageRenderProvider provides a way for a Page to render content.
|
||||||
type PageRenderProvider interface {
|
type PageRenderProvider interface {
|
||||||
Render(layout ...string) template.HTML
|
Render(layout ...string) (template.HTML, error)
|
||||||
|
RenderString(args ...interface{}) (template.HTML, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageWithoutContent is the Page without any of the content methods.
|
// PageWithoutContent is the Page without any of the content methods.
|
||||||
|
|
|
@ -371,8 +371,12 @@ func (p *nopPage) RelRef(argsm map[string]interface{}) (string, error) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *nopPage) Render(layout ...string) template.HTML {
|
func (p *nopPage) Render(layout ...string) (template.HTML, error) {
|
||||||
return ""
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *nopPage) RenderString(args ...interface{}) (template.HTML, error) {
|
||||||
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *nopPage) ResourceType() string {
|
func (p *nopPage) ResourceType() string {
|
||||||
|
|
|
@ -446,7 +446,11 @@ func (p *testPage) RelRefFrom(argsm map[string]interface{}, source interface{})
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *testPage) Render(layout ...string) template.HTML {
|
func (p *testPage) Render(layout ...string) (template.HTML, error) {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *testPage) RenderString(args ...interface{}) (template.HTML, error) {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,7 @@ var (
|
||||||
"type state struct", "type stateOld struct",
|
"type state struct", "type stateOld struct",
|
||||||
"func (s *state) evalFunction", "func (s *state) evalFunctionOld",
|
"func (s *state) evalFunction", "func (s *state) evalFunctionOld",
|
||||||
"func (s *state) evalField(", "func (s *state) evalFieldOld(",
|
"func (s *state) evalField(", "func (s *state) evalFieldOld(",
|
||||||
|
"func (s *state) evalCall(", "func (s *state) evalCallOld(",
|
||||||
)
|
)
|
||||||
|
|
||||||
htmlTemplateReplacers = strings.NewReplacer(
|
htmlTemplateReplacers = strings.NewReplacer(
|
||||||
|
|
|
@ -658,7 +658,7 @@ var (
|
||||||
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
|
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
|
||||||
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
|
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
|
||||||
// as the function itself.
|
// as the function itself.
|
||||||
func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value {
|
func (s *state) evalCallOld(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value {
|
||||||
if args != nil {
|
if args != nil {
|
||||||
args = args[1:] // Zeroth arg is function name/node; not passed to function.
|
args = args[1:] // Zeroth arg is function name/node; not passed to function.
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,9 @@ type Preparer interface {
|
||||||
|
|
||||||
// ExecHelper allows some custom eval hooks.
|
// ExecHelper allows some custom eval hooks.
|
||||||
type ExecHelper interface {
|
type ExecHelper interface {
|
||||||
GetFunc(name string) (reflect.Value, bool)
|
GetFunc(tmpl Preparer, name string) (reflect.Value, bool)
|
||||||
GetMapValue(receiver, key reflect.Value) (reflect.Value, bool)
|
GetMethod(tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value)
|
||||||
|
GetMapValue(tmpl Preparer, receiver, key reflect.Value) (reflect.Value, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Executer executes a given template.
|
// Executer executes a given template.
|
||||||
|
@ -64,6 +65,7 @@ func (t *executer) Execute(p Preparer, wr io.Writer, data interface{}) error {
|
||||||
|
|
||||||
state := &state{
|
state := &state{
|
||||||
helper: t.helper,
|
helper: t.helper,
|
||||||
|
prep: p,
|
||||||
tmpl: tmpl,
|
tmpl: tmpl,
|
||||||
wr: wr,
|
wr: wr,
|
||||||
vars: []variable{{"$", value}},
|
vars: []variable{{"$", value}},
|
||||||
|
@ -75,7 +77,6 @@ func (t *executer) Execute(p Preparer, wr io.Writer, data interface{}) error {
|
||||||
|
|
||||||
// Prepare returns a template ready for execution.
|
// Prepare returns a template ready for execution.
|
||||||
func (t *Template) Prepare() (*Template, error) {
|
func (t *Template) Prepare() (*Template, error) {
|
||||||
|
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +96,7 @@ func (t *Template) executeWithState(state *state, value reflect.Value) (err erro
|
||||||
// can execute in parallel.
|
// can execute in parallel.
|
||||||
type state struct {
|
type state struct {
|
||||||
tmpl *Template
|
tmpl *Template
|
||||||
|
prep Preparer // Added for Hugo.
|
||||||
helper ExecHelper // Added for Hugo.
|
helper ExecHelper // Added for Hugo.
|
||||||
wr io.Writer
|
wr io.Writer
|
||||||
node parse.Node // current node, for errors
|
node parse.Node // current node, for errors
|
||||||
|
@ -110,7 +112,7 @@ func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd
|
||||||
var ok bool
|
var ok bool
|
||||||
if s.helper != nil {
|
if s.helper != nil {
|
||||||
// Added for Hugo.
|
// Added for Hugo.
|
||||||
function, ok = s.helper.GetFunc(name)
|
function, ok = s.helper.GetFunc(s.prep, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -148,9 +150,23 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
|
||||||
if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() {
|
if ptr.Kind() != reflect.Interface && ptr.Kind() != reflect.Ptr && ptr.CanAddr() {
|
||||||
ptr = ptr.Addr()
|
ptr = ptr.Addr()
|
||||||
}
|
}
|
||||||
if method := ptr.MethodByName(fieldName); method.IsValid() {
|
// Added for Hugo.
|
||||||
|
var first reflect.Value
|
||||||
|
var method reflect.Value
|
||||||
|
if s.helper != nil {
|
||||||
|
method, first = s.helper.GetMethod(s.prep, ptr, fieldName)
|
||||||
|
} else {
|
||||||
|
method = ptr.MethodByName(fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if method.IsValid() {
|
||||||
|
if first != zero {
|
||||||
|
return s.evalCall(dot, method, node, fieldName, args, final, first)
|
||||||
|
}
|
||||||
|
|
||||||
return s.evalCall(dot, method, node, fieldName, args, final)
|
return s.evalCall(dot, method, node, fieldName, args, final)
|
||||||
}
|
}
|
||||||
|
|
||||||
hasArgs := len(args) > 1 || final != missingVal
|
hasArgs := len(args) > 1 || final != missingVal
|
||||||
// It's not a method; must be a field of a struct or an element of a map.
|
// It's not a method; must be a field of a struct or an element of a map.
|
||||||
switch receiver.Kind() {
|
switch receiver.Kind() {
|
||||||
|
@ -177,7 +193,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
|
||||||
var result reflect.Value
|
var result reflect.Value
|
||||||
if s.helper != nil {
|
if s.helper != nil {
|
||||||
// Added for Hugo.
|
// Added for Hugo.
|
||||||
result, _ = s.helper.GetMapValue(receiver, nameVal)
|
result, _ = s.helper.GetMapValue(s.prep, receiver, nameVal)
|
||||||
} else {
|
} else {
|
||||||
result = receiver.MapIndex(nameVal)
|
result = receiver.MapIndex(nameVal)
|
||||||
}
|
}
|
||||||
|
@ -209,3 +225,79 @@ func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node,
|
||||||
s.errorf("can't evaluate field %s in type %s", fieldName, typ)
|
s.errorf("can't evaluate field %s in type %s", fieldName, typ)
|
||||||
panic("not reached")
|
panic("not reached")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
|
||||||
|
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
|
||||||
|
// as the function itself.
|
||||||
|
func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value, first ...reflect.Value) reflect.Value {
|
||||||
|
if args != nil {
|
||||||
|
args = args[1:] // Zeroth arg is function name/node; not passed to function.
|
||||||
|
}
|
||||||
|
typ := fun.Type()
|
||||||
|
numFirst := len(first)
|
||||||
|
numIn := len(args) + numFirst // // Added for Hugo
|
||||||
|
if final != missingVal {
|
||||||
|
numIn++
|
||||||
|
}
|
||||||
|
numFixed := len(args) + len(first)
|
||||||
|
if typ.IsVariadic() {
|
||||||
|
numFixed = typ.NumIn() - 1 // last arg is the variadic one.
|
||||||
|
if numIn < numFixed {
|
||||||
|
s.errorf("wrong number of args for %s: want at least %d got %d", name, typ.NumIn()-1, len(args))
|
||||||
|
}
|
||||||
|
} else if numIn != typ.NumIn() {
|
||||||
|
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
|
||||||
|
}
|
||||||
|
if !goodFunc(typ) {
|
||||||
|
// TODO: This could still be a confusing error; maybe goodFunc should provide info.
|
||||||
|
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
|
||||||
|
}
|
||||||
|
// Build the arg list.
|
||||||
|
argv := make([]reflect.Value, numIn)
|
||||||
|
// Args must be evaluated. Fixed args first.
|
||||||
|
i := len(first)
|
||||||
|
for ; i < numFixed && i < len(args)+numFirst; i++ {
|
||||||
|
argv[i] = s.evalArg(dot, typ.In(i), args[i-numFirst])
|
||||||
|
}
|
||||||
|
// Now the ... args.
|
||||||
|
if typ.IsVariadic() {
|
||||||
|
argType := typ.In(typ.NumIn() - 1).Elem() // Argument is a slice.
|
||||||
|
for ; i < len(args)+numFirst; i++ {
|
||||||
|
argv[i] = s.evalArg(dot, argType, args[i-numFirst])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// Add final value if necessary.
|
||||||
|
if final != missingVal {
|
||||||
|
t := typ.In(typ.NumIn() - 1)
|
||||||
|
if typ.IsVariadic() {
|
||||||
|
if numIn-1 < numFixed {
|
||||||
|
// The added final argument corresponds to a fixed parameter of the function.
|
||||||
|
// Validate against the type of the actual parameter.
|
||||||
|
t = typ.In(numIn - 1)
|
||||||
|
} else {
|
||||||
|
// The added final argument corresponds to the variadic part.
|
||||||
|
// Validate against the type of the elements of the variadic slice.
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argv[i] = s.validateType(final, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Added for Hugo
|
||||||
|
for i := 0; i < len(first); i++ {
|
||||||
|
argv[i] = s.validateType(first[i], typ.In(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := safeCall(fun, argv)
|
||||||
|
// If we have an error that is not nil, stop execution and return that
|
||||||
|
// error to the caller.
|
||||||
|
if err != nil {
|
||||||
|
s.at(node)
|
||||||
|
s.errorf("error calling %s: %v", name, err)
|
||||||
|
}
|
||||||
|
if v.Type() == reflectValueType {
|
||||||
|
v = v.Interface().(reflect.Value)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
|
@ -27,10 +27,18 @@ type TestStruct struct {
|
||||||
M map[string]string
|
M map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t TestStruct) Hello1(arg string) string {
|
||||||
|
return arg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TestStruct) Hello2(arg1, arg2 string) string {
|
||||||
|
return arg1 + " " + arg2
|
||||||
|
}
|
||||||
|
|
||||||
type execHelper struct {
|
type execHelper struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *execHelper) GetFunc(name string) (reflect.Value, bool) {
|
func (e *execHelper) GetFunc(tmpl Preparer, name string) (reflect.Value, bool) {
|
||||||
if name == "print" {
|
if name == "print" {
|
||||||
return zero, false
|
return zero, false
|
||||||
}
|
}
|
||||||
|
@ -39,11 +47,19 @@ func (e *execHelper) GetFunc(name string) (reflect.Value, bool) {
|
||||||
}), true
|
}), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *execHelper) GetMapValue(m, key reflect.Value) (reflect.Value, bool) {
|
func (e *execHelper) GetMapValue(tmpl Preparer, m, key reflect.Value) (reflect.Value, bool) {
|
||||||
key = reflect.ValueOf(strings.ToLower(key.String()))
|
key = reflect.ValueOf(strings.ToLower(key.String()))
|
||||||
return m.MapIndex(key), true
|
return m.MapIndex(key), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *execHelper) GetMethod(tmpl Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) {
|
||||||
|
if name != "Hello1" {
|
||||||
|
return zero, zero
|
||||||
|
}
|
||||||
|
m := receiver.MethodByName("Hello2")
|
||||||
|
return m, reflect.ValueOf("v2")
|
||||||
|
}
|
||||||
|
|
||||||
func TestTemplateExecutor(t *testing.T) {
|
func TestTemplateExecutor(t *testing.T) {
|
||||||
c := qt.New(t)
|
c := qt.New(t)
|
||||||
|
|
||||||
|
@ -51,6 +67,7 @@ func TestTemplateExecutor(t *testing.T) {
|
||||||
{{ print "foo" }}
|
{{ print "foo" }}
|
||||||
{{ printf "hugo" }}
|
{{ printf "hugo" }}
|
||||||
Map: {{ .M.A }}
|
Map: {{ .M.A }}
|
||||||
|
Method: {{ .Hello1 "v1" }}
|
||||||
|
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
@ -67,5 +84,6 @@ Map: {{ .M.A }}
|
||||||
c.Assert(got, qt.Contains, "foo")
|
c.Assert(got, qt.Contains, "foo")
|
||||||
c.Assert(got, qt.Contains, "hello hugo")
|
c.Assert(got, qt.Contains, "hello hugo")
|
||||||
c.Assert(got, qt.Contains, "Map: av")
|
c.Assert(got, qt.Contains, "Map: av")
|
||||||
|
c.Assert(got, qt.Contains, "Method: v2 v1")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,9 +116,9 @@ func (ns *Namespace) Include(name string, contextList ...interface{}) (interface
|
||||||
return "", fmt.Errorf("partial %q not found", name)
|
return "", fmt.Errorf("partial %q not found", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
var info tpl.Info
|
var info tpl.ParseInfo
|
||||||
if ip, ok := templ.(tpl.TemplateInfoProvider); ok {
|
if ip, ok := templ.(tpl.Info); ok {
|
||||||
info = ip.TemplateInfo()
|
info = ip.ParseInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
var w io.Writer
|
var w io.Writer
|
||||||
|
|
|
@ -24,8 +24,6 @@ import (
|
||||||
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ TemplateInfoProvider = (*TemplateInfo)(nil)
|
|
||||||
|
|
||||||
// TemplateManager manages the collection of templates.
|
// TemplateManager manages the collection of templates.
|
||||||
type TemplateManager interface {
|
type TemplateManager interface {
|
||||||
TemplateHandler
|
TemplateHandler
|
||||||
|
@ -34,7 +32,6 @@ type TemplateManager interface {
|
||||||
AddLateTemplate(name, tpl string) error
|
AddLateTemplate(name, tpl string) error
|
||||||
LoadTemplates(prefix string) error
|
LoadTemplates(prefix string) error
|
||||||
|
|
||||||
MarkReady() error
|
|
||||||
RebuildClone()
|
RebuildClone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,11 +77,6 @@ type Template interface {
|
||||||
Prepare() (*texttemplate.Template, error)
|
Prepare() (*texttemplate.Template, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateInfoProvider provides some contextual information about a template.
|
|
||||||
type TemplateInfoProvider interface {
|
|
||||||
TemplateInfo() Info
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
|
// TemplateParser is used to parse ad-hoc templates, e.g. in the Resource chain.
|
||||||
type TemplateParser interface {
|
type TemplateParser interface {
|
||||||
Parse(name, tpl string) (Template, error)
|
Parse(name, tpl string) (Template, error)
|
||||||
|
@ -101,10 +93,31 @@ type TemplateDebugger interface {
|
||||||
Debug()
|
Debug()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TemplateInfo wraps a Template with some additional information.
|
// templateInfo wraps a Template with some additional information.
|
||||||
type TemplateInfo struct {
|
type templateInfo struct {
|
||||||
Template
|
Template
|
||||||
Info Info
|
Info
|
||||||
|
}
|
||||||
|
|
||||||
|
// templateInfo wraps a Template with some additional information.
|
||||||
|
type templateInfoManager struct {
|
||||||
|
Template
|
||||||
|
InfoManager
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInfo wraps the info in a template.
|
||||||
|
func WithInfo(templ Template, info Info) Template {
|
||||||
|
if manager, ok := info.(InfoManager); ok {
|
||||||
|
return &templateInfoManager{
|
||||||
|
Template: templ,
|
||||||
|
InfoManager: manager,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &templateInfo{
|
||||||
|
Template: templ,
|
||||||
|
Info: info,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var baseOfRe = regexp.MustCompile("template: (.*?):")
|
var baseOfRe = regexp.MustCompile("template: (.*?):")
|
||||||
|
@ -117,10 +130,6 @@ func extractBaseOf(err string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TemplateInfo) TemplateInfo() Info {
|
|
||||||
return t.Info
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateFuncGetter allows to find a template func by name.
|
// TemplateFuncGetter allows to find a template func by name.
|
||||||
type TemplateFuncGetter interface {
|
type TemplateFuncGetter interface {
|
||||||
GetFunc(name string) (reflect.Value, bool)
|
GetFunc(name string) (reflect.Value, bool)
|
||||||
|
|
|
@ -13,12 +13,44 @@
|
||||||
|
|
||||||
package tpl
|
package tpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
)
|
||||||
|
|
||||||
// Increments on breaking changes.
|
// Increments on breaking changes.
|
||||||
const TemplateVersion = 2
|
const TemplateVersion = 2
|
||||||
|
|
||||||
// Info holds some info extracted from a parsed template.
|
type Info interface {
|
||||||
type Info struct {
|
ParseInfo() ParseInfo
|
||||||
|
|
||||||
|
// Identifies this template and its dependencies.
|
||||||
|
identity.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
type InfoManager interface {
|
||||||
|
ParseInfo() ParseInfo
|
||||||
|
|
||||||
|
// Identifies and manages this template and its dependencies.
|
||||||
|
identity.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultInfo struct {
|
||||||
|
identity.Manager
|
||||||
|
parseInfo ParseInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInfo(id identity.Manager, parseInfo ParseInfo) Info {
|
||||||
|
return &defaultInfo{
|
||||||
|
Manager: id,
|
||||||
|
parseInfo: parseInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (info *defaultInfo) ParseInfo() ParseInfo {
|
||||||
|
return info.parseInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParseInfo struct {
|
||||||
// Set for shortcode templates with any {{ .Inner }}
|
// Set for shortcode templates with any {{ .Inner }}
|
||||||
IsInner bool
|
IsInner bool
|
||||||
|
|
||||||
|
@ -26,17 +58,25 @@ type Info struct {
|
||||||
HasReturn bool
|
HasReturn bool
|
||||||
|
|
||||||
// Config extracted from template.
|
// Config extracted from template.
|
||||||
Config Config
|
Config ParseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (info Info) IsZero() bool {
|
func (info ParseInfo) IsZero() bool {
|
||||||
return info.Config.Version == 0
|
return info.Config.Version == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
// Info holds some info extracted from a parsed template.
|
||||||
|
type Info1 struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParseConfig struct {
|
||||||
Version int
|
Version int
|
||||||
}
|
}
|
||||||
|
|
||||||
var DefaultConfig = Config{
|
var DefaultParseConfig = ParseConfig{
|
||||||
Version: TemplateVersion,
|
Version: TemplateVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var DefaultParseInfo = ParseInfo{
|
||||||
|
Config: DefaultParseConfig,
|
||||||
|
}
|
||||||
|
|
|
@ -83,10 +83,12 @@ func (s *shortcodeTemplates) fromVariantsSlice(variants []string) (shortcodeVari
|
||||||
func (s *shortcodeTemplates) compareVariants(a, b []string) int {
|
func (s *shortcodeTemplates) compareVariants(a, b []string) int {
|
||||||
|
|
||||||
weight := 0
|
weight := 0
|
||||||
|
k := len(a)
|
||||||
for i, av := range a {
|
for i, av := range a {
|
||||||
bv := b[i]
|
bv := b[i]
|
||||||
if av == bv {
|
if av == bv {
|
||||||
weight++
|
// Add more weight to the left side (language...).
|
||||||
|
weight = weight + k - i
|
||||||
} else {
|
} else {
|
||||||
weight--
|
weight--
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,10 +53,10 @@ func TestShortcodesTemplate(t *testing.T) {
|
||||||
name2 string
|
name2 string
|
||||||
expected int
|
expected int
|
||||||
}{
|
}{
|
||||||
{"Same suffix", "figure.html", "figure.html", 3},
|
{"Same suffix", "figure.html", "figure.html", 6},
|
||||||
{"Same suffix and output format", "figure.html.html", "figure.html.html", 3},
|
{"Same suffix and output format", "figure.html.html", "figure.html.html", 6},
|
||||||
{"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 3},
|
{"Same suffix, output format and language", "figure.no.html.html", "figure.no.html.html", 6},
|
||||||
{"No suffix", "figure", "figure", 3},
|
{"No suffix", "figure", "figure", 6},
|
||||||
{"Different output format", "figure.amp.html", "figure.html.html", -1},
|
{"Different output format", "figure.amp.html", "figure.html.html", -1},
|
||||||
{"One with output format, one without", "figure.amp.html", "figure.html", -1},
|
{"One with output format, one without", "figure.amp.html", "figure.html", -1},
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,10 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/hugofs/files"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/common/herrors"
|
"github.com/gohugoio/hugo/common/herrors"
|
||||||
|
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -27,7 +31,6 @@ import (
|
||||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||||
|
|
||||||
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/hugofs"
|
"github.com/gohugoio/hugo/hugofs"
|
||||||
"github.com/gohugoio/hugo/tpl/tplimpl/embedded"
|
"github.com/gohugoio/hugo/tpl/tplimpl/embedded"
|
||||||
|
@ -81,6 +84,7 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler {
|
||||||
common := &templatesCommon{
|
common := &templatesCommon{
|
||||||
nameBaseTemplateName: make(map[string]string),
|
nameBaseTemplateName: make(map[string]string),
|
||||||
transformNotFound: make(map[string]bool),
|
transformNotFound: make(map[string]bool),
|
||||||
|
identityNotFound: make(map[string][]identity.Manager),
|
||||||
}
|
}
|
||||||
|
|
||||||
htmlT := &htmlTemplates{
|
htmlT := &htmlTemplates{
|
||||||
|
@ -100,13 +104,16 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler {
|
||||||
Deps: deps,
|
Deps: deps,
|
||||||
layoutsFs: deps.BaseFs.Layouts.Fs,
|
layoutsFs: deps.BaseFs.Layouts.Fs,
|
||||||
templateHandlerCommon: &templateHandlerCommon{
|
templateHandlerCommon: &templateHandlerCommon{
|
||||||
shortcodes: make(map[string]*shortcodeTemplates),
|
shortcodes: make(map[string]*shortcodeTemplates),
|
||||||
templateInfo: make(map[string]tpl.Info),
|
templateInfo: make(map[string]tpl.Info),
|
||||||
html: htmlT,
|
templateInfoTree: make(map[string]*templateInfoTree),
|
||||||
text: textT,
|
html: htmlT,
|
||||||
|
text: textT,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
textT.textTemplate.templates = textT
|
||||||
|
textT.standalone.templates = textT
|
||||||
common.handler = h
|
common.handler = h
|
||||||
|
|
||||||
return h
|
return h
|
||||||
|
@ -152,27 +159,26 @@ func (t *htmlTemplates) addTemplate(name, tpl string) (*templateContext, error)
|
||||||
return t.addTemplateIn(t.t, name, tpl)
|
return t.addTemplateIn(t.t, name, tpl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) (*templateContext, error) {
|
func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, templstr string) (*templateContext, error) {
|
||||||
templ, err := tt.New(name).Parse(tpl)
|
templ, err := tt.New(name).Parse(templstr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
typ := resolveTemplateType(name)
|
typ := resolveTemplateType(name)
|
||||||
|
|
||||||
c, err := applyTemplateTransformersToHMLTTemplate(typ, templ)
|
c, err := t.handler.applyTemplateTransformersToHMLTTemplate(typ, templ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range c.notFound {
|
for k := range c.templateNotFound {
|
||||||
t.transformNotFound[k] = true
|
t.transformNotFound[k] = true
|
||||||
|
t.identityNotFound[k] = append(t.identityNotFound[k], c.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if typ == templateShortcode {
|
for k := range c.identityNotFound {
|
||||||
t.handler.addShortcodeVariant(name, c.Info, templ)
|
t.identityNotFound[k] = append(t.identityNotFound[k], c.id)
|
||||||
} else {
|
|
||||||
t.handler.templateInfo[name] = c.Info
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
|
@ -208,7 +214,7 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin
|
||||||
// * https://github.com/golang/go/issues/16101
|
// * https://github.com/golang/go/issues/16101
|
||||||
// * https://github.com/gohugoio/hugo/issues/2549
|
// * https://github.com/gohugoio/hugo/issues/2549
|
||||||
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
|
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
|
||||||
if _, err := applyTemplateTransformersToHMLTTemplate(templateUndefined, overlayTpl); err != nil {
|
if _, err := t.handler.applyTemplateTransformersToHMLTTemplate(templateUndefined, overlayTpl); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,6 +259,8 @@ func (l nopLookupVariant) LookupVariant(name string, variants tpl.TemplateVarian
|
||||||
// It implements the templateLoader and tpl.TemplateHandler interfaces.
|
// It implements the templateLoader and tpl.TemplateHandler interfaces.
|
||||||
// There is one templateHandler created per Site.
|
// There is one templateHandler created per Site.
|
||||||
type templateHandler struct {
|
type templateHandler struct {
|
||||||
|
ready bool
|
||||||
|
|
||||||
executor texttemplate.Executer
|
executor texttemplate.Executer
|
||||||
funcs map[string]reflect.Value
|
funcs map[string]reflect.Value
|
||||||
|
|
||||||
|
@ -324,6 +332,7 @@ func (t *templateHandler) LoadTemplates(prefix string) error {
|
||||||
// Lookup tries to find a template with the given name in both template
|
// Lookup tries to find a template with the given name in both template
|
||||||
// collections: First HTML, then the plain text template collection.
|
// collections: First HTML, then the plain text template collection.
|
||||||
func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
|
func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
|
||||||
|
|
||||||
if strings.HasPrefix(name, textTmplNamePrefix) {
|
if strings.HasPrefix(name, textTmplNamePrefix) {
|
||||||
// The caller has explicitly asked for a text template, so only look
|
// The caller has explicitly asked for a text template, so only look
|
||||||
// in the text template collection.
|
// in the text template collection.
|
||||||
|
@ -345,6 +354,9 @@ func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
|
||||||
// This currently only applies to shortcodes and what we get here is the
|
// This currently only applies to shortcodes and what we get here is the
|
||||||
// shortcode name.
|
// shortcode name.
|
||||||
func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
|
func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVariants) (tpl.Template, bool, bool) {
|
||||||
|
if !t.ready {
|
||||||
|
panic("handler not ready")
|
||||||
|
}
|
||||||
name = templateBaseName(templateShortcode, name)
|
name = templateBaseName(templateShortcode, name)
|
||||||
s, found := t.shortcodes[name]
|
s, found := t.shortcodes[name]
|
||||||
if !found {
|
if !found {
|
||||||
|
@ -358,18 +370,17 @@ func (t *templateHandler) LookupVariant(name string, variants tpl.TemplateVarian
|
||||||
|
|
||||||
more := len(s.variants) > 1
|
more := len(s.variants) > 1
|
||||||
|
|
||||||
return &tpl.TemplateInfo{
|
return tpl.WithInfo(sv.templ, sv.info), true, more
|
||||||
Template: sv.templ,
|
|
||||||
Info: sv.info,
|
|
||||||
}, true, more
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarkReady marks the templates as "ready for execution". No changes allowed
|
// markReady marks the templates as "ready for execution". No changes allowed
|
||||||
// after this is set.
|
// after this is set.
|
||||||
// TODO(bep) if this proves to be resource heavy, we could detect
|
func (t *templateHandler) markReady() error {
|
||||||
// earlier if we really need this, or make it lazy.
|
defer func() {
|
||||||
func (t *templateHandler) MarkReady() error {
|
t.ready = true
|
||||||
|
}()
|
||||||
|
|
||||||
if err := t.postTransform(); err != nil {
|
if err := t.postTransform(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -483,6 +494,7 @@ func (t *templateHandler) addInternalTemplate(name, tpl string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) {
|
func (t *templateHandler) addShortcodeVariant(name string, info tpl.Info, templ tpl.Template) {
|
||||||
|
|
||||||
base := templateBaseName(templateShortcode, name)
|
base := templateBaseName(templateShortcode, name)
|
||||||
|
|
||||||
shortcodename, variants := templateNameAndVariants(base)
|
shortcodename, variants := templateNameAndVariants(base)
|
||||||
|
@ -561,18 +573,9 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templateHandler) applyTemplateInfo(templ tpl.Template, found bool) (tpl.Template, bool) {
|
func (t *templateHandler) applyTemplateInfo(templ tpl.Template, found bool) (tpl.Template, bool) {
|
||||||
if adapter, ok := templ.(*tpl.TemplateInfo); ok {
|
if templ != nil {
|
||||||
if adapter.Info.IsZero() {
|
|
||||||
if info, found := t.templateInfo[templ.Name()]; found {
|
|
||||||
adapter.Info = info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if templ != nil {
|
|
||||||
if info, found := t.templateInfo[templ.Name()]; found {
|
if info, found := t.templateInfo[templ.Name()]; found {
|
||||||
return &tpl.TemplateInfo{
|
return tpl.WithInfo(templ, info), true
|
||||||
Template: templ,
|
|
||||||
Info: info,
|
|
||||||
}, true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -586,7 +589,11 @@ func (t *templateHandler) checkState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
|
func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
|
||||||
|
if !t.ready {
|
||||||
|
panic("invalid state")
|
||||||
|
}
|
||||||
c := &templateHandler{
|
c := &templateHandler{
|
||||||
|
ready: true,
|
||||||
Deps: d,
|
Deps: d,
|
||||||
layoutsFs: d.BaseFs.Layouts.Fs,
|
layoutsFs: d.BaseFs.Layouts.Fs,
|
||||||
}
|
}
|
||||||
|
@ -703,36 +710,69 @@ func (t *templateHandler) loadTemplates(prefix string) error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templateHandler) postTransform() error {
|
func (t *templateHandler) getOrCreateTemplateInfo(name string) (identity.Manager, tpl.ParseInfo) {
|
||||||
if len(t.html.transformNotFound) == 0 && len(t.text.transformNotFound) == 0 {
|
info, found := t.templateInfo[name]
|
||||||
return nil
|
if found {
|
||||||
|
return info.(identity.Manager), info.ParseInfo()
|
||||||
|
}
|
||||||
|
return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), tpl.DefaultParseInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *templateHandler) createTemplateInfo(name string) (identity.Manager, tpl.ParseInfo) {
|
||||||
|
_, found := t.templateInfo[name]
|
||||||
|
if found {
|
||||||
|
panic("already created: " + name)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
return identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), tpl.DefaultParseInfo
|
||||||
t.text.transformNotFound = make(map[string]bool)
|
}
|
||||||
t.html.transformNotFound = make(map[string]bool)
|
|
||||||
}()
|
func (t *templateHandler) postTransform() error {
|
||||||
|
for k, v := range t.templateInfoTree {
|
||||||
|
if v.id != nil {
|
||||||
|
info := tpl.NewInfo(
|
||||||
|
v.id,
|
||||||
|
v.info,
|
||||||
|
)
|
||||||
|
t.templateInfo[k] = info
|
||||||
|
|
||||||
|
if v.typ == templateShortcode {
|
||||||
|
t.addShortcodeVariant(k, info, v.templ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, s := range []struct {
|
for _, s := range []struct {
|
||||||
lookup func(name string) *parse.Tree
|
lookup func(name string) *templateInfoTree
|
||||||
transformNotFound map[string]bool
|
transformNotFound map[string]bool
|
||||||
|
identityNotFound map[string][]identity.Manager
|
||||||
}{
|
}{
|
||||||
// html templates
|
// html templates
|
||||||
{func(name string) *parse.Tree {
|
{func(name string) *templateInfoTree {
|
||||||
templ := t.html.lookup(name)
|
templ := t.html.lookup(name)
|
||||||
if templ == nil {
|
if templ == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return templ.Tree
|
id, info := t.getOrCreateTemplateInfo(name)
|
||||||
}, t.html.transformNotFound},
|
return &templateInfoTree{
|
||||||
|
id: id,
|
||||||
|
info: info,
|
||||||
|
tree: templ.Tree,
|
||||||
|
}
|
||||||
|
}, t.html.transformNotFound, t.html.identityNotFound},
|
||||||
// text templates
|
// text templates
|
||||||
{func(name string) *parse.Tree {
|
{func(name string) *templateInfoTree {
|
||||||
templT := t.text.lookup(name)
|
templT := t.text.lookup(name)
|
||||||
if templT == nil {
|
if templT == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return templT.Tree
|
id, info := t.getOrCreateTemplateInfo(name)
|
||||||
}, t.text.transformNotFound},
|
return &templateInfoTree{
|
||||||
|
id: id,
|
||||||
|
info: info,
|
||||||
|
tree: templT.Tree,
|
||||||
|
}
|
||||||
|
}, t.text.transformNotFound, t.text.identityNotFound},
|
||||||
} {
|
} {
|
||||||
for name := range s.transformNotFound {
|
for name := range s.transformNotFound {
|
||||||
templ := s.lookup(name)
|
templ := s.lookup(name)
|
||||||
|
@ -743,6 +783,15 @@ func (t *templateHandler) postTransform() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k, v := range s.identityNotFound {
|
||||||
|
tmpl := s.lookup(k)
|
||||||
|
if tmpl != nil {
|
||||||
|
for _, im := range v {
|
||||||
|
im.Add(tmpl.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -758,7 +807,6 @@ func (t *templateHandler) wrapTextTemplate(tt *textTemplate) tpl.TemplateParseFi
|
||||||
tt,
|
tt,
|
||||||
new(nopLookupVariant),
|
new(nopLookupVariant),
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type templateHandlerCommon struct {
|
type templateHandlerCommon struct {
|
||||||
|
@ -771,6 +819,9 @@ type templateHandlerCommon struct {
|
||||||
// shortcodeTemplates type.
|
// shortcodeTemplates type.
|
||||||
templateInfo map[string]tpl.Info
|
templateInfo map[string]tpl.Info
|
||||||
|
|
||||||
|
// Used to track templates during the AST transformations.
|
||||||
|
templateInfoTree map[string]*templateInfoTree
|
||||||
|
|
||||||
// text holds all the pure text templates.
|
// text holds all the pure text templates.
|
||||||
text *textTemplates
|
text *textTemplates
|
||||||
html *htmlTemplates
|
html *htmlTemplates
|
||||||
|
@ -795,9 +846,12 @@ type templatesCommon struct {
|
||||||
// Used to get proper filenames in errors
|
// Used to get proper filenames in errors
|
||||||
nameBaseTemplateName map[string]string
|
nameBaseTemplateName map[string]string
|
||||||
|
|
||||||
// Holds names of the templates not found during the first AST transformation
|
// Holds names of the template definitions not found during the first AST transformation
|
||||||
// pass.
|
// pass.
|
||||||
transformNotFound map[string]bool
|
transformNotFound map[string]bool
|
||||||
|
|
||||||
|
// Holds identities of templates not found during first pass.
|
||||||
|
identityNotFound map[string][]identity.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t templatesCommon) withNewHandler(h *templateHandler) *templatesCommon {
|
func (t templatesCommon) withNewHandler(h *templateHandler) *templatesCommon {
|
||||||
|
@ -806,8 +860,9 @@ func (t templatesCommon) withNewHandler(h *templateHandler) *templatesCommon {
|
||||||
}
|
}
|
||||||
|
|
||||||
type textTemplate struct {
|
type textTemplate struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
t *texttemplate.Template
|
t *texttemplate.Template
|
||||||
|
templates *textTemplates
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *textTemplate) Lookup(name string) (tpl.Template, bool) {
|
func (t *textTemplate) Lookup(name string) (tpl.Template, bool) {
|
||||||
|
@ -831,7 +886,7 @@ func (t *textTemplate) parseIn(tt *texttemplate.Template, name, tpl string) (*te
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil {
|
if _, err := t.templates.handler.applyTemplateTransformersToTextTemplate(templateUndefined, templ); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return templ, nil
|
return templ, nil
|
||||||
|
@ -868,30 +923,24 @@ func (t *textTemplates) addTemplate(name, tpl string) (*templateContext, error)
|
||||||
return t.addTemplateIn(t.t, name, tpl)
|
return t.addTemplateIn(t.t, name, tpl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl string) (*templateContext, error) {
|
func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tplstr string) (*templateContext, error) {
|
||||||
name = strings.TrimPrefix(name, textTmplNamePrefix)
|
name = strings.TrimPrefix(name, textTmplNamePrefix)
|
||||||
templ, err := t.parseIn(tt, name, tpl)
|
templ, err := t.parseIn(tt, name, tplstr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
typ := resolveTemplateType(name)
|
typ := resolveTemplateType(name)
|
||||||
|
|
||||||
c, err := applyTemplateTransformersToTextTemplate(typ, templ)
|
c, err := t.handler.applyTemplateTransformersToTextTemplate(typ, templ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for k := range c.notFound {
|
for k := range c.templateNotFound {
|
||||||
t.transformNotFound[k] = true
|
t.transformNotFound[k] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if typ == templateShortcode {
|
|
||||||
t.handler.addShortcodeVariant(name, c.Info, templ)
|
|
||||||
} else {
|
|
||||||
t.handler.templateInfo[name] = c.Info
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -924,7 +973,7 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin
|
||||||
}
|
}
|
||||||
|
|
||||||
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
|
overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
|
||||||
if _, err := applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil {
|
if _, err := t.handler.applyTemplateTransformersToTextTemplate(templateUndefined, overlayTpl); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
t.overlays[name] = overlayTpl
|
t.overlays[name] = overlayTpl
|
||||||
|
|
|
@ -44,16 +44,13 @@ func (*TemplateProvider) Update(deps *deps.Deps) error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return newTmpl.MarkReady()
|
return newTmpl.markReady()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone clones.
|
// Clone clones.
|
||||||
func (*TemplateProvider) Clone(d *deps.Deps) error {
|
func (*TemplateProvider) Clone(d *deps.Deps) error {
|
||||||
|
|
||||||
t := d.Tmpl.(*templateHandler)
|
t := d.Tmpl.(*templateHandler)
|
||||||
clone := t.clone(d)
|
t.clone(d)
|
||||||
|
return nil
|
||||||
return clone.MarkReady()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,12 @@
|
||||||
package tplimpl
|
package tplimpl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
|
||||||
|
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||||
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
||||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||||
|
|
||||||
|
@ -34,9 +38,10 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type templateContext struct {
|
type templateContext struct {
|
||||||
visited map[string]bool
|
visited map[string]bool
|
||||||
notFound map[string]bool
|
templateNotFound map[string]bool
|
||||||
lookupFn func(name string) *parse.Tree
|
identityNotFound map[string]bool
|
||||||
|
lookupFn func(name string) *templateInfoTree
|
||||||
|
|
||||||
// The last error encountered.
|
// The last error encountered.
|
||||||
err error
|
err error
|
||||||
|
@ -47,13 +52,14 @@ type templateContext struct {
|
||||||
configChecked bool
|
configChecked bool
|
||||||
|
|
||||||
// Contains some info about the template
|
// Contains some info about the template
|
||||||
tpl.Info
|
parseInfo *tpl.ParseInfo
|
||||||
|
id identity.Manager
|
||||||
|
|
||||||
// Store away the return node in partials.
|
// Store away the return node in partials.
|
||||||
returnNode *parse.CommandNode
|
returnNode *parse.CommandNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c templateContext) getIfNotVisited(name string) *parse.Tree {
|
func (c templateContext) getIfNotVisited(name string) *templateInfoTree {
|
||||||
if c.visited[name] {
|
if c.visited[name] {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -63,59 +69,95 @@ func (c templateContext) getIfNotVisited(name string) *parse.Tree {
|
||||||
// This may be a inline template defined outside of this file
|
// This may be a inline template defined outside of this file
|
||||||
// and not yet parsed. Unusual, but it happens.
|
// and not yet parsed. Unusual, but it happens.
|
||||||
// Store the name to try again later.
|
// Store the name to try again later.
|
||||||
c.notFound[name] = true
|
c.templateNotFound[name] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return templ
|
return templ
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTemplateContext(lookupFn func(name string) *parse.Tree) *templateContext {
|
func newTemplateContext(
|
||||||
return &templateContext{
|
id identity.Manager,
|
||||||
Info: tpl.Info{Config: tpl.DefaultConfig},
|
info *tpl.ParseInfo,
|
||||||
lookupFn: lookupFn,
|
lookupFn func(name string) *templateInfoTree) *templateContext {
|
||||||
visited: make(map[string]bool),
|
|
||||||
notFound: make(map[string]bool)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree {
|
return &templateContext{
|
||||||
return func(nn string) *parse.Tree {
|
id: id,
|
||||||
tt := templ.Lookup(nn)
|
parseInfo: info,
|
||||||
if tt != nil {
|
lookupFn: lookupFn,
|
||||||
return tt.Tree
|
visited: make(map[string]bool),
|
||||||
}
|
templateNotFound: make(map[string]bool),
|
||||||
return nil
|
identityNotFound: make(map[string]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) {
|
func createGetTemplateInfoTreeFor(getID func(name string) *templateInfoTree) func(nn string) *templateInfoTree {
|
||||||
return applyTemplateTransformers(typ, templ.Tree, createParseTreeLookup(templ))
|
return func(nn string) *templateInfoTree {
|
||||||
|
return getID(nn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) {
|
func (t *templateHandler) applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) {
|
||||||
return applyTemplateTransformers(typ, templ.Tree,
|
id, info := t.createTemplateInfo(templ.Name())
|
||||||
func(nn string) *parse.Tree {
|
ti := &templateInfoTree{
|
||||||
tt := templ.Lookup(nn)
|
tree: templ.Tree,
|
||||||
if tt != nil {
|
templ: templ,
|
||||||
return tt.Tree
|
typ: typ,
|
||||||
}
|
id: id,
|
||||||
return nil
|
info: info,
|
||||||
})
|
}
|
||||||
|
t.templateInfoTree[templ.Name()] = ti
|
||||||
|
getTemplateInfoTree := createGetTemplateInfoTreeFor(func(name string) *templateInfoTree {
|
||||||
|
return t.templateInfoTree[name]
|
||||||
|
})
|
||||||
|
|
||||||
|
return applyTemplateTransformers(typ, ti, getTemplateInfoTree)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyTemplateTransformers(typ templateType, templ *parse.Tree, lookupFn func(name string) *parse.Tree) (*templateContext, error) {
|
func (t *templateHandler) applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) {
|
||||||
|
id, info := t.createTemplateInfo(templ.Name())
|
||||||
|
ti := &templateInfoTree{
|
||||||
|
tree: templ.Tree,
|
||||||
|
templ: templ,
|
||||||
|
typ: typ,
|
||||||
|
id: id,
|
||||||
|
info: info,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.templateInfoTree[templ.Name()] = ti
|
||||||
|
getTemplateInfoTree := createGetTemplateInfoTreeFor(func(name string) *templateInfoTree {
|
||||||
|
return t.templateInfoTree[name]
|
||||||
|
})
|
||||||
|
|
||||||
|
return applyTemplateTransformers(typ, ti, getTemplateInfoTree)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type templateInfoTree struct {
|
||||||
|
info tpl.ParseInfo
|
||||||
|
typ templateType
|
||||||
|
id identity.Manager
|
||||||
|
templ tpl.Template
|
||||||
|
tree *parse.Tree
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyTemplateTransformers(
|
||||||
|
typ templateType,
|
||||||
|
templ *templateInfoTree,
|
||||||
|
lookupFn func(name string) *templateInfoTree) (*templateContext, error) {
|
||||||
|
|
||||||
if templ == nil {
|
if templ == nil {
|
||||||
return nil, errors.New("expected template, but none provided")
|
return nil, errors.New("expected template, but none provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
c := newTemplateContext(lookupFn)
|
c := newTemplateContext(templ.id, &templ.info, lookupFn)
|
||||||
c.typ = typ
|
c.typ = typ
|
||||||
|
|
||||||
_, err := c.applyTransformations(templ.Root)
|
_, err := c.applyTransformations(templ.tree.Root)
|
||||||
|
|
||||||
if err == nil && c.returnNode != nil {
|
if err == nil && c.returnNode != nil {
|
||||||
// This is a partial with a return statement.
|
// This is a partial with a return statement.
|
||||||
c.Info.HasReturn = true
|
c.parseInfo.HasReturn = true
|
||||||
templ.Root = c.wrapInPartialReturnWrapper(templ.Root)
|
templ.tree.Root = c.wrapInPartialReturnWrapper(templ.tree.Root)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c, err
|
return c, err
|
||||||
|
@ -125,7 +167,9 @@ const (
|
||||||
partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ with .Arg }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
|
partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ with .Arg }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
|
||||||
)
|
)
|
||||||
|
|
||||||
var partialReturnWrapper *parse.ListNode
|
var (
|
||||||
|
partialReturnWrapper *parse.ListNode
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
|
templ, err := texttemplate.New("").Parse(partialReturnWrapperTempl)
|
||||||
|
@ -133,6 +177,7 @@ func init() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
partialReturnWrapper = templ.Tree.Root
|
partialReturnWrapper = templ.Tree.Root
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
|
func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.ListNode {
|
||||||
|
@ -156,6 +201,7 @@ func (c *templateContext) wrapInPartialReturnWrapper(n *parse.ListNode) *parse.L
|
||||||
// getif works slightly different than the Go built-in in that it also
|
// getif works slightly different than the Go built-in in that it also
|
||||||
// considers any IsZero methods on the values (as in time.Time).
|
// considers any IsZero methods on the values (as in time.Time).
|
||||||
// See https://github.com/gohugoio/hugo/issues/5738
|
// See https://github.com/gohugoio/hugo/issues/5738
|
||||||
|
// TODO(bep) get rid of this.
|
||||||
func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) {
|
func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) {
|
||||||
if len(p.Cmds) == 0 {
|
if len(p.Cmds) == 0 {
|
||||||
return
|
return
|
||||||
|
@ -176,9 +222,9 @@ func (c *templateContext) wrapWithGetIf(p *parse.PipeNode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyTransformations do 3 things:
|
// applyTransformations do 3 things:
|
||||||
// 1) Make all .Params.CamelCase and similar into lowercase.
|
// 1) Wraps every with and if pipe in getif
|
||||||
// 2) Wraps every with and if pipe in getif
|
// 2) Parses partial return statement.
|
||||||
// 3) Collects some information about the template content.
|
// 3) Tracks template (partial) dependencies and some other info.
|
||||||
func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
|
func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
|
||||||
switch x := n.(type) {
|
switch x := n.(type) {
|
||||||
case *parse.ListNode:
|
case *parse.ListNode:
|
||||||
|
@ -198,7 +244,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
|
||||||
case *parse.TemplateNode:
|
case *parse.TemplateNode:
|
||||||
subTempl := c.getIfNotVisited(x.Name)
|
subTempl := c.getIfNotVisited(x.Name)
|
||||||
if subTempl != nil {
|
if subTempl != nil {
|
||||||
c.applyTransformationsToNodes(subTempl.Root)
|
c.applyTransformationsToNodes(subTempl.tree.Root)
|
||||||
}
|
}
|
||||||
case *parse.PipeNode:
|
case *parse.PipeNode:
|
||||||
c.collectConfig(x)
|
c.collectConfig(x)
|
||||||
|
@ -210,6 +256,7 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case *parse.CommandNode:
|
case *parse.CommandNode:
|
||||||
|
c.collectPartialInfo(x)
|
||||||
c.collectInner(x)
|
c.collectInner(x)
|
||||||
keep := c.collectReturnNode(x)
|
keep := c.collectReturnNode(x)
|
||||||
|
|
||||||
|
@ -277,11 +324,10 @@ func (c *templateContext) collectConfig(n *parse.PipeNode) {
|
||||||
c.err = errors.Wrap(err, errMsg)
|
c.err = errors.Wrap(err, errMsg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := mapstructure.WeakDecode(m, &c.Info.Config); err != nil {
|
if err := mapstructure.WeakDecode(m, &c.parseInfo.Config); err != nil {
|
||||||
c.err = errors.Wrap(err, errMsg)
|
c.err = errors.Wrap(err, errMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectInner determines if the given CommandNode represents a
|
// collectInner determines if the given CommandNode represents a
|
||||||
|
@ -290,7 +336,7 @@ func (c *templateContext) collectInner(n *parse.CommandNode) {
|
||||||
if c.typ != templateShortcode {
|
if c.typ != templateShortcode {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.Info.IsInner || len(n.Args) == 0 {
|
if c.parseInfo.IsInner || len(n.Args) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,13 +350,45 @@ func (c *templateContext) collectInner(n *parse.CommandNode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.hasIdent(idents, "Inner") {
|
if c.hasIdent(idents, "Inner") {
|
||||||
c.Info.IsInner = true
|
c.parseInfo.IsInner = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var partialRe = regexp.MustCompile(`^partial(Cached)?$|^partials\.Include(Cached)?$`)
|
||||||
|
|
||||||
|
func (c *templateContext) collectPartialInfo(x *parse.CommandNode) {
|
||||||
|
if len(x.Args) < 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
first := x.Args[0]
|
||||||
|
var id string
|
||||||
|
switch v := first.(type) {
|
||||||
|
case *parse.IdentifierNode:
|
||||||
|
id = v.Ident
|
||||||
|
case *parse.ChainNode:
|
||||||
|
id = v.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if partialRe.MatchString(id) {
|
||||||
|
partialName := strings.Trim(x.Args[1].String(), "\"")
|
||||||
|
if !strings.Contains(partialName, ".") {
|
||||||
|
partialName += ".html"
|
||||||
|
}
|
||||||
|
partialName = "partials/" + partialName
|
||||||
|
info := c.lookupFn(partialName)
|
||||||
|
if info != nil {
|
||||||
|
c.id.Add(info.id)
|
||||||
|
} else {
|
||||||
|
// Delay for later
|
||||||
|
c.identityNotFound[partialName] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
|
func (c *templateContext) collectReturnNode(n *parse.CommandNode) bool {
|
||||||
if c.typ != templatePartial || c.returnNode != nil {
|
if c.typ != templatePartial || c.returnNode != nil {
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -15,14 +15,17 @@ package tplimpl
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
"github.com/gohugoio/hugo/hugofs/files"
|
||||||
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/tpl"
|
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||||
|
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||||
|
|
||||||
qt "github.com/frankban/quicktest"
|
qt "github.com/frankban/quicktest"
|
||||||
|
"github.com/gohugoio/hugo/identity"
|
||||||
|
"github.com/gohugoio/hugo/tpl"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Issue #2927
|
// Issue #2927
|
||||||
|
@ -33,7 +36,7 @@ func TestTransformRecursiveTemplate(t *testing.T) {
|
||||||
{{ define "menu-nodes" }}
|
{{ define "menu-nodes" }}
|
||||||
{{ template "menu-node" }}
|
{{ template "menu-node" }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ define "menu-node" }}
|
{{ define "menu-nßode" }}
|
||||||
{{ template "menu-node" }}
|
{{ template "menu-node" }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ template "menu-nodes" }}
|
{{ template "menu-nodes" }}
|
||||||
|
@ -41,12 +44,25 @@ func TestTransformRecursiveTemplate(t *testing.T) {
|
||||||
|
|
||||||
templ, err := template.New("foo").Parse(recursive)
|
templ, err := template.New("foo").Parse(recursive)
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
parseInfo := tpl.DefaultParseInfo
|
||||||
|
|
||||||
ctx := newTemplateContext(createParseTreeLookup(templ))
|
ctx := newTemplateContext(
|
||||||
|
newTemplateInfo("test").(identity.Manager),
|
||||||
|
&parseInfo,
|
||||||
|
createGetTemplateInfoTree(templ.Tree),
|
||||||
|
)
|
||||||
ctx.applyTransformations(templ.Tree.Root)
|
ctx.applyTransformations(templ.Tree.Root)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createGetTemplateInfoTree(tree *parse.Tree) func(name string) *templateInfoTree {
|
||||||
|
return func(name string) *templateInfoTree {
|
||||||
|
return &templateInfoTree{
|
||||||
|
tree: tree,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type I interface {
|
type I interface {
|
||||||
Method0()
|
Method0()
|
||||||
}
|
}
|
||||||
|
@ -80,13 +96,10 @@ func TestInsertIsZeroFunc(t *testing.T) {
|
||||||
{{ with .TimeZero }}.TimeZero1 with: {{ . }}{{ else }}.TimeZero1 with: FALSE{{ end }}
|
{{ with .TimeZero }}.TimeZero1 with: {{ . }}{{ else }}.TimeZero1 with: FALSE{{ end }}
|
||||||
{{ template "mytemplate" . }}
|
{{ template "mytemplate" . }}
|
||||||
{{ if .T.NonEmptyInterfaceTypedNil }}.NonEmptyInterfaceTypedNil: TRUE{{ else }}.NonEmptyInterfaceTypedNil: FALSE{{ end }}
|
{{ if .T.NonEmptyInterfaceTypedNil }}.NonEmptyInterfaceTypedNil: TRUE{{ else }}.NonEmptyInterfaceTypedNil: FALSE{{ end }}
|
||||||
|
|
||||||
{{ template "other-file-template" . }}
|
{{ template "other-file-template" . }}
|
||||||
|
|
||||||
{{ define "mytemplate" }}
|
{{ define "mytemplate" }}
|
||||||
{{ if .TimeZero }}.TimeZero1: mytemplate: TRUE{{ else }}.TimeZero1: mytemplate: FALSE{{ end }}
|
{{ if .TimeZero }}.TimeZero1: mytemplate: TRUE{{ else }}.TimeZero1: mytemplate: FALSE{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// https://github.com/gohugoio/hugo/issues/5865
|
// https://github.com/gohugoio/hugo/issues/5865
|
||||||
|
@ -97,7 +110,7 @@ func TestInsertIsZeroFunc(t *testing.T) {
|
||||||
)
|
)
|
||||||
|
|
||||||
d := newD(c)
|
d := newD(c)
|
||||||
h := d.Tmpl.(tpl.TemplateManager)
|
h := d.Tmpl.(*templateHandler)
|
||||||
|
|
||||||
// HTML templates
|
// HTML templates
|
||||||
c.Assert(h.AddTemplate("mytemplate.html", templ1), qt.IsNil)
|
c.Assert(h.AddTemplate("mytemplate.html", templ1), qt.IsNil)
|
||||||
|
@ -107,15 +120,13 @@ func TestInsertIsZeroFunc(t *testing.T) {
|
||||||
c.Assert(h.AddTemplate("_text/mytexttemplate.txt", templ1), qt.IsNil)
|
c.Assert(h.AddTemplate("_text/mytexttemplate.txt", templ1), qt.IsNil)
|
||||||
c.Assert(h.AddTemplate("_text/myothertexttemplate.txt", templ2), qt.IsNil)
|
c.Assert(h.AddTemplate("_text/myothertexttemplate.txt", templ2), qt.IsNil)
|
||||||
|
|
||||||
c.Assert(h.MarkReady(), qt.IsNil)
|
c.Assert(h.markReady(), qt.IsNil)
|
||||||
|
|
||||||
for _, name := range []string{"mytemplate.html", "mytexttemplate.txt"} {
|
for _, name := range []string{"mytemplate.html", "mytexttemplate.txt"} {
|
||||||
|
var sb strings.Builder
|
||||||
tt, _ := d.Tmpl.Lookup(name)
|
tt, _ := d.Tmpl.Lookup(name)
|
||||||
sb := &strings.Builder{}
|
err := h.Execute(tt, &sb, ctx)
|
||||||
|
|
||||||
err := d.Tmpl.Execute(tt, sb, ctx)
|
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
result := sb.String()
|
result := sb.String()
|
||||||
|
|
||||||
c.Assert(result, qt.Contains, ".True: TRUE")
|
c.Assert(result, qt.Contains, ".True: TRUE")
|
||||||
|
@ -138,14 +149,10 @@ func TestCollectInfo(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
tplString string
|
tplString string
|
||||||
expected tpl.Info
|
expected tpl.ParseInfo
|
||||||
}{
|
}{
|
||||||
{"Basic Inner", `{{ .Inner }}`, tpl.Info{IsInner: true, Config: tpl.DefaultConfig}},
|
{"Basic Inner", `{{ .Inner }}`, tpl.ParseInfo{IsInner: true, Config: tpl.DefaultParseConfig}},
|
||||||
{"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.Info{
|
{"Basic config map", "{{ $_hugo_config := `" + configStr + "` }}", tpl.ParseInfo{Config: tpl.ParseConfig{Version: 42}}},
|
||||||
Config: tpl.Config{
|
|
||||||
Version: 42,
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
echo := func(in interface{}) interface{} {
|
echo := func(in interface{}) interface{} {
|
||||||
|
@ -162,12 +169,13 @@ func TestCollectInfo(t *testing.T) {
|
||||||
|
|
||||||
templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
|
templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
parseInfo := tpl.DefaultParseInfo
|
||||||
|
|
||||||
ctx := newTemplateContext(createParseTreeLookup(templ))
|
ctx := newTemplateContext(
|
||||||
|
newTemplateInfo("test").(identity.Manager), &parseInfo, createGetTemplateInfoTree(templ.Tree))
|
||||||
ctx.typ = templateShortcode
|
ctx.typ = templateShortcode
|
||||||
ctx.applyTransformations(templ.Tree.Root)
|
ctx.applyTransformations(templ.Tree.Root)
|
||||||
|
c.Assert(ctx.parseInfo, qt.DeepEquals, &test.expected)
|
||||||
c.Assert(ctx.Info, qt.Equals, test.expected)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +213,10 @@ func TestPartialReturn(t *testing.T) {
|
||||||
templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
|
templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString)
|
||||||
c.Assert(err, qt.IsNil)
|
c.Assert(err, qt.IsNil)
|
||||||
|
|
||||||
_, err = applyTemplateTransformers(templatePartial, templ.Tree, createParseTreeLookup(templ))
|
_, err = applyTemplateTransformers(
|
||||||
|
templatePartial,
|
||||||
|
&templateInfoTree{tree: templ.Tree, info: tpl.DefaultParseInfo},
|
||||||
|
createGetTemplateInfoTree(templ.Tree))
|
||||||
|
|
||||||
// Just check that it doesn't fail in this test. We have functional tests
|
// Just check that it doesn't fail in this test. We have functional tests
|
||||||
// in hugoblib.
|
// in hugoblib.
|
||||||
|
@ -215,3 +226,10 @@ func TestPartialReturn(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newTemplateInfo(name string) tpl.Info {
|
||||||
|
return tpl.NewInfo(
|
||||||
|
identity.NewManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)),
|
||||||
|
tpl.DefaultParseInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,8 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gohugoio/hugo/tpl"
|
||||||
|
|
||||||
"github.com/gohugoio/hugo/common/maps"
|
"github.com/gohugoio/hugo/common/maps"
|
||||||
|
|
||||||
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
template "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
||||||
|
@ -62,14 +64,14 @@ type templateExecHelper struct {
|
||||||
funcs map[string]reflect.Value
|
funcs map[string]reflect.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templateExecHelper) GetFunc(name string) (reflect.Value, bool) {
|
func (t *templateExecHelper) GetFunc(tmpl texttemplate.Preparer, name string) (reflect.Value, bool) {
|
||||||
if fn, found := t.funcs[name]; found {
|
if fn, found := t.funcs[name]; found {
|
||||||
return fn, true
|
return fn, true
|
||||||
}
|
}
|
||||||
return zero, false
|
return zero, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *templateExecHelper) GetMapValue(receiver, key reflect.Value) (reflect.Value, bool) {
|
func (t *templateExecHelper) GetMapValue(tmpl texttemplate.Preparer, receiver, key reflect.Value) (reflect.Value, bool) {
|
||||||
if params, ok := receiver.Interface().(maps.Params); ok {
|
if params, ok := receiver.Interface().(maps.Params); ok {
|
||||||
// Case insensitive.
|
// Case insensitive.
|
||||||
keystr := strings.ToLower(key.String())
|
keystr := strings.ToLower(key.String())
|
||||||
|
@ -85,6 +87,22 @@ func (t *templateExecHelper) GetMapValue(receiver, key reflect.Value) (reflect.V
|
||||||
return v, v.IsValid()
|
return v, v.IsValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *templateExecHelper) GetMethod(tmpl texttemplate.Preparer, receiver reflect.Value, name string) (method reflect.Value, firstArg reflect.Value) {
|
||||||
|
// This is a hot path and receiver.MethodByName really shows up in the benchmarks.
|
||||||
|
// Page.Render is the only method with a WithTemplateInfo as of now, so let's just
|
||||||
|
// check that for now.
|
||||||
|
// TODO(bep) find a more flexible, but still fast, way.
|
||||||
|
if name == "Render" {
|
||||||
|
if info, ok := tmpl.(tpl.Info); ok {
|
||||||
|
if m := receiver.MethodByName(name + "WithTemplateInfo"); m.IsValid() {
|
||||||
|
return m, reflect.ValueOf(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return receiver.MethodByName(name), zero
|
||||||
|
}
|
||||||
|
|
||||||
func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
|
func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflect.Value) {
|
||||||
funcs := createFuncMap(d)
|
funcs := createFuncMap(d)
|
||||||
funcsv := make(map[string]reflect.Value)
|
funcsv := make(map[string]reflect.Value)
|
||||||
|
@ -120,9 +138,7 @@ func createFuncMap(d *deps.Deps) map[string]interface{} {
|
||||||
}
|
}
|
||||||
funcMap[alias] = mm.Method
|
funcMap[alias] = mm.Method
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.OverloadedTemplateFuncs != nil {
|
if d.OverloadedTemplateFuncs != nil {
|
||||||
|
|
|
@ -24,18 +24,19 @@ import (
|
||||||
func TestTemplateInfoShortcode(t *testing.T) {
|
func TestTemplateInfoShortcode(t *testing.T) {
|
||||||
c := qt.New(t)
|
c := qt.New(t)
|
||||||
d := newD(c)
|
d := newD(c)
|
||||||
h := d.Tmpl.(tpl.TemplateManager)
|
h := d.Tmpl.(*templateHandler)
|
||||||
|
|
||||||
c.Assert(h.AddTemplate("shortcodes/mytemplate.html", `
|
c.Assert(h.AddTemplate("shortcodes/mytemplate.html", `
|
||||||
{{ .Inner }}
|
{{ .Inner }}
|
||||||
`), qt.IsNil)
|
`), qt.IsNil)
|
||||||
|
|
||||||
|
c.Assert(h.markReady(), qt.IsNil)
|
||||||
tt, found, _ := d.Tmpl.LookupVariant("mytemplate", tpl.TemplateVariants{})
|
tt, found, _ := d.Tmpl.LookupVariant("mytemplate", tpl.TemplateVariants{})
|
||||||
|
|
||||||
c.Assert(found, qt.Equals, true)
|
c.Assert(found, qt.Equals, true)
|
||||||
tti, ok := tt.(tpl.TemplateInfoProvider)
|
tti, ok := tt.(tpl.Info)
|
||||||
c.Assert(ok, qt.Equals, true)
|
c.Assert(ok, qt.Equals, true)
|
||||||
c.Assert(tti.TemplateInfo().IsInner, qt.Equals, true)
|
c.Assert(tti.ParseInfo().IsInner, qt.Equals, true)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue