From 7815dd94ff3a60ada828ddab8a988a6984aee4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 27 Nov 2019 13:42:36 +0100 Subject: [PATCH] Add render template hooks for links and images This commit also revises the change detection for templates used by content files in server mode. Fixes #6545 Fixes #4663 --- deps/deps.go | 15 +- docs/content/en/content-management/formats.md | 8 +- docs/layouts/_markup/render-link.html | 1 + docs/layouts/partials/deleteme.html | 1 + docs/layouts/shortcodes/deleteme.html | 3 + go.mod | 2 + helpers/content.go | 13 +- helpers/content_test.go | 8 +- helpers/general_test.go | 12 +- helpers/testhelpers_test.go | 9 +- hugolib/content_render_hooks_test.go | 92 +++++++++++ hugolib/filesystems/basefs.go | 46 +++++- hugolib/hugo_modules_test.go | 3 + hugolib/hugo_sites.go | 68 +++++++- hugolib/hugo_sites_build.go | 2 +- hugolib/page.go | 79 +++++++++ hugolib/page__meta.go | 16 +- hugolib/page__new.go | 23 ++- hugolib/page__output.go | 41 +++-- hugolib/page__per_output.go | 45 ++++-- hugolib/page_test.go | 6 - hugolib/pagecollections.go | 10 -- hugolib/shortcode_page.go | 1 + hugolib/site.go | 122 +++++++------- hugolib/testhelpers_test.go | 9 +- identity/identity.go | 120 ++++++++++++++ identity/identity_test.go | 42 +++++ markup/asciidoc/convert.go | 5 + markup/blackfriday/convert.go | 5 + markup/converter/converter.go | 13 +- markup/converter/hooks/hooks.go | 54 +++++++ markup/goldmark/ast_hooks.go | 150 ++++++++++++++++++ .../{toc_test.go => ast_hooks_test.go} | 0 markup/goldmark/convert.go | 108 ++++++++++--- markup/goldmark/convert_test.go | 15 +- markup/goldmark/render_link.go | 145 +++++++++++++++++ markup/goldmark/toc.go | 102 ------------ markup/mmark/convert.go | 5 + markup/org/convert.go | 6 + markup/pandoc/convert.go | 5 + markup/rst/convert.go | 5 + output/layout.go | 38 +++-- output/layout_test.go | 2 + public/categories/index.xml | 13 ++ public/index.xml | 13 ++ public/sitemap.xml | 17 ++ public/tags/index.xml | 13 ++ tpl/template_info.go | 6 + tpl/tplimpl/ace.go | 5 +- tpl/tplimpl/template.go | 95 ++++++++--- tpl/tplimpl/template_ast_transformers.go | 111 ++++++++++--- tpl/tplimpl/template_ast_transformers_test.go | 30 ++-- tpl/tplimpl/template_funcs_test.go | 5 + 53 files changed, 1400 insertions(+), 363 deletions(-) create mode 100644 docs/layouts/_markup/render-link.html create mode 100644 docs/layouts/partials/deleteme.html create mode 100644 docs/layouts/shortcodes/deleteme.html create mode 100644 hugolib/content_render_hooks_test.go create mode 100644 identity/identity.go create mode 100644 identity/identity_test.go create mode 100644 markup/converter/hooks/hooks.go create mode 100644 markup/goldmark/ast_hooks.go rename markup/goldmark/{toc_test.go => ast_hooks_test.go} (100%) create mode 100644 markup/goldmark/render_link.go delete mode 100644 markup/goldmark/toc.go create mode 100644 public/categories/index.xml create mode 100644 public/index.xml create mode 100644 public/sitemap.xml create mode 100644 public/tags/index.xml diff --git a/deps/deps.go b/deps/deps.go index d7b381ce92e..3236f7244c0 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -4,6 +4,7 @@ import ( "sync" "time" + "github.com/gohugoio/hugo/markup/converter" "github.com/pkg/errors" "github.com/gohugoio/hugo/cache/filecache" @@ -223,11 +224,6 @@ func New(cfg DepsCfg) (*Deps, error) { return nil, err } - contentSpec, err := helpers.NewContentSpec(cfg.Language, logger, ps.BaseFs.Content.Fs) - if err != nil { - return nil, err - } - sp := source.NewSourceSpec(ps, fs.Source) timeoutms := cfg.Language.GetInt("timeout") @@ -247,7 +243,6 @@ func New(cfg DepsCfg) (*Deps, error) { translationProvider: cfg.TranslationProvider, WithTemplate: cfg.WithTemplate, PathSpec: ps, - ContentSpec: contentSpec, SourceSpec: sp, ResourceSpec: resourceSpec, Cfg: cfg.Language, @@ -277,7 +272,13 @@ func (d Deps) ForLanguage(cfg DepsCfg, onCreated func(d *Deps) error) (*Deps, er return nil, err } - d.ContentSpec, err = helpers.NewContentSpec(l, d.Log, d.BaseFs.Content.Fs) + d.ContentSpec, err = helpers.NewContentSpec( + converter.ProviderConfig{ + Cfg: l, + Logger: d.Log, + ContentFs: d.BaseFs.Content.Fs, + }, + ) if err != nil { return nil, err } diff --git a/docs/content/en/content-management/formats.md b/docs/content/en/content-management/formats.md index ea056861657..3704ed8b071 100644 --- a/docs/content/en/content-management/formats.md +++ b/docs/content/en/content-management/formats.md @@ -23,7 +23,13 @@ You can put any file type into your `/content` directories, but Hugo uses the `m * [Shortcodes](/content-management/shortcodes/) processed * Layout applied -## List of content formats +{{< deleteme >}} + + +## List of content formats. + + + The current list of content formats in Hugo: diff --git a/docs/layouts/_markup/render-link.html b/docs/layouts/_markup/render-link.html new file mode 100644 index 00000000000..0df3929f6b6 --- /dev/null +++ b/docs/layouts/_markup/render-link.html @@ -0,0 +1 @@ +😉😉{{ .Text | safeHTML }} 😉😉 \ No newline at end of file diff --git a/docs/layouts/partials/deleteme.html b/docs/layouts/partials/deleteme.html new file mode 100644 index 00000000000..e4df2ce650f --- /dev/null +++ b/docs/layouts/partials/deleteme.html @@ -0,0 +1 @@ +THIS IS PARTIAL!!! \ No newline at end of file diff --git a/docs/layouts/shortcodes/deleteme.html b/docs/layouts/shortcodes/deleteme.html new file mode 100644 index 00000000000..b85b58fc1fc --- /dev/null +++ b/docs/layouts/shortcodes/deleteme.html @@ -0,0 +1,3 @@ +DELETEME PARTIAL: + +{{ partial "deleteme" }} \ No newline at end of file diff --git a/go.mod b/go.mod index 787bb27d511..740ae91c4c8 100644 --- a/go.mod +++ b/go.mod @@ -74,4 +74,6 @@ require ( replace github.com/markbates/inflect => github.com/markbates/inflect v0.0.0-20171215194931-a12c3aec81a6 +replace github.com/yuin/goldmark => /Users/bep/dev/go/dump/goldmark + go 1.12 diff --git a/helpers/content.go b/helpers/content.go index 4dc4cd413bd..4e5903ccd7a 100644 --- a/helpers/content.go +++ b/helpers/content.go @@ -23,15 +23,12 @@ import ( "unicode" "unicode/utf8" - "github.com/gohugoio/hugo/common/loggers" - "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" ) @@ -62,7 +59,8 @@ type ContentSpec struct { // NewContentSpec returns a ContentSpec initialized // with the appropriate fields from the given config.Provider. -func NewContentSpec(cfg config.Provider, logger *loggers.Logger, contentFs afero.Fs) (*ContentSpec, error) { +func NewContentSpec(pcfg converter.ProviderConfig) (*ContentSpec, error) { + cfg := pcfg.Cfg spec := &ContentSpec{ summaryLength: cfg.GetInt("summaryLength"), @@ -73,11 +71,8 @@ func NewContentSpec(cfg config.Provider, logger *loggers.Logger, contentFs afero Cfg: cfg, } - converterProvider, err := markup.NewConverterProvider(converter.ProviderConfig{ - Cfg: cfg, - ContentFs: contentFs, - Logger: logger, - }) + converterProvider, err := markup.NewConverterProvider(pcfg) + if err != nil { return nil, err } diff --git a/helpers/content_test.go b/helpers/content_test.go index 7f82abc9da0..268bb8e9aca 100644 --- a/helpers/content_test.go +++ b/helpers/content_test.go @@ -19,6 +19,8 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/markup/converter" + "github.com/spf13/afero" "github.com/gohugoio/hugo/common/loggers" @@ -110,7 +112,11 @@ func TestNewContentSpec(t *testing.T) { cfg.Set("buildExpired", true) cfg.Set("buildDrafts", true) - spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs()) + spec, err := NewContentSpec(converter.ProviderConfig{ + Cfg: cfg, + Logger: loggers.NewErrorLogger(), + ContentFs: afero.NewMemMapFs(), + }) c.Assert(err, qt.IsNil) c.Assert(spec.summaryLength, qt.Equals, 32) diff --git a/helpers/general_test.go b/helpers/general_test.go index 104a4c35def..8b8d531d165 100644 --- a/helpers/general_test.go +++ b/helpers/general_test.go @@ -21,16 +21,22 @@ import ( "github.com/spf13/viper" - "github.com/gohugoio/hugo/common/loggers" - qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/markup/converter" "github.com/spf13/afero" ) func TestResolveMarkup(t *testing.T) { c := qt.New(t) cfg := viper.New() - spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs()) + spec, err := NewContentSpec( + converter.ProviderConfig{ + Cfg: cfg, + Logger: loggers.NewErrorLogger(), + ContentFs: afero.NewMemMapFs(), + }, + ) c.Assert(err, qt.IsNil) for i, this := range []struct { diff --git a/helpers/testhelpers_test.go b/helpers/testhelpers_test.go index bf249059d76..fe3151d09a4 100644 --- a/helpers/testhelpers_test.go +++ b/helpers/testhelpers_test.go @@ -2,6 +2,7 @@ package helpers import ( "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/markup/converter" "github.com/spf13/afero" "github.com/spf13/viper" @@ -58,7 +59,13 @@ func newTestCfg() *viper.Viper { func newTestContentSpec() *ContentSpec { v := viper.New() - spec, err := NewContentSpec(v, loggers.NewErrorLogger(), afero.NewMemMapFs()) + spec, err := NewContentSpec( + converter.ProviderConfig{ + Cfg: v, + Logger: loggers.NewErrorLogger(), + ContentFs: afero.NewMemMapFs(), + }, + ) if err != nil { panic(err) } diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go new file mode 100644 index 00000000000..bc6b6c7c9b5 --- /dev/null +++ b/hugolib/content_render_hooks_test.go @@ -0,0 +1,92 @@ +// 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 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("partials/mypartial1.html", `PARTIAL1`) + b.WithTemplatesAdded("partials/mypartial2.html", `PARTIAL2 {{ partial "mypartial3.html" }}`) + b.WithTemplatesAdded("partials/mypartial3.html", `PARTIAL3`) + b.WithTemplatesAdded("_default/_markup/render-link.html", `{{ .Page.Title }}|{{ .Destination | safeURL }}|Title: {{ .Title | safeHTML }}|Text: {{ .Text | safeHTML }}|END`) + b.WithTemplatesAdded("docs/_markup/render-link.html", `Link docs section: {{ .Text | safeHTML }}|END`) + + b.WithContent("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") + + +`, "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") + + +`) + b.Build(BuildCfg{}) + b.AssertFileContent("public/blog/p1/index.html", `

Cool Page|https://www.google.com|Title: Google's Homepage|Text: First Link|END

`, `Text: Second`, "SHORT3|") + b.AssertFileContent("public/blog/p2/index.html", `PARTIAL`) + b.AssertFileContent("public/blog/p3/index.html", `PARTIAL3`) + b.AssertFileContent("public/docs/docs1/index.html", `Link docs section: Docs 1|END`) + + b.EditFiles( + "layouts/_default/_markup/render-link.html", `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/shortcodes/myshortcode3.html", `SHORT3_EDITED|`, + ) + b.Build(BuildCfg{}) + + b.AssertFileContent("public/blog/p1/index.html", `

EDITED: https://www.google.com|

`, "SHORT3_EDITED|") + b.AssertFileContent("public/blog/p2/index.html", `PARTIAL1_EDITED`) + b.AssertFileContent("public/blog/p3/index.html", `PARTIAL3_EDITED`) + b.AssertFileContent("public/docs/docs1/index.html", `DOCS EDITED: https://www.google.com|

`) + +} diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go index de6baa130d7..66690310c49 100644 --- a/hugolib/filesystems/basefs.go +++ b/hugolib/filesystems/basefs.go @@ -126,10 +126,25 @@ type SourceFilesystems struct { StaticDirs []hugofs.FileMetaInfo } +func (s *SourceFilesystems) FileSystems() []*SourceFilesystem { + return []*SourceFilesystem{ + s.Content, + s.Data, + s.I18n, + s.Layouts, + s.Archetypes, + // TODO1 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 +290,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 +377,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 +399,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 +422,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 +439,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 +451,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 diff --git a/hugolib/hugo_modules_test.go b/hugolib/hugo_modules_test.go index 9ba039c7430..ddc0ef59b46 100644 --- a/hugolib/hugo_modules_test.go +++ b/hugolib/hugo_modules_test.go @@ -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 hugo.GoMinorVersion() < 12 { diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index c0d75c09f52..026a4c3ff9c 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -20,6 +20,10 @@ import ( "strings" "sync" + "github.com/gohugoio/hugo/identity" + + "github.com/gohugoio/hugo/markup/converter" + radix "github.com/armon/go-radix" "github.com/gohugoio/hugo/output" @@ -412,6 +416,19 @@ func applyDeps(cfg deps.DepsCfg, sites ...*Site) error { d.OutputFormatsConfig = s.outputFormatsConfig } + contentSpec, err := helpers.NewContentSpec( + converter.ProviderConfig{ + Cfg: d.Language, + Logger: s.Log, + ContentFs: s.BaseFs.Content.Fs, + }, + ) + if err != nil { + return err + } + + d.ContentSpec = contentSpec + } return nil @@ -806,12 +823,55 @@ 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(ids identity.Identities) { + idset := ids.ToIdentitySet() + hasIdentify := func(v interface{}) bool { + if id, ok := v.(identity.Provider); ok { + if idset[id.GetIdentity()] { + return true + } + } + if idp, ok := v.(identity.ChildIdentitiesProvider); ok { + for _, id := range idp.GetChildIdentities() { + if idset[id.GetIdentity()] { + return true + } + } + } + return false + } + for _, s := range h.Sites { - pages = append(pages, s.findPagesByShortcode(shortcode)...) + PAGES: + for _, p := range s.rawAllPages { + OUTPUTS: + for _, po := range p.pageOutputs { + if c := po.cp; c != nil { + if converted := c.convertedResult; converted != nil { + if hasIdentify(converted) { + c.Reset() + p.forceRender = true + continue OUTPUTS + } + } + } + } + + for _, s := range p.shortcodeState.shortcodes { + for _, id := range ids { + if s.info.Search(id.GetIdentity()) != 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. diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index a70a19e7c31..d749ff581d5 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -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 diff --git a/hugolib/page.go b/hugolib/page.go index b0e8c4359fd..d69ceed4eeb 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -23,6 +23,10 @@ import ( "sort" "strings" + "github.com/gohugoio/hugo/tpl" + + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/common/maps" @@ -46,6 +50,7 @@ import ( "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" @@ -317,6 +322,80 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error { return nil } +func (ps *pageState) initOutputFormats() error { + + if len(ps.pageOutputs) == 0 { + return nil + } + + c := ps.getContentConverter() + if c == nil || !c.Supports(converter.FeatureRenderHooks) { + return nil + } + + templSet := make(map[identity.Identity]bool) + canReuse := true + + for _, o := range ps.pageOutputs { + if !o.render || o.cp == nil { + continue + } + hooks, err := ps.createRenderHooks(o.f) + if err != nil { + return err + } + if canReuse { + if len(templSet) != 0 { + // There may be a template per output format. + // In that case we need to re-render. + canReuse = templSet[hooks.LinkRenderer.GetIdentity()] + } + templSet[hooks.LinkRenderer.GetIdentity()] = true + } + o.cp.renderHooks = hooks + o.cp.renderHooksHaveVariants = !canReuse + } + + return nil +} + +func (p *pageState) createRenderHooks(f output.Format) (*hooks.Render, error) { + + layoutDescriptor := p.getLayoutDescriptor() + layoutDescriptor.RenderingHook = true + layoutDescriptor.LayoutOverride = false + + 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.ImageRenderer + + if templ, found := p.s.lookupTemplate(linkLayouts...); found { + linkRenderer = contentLinkRenderer{ + Provider: templ.(tpl.TemplateInfoProvider).TemplateInfo(), + templ: templ, + } + } + + return &hooks.Render{ + LinkRenderer: linkRenderer, + }, nil +} + func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor { p.layoutDescriptorInit.Do(func() { var section string diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index 1fc69c21826..84a052f92bc 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -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 @@ -656,15 +656,19 @@ func (p *pageMeta) applyDefaultValues() error { return errors.Errorf("no content renderer found for markup %q", p.markup) } - cpp, err := cp.New(converter.DocumentContext{ - DocumentID: p.f.UniqueID(), - DocumentName: p.f.Path(), - ConfigOverrides: renderingConfigOverrides, - }) + cpp, err := cp.New( + converter.DocumentContext{ + Document: newPageForShortcode(ps), + DocumentID: p.f.UniqueID(), + DocumentName: p.f.Path(), + ConfigOverrides: renderingConfigOverrides, + }, + ) if err != nil { return err } + p.contentConverter = cpp } diff --git a/hugolib/page__new.go b/hugolib/page__new.go index 99bf305aa58..37db148f8bd 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -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 { @@ -158,6 +158,10 @@ func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*page return nil, err } + if err := ps.initOutputFormats(); err != nil { + return nil, err + } + return nil, nil }) @@ -234,7 +238,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 } @@ -264,18 +268,17 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope } _, render := outputFormatsForPage.GetByName(f.Name) - var contentProvider *pageContentOutput + po := newPageOutput(ps, pp, f, render) if reuseContent && i > 0 { - contentProvider = ps.pageOutputs[0].cp + po.initContentProvider(ps.pageOutputs[0].cp) } else { - var err error - contentProvider, err = contentPerOutput(f) + contentProvider, err := contentPerOutput(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 } @@ -284,6 +287,10 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope return nil, err } + if err := ps.initOutputFormats(); err != nil { + return nil, err + } + return nil, nil }) diff --git a/hugolib/page__output.go b/hugolib/page__output.go index 764c46a937b..1af666ed275 100644 --- a/hugolib/page__output.go +++ b/hugolib/page__output.go @@ -20,7 +20,6 @@ import ( ) func newPageOutput( - cp *pageContentOutput, // may be nil ps *pageState, pp pagePaths, f output.Format, @@ -45,36 +44,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 @@ -98,12 +84,25 @@ type pageOutput struct { // output format. pagePerOutputProviders - // This may be nil. + page.ContentProvider + page.TableOfContentsProvider + + // May be nil. cp *pageContentOutput } +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() } + } diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index bc2a0accc04..5bbb5800937 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -23,6 +23,8 @@ import ( "sync" "unicode/utf8" + "github.com/gohugoio/hugo/markup/converter/hooks" + "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/lazy" @@ -58,14 +60,14 @@ var ( } ) -func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutput, error) { +func newPageContentOutput(p *pageState) func(po *pageOutput) (*pageContentOutput, error) { parent := p.init - return func(f output.Format) (*pageContentOutput, error) { + return func(po *pageOutput) (*pageContentOutput, error) { cp := &pageContentOutput{ p: p, - f: f, + f: po.f, } initContent := func() (err error) { @@ -83,13 +85,22 @@ func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutpu var hasVariants bool + f := po.f cp.contentPlaceholders, hasVariants, err = p.shortcodeState.renderShortcodesForPage(p, f) if err != nil { return err } - if p.render && !hasVariants { - // We can reuse this for the other output formats + enableReuse := p.render && !hasVariants + enableReuse = enableReuse && !cp.renderHooksHaveVariants + + 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() } @@ -177,6 +188,7 @@ func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutpu // Recursive loops can only happen in content files with template code (shortcodes etc.) // Avoid creating new goroutines if we don't have to. + // TODO1 needTimeout := !p.renderable || p.shortcodeState.hasShortcodes() if needTimeout { @@ -212,6 +224,7 @@ type pageContentOutput struct { f output.Format // If we can safely reuse this for other output formats. + // TODO1 reuse bool reuseInit sync.Once @@ -224,6 +237,11 @@ 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 + // Content state workContent []byte @@ -232,6 +250,7 @@ type pageContentOutput struct { // Temporary storage of placeholders mapped to their content. // These are shortcodes etc. Some of these will need to be replaced // after any markup is rendered, so they share a common prefix. + // TODO1 contentPlaceholders map[string]string // Content sections @@ -248,6 +267,12 @@ type pageContentOutput struct { readingTime int } +func (p *pageContentOutput) Reset() { + p.p.initOutputFormats() + 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 @@ -332,10 +357,12 @@ func (p *pageContentOutput) setAutoSummary() error { } func (cp *pageContentOutput) renderContent(content []byte) (converter.Result, error) { - return cp.p.getContentConverter().Convert( + c := cp.p.getContentConverter() + return c.Convert( converter.RenderContext{ - Src: content, - RenderTOC: true, + Src: content, + RenderTOC: true, + RenderHooks: cp.renderHooks, }) } @@ -392,9 +419,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 } diff --git a/hugolib/page_test.go b/hugolib/page_test.go index dc8bc821c15..8fb57676657 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -93,12 +93,6 @@ Summary Next Line. {{
}}. More text here. Some more text -` - - simplePageWithEmbeddedScript = `--- -title: Simple ---- - ` simplePageWithSummaryDelimiterSameLine = `--- diff --git a/hugolib/pagecollections.go b/hugolib/pagecollections.go index 7e9682e90e1..adcbbccefef 100644 --- a/hugolib/pagecollections.go +++ b/hugolib/pagecollections.go @@ -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) diff --git a/hugolib/shortcode_page.go b/hugolib/shortcode_page.go index e8a3a37e19b..44dc498cbc3 100644 --- a/hugolib/shortcode_page.go +++ b/hugolib/shortcode_page.go @@ -26,6 +26,7 @@ var tocShortcodePlaceholder = createShortcodePlaceholder("TOC", 0) // // Go doesn't support virtual methods, so this careful dance is currently (I think) // the best we can do. +// TODO1 type pageForShortcode struct { page.PageWithoutContent page.ContentProvider diff --git a/hugolib/site.go b/hugolib/site.go index 1df7d6076db..be1d178ba6d 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -28,6 +28,10 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/identity" + + "github.com/gohugoio/hugo/markup/converter/hooks" + "github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/markup/converter" @@ -801,7 +805,6 @@ func (s *Site) multilingual() *Multilingual { type whatChanged struct { source bool - other bool files map[string]bool } @@ -888,10 +891,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) + var changeIdentities identity.Identities + s.Log.DEBUG.Printf("Rebuild for events %q", events) h := s.h @@ -902,11 +906,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() @@ -915,37 +920,34 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro var cachePartitions []string for _, ev := range events { - if assetsFilename := s.BaseFs.Assets.MakePathRelative(ev.Name); assetsFilename != "" { - cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...) - } + id, found := s.eventToIdentity(ev) + if found { + changeIdentities = append(changeIdentities, id) + + if assetsFilename := s.BaseFs.Assets.MakePathRelative(ev.Name); assetsFilename != "" { + cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...) + } + + 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.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 } - } - 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 +962,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 +991,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 +1020,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 +1209,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 +1547,25 @@ var infoOnMissingLayout = map[string]bool{ "404": true, } +type contentLinkRenderer struct { + identity.Provider + templ tpl.Template +} + +func (r contentLinkRenderer) Render(w io.Writer, ctx hooks.LinkContext) error { + return r.templ.Execute(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 { diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index d861a5e0941..9912af65b40 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -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 index 00000000000..90132bac309 --- /dev/null +++ b/identity/identity.go @@ -0,0 +1,120 @@ +package identity + +import ( + "path/filepath" + "strings" + "sync" +) + +// NewIdentityManager creates a new Manager starting at root. +func NewIdentityManager(root Provider) Manager { + return &identityManager{ + Provider: root, + children: make(Identities, 0), + } +} + +// NewPathIdentity creates a new Identity with the two identifiers +// type and path. +func NewPathIdentity(typ, path string) PathIdentity { + path = strings.TrimPrefix(filepath.ToSlash(path), "/") + return PathIdentity{Type: typ, Path: path} +} + +// Identities stores identity providers. +type Identities []Provider + +// ToIdentitySet creates a set of these Identities. +func (ids Identities) ToIdentitySet() map[Identity]bool { + m := make(map[Identity]bool) + for _, id := range ids { + m[id.GetIdentity()] = true + } + return m +} + +func (ids Identities) search(id Identity) Provider { + for _, v := range ids { + vid := v.GetIdentity() + + if vid == id { + return v + } + + if idsp, ok := v.(ChildIdentitiesProvider); ok { + if nested := idsp.GetChildIdentities().search(id); nested != nil { + return nested + } + } + } + + return nil +} + +// ChildIdentitiesProvider provides child Identities. +type ChildIdentitiesProvider interface { + GetChildIdentities() 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 { + ChildIdentitiesProvider + Provider + Add(ids ...Provider) + Search(id Identity) Provider +} + +// 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 +} + +// Provider provides the hashable Identity. +type Provider interface { + GetIdentity() Identity +} + +type identityManager struct { + sync.RWMutex + Provider + children Identities +} + +func (im *identityManager) Add(ids ...Provider) { + im.Lock() + im.children = append(im.children, ids...) + im.Unlock() + +} + +func (im *identityManager) GetChildIdentities() Identities { + return im.children +} + +func (im *identityManager) Search(id Identity) Provider { + im.RLock() + if id == im.GetIdentity() { + return im + } + v := im.children.search(id) + im.RUnlock() + return v +} diff --git a/identity/identity_test.go b/identity/identity_test.go new file mode 100644 index 00000000000..78e7a3b5e15 --- /dev/null +++ b/identity/identity_test.go @@ -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 := NewIdentityManager(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 +} diff --git a/markup/asciidoc/convert.go b/markup/asciidoc/convert.go index 65fdde0f564..a72aac39198 100644 --- a/markup/asciidoc/convert.go +++ b/markup/asciidoc/convert.go @@ -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 { diff --git a/markup/blackfriday/convert.go b/markup/blackfriday/convert.go index 350defcb63c..3df23c7ae74 100644 --- a/markup/blackfriday/convert.go +++ b/markup/blackfriday/convert.go @@ -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) diff --git a/markup/converter/converter.go b/markup/converter/converter.go index a1141f65ccc..dd7b4a347d8 100644 --- a/markup/converter/converter.go +++ b/markup/converter/converter.go @@ -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 index 00000000000..6111a741578 --- /dev/null +++ b/markup/converter/hooks/hooks.go @@ -0,0 +1,54 @@ +// 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 +} + +// TODO1 +type ImageContext interface { +} + +type Render struct { + LinkRenderer +} + +func (h *Render) GetChildIdentities() identity.Identities { + var ids identity.Identities + if h.LinkRenderer != nil { + ids = append(ids, h.LinkRenderer) + } + return ids + +} + +type LinkRenderer interface { + Render(w io.Writer, ctx LinkContext) error + identity.Provider +} + +type ImageRenderer interface { + Render(w io.Writer, ctx ImageContext) error + identity.Provider +} diff --git a/markup/goldmark/ast_hooks.go b/markup/goldmark/ast_hooks.go new file mode 100644 index 00000000000..341d5bc7a02 --- /dev/null +++ b/markup/goldmark/ast_hooks.go @@ -0,0 +1,150 @@ +// 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 ( + "bytes" + + "github.com/gohugoio/hugo/markup/converter" + + "github.com/gohugoio/hugo/markup/tableofcontents" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/text" + "github.com/yuin/goldmark/util" +) + +type astTransformer struct { + cfg converter.ProviderConfig +} + +func (t *astTransformer) Transform(n *ast.Document, reader text.Reader, pc parser.Context) { + var hooks []astHook + + if b, ok := pc.Get(renderContextKey).(converter.RenderContext); ok && b.RenderTOC { + hooks = append(hooks, newTocAstHook(reader, pc)) + } + + if len(hooks) == 0 { + return + } + + ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) { + s := ast.WalkStatus(ast.WalkContinue) + + for i, hook := range hooks { + t, err := hook.Visit(n, entering) + if err != nil { + return t, err + } + if i == 0 || t > s { + s = t + } + } + + return s, nil + }) + + for _, hook := range hooks { + hook.Done() + } +} + +type astExtension struct { + cfg converter.ProviderConfig +} + +func newASTExtension(cfg converter.ProviderConfig) goldmark.Extender { + return &astExtension{cfg: cfg} +} + +func (e *astExtension) Extend(m goldmark.Markdown) { + m.Parser().AddOptions(parser.WithASTTransformers(util.Prioritized(&astTransformer{cfg: e.cfg}, 10))) +} + +type astHook interface { + Visit(n ast.Node, entering bool) (ast.WalkStatus, error) + Done() +} + +var ( + tocResultKey = parser.NewContextKey() + renderContextKey = parser.NewContextKey() +) + +type tocAstHook struct { + reader text.Reader + pc parser.Context + + // ToC state + toc tableofcontents.Root + header tableofcontents.Header + level int + row int + inHeading bool + headingText bytes.Buffer +} + +func newTocAstHook(reader text.Reader, pc parser.Context) *tocAstHook { + return &tocAstHook{ + reader: reader, + pc: pc, + row: -1, + } +} + +func (h *tocAstHook) Visit(n ast.Node, entering bool) (ast.WalkStatus, error) { + s := ast.WalkStatus(ast.WalkContinue) + if n.Kind() == ast.KindHeading { + if h.inHeading && !entering { + h.header.Text = h.headingText.String() + h.headingText.Reset() + h.toc.AddAt(h.header, h.row, h.level-1) + h.header = tableofcontents.Header{} + h.inHeading = false + return s, nil + } + + h.inHeading = true + } + + if !(h.inHeading && entering) { + return s, nil + } + + switch n.Kind() { + case ast.KindHeading: + heading := n.(*ast.Heading) + h.level = heading.Level + + if h.level == 1 || h.row == -1 { + h.row++ + } + + id, found := heading.AttributeString("id") + if found { + h.header.ID = string(id.([]byte)) + } + case ast.KindText: + textNode := n.(*ast.Text) + h.headingText.Write(textNode.Text(h.reader.Source())) + } + + return s, nil +} + +func (h *tocAstHook) Done() { + h.pc.Set(tocResultKey, h.toc) +} diff --git a/markup/goldmark/toc_test.go b/markup/goldmark/ast_hooks_test.go similarity index 100% rename from markup/goldmark/toc_test.go rename to markup/goldmark/ast_hooks_test.go diff --git a/markup/goldmark/convert.go b/markup/goldmark/convert.go index 15b0f0d77c8..897c70d213f 100644 --- a/markup/goldmark/convert.go +++ b/markup/goldmark/convert.go @@ -15,21 +15,22 @@ package goldmark import ( + "bufio" "bytes" "fmt" "path/filepath" "runtime/debug" + "github.com/gohugoio/hugo/identity" + "github.com/pkg/errors" "github.com/spf13/afero" "github.com/gohugoio/hugo/hugofs" - "github.com/alecthomas/chroma/styles" "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/highlight" - "github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/yuin/goldmark" hl "github.com/yuin/goldmark-highlighting" @@ -48,7 +49,7 @@ type provide struct { } func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) { - md := newMarkdown(cfg.MarkupConfig) + md := newMarkdown(cfg) return converter.NewProvider("goldmark", func(ctx converter.DocumentContext) (converter.Converter, error) { return &goldmarkConverter{ ctx: ctx, @@ -64,12 +65,14 @@ 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{ - newTocExtension(), + newLinks(), + newASTExtension(pcfg), } rendererOptions []renderer.Option parserOptions []parser.Option @@ -143,15 +146,51 @@ func newMarkdown(mcfg markup_config.Config) goldmark.Markdown { } +var _ identity.ChildIdentitiesProvider = (*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) GetChildIdentities() 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.Identities +} + +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 = append(ctx.ids, id) +} + func (c *goldmarkConverter) Convert(ctx converter.RenderContext) (result converter.Result, err error) { defer func() { if r := recover(); r != nil { @@ -166,8 +205,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() reader := text.NewReader(ctx.Src) @@ -176,27 +214,55 @@ 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, + } + + 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, + 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() *parserContext { + return &parserContext{ + Context: parser.NewContext(), } +} - 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 +296,4 @@ func newHighlighting(cfg highlight.Config) goldmark.Extender { }), ) - - return e } diff --git a/markup/goldmark/convert_test.go b/markup/goldmark/convert_test.go index b6816d2e54a..62af8d809a2 100644 --- a/markup/goldmark/convert_test.go +++ b/markup/goldmark/convert_test.go @@ -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, `Live Demo here!`) + // c.Assert(got, qt.Contains, `Live Demo here!`) // Header IDs c.Assert(got, qt.Contains, `

Custom ID

`, qt.Commentf(got)) @@ -137,6 +141,11 @@ description c.Assert(got, qt.Contains, `
`) c.Assert(got, qt.Contains, `
date
`) + toc, ok := b.(converter.TableOfContentsProvider) + c.Assert(ok, qt.Equals, true) + tocHTML := toc.TableOfContents().ToHTML(1, 2) + 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 index 00000000000..4b9f44193d0 --- /dev/null +++ b/markup/goldmark/render_link.go @@ -0,0 +1,145 @@ +// 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" +) + +func (c *linkRenderer) SetOption(name renderer.OptionName, value interface{}) { + c.Config.SetOption(name, value) +} + +var _ renderer.SetOptioner = (*linkRenderer)(nil) + +type linkRenderer struct { + html.Config +} + +func newLinkRenderer() renderer.NodeRenderer { + r := &linkRenderer{ + Config: html.Config{ + Writer: html.DefaultWriter, + }, + } + return r +} + +// RegisterFuncs implements NodeRenderer.RegisterFuncs. +func (r *linkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(ast.KindLink, r.renderLink) +} + +type linkContext struct { + page interface{} + destination string + title string + text string +} + +func (ctx linkContext) Page() interface{} { + return ctx.page +} + +func (ctx linkContext) Destination() string { + return ctx.destination +} + +func (ctx linkContext) Title() string { + return ctx.title +} + +func (ctx linkContext) Text() string { + return ctx.text +} + +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)), + }, + ) + + if err == nil { + ctx.AddIdentity(h.LinkRenderer.GetIdentity()) + } + + // Do not render the inner text. + return ast.WalkSkipChildren, err + +} + +// Fall back to the default Goldmark link render func. 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("') + } else { + _, _ = w.WriteString("") + } + return ast.WalkContinue, nil +} + +type links struct { +} + +// TODO1 rename +func newLinks() goldmark.Extender { + return &links{} +} + +// Extend implements goldmark.Extender. +func (e *links) Extend(m goldmark.Markdown) { + m.Renderer().AddOptions(renderer.WithNodeRenderers( + util.Prioritized(newLinkRenderer(), 100), + )) +} diff --git a/markup/goldmark/toc.go b/markup/goldmark/toc.go deleted file mode 100644 index 897f0098b6d..00000000000 --- a/markup/goldmark/toc.go +++ /dev/null @@ -1,102 +0,0 @@ -// 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 ( - "bytes" - - "github.com/gohugoio/hugo/markup/tableofcontents" - - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/parser" - "github.com/yuin/goldmark/text" - "github.com/yuin/goldmark/util" -) - -var ( - tocResultKey = parser.NewContextKey() - tocEnableKey = parser.NewContextKey() -) - -type tocTransformer struct { -} - -func (t *tocTransformer) Transform(n *ast.Document, reader text.Reader, pc parser.Context) { - if b, ok := pc.Get(tocEnableKey).(bool); !ok || !b { - return - } - - var ( - toc tableofcontents.Root - header tableofcontents.Header - level int - row = -1 - inHeading bool - headingText bytes.Buffer - ) - - ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) { - s := ast.WalkStatus(ast.WalkContinue) - if n.Kind() == ast.KindHeading { - if inHeading && !entering { - header.Text = headingText.String() - headingText.Reset() - toc.AddAt(header, row, level-1) - header = tableofcontents.Header{} - inHeading = false - return s, nil - } - - inHeading = true - } - - if !(inHeading && entering) { - return s, nil - } - - switch n.Kind() { - case ast.KindHeading: - heading := n.(*ast.Heading) - level = heading.Level - - if level == 1 || row == -1 { - row++ - } - - id, found := heading.AttributeString("id") - if found { - header.ID = string(id.([]byte)) - } - case ast.KindText: - textNode := n.(*ast.Text) - headingText.Write(textNode.Text(reader.Source())) - } - - return s, nil - }) - - pc.Set(tocResultKey, toc) -} - -type tocExtension struct { -} - -func newTocExtension() goldmark.Extender { - return &tocExtension{} -} - -func (e *tocExtension) Extend(m goldmark.Markdown) { - m.Parser().AddOptions(parser.WithASTTransformers(util.Prioritized(&tocTransformer{}, 10))) -} diff --git a/markup/mmark/convert.go b/markup/mmark/convert.go index 07b2a6f81e5..0682ad276c6 100644 --- a/markup/mmark/convert.go +++ b/markup/mmark/convert.go @@ -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, diff --git a/markup/org/convert.go b/markup/org/convert.go index 4d6e5e2fa0f..2b1fbb73c3a 100644 --- a/markup/org/convert.go +++ b/markup/org/convert.go @@ -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 +} diff --git a/markup/pandoc/convert.go b/markup/pandoc/convert.go index d538d4a5265..d6d5ab18c8c 100644 --- a/markup/pandoc/convert.go +++ b/markup/pandoc/convert.go @@ -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 diff --git a/markup/rst/convert.go b/markup/rst/convert.go index 040b40d792d..64cc8b5114f 100644 --- a/markup/rst/convert.go +++ b/markup/rst/convert.go @@ -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 { diff --git a/output/layout.go b/output/layout.go index 055d742b15f..7d935d0ba89 100644 --- a/output/layout.go +++ b/output/layout.go @@ -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,25 @@ 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 { + if d.Type != "" { + b.addTypeVariations(d.Type) + } + b.addLayoutVariations(d.Kind) + b.addSectionType() + } else { + if d.Layout != "" { + b.addLayoutVariations(d.Layout) + } + if d.Type != "" { + b.addTypeVariations(d.Type) + } } switch d.Kind { @@ -159,7 +177,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 +185,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") } diff --git a/output/layout_test.go b/output/layout_test.go index c6267b27434..9e4f89098c3 100644 --- a/output/layout_test.go +++ b/output/layout_test.go @@ -111,6 +111,8 @@ 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}, + {"Content hook", LayoutDescriptor{Kind: "render-link", RenderingHook: true, Section: "blog"}, "", ampType, + []string{"blog/_markup/render-link.amp.html", "blog/_markup/render-link.html", "_default/_markup/render-link.amp.html", "_default/_markup/render-link.html"}, 4}, } { c.Run(this.name, func(c *qt.C) { l := NewLayoutHandler() diff --git a/public/categories/index.xml b/public/categories/index.xml new file mode 100644 index 00000000000..ae8c7f8f749 --- /dev/null +++ b/public/categories/index.xml @@ -0,0 +1,13 @@ + + + + Categories on + /categories/ + Recent content in Categories on + Hugo -- gohugo.io + + + + + + \ No newline at end of file diff --git a/public/index.xml b/public/index.xml new file mode 100644 index 00000000000..b70aeed6e08 --- /dev/null +++ b/public/index.xml @@ -0,0 +1,13 @@ + + + + + / + Recent content on + Hugo -- gohugo.io + + + + + + \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 00000000000..95cee9f7cdc --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,17 @@ + + + + + / + + + + /categories/ + + + + /tags/ + + + \ No newline at end of file diff --git a/public/tags/index.xml b/public/tags/index.xml new file mode 100644 index 00000000000..43dbc43baa7 --- /dev/null +++ b/public/tags/index.xml @@ -0,0 +1,13 @@ + + + + Tags on + /tags/ + Recent content in Tags on + Hugo -- gohugo.io + + + + + + \ No newline at end of file diff --git a/tpl/template_info.go b/tpl/template_info.go index be056695895..a7bb7f5476b 100644 --- a/tpl/template_info.go +++ b/tpl/template_info.go @@ -13,6 +13,10 @@ package tpl +import ( + "github.com/gohugoio/hugo/identity" +) + // Increments on breaking changes. const TemplateVersion = 2 @@ -27,6 +31,8 @@ type Info struct { // Config extracted from template. Config Config + + identity.Manager } func (info Info) IsZero() bool { diff --git a/tpl/tplimpl/ace.go b/tpl/tplimpl/ace.go index bdbc7105992..63c36b7cb66 100644 --- a/tpl/tplimpl/ace.go +++ b/tpl/tplimpl/ace.go @@ -53,15 +53,14 @@ func (t *templateHandler) addAceTemplate(name, basePath, innerPath string, baseC typ := resolveTemplateType(name) - c, err := applyTemplateTransformersToHMLTTemplate(typ, templ) + c, err := t.applyTemplateTransformersToHMLTTemplate(typ, templ) if err != nil { return err } + t.templateInfo[name] = c.Info if typ == templateShortcode { t.addShortcodeVariant(name, c.Info, templ) - } else { - t.templateInfo[name] = c.Info } return nil diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go index 6027775243d..dcd979d7e74 100644 --- a/tpl/tplimpl/template.go +++ b/tpl/tplimpl/template.go @@ -18,7 +18,10 @@ import ( "html/template" "strings" texttemplate "text/template" - "text/template/parse" + + "github.com/gohugoio/hugo/hugofs/files" + + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/tpl/tplimpl/embedded" @@ -274,8 +277,8 @@ func (t *templateHandler) clone(d *deps.Deps) *templateHandler { templateInfo: t.templateInfo, html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template), templatesCommon: t.html.templatesCommon}, text: &textTemplates{ - textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, - standalone: &textTemplate{t: texttemplate.New("")}, + textTemplate: &textTemplate{templates: t.text, t: texttemplate.Must(t.text.t.Clone())}, + standalone: &textTemplate{templates: t.text, t: texttemplate.New("")}, overlays: make(map[string]*texttemplate.Template), templatesCommon: t.text.templatesCommon}, errors: make([]*templateErr, 0), } @@ -324,6 +327,7 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler { common := &templatesCommon{ nameBaseTemplateName: make(map[string]string), transformNotFound: make(map[string]bool), + identityNotFound: make(map[string][]tpl.Info), } htmlT := &htmlTemplates{ @@ -347,6 +351,8 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler { errors: make([]*templateErr, 0), } + textT.textTemplate.templates = textT + textT.standalone.templates = textT common.handler = h return h @@ -364,7 +370,10 @@ type templatesCommon struct { // Holds names of the templates not found during the first AST transformation // pass. transformNotFound map[string]bool + + identityNotFound map[string][]tpl.Info } + type htmlTemplates struct { mu sync.RWMutex @@ -504,15 +513,19 @@ func (t *htmlTemplates) addTemplateIn(tt *template.Template, name, tpl string) ( 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 } + for k := range c.identityNotFound { + t.identityNotFound[k] = append(t.identityNotFound[k], c.Info) + } + if typ == templateShortcode { t.handler.addShortcodeVariant(name, c.Info, templ) } else { @@ -532,8 +545,9 @@ func (t *htmlTemplates) addLateTemplate(name, tpl string) error { } type textTemplate struct { - mu sync.RWMutex - t *texttemplate.Template + mu sync.RWMutex + t *texttemplate.Template + templates *textTemplates } func (t *textTemplate) Parse(name, tpl string) (tpl.Template, error) { @@ -557,7 +571,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 @@ -572,12 +586,12 @@ func (t *textTemplates) addTemplateIn(tt *texttemplate.Template, name, tpl strin 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 } @@ -603,36 +617,59 @@ func (t *templateHandler) addTemplate(name, tpl string) error { return t.AddTemplate(name, tpl) } -func (t *templateHandler) postTransform() error { - if len(t.html.transformNotFound) == 0 && len(t.text.transformNotFound) == 0 { - return nil +func (t *templateHandler) getOrCreateTemplateInfo(name string) tpl.Info { + info, found := t.templateInfo[name] + if found { + return info } + info = newTemplateInfo(name) + t.templateInfo[name] = info + return info +} +func newTemplateInfo(name string) tpl.Info { + return tpl.Info{ + Manager: identity.NewIdentityManager(identity.NewPathIdentity(files.ComponentFolderLayouts, name)), + Config: tpl.DefaultConfig, + } +} + +func (t *templateHandler) postTransform() error { defer func() { t.text.transformNotFound = make(map[string]bool) t.html.transformNotFound = make(map[string]bool) + // TODO1 }() for _, s := range []struct { - lookup func(name string) *parse.Tree + lookup func(name string) *templateInfoTree transformNotFound map[string]bool + identityNotFound map[string][]tpl.Info }{ // 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}, + info := t.getOrCreateTemplateInfo(name) + return &templateInfoTree{ + 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}, + info := t.getOrCreateTemplateInfo(name) + return &templateInfoTree{ + info: info, + tree: templT.Tree, + } + }, t.text.transformNotFound, t.text.identityNotFound}, } { for name := range s.transformNotFound { templ := s.lookup(name) @@ -643,6 +680,16 @@ func (t *templateHandler) postTransform() error { } } } + + for k, v := range s.identityNotFound { + templ := s.lookup(k) + if templ != nil { + id := templ.info.GetIdentity() + for _, im := range v { + im.Add(id) + } + } + } } return nil @@ -677,8 +724,6 @@ func (t *templateHandler) AddTemplate(name, tpl string) error { // 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 { if err := t.postTransform(); err != nil { return err @@ -839,7 +884,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 } @@ -879,7 +924,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 @@ -951,7 +996,7 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e typ := resolveTemplateType(name) - c, err := applyTemplateTransformersToHMLTTemplate(typ, templ) + c, err := t.applyTemplateTransformersToHMLTTemplate(typ, templ) if err != nil { return err } diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go index e25e70e350e..900c9cbed5d 100644 --- a/tpl/tplimpl/template_ast_transformers.go +++ b/tpl/tplimpl/template_ast_transformers.go @@ -47,10 +47,11 @@ const ( ) type templateContext struct { - decl decl - visited map[string]bool - notFound map[string]bool - lookupFn func(name string) *parse.Tree + decl decl + visited map[string]bool + templateNotFound map[string]bool + identityNotFound map[string]bool + lookupFn func(name string) *templateInfoTree // The last error encountered. err error @@ -67,7 +68,7 @@ type templateContext struct { returnNode *parse.CommandNode } -func (c templateContext) getIfNotVisited(name string) *parse.Tree { +func (c templateContext) getIfNotVisited(name string) *templateInfoTree { if c.visited[name] { return nil } @@ -77,60 +78,98 @@ 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(info tpl.Info, lookupFn func(name string) *templateInfoTree) *templateContext { + if info.Manager == nil { + panic("identity manager not set") + } return &templateContext{ - Info: tpl.Info{Config: tpl.DefaultConfig}, - lookupFn: lookupFn, - decl: make(map[string]string), - visited: make(map[string]bool), - notFound: make(map[string]bool)} + Info: info, + lookupFn: lookupFn, + decl: make(map[string]string), + visited: make(map[string]bool), + templateNotFound: make(map[string]bool), + identityNotFound: make(map[string]bool), + } +} + +func createParseTreeLookup(templ *template.Template) func(nn string) *templateInfoTree { + return createParseTreeLookupFor(templ, func(name string) tpl.Info { return newTemplateInfo(name) }) + } -func createParseTreeLookup(templ *template.Template) func(nn string) *parse.Tree { - return func(nn string) *parse.Tree { +func createParseTreeLookupFor(templ *template.Template, infoFn func(name string) tpl.Info) func(nn string) *templateInfoTree { + return func(nn string) *templateInfoTree { tt := templ.Lookup(nn) if tt != nil { - return tt.Tree + return &templateInfoTree{ + tree: tt.Tree, + info: infoFn(nn), + } } return nil } } -func applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) { - return applyTemplateTransformers(typ, templ.Tree, createParseTreeLookup(templ)) +func (t *templateHandler) createParseTreeLookup(templ *template.Template) func(nn string) *templateInfoTree { + return createParseTreeLookupFor(templ, func(name string) tpl.Info { return t.templateInfo[name] }) } -func applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) { - return applyTemplateTransformers(typ, templ.Tree, - func(nn string) *parse.Tree { +func (t *templateHandler) applyTemplateTransformersToHMLTTemplate(typ templateType, templ *template.Template) (*templateContext, error) { + ti := &templateInfoTree{ + tree: templ.Tree, + info: t.getOrCreateTemplateInfo(templ.Name()), + } + return applyTemplateTransformers(typ, ti, t.createParseTreeLookup(templ)) +} + +func (t *templateHandler) applyTemplateTransformersToTextTemplate(typ templateType, templ *texttemplate.Template) (*templateContext, error) { + ti := &templateInfoTree{ + tree: templ.Tree, + info: t.getOrCreateTemplateInfo(templ.Name()), + } + + return applyTemplateTransformers(typ, ti, + func(nn string) *templateInfoTree { tt := templ.Lookup(nn) if tt != nil { - return tt.Tree + return &templateInfoTree{ + tree: tt.Tree, + info: t.getOrCreateTemplateInfo(nn), + } } return nil }) } -func applyTemplateTransformers(typ templateType, templ *parse.Tree, lookupFn func(name string) *parse.Tree) (*templateContext, error) { +type templateInfoTree struct { + info tpl.Info + tree *parse.Tree +} + +func applyTemplateTransformers( + typ templateType, + templ *templateInfoTree, + lookupFn func(name string) *templateInfoTree) (*templateContext, error) { + if templ == nil { return nil, errors.New("expected template, but none provided") } - c := newTemplateContext(lookupFn) + c := newTemplateContext(templ.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) + templ.tree.Root = c.wrapInPartialReturnWrapper(templ.tree.Root) } return c, err @@ -211,9 +250,10 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) { case *parse.RangeNode: c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList) case *parse.TemplateNode: + // TODO1 subTempl := c.getIfNotVisited(x.Name) if subTempl != nil { - c.applyTransformationsToNodes(subTempl.Root) + c.applyTransformationsToNodes(subTempl.tree.Root) } case *parse.PipeNode: c.collectConfig(x) @@ -230,6 +270,25 @@ func (c *templateContext) applyTransformations(n parse.Node) (bool, error) { } case *parse.CommandNode: + if len(x.Args) > 1 { + if id, ok := x.Args[0].(*parse.IdentifierNode); ok { + if id.Ident == "partial" { + partialName := strings.Trim(x.Args[1].String(), "\"") + if !strings.Contains(partialName, ".") { + partialName += ".html" + } + partialName = "partials/" + partialName + info := c.lookupFn(partialName) + if info != nil { + c.Info.Add(info.info) + } else { + // Delay for later + c.identityNotFound[partialName] = true + } + } + } + } + c.collectInner(x) keep := c.collectReturnNode(x) diff --git a/tpl/tplimpl/template_ast_transformers_test.go b/tpl/tplimpl/template_ast_transformers_test.go index 682af277239..76f6f54977a 100644 --- a/tpl/tplimpl/template_ast_transformers_test.go +++ b/tpl/tplimpl/template_ast_transformers_test.go @@ -21,11 +21,19 @@ import ( "github.com/gohugoio/hugo/tpl" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/cast" qt "github.com/frankban/quicktest" ) +var eq = qt.CmpEquals( + cmp.Comparer(func(i1, i2 tpl.Info) bool { + return cmp.Equal(i1, i2, cmpopts.IgnoreFields(tpl.Info{}, "Manager")) + }), +) + type paramsHolder struct { params map[string]interface{} page *paramsHolder @@ -218,7 +226,7 @@ func TestParamsKeysToLower(t *testing.T) { c.Assert(err, qt.IsNil) - ctx := newTemplateContext(createParseTreeLookup(templ)) + ctx := newTemplateContext(newTemplateInfo("test"), createParseTreeLookup(templ)) c.Assert(ctx.decl.indexOfReplacementStart([]string{}), qt.Equals, -1) @@ -307,7 +315,7 @@ func BenchmarkTemplateParamsKeysToLower(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - c := newTemplateContext(createParseTreeLookup(templates[i])) + c := newTemplateContext(newTemplateInfo("test"), createParseTreeLookup(templates[i])) c.applyTransformations(templ.Tree.Root) } } @@ -347,7 +355,7 @@ Pretty First3: {{ $__amber_4.COLORS.PRETTY.FIRST}} c.Assert(err, qt.IsNil) - ctx := newTemplateContext(createParseTreeLookup(templ)) + ctx := newTemplateContext(newTemplateInfo("test"), createParseTreeLookup(templ)) ctx.applyTransformations(templ.Tree.Root) @@ -392,7 +400,7 @@ P2: {{ .Params.LOWER }} c.Assert(err, qt.IsNil) overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) - ctx := newTemplateContext(createParseTreeLookup(overlayTpl)) + ctx := newTemplateContext(newTemplateInfo("test"), createParseTreeLookup(overlayTpl)) ctx.applyTransformations(overlayTpl.Tree.Root) @@ -414,7 +422,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" }} @@ -423,7 +431,10 @@ func TestTransformRecursiveTemplate(t *testing.T) { templ, err := template.New("foo").Parse(recursive) c.Assert(err, qt.IsNil) - ctx := newTemplateContext(createParseTreeLookup(templ)) + ctx := newTemplateContext( + newTemplateInfo("test"), + createParseTreeLookup(templ), + ) ctx.applyTransformations(templ.Tree.Root) } @@ -540,11 +551,12 @@ func TestCollectInfo(t *testing.T) { templ, err := template.New("foo").Funcs(funcs).Parse(test.tplString) c.Assert(err, qt.IsNil) - ctx := newTemplateContext(createParseTreeLookup(templ)) + ctx := newTemplateContext( + newTemplateInfo("test"), createParseTreeLookup(templ)) ctx.typ = templateShortcode ctx.applyTransformations(templ.Tree.Root) - c.Assert(ctx.Info, qt.Equals, test.expected) + c.Assert(ctx.Info, eq, test.expected) }) } @@ -582,7 +594,7 @@ 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: newTemplateInfo("test")}, createParseTreeLookup(templ)) // Just check that it doesn't fail in this test. We have functional tests // in hugoblib. diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go index 10fbc2375c0..4bb9432a85f 100644 --- a/tpl/tplimpl/template_funcs_test.go +++ b/tpl/tplimpl/template_funcs_test.go @@ -21,6 +21,9 @@ import ( "testing" "time" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/resources/page" @@ -101,6 +104,8 @@ func TestTemplateFuncsExamples(t *testing.T) { depsCfg.Fs = fs d, err := deps.New(depsCfg) c.Assert(err, qt.IsNil) + d.ContentSpec, err = helpers.NewContentSpec(converter.ProviderConfig{Cfg: d.Cfg}) + c.Assert(err, qt.IsNil) var data struct { Title string