diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-11-27 13:42:36 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2019-12-18 11:44:40 +0100 |
commit | e625088ef5a970388ad50e464e87db56b358dac4 (patch) | |
tree | f7b26dec1f3695411558d05ca7d0995817a42250 /hugolib/page__per_output.go | |
parent | 67f3aa72cf9aaf3d6e447fa6bc12de704d46adf7 (diff) |
Add render template hooks for links and images
This commit also
* revises the change detection for templates used by content files in server mode.
* Adds a Page.RenderString method
Fixes #6545
Fixes #4663
Closes #6043
Diffstat (limited to 'hugolib/page__per_output.go')
-rw-r--r-- | hugolib/page__per_output.go | 303 |
1 files changed, 180 insertions, 123 deletions
diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index d3a32e15c..03448ba80 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -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 } |