Add render template hooks for links and images
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 27 Nov 2019 12:42:36 +0000 (13:42 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Wed, 18 Dec 2019 10:44:40 +0000 (11:44 +0100)
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

59 files changed:
docs/content/en/functions/RenderString.md [new file with mode: 0644]
docs/content/en/getting-started/configuration-markup.md
helpers/content.go
hugolib/content_render_hooks_test.go [new file with mode: 0644]
hugolib/filesystems/basefs.go
hugolib/hugo_modules_test.go
hugolib/hugo_sites.go
hugolib/hugo_sites_build.go
hugolib/hugo_sites_build_test.go
hugolib/page.go
hugolib/page__content.go
hugolib/page__meta.go
hugolib/page__new.go
hugolib/page__output.go
hugolib/page__per_output.go
hugolib/page_test.go
hugolib/page_unwrap_test.go
hugolib/pagebundler_test.go
hugolib/pagecollections.go
hugolib/shortcode.go
hugolib/shortcode_page.go
hugolib/shortcode_test.go
hugolib/site.go
hugolib/site_benchmark_new_test.go
hugolib/template_test.go
hugolib/testhelpers_test.go
identity/identity.go [new file with mode: 0644]
identity/identity_test.go [new file with mode: 0644]
markup/asciidoc/convert.go
markup/blackfriday/convert.go
markup/converter/converter.go
markup/converter/hooks/hooks.go [new file with mode: 0644]
markup/goldmark/convert.go
markup/goldmark/convert_test.go
markup/goldmark/render_link.go [new file with mode: 0644]
markup/mmark/convert.go
markup/org/convert.go
markup/pandoc/convert.go
markup/rst/convert.go
output/layout.go
output/layout_test.go
resources/page/page.go
resources/page/page_nop.go
resources/page/testhelpers_test.go
scripts/fork_go_templates/main.go
tpl/internal/go_templates/texttemplate/exec.go
tpl/internal/go_templates/texttemplate/hugo_template.go
tpl/internal/go_templates/texttemplate/hugo_template_test.go
tpl/partials/partials.go
tpl/template.go
tpl/template_info.go
tpl/tplimpl/shortcodes.go
tpl/tplimpl/shortcodes_test.go
tpl/tplimpl/template.go
tpl/tplimpl/templateProvider.go
tpl/tplimpl/template_ast_transformers.go
tpl/tplimpl/template_ast_transformers_test.go
tpl/tplimpl/template_funcs.go
tpl/tplimpl/template_info_test.go

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