summaryrefslogtreecommitdiffstats
path: root/hugolib/page__per_output.go
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-11-27 13:42:36 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2019-12-18 11:44:40 +0100
commite625088ef5a970388ad50e464e87db56b358dac4 (patch)
treef7b26dec1f3695411558d05ca7d0995817a42250 /hugolib/page__per_output.go
parent67f3aa72cf9aaf3d6e447fa6bc12de704d46adf7 (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.go303
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
}