Make the RenderString content provider fix more general
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 27 Jan 2022 08:46:51 +0000 (09:46 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 27 Jan 2022 10:51:13 +0000 (11:51 +0100)
Updates #9383

hugolib/page.go
hugolib/page__common.go
hugolib/page__new.go
hugolib/page__output.go
hugolib/page__per_output.go
hugolib/page_test.go
resources/page/page_lazy_contentprovider.go

index d5055e7c11ed49903d027ee16b5faea742e54954..11b41e169206389e1c3e894be9d826587ddb05a9 100644 (file)
@@ -16,15 +16,12 @@ package hugolib
 import (
        "bytes"
        "fmt"
-       "html/template"
        "os"
        "path"
        "path/filepath"
        "sort"
        "strings"
 
-       "github.com/mitchellh/mapstructure"
-
        "github.com/gohugoio/hugo/identity"
 
        "github.com/gohugoio/hugo/markup/converter"
@@ -47,7 +44,6 @@ import (
 
        "github.com/gohugoio/hugo/media"
        "github.com/gohugoio/hugo/source"
-       "github.com/spf13/cast"
 
        "github.com/gohugoio/hugo/common/collections"
        "github.com/gohugoio/hugo/common/text"
@@ -593,76 +589,6 @@ var defaultRenderStringOpts = renderStringOpts{
        Markup:  "", // Will inherit the page's value when not set.
 }
 
-func (p *pageState) RenderString(args ...interface{}) (template.HTML, error) {
-       if len(args) < 1 || len(args) > 2 {
-               return "", errors.New("want 1 or 2 arguments")
-       }
-
-       var s string
-       opts := defaultRenderStringOpts
-       sidx := 1
-
-       if len(args) == 1 {
-               sidx = 0
-       } else {
-               m, ok := args[0].(map[string]interface{})
-               if !ok {
-                       return "", errors.New("first argument must be a map")
-               }
-
-               if err := mapstructure.WeakDecode(m, &opts); err != nil {
-                       return "", errors.WithMessage(err, "failed to decode options")
-               }
-       }
-
-       var err error
-       s, err = cast.ToStringE(args[sidx])
-       if err != nil {
-               return "", err
-       }
-
-       if err = p.pageOutput.initRenderHooks(); err != nil {
-               return "", err
-       }
-
-       conv := p.getContentConverter()
-       if opts.Markup != "" && opts.Markup != p.m.markup {
-               var err error
-               // TODO(bep) consider cache
-               conv, err = p.m.newContentConverter(p, opts.Markup, nil)
-               if err != nil {
-                       return "", p.wrapError(err)
-               }
-       }
-
-       var cp *pageContentOutput
-
-       // If the current content provider is not yet initialized, do so now.
-       if lcp, ok := p.pageOutput.ContentProvider.(*page.LazyContentProvider); ok {
-               c := lcp.Init()
-               if pco, ok := c.(*pageContentOutput); ok {
-                       cp = pco
-               }
-       } else {
-               cp = p.pageOutput.cp
-       }
-
-       c, err := cp.renderContentWithConverter(conv, []byte(s), false)
-       if err != nil {
-               return "", p.wrapError(err)
-       }
-
-       b := c.Bytes()
-
-       if opts.Display == "inline" {
-               // We may have to rethink this in the future when we get other
-               // renderers.
-               b = p.s.ContentSpec.TrimShortHTML(b)
-       }
-
-       return template.HTML(string(b)), nil
-}
-
 func (p *pageState) addDependency(dep identity.Provider) {
        if !p.s.running() || p.pageOutput.cp == nil {
                return
@@ -670,29 +596,6 @@ func (p *pageState) addDependency(dep identity.Provider) {
        p.pageOutput.cp.dependencyTracker.Add(dep)
 }
 
-func (p *pageState) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) {
-       p.addDependency(info)
-       return p.Render(layout...)
-}
-
-func (p *pageState) Render(layout ...string) (template.HTML, error) {
-       templ, found, err := p.resolveTemplate(layout...)
-       if err != nil {
-               return "", p.wrapError(err)
-       }
-
-       if !found {
-               return "", nil
-       }
-
-       p.addDependency(templ.(tpl.Info))
-       res, err := executeToString(p.s.Tmpl(), templ, p)
-       if err != nil {
-               return "", p.wrapError(errors.Wrapf(err, "failed to execute template %q v", layout))
-       }
-       return template.HTML(res), nil
-}
-
 // wrapError adds some more context to the given error if possible/needed
 func (p *pageState) wrapError(err error) error {
        if _, ok := err.(*herrors.ErrorWithFileContext); ok {
@@ -993,13 +896,16 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
                if lcp, ok := (p.pageOutput.ContentProvider.(*page.LazyContentProvider)); ok {
                        lcp.Reset()
                } else {
-                       p.pageOutput.ContentProvider = page.NewLazyContentProvider(func() (page.ContentProvider, error) {
+                       lcp = page.NewLazyContentProvider(func() (page.OutputFormatContentProvider, error) {
                                cp, err := newPageContentOutput(p, p.pageOutput)
                                if err != nil {
                                        return nil, err
                                }
                                return cp, nil
                        })
+                       p.pageOutput.ContentProvider = lcp
+                       p.pageOutput.TableOfContentsProvider = lcp
+                       p.pageOutput.PageRenderProvider = lcp
                }
        }
 
index e718721f7fcd52da38d44e661ca045e9ebd7ae13..ccef79a3fe4991b46749d2638a5ea56e7a3b9efa 100644 (file)
@@ -64,7 +64,6 @@ type pageCommon struct {
        maps.Scratcher
        navigation.PageMenusProvider
        page.AuthorProvider
-       page.PageRenderProvider
        page.AlternativeOutputFormatsProvider
        page.ChildCareProvider
        page.FileProvider
index 8c96d5014dd60ff1c1d771e945199fa3548110cc..17bdb30ff6f9f0da6f10671909ccf5f13e578075 100644 (file)
@@ -86,7 +86,6 @@ func newPageBase(metaProvider *pageMeta) (*pageState, error) {
        ps.Eqer = ps
        ps.TranslationKeyProvider = ps
        ps.ShortcodeInfoProvider = ps
-       ps.PageRenderProvider = ps
        ps.AlternativeOutputFormatsProvider = ps
 
        return ps, nil
index 377e16df522191f1972976db34899d28d39b473d..4133234779786a654381e74bdc628e9810003526 100644 (file)
@@ -14,7 +14,6 @@
 package hugolib
 
 import (
-       "github.com/gohugoio/hugo/markup/converter"
        "github.com/gohugoio/hugo/output"
        "github.com/gohugoio/hugo/resources/page"
        "github.com/gohugoio/hugo/resources/resource"
@@ -59,6 +58,7 @@ func newPageOutput(
                pagePerOutputProviders:  providers,
                ContentProvider:         page.NopPage,
                TableOfContentsProvider: page.NopPage,
+               PageRenderProvider:      page.NopPage,
                render:                  render,
                paginator:               pag,
        }
@@ -84,73 +84,19 @@ type pageOutput struct {
        pagePerOutputProviders
        page.ContentProvider
        page.TableOfContentsProvider
+       page.PageRenderProvider
 
        // May be nil.
        cp *pageContentOutput
 }
 
-func (o *pageOutput) initRenderHooks() error {
-       if o.cp == nil {
-               return nil
-       }
-
-       var initErr error
-
-       o.cp.renderHooks.init.Do(func() {
-               ps := o.cp.p
-
-               c := ps.getContentConverter()
-               if c == nil || !c.Supports(converter.FeatureRenderHooks) {
-                       return
-               }
-
-               h, err := ps.createRenderHooks(o.f)
-               if err != nil {
-                       initErr = err
-                       return
-               }
-               o.cp.renderHooks.hooks = h
-
-               if !o.cp.renderHooksHaveVariants || h.IsZero() {
-                       // Check if there is a different render hooks template
-                       // for any of the other page output formats.
-                       // If not, we can reuse this.
-                       for _, po := range ps.pageOutputs {
-                               if po.f.Name != o.f.Name {
-                                       h2, err := ps.createRenderHooks(po.f)
-                                       if err != nil {
-                                               initErr = err
-                                               return
-                                       }
-
-                                       if h2.IsZero() {
-                                               continue
-                                       }
-
-                                       if o.cp.renderHooks.hooks.IsZero() {
-                                               o.cp.renderHooks.hooks = h2
-                                       }
-
-                                       o.cp.renderHooksHaveVariants = !h2.Eq(o.cp.renderHooks.hooks)
-
-                                       if o.cp.renderHooksHaveVariants {
-                                               break
-                                       }
-
-                               }
-                       }
-               }
-       })
-
-       return initErr
-}
-
 func (p *pageOutput) initContentProvider(cp *pageContentOutput) {
        if cp == nil {
                return
        }
        p.ContentProvider = cp
        p.TableOfContentsProvider = cp
+       p.PageRenderProvider = cp
        p.cp = cp
 }
 
index f59b5f9b545ee3538a6a30b204f49913b67063a0..bd4e35a5b0cd062624f235844e6969ad310f1ccb 100644 (file)
@@ -24,6 +24,9 @@ import (
        "unicode/utf8"
 
        "github.com/gohugoio/hugo/identity"
+       "github.com/mitchellh/mapstructure"
+       "github.com/pkg/errors"
+       "github.com/spf13/cast"
 
        "github.com/gohugoio/hugo/markup/converter/hooks"
 
@@ -94,7 +97,7 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err
                        }
                }()
 
-               if err := po.initRenderHooks(); err != nil {
+               if err := po.cp.initRenderHooks(); err != nil {
                        return err
                }
 
@@ -349,6 +352,145 @@ func (p *pageContentOutput) WordCount() int {
        return p.wordCount
 }
 
+func (p *pageContentOutput) RenderString(args ...interface{}) (template.HTML, error) {
+       if len(args) < 1 || len(args) > 2 {
+               return "", errors.New("want 1 or 2 arguments")
+       }
+
+       var s string
+       opts := defaultRenderStringOpts
+       sidx := 1
+
+       if len(args) == 1 {
+               sidx = 0
+       } else {
+               m, ok := args[0].(map[string]interface{})
+               if !ok {
+                       return "", errors.New("first argument must be a map")
+               }
+
+               if err := mapstructure.WeakDecode(m, &opts); err != nil {
+                       return "", errors.WithMessage(err, "failed to decode options")
+               }
+       }
+
+       var err error
+       s, err = cast.ToStringE(args[sidx])
+       if err != nil {
+               return "", err
+       }
+
+       if err = p.initRenderHooks(); err != nil {
+               return "", err
+       }
+
+       conv := p.p.getContentConverter()
+       if opts.Markup != "" && opts.Markup != p.p.m.markup {
+               var err error
+               // TODO(bep) consider cache
+               conv, err = p.p.m.newContentConverter(p.p, opts.Markup, nil)
+               if err != nil {
+                       return "", p.p.wrapError(err)
+               }
+       }
+
+       c, err := p.renderContentWithConverter(conv, []byte(s), false)
+       if err != nil {
+               return "", p.p.wrapError(err)
+       }
+
+       b := c.Bytes()
+
+       if opts.Display == "inline" {
+               // We may have to rethink this in the future when we get other
+               // renderers.
+               b = p.p.s.ContentSpec.TrimShortHTML(b)
+       }
+
+       return template.HTML(string(b)), nil
+}
+
+func (p *pageContentOutput) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) {
+       p.p.addDependency(info)
+       return p.Render(layout...)
+}
+
+func (p *pageContentOutput) Render(layout ...string) (template.HTML, error) {
+       templ, found, err := p.p.resolveTemplate(layout...)
+       if err != nil {
+               return "", p.p.wrapError(err)
+       }
+
+       if !found {
+               return "", nil
+       }
+
+       p.p.addDependency(templ.(tpl.Info))
+
+       // Make sure to send the *pageState and not the *pageContentOutput to the template.
+       res, err := executeToString(p.p.s.Tmpl(), templ, p.p)
+       if err != nil {
+               return "", p.p.wrapError(errors.Wrapf(err, "failed to execute template %q v", layout))
+       }
+       return template.HTML(res), nil
+}
+
+func (p *pageContentOutput) initRenderHooks() error {
+       if p == nil {
+               return nil
+       }
+
+       var initErr error
+
+       p.renderHooks.init.Do(func() {
+               ps := p.p
+
+               c := ps.getContentConverter()
+               if c == nil || !c.Supports(converter.FeatureRenderHooks) {
+                       return
+               }
+
+               h, err := ps.createRenderHooks(p.f)
+               if err != nil {
+                       initErr = err
+                       return
+               }
+               p.renderHooks.hooks = h
+
+               if !p.renderHooksHaveVariants || h.IsZero() {
+                       // Check if there is a different render hooks template
+                       // for any of the other page output formats.
+                       // If not, we can reuse this.
+                       for _, po := range ps.pageOutputs {
+                               if po.f.Name != p.f.Name {
+                                       h2, err := ps.createRenderHooks(po.f)
+                                       if err != nil {
+                                               initErr = err
+                                               return
+                                       }
+
+                                       if h2.IsZero() {
+                                               continue
+                                       }
+
+                                       if p.renderHooks.hooks.IsZero() {
+                                               p.renderHooks.hooks = h2
+                                       }
+
+                                       p.renderHooksHaveVariants = !h2.Eq(p.renderHooks.hooks)
+
+                                       if p.renderHooksHaveVariants {
+                                               break
+                                       }
+
+                               }
+                       }
+               }
+       })
+
+       return initErr
+}
+
 func (p *pageContentOutput) setAutoSummary() error {
        if p.p.source.hasSummaryDivider || p.p.m.summary != "" {
                return nil
index 48a81ee4aa060511b59241525455560768c6d80a..5e89278e1f4b23e4ae20869782fe4d79aa8dd9cf 100644 (file)
@@ -772,7 +772,7 @@ Here is the last report for commits in the year 2016. It covers hrev50718-hrev50
 func TestRenderStringForRegularPageTranslations(t *testing.T) {
        c := qt.New(t)
        b := newTestSitesBuilder(t)
-       b.WithLogger(loggers.NewBasicLoggerForWriter(jwalterweatherman.LevelDebug, os.Stderr))
+       b.WithLogger(loggers.NewBasicLoggerForWriter(jwalterweatherman.LevelError, os.Stderr))
 
        b.WithConfigFile("toml",
                `baseurl = "https://example.org/"
@@ -828,7 +828,6 @@ home = ["HTML", "JSON"]`)
 <p>bar</p>
 <p>bar</p>
 `)
-
 }
 
 // Issue 8919
index 9979856f8c50599c0895947c6f62264e89362723..206695d181a8d613cb22802670805ec0bd860100 100644 (file)
@@ -19,6 +19,16 @@ import (
        "github.com/gohugoio/hugo/lazy"
 )
 
+// OutputFormatContentProvider represents the method set that is "outputFormat aware" and that we
+// provide lazy initialization for in case they get invoked outside of their normal rendering context, e.g. via .Translations.
+// Note that this set is currently not complete, but should cover the most common use cases.
+// For the others, the implementation will be from the page.NoopPage.
+type OutputFormatContentProvider interface {
+       ContentProvider
+       TableOfContentsProvider
+       PageRenderProvider
+}
+
 // LazyContentProvider initializes itself when read. Each method of the
 // ContentProvider interface initializes a content provider and shares it
 // with other methods.
@@ -27,13 +37,13 @@ import (
 // will be needed. Must create via NewLazyContentProvider.
 type LazyContentProvider struct {
        init *lazy.Init
-       cp   ContentProvider
+       cp   OutputFormatContentProvider
 }
 
 // NewLazyContentProvider returns a LazyContentProvider initialized with
 // function f. The resulting LazyContentProvider calls f in order to
 // retrieve a ContentProvider
-func NewLazyContentProvider(f func() (ContentProvider, error)) *LazyContentProvider {
+func NewLazyContentProvider(f func() (OutputFormatContentProvider, error)) *LazyContentProvider {
        lcp := LazyContentProvider{
                init: lazy.New(),
                cp:   NopPage,
@@ -49,11 +59,6 @@ func NewLazyContentProvider(f func() (ContentProvider, error)) *LazyContentProvi
        return &lcp
 }
 
-func (lcp *LazyContentProvider) Init() ContentProvider {
-       lcp.init.Do()
-       return lcp.cp
-}
-
 func (lcp *LazyContentProvider) Reset() {
        lcp.init.Reset()
 }
@@ -102,3 +107,18 @@ func (lcp *LazyContentProvider) Len() int {
        lcp.init.Do()
        return lcp.cp.Len()
 }
+
+func (lcp *LazyContentProvider) Render(layout ...string) (template.HTML, error) {
+       lcp.init.Do()
+       return lcp.cp.Render(layout...)
+}
+
+func (lcp *LazyContentProvider) RenderString(args ...interface{}) (template.HTML, error) {
+       lcp.init.Do()
+       return lcp.cp.RenderString(args...)
+}
+
+func (lcp *LazyContentProvider) TableOfContents() template.HTML {
+       lcp.init.Do()
+       return lcp.cp.TableOfContents()
+}