Make .Content (almost) always available in shortcodes

This resolves some surprising behaviour when reading other pages' content from shortcodes. Before this commit, that behaviour was undefined. Note that this has never been an issue from regular templates.

It will still not be possible to get **the current shortcode's  page's rendered content**. That would have impressed Einstein.

The new and well defined rules are:

* `.Page.Content` from a shortcode will be empty. The related `.Page.Truncated` `.Page.Summary`, `.Page.WordCount`, `.Page.ReadingTime`, `.Page.Plain` and `.Page.PlainWords` will also have empty values.
* For _other pages_ (retrieved via `.Page.Site.GetPage`, `.Site.Pages` etc.) the `.Content` is there to use as you please as long as you don't have infinite content recursion in your shortcode/content setup. See below.
* `.Page.TableOfContents` is good to go (but does not support shortcodes in headlines; this is unchanged)

If you get into a situation of infinite recursion, the `.Content` will be empty. Run `hugo -v` for more information.

Fixes #4632
Fixes #4653
Fixes #4655
This commit is contained in:
Bjørn Erik Pedersen
2018-04-19 18:06:40 +02:00
parent d6a2024e6b
commit 4d26ab33dc
18 changed files with 392 additions and 153 deletions

View File

@@ -15,6 +15,7 @@ package hugolib
import (
"bytes"
"context"
"errors"
"fmt"
"reflect"
@@ -89,6 +90,7 @@ const (
type Page struct {
*pageInit
*pageContentInit
// Kind is the discriminator that identifies the different page types
// in the different page collections. This can, as an example, be used
@@ -127,17 +129,22 @@ type Page struct {
// Params contains configuration defined in the params section of page frontmatter.
params map[string]interface{}
// Called when needed to init the content (render shortcodes etc.).
contentInitFn func(p *Page) func()
// Content sections
content template.HTML
Summary template.HTML
contentv template.HTML
summary template.HTML
TableOfContents template.HTML
// Passed to the shortcodes
pageWithoutContent *PageWithoutContent
Aliases []string
Images []Image
Videos []Video
Truncated bool
truncated bool
Draft bool
Status string
@@ -263,8 +270,69 @@ type Page struct {
targetPathDescriptorPrototype *targetPathDescriptor
}
func (p *Page) initContent() {
p.contentInit.Do(func() {
// This careful dance is here to protect against circular loops in shortcode/content
// constructs.
// TODO(bep) context vs the remote shortcodes
ctx, cancel := context.WithTimeout(context.Background(), p.s.Timeout)
defer cancel()
c := make(chan error, 1)
go func() {
var err error
p.contentInitMu.Lock()
defer p.contentInitMu.Unlock()
if p.contentInitFn != nil {
p.contentInitFn(p)()
}
if len(p.summary) == 0 {
if err = p.setAutoSummary(); err != nil {
err = fmt.Errorf("Failed to set user auto summary for page %q: %s", p.pathOrTitle(), err)
}
}
c <- err
}()
select {
case <-ctx.Done():
p.s.Log.WARN.Printf(`WARNING: Timed out creating content for page %q (.Content will be empty). This is most likely a circular shortcode content loop that should be fixed. If this is just a shortcode calling a slow remote service, try to set "timeout=20000" (or higher, value is in milliseconds) in config.toml.`, p.pathOrTitle())
case err := <-c:
if err != nil {
p.s.Log.ERROR.Println(err)
}
}
})
}
// This is sent to the shortcodes for this page. Not doing that will create an infinite regress. So,
// shortcodes can access .Page.TableOfContents, but not .Page.Content etc.
func (p *Page) withoutContent() *PageWithoutContent {
p.pageInit.withoutContentInit.Do(func() {
p.pageWithoutContent = &PageWithoutContent{Page: p}
})
return p.pageWithoutContent
}
func (p *Page) Content() (interface{}, error) {
return p.content, nil
return p.content(), nil
}
func (p *Page) Truncated() bool {
p.initContent()
return p.truncated
}
func (p *Page) content() template.HTML {
p.initContent()
return p.contentv
}
func (p *Page) Summary() template.HTML {
p.initContent()
return p.summary
}
// Sites is a convenience method to get all the Hugo sites/languages configured.
@@ -341,9 +409,25 @@ type pageInit struct {
pageMenusInit sync.Once
pageMetaInit sync.Once
pageOutputInit sync.Once
plainInit sync.Once
plainWordsInit sync.Once
renderingConfigInit sync.Once
withoutContentInit sync.Once
}
type pageContentInit struct {
contentInitMu sync.Mutex
contentInit sync.Once
plainInit sync.Once
plainWordsInit sync.Once
}
func (p *Page) resetContent(init func(page *Page) func()) {
p.pageContentInit = &pageContentInit{}
if init == nil {
init = func(page *Page) func() {
return func() {}
}
}
p.contentInitFn = init
}
// IsNode returns whether this is an item of one of the list types in Hugo,
@@ -455,26 +539,34 @@ func (p *Page) createWorkContentCopy() {
}
func (p *Page) Plain() string {
p.initPlain()
p.initContent()
p.initPlain(true)
return p.plain
}
func (p *Page) PlainWords() []string {
p.initPlainWords()
return p.plainWords
}
func (p *Page) initPlain() {
func (p *Page) initPlain(lock bool) {
p.plainInit.Do(func() {
p.plain = helpers.StripHTML(string(p.content))
return
if lock {
p.contentInitMu.Lock()
defer p.contentInitMu.Unlock()
}
p.plain = helpers.StripHTML(string(p.contentv))
})
}
func (p *Page) initPlainWords() {
func (p *Page) PlainWords() []string {
p.initContent()
p.initPlainWords(true)
return p.plainWords
}
func (p *Page) initPlainWords(lock bool) {
p.plainWordsInit.Do(func() {
p.plainWords = strings.Fields(p.Plain())
return
if lock {
p.contentInitMu.Lock()
defer p.contentInitMu.Unlock()
}
p.plainWords = strings.Fields(p.plain)
})
}
@@ -622,7 +714,7 @@ func (p *Page) replaceDivider(content []byte) []byte {
replaced, truncated := replaceDivider(content, summaryDivider, internalSummaryDivider)
p.Truncated = truncated
p.truncated = truncated
return replaced
}
@@ -641,7 +733,7 @@ func (p *Page) setUserDefinedSummaryIfProvided(rawContentCopy []byte) (*summaryC
return nil, nil
}
p.Summary = helpers.BytesToHTML(sc.summary)
p.summary = helpers.BytesToHTML(sc.summary)
return sc, nil
}
@@ -731,15 +823,21 @@ func splitUserDefinedSummaryAndContent(markup string, c []byte) (sc *summaryCont
func (p *Page) setAutoSummary() error {
var summary string
var truncated bool
// This careful init dance could probably be refined, but it is purely for performance
// reasons. These "plain" methods are expensive if the plain content is never actually
// used.
p.initPlain(false)
if p.isCJKLanguage {
summary, truncated = p.s.ContentSpec.TruncateWordsByRune(p.PlainWords())
p.initPlainWords(false)
summary, truncated = p.s.ContentSpec.TruncateWordsByRune(p.plainWords)
} else {
summary, truncated = p.s.ContentSpec.TruncateWordsToWholeSentence(p.Plain())
summary, truncated = p.s.ContentSpec.TruncateWordsToWholeSentence(p.plain)
}
p.Summary = template.HTML(summary)
p.Truncated = truncated
p.summary = template.HTML(summary)
p.truncated = truncated
return nil
}
func (p *Page) renderContent(content []byte) []byte {
@@ -788,11 +886,12 @@ func (s *Site) newPage(filename string) *Page {
func (s *Site) newPageFromFile(fi *fileInfo) *Page {
return &Page{
pageInit: &pageInit{},
Kind: kindFromFileInfo(fi),
contentType: "",
Source: Source{File: fi},
Keywords: []string{}, Sitemap: Sitemap{Priority: -1},
pageInit: &pageInit{},
pageContentInit: &pageContentInit{},
Kind: kindFromFileInfo(fi),
contentType: "",
Source: Source{File: fi},
Keywords: []string{}, Sitemap: Sitemap{Priority: -1},
params: make(map[string]interface{}),
translations: make(Pages, 0),
sections: sectionsFromFile(fi),
@@ -876,10 +975,11 @@ func (p *Page) FuzzyWordCount() int {
}
func (p *Page) analyzePage() {
p.initContent()
p.pageMetaInit.Do(func() {
if p.isCJKLanguage {
p.wordCount = 0
for _, word := range p.PlainWords() {
for _, word := range p.plainWords {
runeCount := utf8.RuneCountInString(word)
if len(word) == runeCount {
p.wordCount++
@@ -888,7 +988,7 @@ func (p *Page) analyzePage() {
}
}
} else {
p.wordCount = helpers.TotalWords(p.Plain())
p.wordCount = helpers.TotalWords(p.plain)
}
// TODO(bep) is set in a test. Fix that.
@@ -1045,10 +1145,8 @@ func (p *Page) subResourceTargetPathFactory(base string) string {
return path.Join(p.relTargetPathBase, base)
}
func (p *Page) prepareForRender(cfg *BuildCfg) error {
s := p.s
if !p.shouldRenderTo(s.rc.Format) {
func (p *Page) setContentInit(cfg *BuildCfg) error {
if !p.shouldRenderTo(p.s.rc.Format) {
// No need to prepare
return nil
}
@@ -1058,11 +1156,40 @@ func (p *Page) prepareForRender(cfg *BuildCfg) error {
shortcodeUpdate = p.shortcodeState.updateDelta()
}
if !shortcodeUpdate && !cfg.whatChanged.other {
// No need to process it again.
return nil
resetFunc := func(page *Page) func() {
return func() {
err := page.prepareForRender(cfg)
if err != nil {
p.s.Log.ERROR.Printf("Failed to prepare page %q for render: %s", page.Path(), err)
}
}
}
if shortcodeUpdate || cfg.whatChanged.other {
p.resetContent(resetFunc)
}
// Handle bundled pages.
for _, r := range p.Resources.ByType(pageResourceType) {
shortcodeUpdate = false
bp := r.(*Page)
if bp.shortcodeState != nil {
shortcodeUpdate = bp.shortcodeState.updateDelta()
}
if shortcodeUpdate || cfg.whatChanged.other {
p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Pages)
bp.resetContent(resetFunc)
}
}
return nil
}
func (p *Page) prepareForRender(cfg *BuildCfg) error {
s := p.s
// If we got this far it means that this is either a new Page pointer
// or a template or similar has changed so wee need to do a rerendering
// of the shortcodes etc.
@@ -1080,14 +1207,10 @@ func (p *Page) prepareForRender(cfg *BuildCfg) error {
workContentCopy = p.workContent
}
if p.Markup == "markdown" {
tmpContent, tmpTableOfContents := helpers.ExtractTOC(workContentCopy)
p.TableOfContents = helpers.BytesToHTML(tmpTableOfContents)
workContentCopy = tmpContent
}
var err error
if workContentCopy, err = handleShortcodes(p, workContentCopy); err != nil {
// Note: The shortcodes in a page cannot access the page content it lives in,
// hence the withoutContent().
if workContentCopy, err = handleShortcodes(p.withoutContent(), workContentCopy); err != nil {
s.Log.ERROR.Printf("Failed to handle shortcodes for page %s: %s", p.BaseFileName(), err)
}
@@ -1102,28 +1225,10 @@ func (p *Page) prepareForRender(cfg *BuildCfg) error {
workContentCopy = summaryContent.content
}
p.content = helpers.BytesToHTML(workContentCopy)
if summaryContent == nil {
if err := p.setAutoSummary(); err != nil {
s.Log.ERROR.Printf("Failed to set user auto summary for page %q: %s", p.pathOrTitle(), err)
}
}
p.contentv = helpers.BytesToHTML(workContentCopy)
} else {
p.content = helpers.BytesToHTML(workContentCopy)
}
//analyze for raw stats
p.analyzePage()
// Handle bundled pages.
for _, r := range p.Resources.ByType(pageResourceType) {
p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Pages)
bp := r.(*Page)
if err := bp.prepareForRender(cfg); err != nil {
s.Log.ERROR.Printf("Failed to prepare bundled page %q for render: %s", bp.BaseFileName(), err)
}
p.contentv = helpers.BytesToHTML(workContentCopy)
}
return nil
@@ -1701,9 +1806,10 @@ func (p *Page) SaveSource() error {
return p.SaveSourceAs(p.FullFilePath())
}
// TODO(bep) lazy consolidate
func (p *Page) processShortcodes() error {
p.shortcodeState = newShortcodeHandler(p)
tmpContent, err := p.shortcodeState.extractShortcodes(string(p.workContent), p)
tmpContent, err := p.shortcodeState.extractShortcodes(string(p.workContent), p.withoutContent())
if err != nil {
return err
}
@@ -1724,7 +1830,7 @@ func (p *Page) prepareLayouts() error {
if p.Kind == KindPage {
if !p.IsRenderable() {
self := "__" + p.UniqueID()
err := p.s.TemplateHandler().AddLateTemplate(self, string(p.content))
err := p.s.TemplateHandler().AddLateTemplate(self, string(p.content()))
if err != nil {
return err
}
@@ -1833,8 +1939,11 @@ func (p *Page) updatePageDates() {
// copy creates a copy of this page with the lazy sync.Once vars reset
// so they will be evaluated again, for word count calculations etc.
func (p *Page) copy() *Page {
p.contentInitMu.Lock()
c := *p
p.contentInitMu.Unlock()
c.pageInit = &pageInit{}
c.pageContentInit = &pageContentInit{}
return &c
}