mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-24 21:56:05 +02:00
Add render template hooks for links and images
This commit also * revises the change detection for templates used by content files in server mode. * Adds a Page.RenderString method Fixes #6545 Fixes #4663 Closes #6043
This commit is contained in:
@@ -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,160 +62,182 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func newPageContentOutput(p *pageState) func(f output.Format) (*pageContentOutput, error) {
|
||||
var pageContentOutputDependenciesID = identity.KeyValueIdentity{Key: "pageOutput", Value: "dependencies"}
|
||||
|
||||
func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, error) {
|
||||
|
||||
parent := p.init
|
||||
|
||||
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
|
||||
}
|
||||
defer func() {
|
||||
// See https://github.com/gohugoio/hugo/issues/6210
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("%s", r)
|
||||
p.s.Log.ERROR.Printf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
|
||||
var hasVariants bool
|
||||
|
||||
cp.contentPlaceholders, hasVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.render && !hasVariants {
|
||||
// We can reuse this for the other output formats
|
||||
cp.enableReuse()
|
||||
}
|
||||
|
||||
cp.workContent = p.contentToRender(cp.contentPlaceholders)
|
||||
|
||||
isHTML := cp.p.m.markup == "html"
|
||||
|
||||
if p.renderable {
|
||||
if !isHTML {
|
||||
r, err := cp.renderContent(cp.workContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cp.convertedResult = r
|
||||
cp.workContent = r.Bytes()
|
||||
|
||||
if _, ok := r.(converter.TableOfContentsProvider); !ok {
|
||||
tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
|
||||
cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
|
||||
cp.workContent = tmpContent
|
||||
}
|
||||
}
|
||||
|
||||
if cp.placeholdersEnabled {
|
||||
// ToC was accessed via .Page.TableOfContents in the shortcode,
|
||||
// at a time when the ToC wasn't ready.
|
||||
cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
|
||||
}
|
||||
|
||||
if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
|
||||
// There are one or more replacement tokens to be replaced.
|
||||
cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cp.p.source.hasSummaryDivider {
|
||||
if isHTML {
|
||||
src := p.source.parsed.Input()
|
||||
|
||||
// Use the summary sections as they are provided by the user.
|
||||
if p.source.posSummaryEnd != -1 {
|
||||
cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
|
||||
}
|
||||
|
||||
if cp.p.source.posBodyStart != -1 {
|
||||
cp.workContent = src[cp.p.source.posBodyStart:]
|
||||
}
|
||||
|
||||
} else {
|
||||
summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
|
||||
if err != nil {
|
||||
cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
|
||||
} else {
|
||||
cp.workContent = content
|
||||
cp.summary = helpers.BytesToHTML(summary)
|
||||
}
|
||||
}
|
||||
} else if cp.p.m.summary != "" {
|
||||
b, err := cp.p.getContentConverter().Convert(
|
||||
converter.RenderContext{
|
||||
Src: []byte(cp.p.m.summary),
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
|
||||
cp.summary = helpers.BytesToHTML(html)
|
||||
}
|
||||
}
|
||||
|
||||
cp.content = helpers.BytesToHTML(cp.workContent)
|
||||
|
||||
if !p.renderable {
|
||||
err := cp.addSelfTemplate()
|
||||
return err
|
||||
}
|
||||
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()))
|
||||
}
|
||||
}()
|
||||
|
||||
if err := po.initRenderHooks(); err != nil {
|
||||
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()
|
||||
var hasShortcodeVariants bool
|
||||
|
||||
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()
|
||||
})
|
||||
f := po.f
|
||||
cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
enableReuse := !(hasShortcodeVariants || cp.renderHooksHaveVariants)
|
||||
|
||||
if err := cp.setAutoSummary(); err != nil {
|
||||
return err, nil
|
||||
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()
|
||||
}
|
||||
|
||||
cp.workContent = p.contentToRender(cp.contentPlaceholders)
|
||||
|
||||
isHTML := cp.p.m.markup == "html"
|
||||
|
||||
if p.renderable {
|
||||
if !isHTML {
|
||||
r, err := cp.renderContent(cp.workContent, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cp.workContent = r.Bytes()
|
||||
|
||||
if tocProvider, ok := r.(converter.TableOfContentsProvider); ok {
|
||||
cfg := p.s.ContentSpec.Converters.GetMarkupConfig()
|
||||
cp.tableOfContents = template.HTML(
|
||||
tocProvider.TableOfContents().ToHTML(
|
||||
cfg.TableOfContents.StartLevel,
|
||||
cfg.TableOfContents.EndLevel,
|
||||
cfg.TableOfContents.Ordered,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent)
|
||||
cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents)
|
||||
cp.workContent = tmpContent
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
})
|
||||
if cp.placeholdersEnabled {
|
||||
// ToC was accessed via .Page.TableOfContents in the shortcode,
|
||||
// at a time when the ToC wasn't ready.
|
||||
cp.contentPlaceholders[tocShortcodePlaceholder] = string(cp.tableOfContents)
|
||||
}
|
||||
|
||||
return cp, nil
|
||||
if p.cmap.hasNonMarkdownShortcode || cp.placeholdersEnabled {
|
||||
// There are one or more replacement tokens to be replaced.
|
||||
cp.workContent, err = replaceShortcodeTokens(cp.workContent, cp.contentPlaceholders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cp.p.source.hasSummaryDivider {
|
||||
if isHTML {
|
||||
src := p.source.parsed.Input()
|
||||
|
||||
// Use the summary sections as they are provided by the user.
|
||||
if p.source.posSummaryEnd != -1 {
|
||||
cp.summary = helpers.BytesToHTML(src[p.source.posMainContent:p.source.posSummaryEnd])
|
||||
}
|
||||
|
||||
if cp.p.source.posBodyStart != -1 {
|
||||
cp.workContent = src[cp.p.source.posBodyStart:]
|
||||
}
|
||||
|
||||
} else {
|
||||
summary, content, err := splitUserDefinedSummaryAndContent(cp.p.m.markup, cp.workContent)
|
||||
if err != nil {
|
||||
cp.p.s.Log.ERROR.Printf("Failed to set user defined summary for page %q: %s", cp.p.pathOrTitle(), err)
|
||||
} else {
|
||||
cp.workContent = content
|
||||
cp.summary = helpers.BytesToHTML(summary)
|
||||
}
|
||||
}
|
||||
} else if cp.p.m.summary != "" {
|
||||
b, err := cp.renderContent([]byte(cp.p.m.summary), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
html := cp.p.s.ContentSpec.TrimShortHTML(b.Bytes())
|
||||
cp.summary = helpers.BytesToHTML(html)
|
||||
}
|
||||
}
|
||||
|
||||
cp.content = helpers.BytesToHTML(cp.workContent)
|
||||
|
||||
if !p.renderable {
|
||||
err := cp.addSelfTemplate()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Recursive loops can only happen in content files with template code (shortcodes etc.)
|
||||
// Avoid creating new goroutines if we don't have to.
|
||||
needTimeout := !p.renderable || p.shortcodeState.hasShortcodes()
|
||||
needTimeout = needTimeout || cp.renderHooks != nil
|
||||
|
||||
if needTimeout {
|
||||
cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (interface{}, error) {
|
||||
return nil, initContent()
|
||||
})
|
||||
} else {
|
||||
cp.initMain = parent.Branch(func() (interface{}, error) {
|
||||
return nil, initContent()
|
||||
})
|
||||
}
|
||||
|
||||
cp.initPlain = cp.initMain.Branch(func() (interface{}, error) {
|
||||
cp.plain = helpers.StripHTML(string(cp.content))
|
||||
cp.plainWords = strings.Fields(cp.plain)
|
||||
cp.setWordCounts(p.m.isCJKLanguage)
|
||||
|
||||
if err := cp.setAutoSummary(); err != nil {
|
||||
return err, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
return cp, nil
|
||||
|
||||
}
|
||||
|
||||
// pageContentOutput represents the Page content for a given output format.
|
||||
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
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user