diff options
author | Bjørn Erik Pedersen <[email protected]> | 2023-02-11 16:20:24 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2023-02-21 17:56:41 +0100 |
commit | 90da7664bf1f3a0ca2e18144b5deacf532c6e3cf (patch) | |
tree | 78d8ac72ebb2ccee4ca4bbeeb9add3365c743e90 /hugolib | |
parent | 0afec0a9f4aace1f5f4af6822aeda6223ee3e3a9 (diff) | |
download | hugo-90da7664bf1f3a0ca2e18144b5deacf532c6e3cf.tar.gz hugo-90da7664bf1f3a0ca2e18144b5deacf532c6e3cf.zip |
Add page fragments support to Related
The main topic of this commit is that you can now index fragments (content heading identifiers) when calling `.Related`.
You can do this by:
* Configure one or more indices with type `fragments`
* The name of those index configurations maps to an (optional) front matter slice with fragment references. This allows you to link
page<->fragment and page<->page.
* This also will index all the fragments (heading identifiers) of the pages.
It's also possible to use type `fragments` indices in shortcode, e.g.:
```
{{ $related := site.RegularPages.Related .Page }}
```
But, and this is important, you need to include the shortcode using the `{{<` delimiter. Not doing so will create infinite loops and timeouts.
This commit also:
* Adds two new methods to Page: Fragments (can also be used to build ToC) and HeadingsFiltered (this is only used in Related Content with
index type `fragments` and `enableFilter` set to true.
* Consolidates all `.Related*` methods into one, which takes either a `Page` or an options map as its only argument.
* Add `context.Context` to all of the content related Page API. Turns out it wasn't strictly needed for this particular feature, but it will
soon become usefil, e.g. in #9339.
Closes #10711
Updates #9339
Updates #10725
Diffstat (limited to 'hugolib')
-rw-r--r-- | hugolib/content_factory.go | 3 | ||||
-rw-r--r-- | hugolib/content_map_page.go | 2 | ||||
-rw-r--r-- | hugolib/embedded_shortcodes_test.go | 3 | ||||
-rw-r--r-- | hugolib/hugo_sites.go | 14 | ||||
-rw-r--r-- | hugolib/hugo_sites_build.go | 2 | ||||
-rw-r--r-- | hugolib/hugo_sites_build_errors_test.go | 2 | ||||
-rw-r--r-- | hugolib/image_test.go | 153 | ||||
-rw-r--r-- | hugolib/language_content_dir_test.go | 3 | ||||
-rw-r--r-- | hugolib/page.go | 48 | ||||
-rw-r--r-- | hugolib/page__content.go | 14 | ||||
-rw-r--r-- | hugolib/page__menus.go | 7 | ||||
-rw-r--r-- | hugolib/page__new.go | 3 | ||||
-rw-r--r-- | hugolib/page__per_output.go | 219 | ||||
-rw-r--r-- | hugolib/page__position.go | 6 | ||||
-rw-r--r-- | hugolib/page_test.go | 53 | ||||
-rw-r--r-- | hugolib/shortcode.go | 101 | ||||
-rw-r--r-- | hugolib/shortcode_page.go | 41 | ||||
-rw-r--r-- | hugolib/shortcode_test.go | 28 | ||||
-rw-r--r-- | hugolib/site.go | 22 | ||||
-rw-r--r-- | hugolib/site_render.go | 3 | ||||
-rw-r--r-- | hugolib/site_test.go | 7 | ||||
-rw-r--r-- | hugolib/testhelpers_test.go | 3 |
22 files changed, 394 insertions, 343 deletions
diff --git a/hugolib/content_factory.go b/hugolib/content_factory.go index 017a0bc97..e22f46513 100644 --- a/hugolib/content_factory.go +++ b/hugolib/content_factory.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "fmt" "io" "path/filepath" @@ -83,7 +84,7 @@ func (f ContentFactory) ApplyArchetypeTemplate(w io.Writer, p page.Page, archety return fmt.Errorf("failed to parse archetype template: %s: %w", err, err) } - result, err := executeToString(ps.s.Tmpl(), templ, d) + result, err := executeToString(context.TODO(), ps.s.Tmpl(), templ, d) if err != nil { return fmt.Errorf("failed to execute archetype template: %s: %w", err, err) } diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index d8f28286c..70c5d6a27 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -171,7 +171,7 @@ func (m *pageMap) newPageFromContentNode(n *contentNode, parentBucket *pagesMapB return nil, err } - ps.init.Add(func() (any, error) { + ps.init.Add(func(context.Context) (any, error) { pp, err := newPagePaths(s, ps, metaProvider) if err != nil { return nil, err diff --git a/hugolib/embedded_shortcodes_test.go b/hugolib/embedded_shortcodes_test.go index 1707bcfa7..1e06494bf 100644 --- a/hugolib/embedded_shortcodes_test.go +++ b/hugolib/embedded_shortcodes_test.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "encoding/json" "fmt" "html/template" @@ -70,7 +71,7 @@ func doTestShortcodeCrossrefs(t *testing.T, relative bool) { c.Assert(len(s.RegularPages()), qt.Equals, 1) - content, err := s.RegularPages()[0].Content() + content, err := s.RegularPages()[0].Content(context.Background()) c.Assert(err, qt.IsNil) output := cast.ToString(content) diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 569c27be5..cdc5d97fb 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -194,7 +194,7 @@ func (h *hugoSitesInit) Reset() { } func (h *HugoSites) Data() map[string]any { - if _, err := h.init.data.Do(); err != nil { + if _, err := h.init.data.Do(context.Background()); err != nil { h.SendError(fmt.Errorf("failed to load data: %w", err)) return nil } @@ -202,7 +202,7 @@ func (h *HugoSites) Data() map[string]any { } func (h *HugoSites) gitInfoForPage(p page.Page) (source.GitInfo, error) { - if _, err := h.init.gitInfo.Do(); err != nil { + if _, err := h.init.gitInfo.Do(context.Background()); err != nil { return source.GitInfo{}, err } @@ -214,7 +214,7 @@ func (h *HugoSites) gitInfoForPage(p page.Page) (source.GitInfo, error) { } func (h *HugoSites) codeownersForPage(p page.Page) ([]string, error) { - if _, err := h.init.gitInfo.Do(); err != nil { + if _, err := h.init.gitInfo.Do(context.Background()); err != nil { return nil, err } @@ -363,7 +363,7 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) { donec: make(chan bool), } - h.init.data.Add(func() (any, error) { + h.init.data.Add(func(context.Context) (any, error) { err := h.loadData(h.PathSpec.BaseFs.Data.Dirs) if err != nil { return nil, fmt.Errorf("failed to load data: %w", err) @@ -371,7 +371,7 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) { return nil, nil }) - h.init.layouts.Add(func() (any, error) { + h.init.layouts.Add(func(context.Context) (any, error) { for _, s := range h.Sites { if err := s.Tmpl().(tpl.TemplateManager).MarkReady(); err != nil { return nil, err @@ -380,7 +380,7 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) { return nil, nil }) - h.init.translations.Add(func() (any, error) { + h.init.translations.Add(func(context.Context) (any, error) { if len(h.Sites) > 1 { allTranslations := pagesToTranslationsMap(h.Sites) assignTranslationsToPages(allTranslations, h.Sites) @@ -389,7 +389,7 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) { return nil, nil }) - h.init.gitInfo.Add(func() (any, error) { + h.init.gitInfo.Add(func(context.Context) (any, error) { err := h.loadGitInfo() if err != nil { return nil, fmt.Errorf("failed to load Git info: %w", err) diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index 5eee564aa..66abf4f16 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -268,7 +268,7 @@ func (h *HugoSites) assemble(bcfg *BuildCfg) error { } func (h *HugoSites) render(config *BuildCfg) error { - if _, err := h.init.layouts.Do(); err != nil { + if _, err := h.init.layouts.Do(context.Background()); err != nil { return err } diff --git a/hugolib/hugo_sites_build_errors_test.go b/hugolib/hugo_sites_build_errors_test.go index ffbfe1c17..f42b44461 100644 --- a/hugolib/hugo_sites_build_errors_test.go +++ b/hugolib/hugo_sites_build_errors_test.go @@ -396,7 +396,7 @@ line 4 } -func TestErrorNestedShortocde(t *testing.T) { +func TestErrorNestedShortcode(t *testing.T) { t.Parallel() files := ` diff --git a/hugolib/image_test.go b/hugolib/image_test.go index ac18b9423..db1707c22 100644 --- a/hugolib/image_test.go +++ b/hugolib/image_test.go @@ -14,162 +14,9 @@ package hugolib import ( - "io" - "os" - "path/filepath" - "runtime" - "strings" "testing" - - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/htesting" - - qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/hugofs" ) -// We have many tests for the different resize operations etc. in the resource package, -// this is an integration test. -func TestImageOps(t *testing.T) { - c := qt.New(t) - // Make this a real as possible. - workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "image-resize") - c.Assert(err, qt.IsNil) - defer clean() - - newBuilder := func(timeout any) *sitesBuilder { - v := config.NewWithTestDefaults() - v.Set("workingDir", workDir) - v.Set("baseURL", "https://example.org") - v.Set("timeout", timeout) - - b := newTestSitesBuilder(t).WithWorkingDir(workDir) - b.Fs = hugofs.NewDefault(v) - b.WithViper(v) - b.WithContent("mybundle/index.md", ` ---- -title: "My bundle" ---- - -{{< imgproc >}} - -`) - - b.WithTemplatesAdded( - "shortcodes/imgproc.html", ` -{{ $img := resources.Get "images/sunset.jpg" }} -{{ $r := $img.Resize "129x239" }} -IMG SHORTCODE: {{ $r.RelPermalink }}/{{ $r.Width }} -`, - "index.html", ` -{{ $p := .Site.GetPage "mybundle" }} -{{ $img1 := resources.Get "images/sunset.jpg" }} -{{ $img2 := $p.Resources.GetMatch "sunset.jpg" }} -{{ $img3 := resources.GetMatch "images/*.jpg" }} -{{ $r := $img1.Resize "123x234" }} -{{ $r2 := $r.Resize "12x23" }} -{{ $b := $img2.Resize "345x678" }} -{{ $b2 := $b.Resize "34x67" }} -{{ $c := $img3.Resize "456x789" }} -{{ $fingerprinted := $img1.Resize "350x" | fingerprint }} - -{{ $images := slice $r $r2 $b $b2 $c $fingerprinted }} - -{{ range $i, $r := $images }} -{{ printf "Resized%d:" (add $i 1) }} {{ $r.Name }}|{{ $r.Width }}|{{ $r.Height }}|{{ $r.MediaType }}|{{ $r.RelPermalink }}| -{{ end }} - -{{ $blurryGrayscale1 := $r | images.Filter images.Grayscale (images.GaussianBlur 8) }} -BG1: {{ $blurryGrayscale1.RelPermalink }}/{{ $blurryGrayscale1.Width }} -{{ $blurryGrayscale2 := $r.Filter images.Grayscale (images.GaussianBlur 8) }} -BG2: {{ $blurryGrayscale2.RelPermalink }}/{{ $blurryGrayscale2.Width }} -{{ $blurryGrayscale2_2 := $r.Filter images.Grayscale (images.GaussianBlur 8) }} -BG2_2: {{ $blurryGrayscale2_2.RelPermalink }}/{{ $blurryGrayscale2_2.Width }} - -{{ $filters := slice images.Grayscale (images.GaussianBlur 9) }} -{{ $blurryGrayscale3 := $r | images.Filter $filters }} -BG3: {{ $blurryGrayscale3.RelPermalink }}/{{ $blurryGrayscale3.Width }} - -{{ $blurryGrayscale4 := $r.Filter $filters }} -BG4: {{ $blurryGrayscale4.RelPermalink }}/{{ $blurryGrayscale4.Width }} - -{{ $p.Content }} - -`) - - return b - } - - imageDir := filepath.Join(workDir, "assets", "images") - bundleDir := filepath.Join(workDir, "content", "mybundle") - - c.Assert(os.MkdirAll(imageDir, 0777), qt.IsNil) - c.Assert(os.MkdirAll(bundleDir, 0777), qt.IsNil) - src, err := os.Open("testdata/sunset.jpg") - c.Assert(err, qt.IsNil) - out, err := os.Create(filepath.Join(imageDir, "sunset.jpg")) - c.Assert(err, qt.IsNil) - _, err = io.Copy(out, src) - c.Assert(err, qt.IsNil) - out.Close() - - src.Seek(0, 0) - - out, err = os.Create(filepath.Join(bundleDir, "sunset.jpg")) - c.Assert(err, qt.IsNil) - _, err = io.Copy(out, src) - c.Assert(err, qt.IsNil) - out.Close() - src.Close() - - // First build it with a very short timeout to trigger errors. - b := newBuilder("10ns") - - imgExpect := ` -Resized1: images/sunset.jpg|123|234|image/jpeg|/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_123x234_resize_q75_box.jpg| -Resized2: images/sunset.jpg|12|23|image/jpeg|/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_ada4bb1a57f77a63306e3bd67286248e.jpg| -Resized3: sunset.jpg|345|678|image/jpeg|/mybundle/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_345x678_resize_q75_box.jpg| -Resized4: sunset.jpg|34|67|image/jpeg|/mybundle/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_44d8c928664d7c5a67377c6ec58425ce.jpg| -Resized5: images/sunset.jpg|456|789|image/jpeg|/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_456x789_resize_q75_box.jpg| -Resized6: images/sunset.jpg|350|219|image/jpeg|/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_350x0_resize_q75_box.a86fe88d894e5db613f6aa8a80538fefc25b20fa24ba0d782c057adcef616f56.jpg| -BG1: /images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_2ae8bb993431ec1aec40fe59927b46b4.jpg/123 -BG2: /images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_2ae8bb993431ec1aec40fe59927b46b4.jpg/123 -BG3: /images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_ed7740a90b82802261c2fbdb98bc8082.jpg/123 -BG4: /images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_ed7740a90b82802261c2fbdb98bc8082.jpg/123 -IMG SHORTCODE: /images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_129x239_resize_q75_box.jpg/129 -` - - assertImages := func() { - b.Helper() - b.AssertFileContent("public/index.html", imgExpect) - b.AssertImage(350, 219, "public/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_350x0_resize_q75_box.a86fe88d894e5db613f6aa8a80538fefc25b20fa24ba0d782c057adcef616f56.jpg") - b.AssertImage(129, 239, "public/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_129x239_resize_q75_box.jpg") - } - - err = b.BuildE(BuildCfg{}) - if runtime.GOOS != "windows" && !strings.Contains(runtime.GOARCH, "arm") && !htesting.IsGitHubAction() { - // TODO(bep) - c.Assert(err, qt.Not(qt.IsNil)) - } - - b = newBuilder(29000) - b.Build(BuildCfg{}) - - assertImages() - - // Truncate one image. - imgInCache := filepath.Join(workDir, "resources/_gen/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_ed7740a90b82802261c2fbdb98bc8082.jpg") - f, err := os.Create(imgInCache) - c.Assert(err, qt.IsNil) - f.Close() - - // Build it again to make sure we read images from file cache. - b = newBuilder("30s") - b.Build(BuildCfg{}) - - assertImages() -} - func TestImageResizeMultilingual(t *testing.T) { b := newTestSitesBuilder(t).WithConfigFile("toml", ` baseURL="https://example.org" diff --git a/hugolib/language_content_dir_test.go b/hugolib/language_content_dir_test.go index 57cdab67b..23809f4df 100644 --- a/hugolib/language_content_dir_test.go +++ b/hugolib/language_content_dir_test.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "fmt" "os" "path/filepath" @@ -245,7 +246,7 @@ Content. c.Assert(svP2.Language().Lang, qt.Equals, "sv") c.Assert(nnP2.Language().Lang, qt.Equals, "nn") - content, _ := nnP2.Content() + content, _ := nnP2.Content(context.Background()) contentStr := cast.ToString(content) c.Assert(contentStr, qt.Contains, "SVP3-REF: https://example.org/sv/sect/p-sv-3/") c.Assert(contentStr, qt.Contains, "SVP3-RELREF: /sv/sect/p-sv-3/") diff --git a/hugolib/page.go b/hugolib/page.go index 97f1ed351..40972d7c5 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -15,6 +15,7 @@ package hugolib import ( "bytes" + "context" "fmt" "path" "path/filepath" @@ -24,8 +25,10 @@ import ( "go.uber.org/atomic" "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/related" "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/tpl" @@ -148,6 +151,43 @@ func (p *pageState) GetIdentity() identity.Identity { return identity.NewPathIdentity(files.ComponentFolderContent, filepath.FromSlash(p.Pathc())) } +func (p *pageState) Fragments(ctx context.Context) *tableofcontents.Fragments { + p.s.initInit(ctx, p.cp.initToC, p) + if p.pageOutput.cp.tableOfContents == nil { + return tableofcontents.Empty + } + return p.pageOutput.cp.tableOfContents +} + +func (p *pageState) HeadingsFiltered(context.Context) tableofcontents.Headings { + return nil +} + +type pageHeadingsFiltered struct { + *pageState + headings tableofcontents.Headings +} + +func (p *pageHeadingsFiltered) HeadingsFiltered(context.Context) tableofcontents.Headings { + return p.headings +} + +func (p *pageHeadingsFiltered) page() page.Page { + return p.pageState +} + +// For internal use by the related content feature. +func (p *pageState) ApplyFilterToHeadings(ctx context.Context, fn func(*tableofcontents.Heading) bool) related.Document { + if p.pageOutput.cp.tableOfContents == nil { + return p + } + headings := p.pageOutput.cp.tableOfContents.Headings.FilterBy(fn) + return &pageHeadingsFiltered{ + pageState: p, + headings: headings, + } +} + func (p *pageState) GitInfo() source.GitInfo { return p.gitInfo } @@ -351,7 +391,7 @@ func (p *pageState) String() string { // IsTranslated returns whether this content file is translated to // other language(s). func (p *pageState) IsTranslated() bool { - p.s.h.init.translations.Do() + p.s.h.init.translations.Do(context.Background()) return len(p.translations) > 0 } @@ -375,13 +415,13 @@ func (p *pageState) TranslationKey() string { // AllTranslations returns all translations, including the current Page. func (p *pageState) AllTranslations() page.Pages { - p.s.h.init.translations.Do() + p.s.h.init.translations.Do(context.Background()) return p.allTranslations } // Translations returns the translations excluding the current Page. func (p *pageState) Translations() page.Pages { - p.s.h.init.translations.Do() + p.s.h.init.translations.Do(context.Background()) return p.translations } @@ -461,7 +501,7 @@ func (p *pageState) initOutputFormat(isRenderingSite bool, idx int) error { // Must be run after the site section tree etc. is built and ready. func (p *pageState) initPage() error { - if _, err := p.init.Do(); err != nil { + if _, err := p.init.Do(context.Background()); err != nil { return err } return nil diff --git a/hugolib/page__content.go b/hugolib/page__content.go index a721d1fce..89c38bd84 100644 --- a/hugolib/page__content.go +++ b/hugolib/page__content.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "fmt" "github.com/gohugoio/hugo/output" @@ -37,9 +38,9 @@ type pageContent struct { } // returns the content to be processed by Goldmark or similar. -func (p pageContent) contentToRender(parsed pageparser.Result, pm *pageContentMap, renderedShortcodes map[string]string) []byte { +func (p pageContent) contentToRender(ctx context.Context, parsed pageparser.Result, pm *pageContentMap, renderedShortcodes map[string]shortcodeRenderer) ([]byte, bool, error) { source := parsed.Input() - + var hasVariants bool c := make([]byte, 0, len(source)+(len(source)/10)) for _, it := range pm.items { @@ -57,7 +58,12 @@ func (p pageContent) contentToRender(parsed pageparser.Result, pm *pageContentMa panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder)) } - c = append(c, []byte(renderedShortcode)...) + b, more, err := renderedShortcode.renderShortcode(ctx) + if err != nil { + return nil, false, fmt.Errorf("failed to render shortcode: %w", err) + } + hasVariants = hasVariants || more + c = append(c, []byte(b)...) } else { // Insert the placeholder so we can insert the content after @@ -69,7 +75,7 @@ func (p pageContent) contentToRender(parsed pageparser.Result, pm *pageContentMa } } - return c + return c, hasVariants, nil } func (p pageContent) selfLayoutForOutput(f output.Format) string { diff --git a/hugolib/page__menus.go b/hugolib/page__menus.go index 49d392c2f..5bed2bc03 100644 --- a/hugolib/page__menus.go +++ b/hugolib/page__menus.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "sync" "github.com/gohugoio/hugo/navigation" @@ -29,13 +30,13 @@ type pageMenus struct { } func (p *pageMenus) HasMenuCurrent(menuID string, me *navigation.MenuEntry) bool { - p.p.s.init.menus.Do() + p.p.s.init.menus.Do(context.Background()) p.init() return p.q.HasMenuCurrent(menuID, me) } func (p *pageMenus) IsMenuCurrent(menuID string, inme *navigation.MenuEntry) bool { - p.p.s.init.menus.Do() + p.p.s.init.menus.Do(context.Background()) p.init() return p.q.IsMenuCurrent(menuID, inme) } @@ -43,7 +44,7 @@ func (p *pageMenus) IsMenuCurrent(menuID string, inme *navigation.MenuEntry) boo func (p *pageMenus) Menus() navigation.PageMenus { // There is a reverse dependency here. initMenus will, once, build the // site menus and update any relevant page. - p.p.s.init.menus.Do() + p.p.s.init.menus.Do(context.Background()) return p.menus() } diff --git a/hugolib/page__new.go b/hugolib/page__new.go index e52b9476b..3787cd2bd 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "html/template" "strings" @@ -121,7 +122,7 @@ func newPageFromMeta( return nil, err } - ps.init.Add(func() (any, error) { + ps.init.Add(func(context.Context) (any, error) { pp, err := newPagePaths(metaProvider.s, ps, metaProvider) if err != nil { return nil, err diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 97e9cc465..827a6b792 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -18,7 +18,6 @@ import ( "context" "fmt" "html/template" - "runtime/debug" "strings" "sync" "unicode/utf8" @@ -34,6 +33,7 @@ import ( "github.com/gohugoio/hugo/markup/converter/hooks" "github.com/gohugoio/hugo/markup/highlight/chromalexers" + "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/markup/converter" @@ -87,43 +87,35 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err renderHooks: &renderHooks{}, } - initContent := func() (err error) { - p.s.h.IncrContentRender() - + initToC := func(ctx context.Context) (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.Errorf("[BUG] Got panic:\n%s\n%s", r, string(debug.Stack())) - } - }() if err := po.cp.initRenderHooks(); err != nil { return err } - var hasShortcodeVariants bool - f := po.f - cp.contentPlaceholders, hasShortcodeVariants, err = p.shortcodeState.renderShortcodesForPage(p, f) + cp.contentPlaceholders, err = p.shortcodeState.prepareShortcodesForPage(ctx, p, f) if err != nil { return err } - if hasShortcodeVariants { + var hasVariants bool + cp.workContent, hasVariants, err = p.contentToRender(ctx, p.source.parsed, p.cmap, cp.contentPlaceholders) + if err != nil { + return err + } + if hasVariants { p.pageOutputTemplateVariationsState.Store(2) } - cp.workContent = p.contentToRender(p.source.parsed, p.cmap, cp.contentPlaceholders) - isHTML := cp.p.m.markup == "html" if !isHTML { - r, err := po.contentRenderer.RenderContent(cp.workContent, true) + r, err := po.contentRenderer.RenderContent(ctx, cp.workContent, true) if err != nil { return err } @@ -132,8 +124,9 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err if tocProvider, ok := r.(converter.TableOfContentsProvider); ok { cfg := p.s.ContentSpec.Converters.GetMarkupConfig() - cp.tableOfContents = template.HTML( - tocProvider.TableOfContents().ToHTML( + cp.tableOfContents = tocProvider.TableOfContents() + cp.tableOfContentsHTML = template.HTML( + cp.tableOfContents.ToHTML( cfg.TableOfContents.StartLevel, cfg.TableOfContents.EndLevel, cfg.TableOfContents.Ordered, @@ -141,26 +134,60 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err ) } else { tmpContent, tmpTableOfContents := helpers.ExtractTOC(cp.workContent) - cp.tableOfContents = helpers.BytesToHTML(tmpTableOfContents) + cp.tableOfContentsHTML = helpers.BytesToHTML(tmpTableOfContents) + cp.tableOfContents = tableofcontents.Empty 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) + return nil + + } + + initContent := func(ctx context.Context) (err error) { + + p.s.h.IncrContentRender() + + if p.cmap == nil { + // Nothing to do. + return 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) + var hasShortcodeVariants bool + tokenHandler := func(ctx context.Context, token string) ([]byte, error) { + if token == tocShortcodePlaceholder { + // The Page's TableOfContents was accessed in a shortcode. + if cp.tableOfContentsHTML == "" { + cp.p.s.initInit(ctx, cp.initToC, cp.p) + } + return []byte(cp.tableOfContentsHTML), nil + } + renderer, found := cp.contentPlaceholders[token] + if found { + repl, more, err := renderer.renderShortcode(ctx) + if err != nil { + return nil, err + } + hasShortcodeVariants = hasShortcodeVariants || more + return repl, nil + } + // This should never happen. + return nil, fmt.Errorf("unknown shortcode token %q", token) + } + + cp.workContent, err = expandShortcodeTokens(ctx, cp.workContent, tokenHandler) if err != nil { return err } + if hasShortcodeVariants { + p.pageOutputTemplateVariationsState.Store(2) + } } if cp.p.source.hasSummaryDivider { + isHTML := cp.p.m.markup == "html" if isHTML { src := p.source.parsed.Input() @@ -183,7 +210,7 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err } } } else if cp.p.m.summary != "" { - b, err := po.contentRenderer.RenderContent([]byte(cp.p.m.summary), false) + b, err := po.contentRenderer.RenderContent(ctx, []byte(cp.p.m.summary), false) if err != nil { return err } @@ -196,12 +223,16 @@ func newPageContentOutput(p *pageState, po *pageOutput) (*pageContentOutput, err return nil } + cp.initToC = parent.Branch(func(ctx context.Context) (any, error) { + return nil, initToC(ctx) + }) + // There may be recursive loops in shortcodes and render hooks. - cp.initMain = parent.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (any, error) { - return nil, initContent() + cp.initMain = cp.initToC.BranchWithTimeout(p.s.siteCfg.timeout, func(ctx context.Context) (any, error) { + return nil, initContent(ctx) }) - cp.initPlain = cp.initMain.Branch(func() (any, error) { + cp.initPlain = cp.initMain.Branch(func(context.Context) (any, error) { cp.plain = tpl.StripHTML(string(cp.content)) cp.plainWords = strings.Fields(cp.plain) cp.setWordCounts(p.m.isCJKLanguage) @@ -228,6 +259,7 @@ type pageContentOutput struct { p *pageState // Lazy load dependencies + initToC *lazy.Init initMain *lazy.Init initPlain *lazy.Init @@ -243,12 +275,13 @@ type pageContentOutput struct { // Temporary storage of placeholders mapped to their content. // These are shortcodes etc. Some of these will need to be replaced // after any markup is rendered, so they share a common prefix. - contentPlaceholders map[string]string + contentPlaceholders map[string]shortcodeRenderer // Content sections - content template.HTML - summary template.HTML - tableOfContents template.HTML + content template.HTML + summary template.HTML + tableOfContents *tableofcontents.Fragments + tableOfContentsHTML template.HTML truncated bool @@ -263,76 +296,76 @@ 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.initToC.Reset() p.initMain.Reset() p.initPlain.Reset() p.renderHooks = &renderHooks{} } -func (p *pageContentOutput) Content() (any, error) { - if p.p.s.initInit(p.initMain, p.p) { - return p.content, nil - } - return nil, nil +func (p *pageContentOutput) Content(ctx context.Context) (any, error) { + p.p.s.initInit(ctx, p.initMain, p.p) + return p.content, nil } -func (p *pageContentOutput) FuzzyWordCount() int { - p.p.s.initInit(p.initPlain, p.p) +func (p *pageContentOutput) FuzzyWordCount(ctx context.Context) int { + p.p.s.initInit(ctx, p.initPlain, p.p) return p.fuzzyWordCount } -func (p *pageContentOutput) Len() int { - p.p.s.initInit(p.initMain, p.p) +func (p *pageContentOutput) Len(ctx context.Context) int { + p.p.s.initInit(ctx, p.initMain, p.p) return len(p.content) } -func (p *pageContentOutput) Plain() string { - p.p.s.initInit(p.initPlain, p.p) +func (p *pageContentOutput) Plain(ctx context.Context) string { + p.p.s.initInit(ctx, p.initPlain, p.p) return p.plain } -func (p *pageContentOutput) PlainWords() []string { - p.p.s.initInit(p.initPlain, p.p) +func (p *pageContentOutput) PlainWords(ctx context.Context) []string { + p.p.s.initInit(ctx, p.initPlain, p.p) return p.plainWords } -func (p *pageContentOutput) ReadingTime() int { - p.p.s.initInit(p.initPlain, p.p) +func (p *pageContentOutput) ReadingTime(ctx context.Context) int { + p.p.s.initInit(ctx, p.initPlain, p.p) return p.readingTime } -func (p *pageContentOutput) Summary() template.HTML { - p.p.s.initInit(p.initMain, p.p) +func (p *pageContentOutput) Summary(ctx context.Context) template.HTML { + p.p.s.initInit(ctx, p.initMain, p.p) if !p.p.source.hasSummaryDivider { - p.p.s.initInit(p.initPlain, p.p) + p.p.s.initInit(ctx, p.initPlain, p.p) } return p.summary } -func (p *pageContentOutput) TableOfContents() template.HTML { - p.p.s.initInit(p.initMain, p.p) - return p.tableOfContents +func (p *pageContentOutput) TableOfContents(ctx context.Context) template.HTML { + p.p.s.initInit(ctx, p.initMain, p.p) + return p.tableOfContentsHTML } -func (p *pageContentOutput) Truncated() bool { +func (p *pageContentOutput) Truncated(ctx context.Context) bool { if p.p.truncated { return true } - p.p.s.initInit(p.initPlain, p.p) + p.p.s.initInit(ctx, p.initPlain, p.p) return p.truncated } -func (p *pageContentOutput) WordCount() int { - p.p.s.initInit(p.initPlain, p.p) +func (p *pageContentOutput) WordCount(ctx context.Context) int { + p.p.s.initInit(ctx, p.initPlain, p.p) return p.wordCount } -func (p *pageContentOutput) RenderString(args ...any) (template.HTML, error) { +func (p *pageContentOutput) RenderString(ctx context.Context, args ...any) (template.HTML, error) { if len(args) < 1 || len(args) > 2 { return "", errors.New("want 1 or 2 arguments") } @@ -405,42 +438,62 @@ func (p *pageContentOutput) RenderString(args ...any) (template.HTML, error) { return "", err } - placeholders, hasShortcodeVariants, err := s.renderShortcodesForPage(p.p, p.f) + placeholders, err := s.prepareShortcodesForPage(ctx, p.p, p.f) if err != nil { return "", err } - if hasShortcodeVariants { + contentToRender, hasVariants, err := p.p.contentToRender(ctx, parsed, pm, placeholders) + if err != nil { + return "", err + } + if hasVariants { p.p.pageOutputTemplateVariationsState.Store(2) } - - b, err := p.renderContentWithConverter(conv, p.p.contentToRender(parsed, pm, placeholders), false) + b, err := p.renderContentWithConverter(ctx, conv, contentToRender, false) if err != nil { return "", p.p.wrapError(err) } rendered = b.Bytes() - if p.placeholdersEnabled { - // ToC was accessed via .Page.TableOfContents in the shortcode, - // at a time when the ToC wasn't ready. - if _, err := p.p.Content(); err != nil { - return "", err + if pm.hasNonMarkdownShortcode || p.placeholdersEnabled { + var hasShortcodeVariants bool + + tokenHandler := func(ctx context.Context, token string) ([]byte, error) { + if token == tocShortcodePlaceholder { + // The Page's TableOfContents was accessed in a shortcode. + if p.tableOfContentsHTML == "" { + p.p.s.initInit(ctx, p.initToC, p.p) + } + return []byte(p.tableOfContentsHTML), nil + } + renderer, found := placeholders[token] + if found { + repl, more, err := renderer.renderShortcode(ctx) + if err != nil { + return nil, err + } + hasShortcodeVariants = hasShortcodeVariants || more + return repl, nil + } + // This should not happen. + return nil, fmt.Errorf("unknown shortcode token %q", token) } - placeholders[tocShortcodePlaceholder] = string(p.tableOfContents) - } - if pm.hasNonMarkdownShortcode || p.placeholdersEnabled { - rendered, err = replaceShortcodeTokens(rendered, placeholders) + rendered, err = expandShortcodeTokens(ctx, rendered, tokenHandler) if err != nil { return "", err } + if hasShortcodeVariants { + p.p.pageOutputTemplateVariationsState.Store(2) + } } // We need a consolidated view in $page.HasShortcode p.p.shortcodeState.transferNames(s) } else { - c, err := p.renderContentWithConverter(conv, []byte(contentToRender), false) + c, err := p.renderContentWithConverter(ctx, conv, []byte(contentToRender), false) if err != nil { return "", p.p.wrapError(err) } @@ -457,12 +510,12 @@ func (p *pageContentOutput) RenderString(args ...any) (template.HTML, error) { return template.HTML(string(rendered)), nil } -func (p *pageContentOutput) RenderWithTemplateInfo(info tpl.Info, layout ...string) (template.HTML, error) { +func (p *pageContentOutput) RenderWithTemplateInfo(ctx context.Context, info tpl.Info, layout ...string) (template.HTML, error) { p.p.addDependency(info) - return p.Render(layout...) + return p.Render(ctx, layout...) } -func (p *pageContentOutput) Render(layout ...string) (template.HTML, error) { +func (p *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) { templ, found, err := p.p.resolveTemplate(layout...) if err != nil { return "", p.p.wrapError(err) @@ -475,7 +528,7 @@ func (p *pageContentOutput) Render(layout ...string) (template.HTML, error) { 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) + res, err := executeToString(ctx, p.p.s.Tmpl(), templ, p.p) if err != nil { return "", p.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err)) } @@ -629,15 +682,15 @@ func (p *pageContentOutput) setAutoSummary() error { return nil } -func (cp *pageContentOutput) RenderContent(content []byte, renderTOC bool) (converter.Result, error) { +func (cp *pageContentOutput) RenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.Result, error) { if err := cp.initRenderHooks(); err != nil { return nil, err } c := cp.p.getContentConverter() - return cp.renderContentWithConverter(c, content, renderTOC) + return cp.renderContentWithConverter(ctx, c, content, renderTOC) } -func (cp *pageContentOutput) renderContentWithConverter(c converter.Converter, content []byte, renderTOC bool) (converter.Result, error) { +func (cp *pageContentOutput) renderContentWithConverter(ctx context.Context, c converter.Converter, content []byte, renderTOC bool) (converter.Result, error) { r, err := c.Convert( converter.RenderContext{ Src: content, @@ -711,10 +764,10 @@ func (t targetPathsHolder) targetPaths() page.TargetPaths { return t.paths } -func executeToString(h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) { +func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) { b := bp.GetBuffer() defer bp.PutBuffer(b) - if err := h.Execute(templ, b, data); err != nil { + if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil { return "", err } return b.String(), nil diff --git a/hugolib/page__position.go b/hugolib/page__position.go index a087872cc..d977a7052 100644 --- a/hugolib/page__position.go +++ b/hugolib/page__position.go @@ -14,6 +14,8 @@ package hugolib import ( + "context" + "github.com/gohugoio/hugo/lazy" "github.com/gohugoio/hugo/resources/page" ) @@ -33,12 +35,12 @@ type nextPrev struct { } func (n *nextPrev) next() page.Page { - n.init.Do() + n.init.Do(context.Background()) return n.nextPage } func (n *nextPrev) prev() page.Page { - n.init.Do() + n.init.Do(context.Background()) return n.prevPage } diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 939d06d41..49617f17e 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "fmt" "html/template" "os" @@ -311,13 +312,13 @@ func normalizeContent(c string) string { func checkPageTOC(t *testing.T, page page.Page, toc string) { t.Helper() - if page.TableOfContents() != template.HTML(toc) { - t.Fatalf("Page TableOfContents is:\n%q.\nExpected %q", page.TableOfContents(), toc) + if page.TableOfContents(context.Background()) != template.HTML(toc) { + t.Fatalf("Page TableOfContents is:\n%q.\nExpected %q", page.TableOfContents(context.Background()), toc) } } func checkPageSummary(t *testing.T, page page.Page, summary string, msg ...any) { - a := normalizeContent(string(page.Summary())) + a := normalizeContent(string(page.Summary(context.Background()))) b := normalizeContent(summary) if a != b { t.Fatalf("Page summary is:\n%q.\nExpected\n%q (%q)", a, b, msg) @@ -443,9 +444,9 @@ func TestPageWithDelimiterForMarkdownThatCrossesBorder(t *testing.T) { p := s.RegularPages()[0] - if p.Summary() != template.HTML( + if p.Summary(context.Background()) != template.HTML( "<p>The <a href=\"http://gohugo.io/\">best static site generator</a>.<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup></p>") { - t.Fatalf("Got summary:\n%q", p.Summary()) + t.Fatalf("Got summary:\n%q", p.Summary(context.Background())) } cnt := content(p) @@ -719,7 +720,7 @@ func TestSummaryWithHTMLTagsOnNextLine(t *testing.T) { assertFunc := func(t *testing.T, ext string, pages page.Pages) { c := qt.New(t) p := pages[0] - s := string(p.Summary()) + s := string(p.Summary(context.Background())) c.Assert(s, qt.Contains, "Happy new year everyone!") c.Assert(s, qt.Not(qt.Contains), "User interface") } @@ -1122,8 +1123,8 @@ func TestWordCountWithAllCJKRunesWithoutHasCJKLanguage(t *testing.T) { t.Parallel() assertFunc := func(t *testing.T, ext string, pages page.Pages) { p := pages[0] - if p.WordCount() != 8 { - t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 8, p.WordCount()) + if p.WordCount(context.Background()) != 8 { + t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 8, p.WordCount(context.Background())) } } @@ -1136,8 +1137,8 @@ func TestWordCountWithAllCJKRunesHasCJKLanguage(t *testing.T) { assertFunc := func(t *testing.T, ext string, pages page.Pages) { p := pages[0] - if p.WordCount() != 15 { - t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 15, p.WordCount()) + if p.WordCount(context.Background()) != 15 { + t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 15, p.WordCount(context.Background())) } } testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithAllCJKRunes) @@ -1149,13 +1150,13 @@ func TestWordCountWithMainEnglishWithCJKRunes(t *testing.T) { assertFunc := func(t *testing.T, ext string, pages page.Pages) { p := pages[0] - if p.WordCount() != 74 { - t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 74, p.WordCount()) + if p.WordCount(context.Background()) != 74 { + t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 74, p.WordCount(context.Background())) } - if p.Summary() != simplePageWithMainEnglishWithCJKRunesSummary { - t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.Plain(), - simplePageWithMainEnglishWithCJKRunesSummary, p.Summary()) + if p.Summary(context.Background()) != simplePageWithMainEnglishWithCJKRunesSummary { + t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.Plain(context.Background()), + simplePageWithMainEnglishWithCJKRunesSummary, p.Summary(context.Background())) } } @@ -1170,13 +1171,13 @@ func TestWordCountWithIsCJKLanguageFalse(t *testing.T) { assertFunc := func(t *testing.T, ext string, pages page.Pages) { p := pages[0] - if p.WordCount() != 75 { - t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.Plain(), 74, p.WordCount()) + if p.WordCount(context.Background()) != 75 { + t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.Plain(context.Background()), 74, p.WordCount(context.Background())) } - if p.Summary() != simplePageWithIsCJKLanguageFalseSummary { - t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.Plain(), - simplePageWithIsCJKLanguageFalseSummary, p.Summary()) + if p.Summary(context.Background()) != simplePageWithIsCJKLanguageFalseSummary { + t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.Plain(context.Background()), + simplePageWithIsCJKLanguageFalseSummary, p.Summary(context.Background())) } } @@ -1187,16 +1188,16 @@ func TestWordCount(t *testing.T) { t.Parallel() assertFunc := func(t *testing.T, ext string, pages page.Pages) { p := pages[0] - if p.WordCount() != 483 { - t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 483, p.WordCount()) + if p.WordCount(context.Background()) != 483 { + t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 483, p.WordCount(context.Background())) } - if p.FuzzyWordCount() != 500 { - t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 500, p.FuzzyWordCount()) + if p.FuzzyWordCount(context.Background()) != 500 { + t.Fatalf("[%s] incorrect word count. expected %v, got %v", ext, 500, p.FuzzyWordCount(context.Background())) } - if p.ReadingTime() != 3 { - t.Fatalf("[%s] incorrect min read. expected %v, got %v", ext, 3, p.ReadingTime()) + if p.ReadingTime(context.Background()) != 3 { + t.Fatalf("[%s] incorrect min read. expected %v, got %v", ext, 3, p.ReadingTime(context.Background())) } } diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index 2951a1436..a82caff43 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -15,6 +15,7 @@ package hugolib import ( "bytes" + "context" "fmt" "html/template" "path" @@ -302,13 +303,44 @@ const ( innerCleanupExpand = "$1" ) -func renderShortcode( +func prepareShortcode( + ctx context.Context, level int, s *Site, tplVariants tpl.TemplateVariants, sc *shortcode, parent *ShortcodeWithPage, - p *pageState) (string, bool, error) { + p *pageState) (shortcodeRenderer, error) { + + toParseErr := func(err error) error { + return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), p.source.parsed.Input(), sc.pos) + } + + // Allow the caller to delay the rendering of the shortcode if needed. + var fn shortcodeRenderFunc = func(ctx context.Context) ([]byte, bool, error) { + r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p) + if err != nil { + return nil, false, toParseErr(err) + } + b, hasVariants, err := r.renderShortcode(ctx) + if err != nil { + return nil, false, toParseErr(err) + } + return b, hasVariants, nil + } + + return fn, nil + +} + +func doRenderShortcode( + ctx context.Context, + level int, + s *Site, + tplVariants tpl.TemplateVariants, + sc *shortcode, + parent *ShortcodeWithPage, + p *pageState) (shortcodeRenderer, error) { var tmpl tpl.Template // Tracks whether this shortcode or any of its children has template variations @@ -319,7 +351,7 @@ func renderShortcode( if sc.isInline { if !p.s.ExecHelper.Sec().EnableInlineShortcodes { - return "", false, nil + return zeroShortcode, nil } templName := path.Join("_inline_shortcode", p.File().Path(), sc.name) if sc.isClosing { @@ -332,7 +364,7 @@ func renderShortcode( pos := fe.Position() pos.LineNumber += p.posOffset(sc.pos).LineNumber fe = fe.UpdatePosition(pos) - return "", false, p.wrapError(fe) + return zeroShortcode, p.wrapError(fe) } } else { @@ -340,7 +372,7 @@ func renderShortcode( var found bool tmpl, found = s.TextTmpl().Lookup(templName) if !found { - return "", false, fmt.Errorf("no earlier definition of shortcode %q found", sc.name) + return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name) } } } else { @@ -348,7 +380,7 @@ func renderShortcode( tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants) if !found { s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path()) - return "", false, nil + return zeroShortcode, nil } hasVariants = hasVariants || more } @@ -365,16 +397,20 @@ func renderShortcode( case string: inner += innerData case *shortcode: - s, more, err := renderShortcode(level+1, s, tplVariants, innerData, data, p) + s, err := prepareShortcode(ctx, level+1, s, tplVariants, innerData, data, p) if err != nil { - return "", false, err + return zeroShortcode, err } + ss, more, err := s.renderShortcodeString(ctx) hasVariants = hasVariants || more - inner += s + if err != nil { + return zeroShortcode, err + } + inner += ss default: s.Log.Errorf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ", sc.name, p.File().Path(), reflect.TypeOf(innerData)) - return "", false, nil + return zeroShortcode, nil } } @@ -382,9 +418,9 @@ func renderShortcode( // shortcode. if sc.doMarkup && (level > 0 || sc.configVersion() == 1) { var err error - b, err := p.pageOutput.contentRenderer.RenderContent([]byte(inner), false) + b, err := p.pageOutput.contentRenderer.RenderContent(ctx, []byte(inner), false) if err != nil { - return "", false, err + return zeroShortcode, err } newInner := b.Bytes() @@ -418,14 +454,14 @@ func renderShortcode( } - result, err := renderShortcodeWithPage(s.Tmpl(), tmpl, data) + result, err := renderShortcodeWithPage(ctx, s.Tmpl(), tmpl, data) if err != nil && sc.isInline { fe := herrors.NewFileErrorFromName(err, p.File().Filename()) pos := fe.Position() pos.LineNumber += p.posOffset(sc.pos).LineNumber fe = fe.UpdatePosition(pos) - return "", false, fe + return zeroShortcode, fe } if len(sc.inner) == 0 && len(sc.indentation) > 0 { @@ -444,7 +480,7 @@ func renderShortcode( bp.PutBuffer(b) } - return result, hasVariants, err + return prerenderedShortcode{s: result, hasVariants: hasVariants}, err } func (s *shortcodeHandler) hasShortcodes() bool { @@ -473,28 +509,24 @@ func (s *shortcodeHandler) hasName(name string) bool { return ok } -func (s *shortcodeHandler) renderShortcodesForPage(p *pageState, f output.Format) (map[string]string, bool, error) { - rendered := make(map[string]string) +func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, p *pageState, f output.Format) (map[string]shortcodeRenderer, error) { + rendered := make(map[string]shortcodeRenderer) tplVariants := tpl.TemplateVariants{ Language: p.Language().Lang, OutputFormat: f, } - var hasVariants bool - for _, v := range s.shortcodes { - s, more, err := renderShortcode(0, s.s, tplVariants, v, nil, p) + s, err := prepareShortcode(ctx, 0, s.s, tplVariants, v, nil, p) if err != nil { - err = p.parseError(fmt.Errorf("failed to render shortcode %q: %w", v.name, err), p.source.parsed.Input(), v.pos) - return nil, false, err + return nil, err } - hasVariants = hasVariants || more rendered[v.placeholder] = s } - return rendered, hasVariants, nil + return rendered, nil } func (s *shortcodeHandler) parseError(err error, input []byte, pos int) error { @@ -668,11 +700,11 @@ Loop: // Replace prefixed shortcode tokens with the real content. // Note: This function will rewrite the input slice. -func replaceShortcodeTokens(source []byte, replacements map[string]string) ([]byte, error) { - if len(replacements) == 0 { - return source, nil - } - +func expandShortcodeTokens( + ctx context.Context, + source []byte, + tokenHandler func(ctx context.Context, token string) ([]byte, error), +) ([]byte, error) { start := 0 pre := []byte(shortcodePlaceholderPrefix) @@ -691,8 +723,11 @@ func replaceShortcodeTokens(source []byte, replacements map[string]string) ([]by } end := j + postIdx + 4 - - newVal := []byte(replacements[string(source[j:end])]) + key := string(source[j:end]) + newVal, err := tokenHandler(ctx, key) + if err != nil { + return nil, err + } // Issue #1148: Check for wrapping p-tags <p> if j >= 3 && bytes.Equal(source[j-3:j], pStart) { @@ -712,11 +747,11 @@ func replaceShortcodeTokens(source []byte, replacements map[string]string) ([]by return source, nil } -func renderShortcodeWithPage(h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) { +func renderShortcodeWithPage(ctx context.Context, h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) { buffer := bp.GetBuffer() defer bp.PutBuffer(buffer) - err := h.Execute(tmpl, buffer, data) + err := h.ExecuteWithContext(ctx, tmpl, buffer, data) if err != nil { return "", fmt.Errorf("failed to process shortcode: %w", err) } diff --git a/hugolib/shortcode_page.go b/hugolib/shortcode_page.go index 5a56e434f..3bc061bc0 100644 --- a/hugolib/shortcode_page.go +++ b/hugolib/shortcode_page.go @@ -14,13 +14,48 @@ package hugolib import ( + "context" "html/template" "github.com/gohugoio/hugo/resources/page" ) +// A placeholder for the TableOfContents markup. This is what we pass to the Goldmark etc. renderers. var tocShortcodePlaceholder = createShortcodePlaceholder("TOC", 0) +// shortcodeRenderer is typically used to delay rendering of inner shortcodes +// marked with placeholders in the content. +type shortcodeRenderer interface { + renderShortcode(context.Context) ([]byte, bool, error) + renderShortcodeString(context.Context) (string, bool, error) +} + +type shortcodeRenderFunc func(context.Context) ([]byte, bool, error) + +func (f shortcodeRenderFunc) renderShortcode(ctx context.Context) ([]byte, bool, error) { + return f(ctx) +} + +func (f shortcodeRenderFunc) renderShortcodeString(ctx context.Context) (string, bool, error) { + b, has, err := f(ctx) + return string(b), has, err +} + +type prerenderedShortcode struct { + s string + hasVariants bool +} + +func (p prerenderedShortcode) renderShortcode(context.Context) ([]byte, bool, error) { + return []byte(p.s), p.hasVariants, nil +} + +func (p prerenderedShortcode) renderShortcodeString(context.Context) (string, bool, error) { + return p.s, p.hasVariants, nil +} + +var zeroShortcode = prerenderedShortcode{} + // This is sent to the shortcodes. They cannot access the content // they're a part of. It would cause an infinite regress. // @@ -50,7 +85,11 @@ func (p *pageForShortcode) page() page.Page { return p.PageWithoutContent.(page.Page) } -func (p *pageForShortcode) TableOfContents() template.HTML { +func (p *pageForShortcode) String() string { + return p.p.String() +} + +func (p *pageForShortcode) TableOfContents(context.Context) template.HTML { p.p.enablePlaceholders() return p.toc } diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index b5f27d621..2f285d0da 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "fmt" "path/filepath" "reflect" @@ -247,7 +248,7 @@ CSV: {{< myShort >}} func BenchmarkReplaceShortcodeTokens(b *testing.B) { type input struct { in []byte - replacements map[string]string + tokenHandler func(ctx context.Context, token string) ([]byte, error) expect []byte } @@ -263,22 +264,30 @@ func BenchmarkReplaceShortcodeTokens(b *testing.B) { {strings.Repeat("A ", 3000) + " HAHAHUGOSHORTCODE-1HBHB." + strings.Repeat("BC ", 1000) + " HAHAHUGOSHORTCODE-1HBHB.", map[string]string{"HAHAHUGOSHORTCODE-1HBHB": "Hello World"}, []byte(strings.Repeat("A ", 3000) + " Hello World." + strings.Repeat("BC ", 1000) + " Hello World.")}, } - in := make([]input, b.N*len(data)) cnt := 0 + in := make([]input, b.N*len(data)) for i := 0; i < b.N; i++ { for _, this := range data { - in[cnt] = input{[]byte(this.input), this.replacements, this.expect} + replacements := make(map[string]shortcodeRenderer) + for k, v := range this.replacements { + replacements[k] = prerenderedShortcode{s: v} + } + tokenHandler := func(ctx context.Context, token string) ([]byte, error) { + return []byte(this.replacements[token]), nil + } + in[cnt] = input{[]byte(this.input), tokenHandler, this.expect} cnt++ } } b.ResetTimer() cnt = 0 + ctx := context.Background() for i := 0; i < b.N; i++ { for j := range data { currIn := in[cnt] cnt++ - results, err := replaceShortcodeTokens(currIn.in, currIn.replacements) + results, err := expandShortcodeTokens(ctx, currIn.in, currIn.tokenHandler) if err != nil { b.Fatalf("[%d] failed: %s", i, err) continue @@ -383,7 +392,16 @@ func TestReplaceShortcodeTokens(t *testing.T) { }, } { - results, err := replaceShortcodeTokens([]byte(this.input), this.replacements) + replacements := make(map[string]shortcodeRenderer) + for k, v := range this.replacements { + replacements[k] = prerenderedShortcode{s: v} + } + tokenHandler := func(ctx context.Context, token string) ([]byte, error) { + return []byte(this.replacements[token]), nil + } + + ctx := context.Background() + results, err := expandShortcodeTokens(ctx, []byte(this.input), tokenHandler) if b, ok := this.expect.(bool); ok && !b { if err == nil { diff --git a/hugolib/site.go b/hugolib/site.go index 0ca7a81b4..e90fa41ff 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "fmt" "html/template" "io" @@ -173,7 +174,7 @@ type Site struct { } func (s *Site) Taxonomies() page.TaxonomyList { - s.init.taxonomies.Do() + s.init.taxonomies.Do(context.Background()) return s.taxonomies } @@ -214,8 +215,9 @@ func (init *siteInit) Reset() { init.taxonomies.Reset() } -func (s *Site) initInit(init *lazy.Init, pctx pageContext) bool { - _, err := init.Do() +func (s *Site) initInit(ctx context.Context, init *lazy.Init, pctx pageContext) bool { + _, err := init.Do(ctx) + if err != nil { s.h.FatalError(pctx.wrapError(err)) } @@ -227,7 +229,7 @@ func (s *Site) prepareInits() { var init lazy.Init - s.init.prevNext = init.Branch(func() (any, error) { + s.init.prevNext = init.Branch(func(context.Context) (any, error) { regularPages := s.RegularPages() for i, p := range regularPages { np, ok := p.(nextPrevProvider) @@ -254,7 +256,7 @@ func (s *Site) prepareInits() { return nil, nil }) - s.init.prevNextInSection = init.Branch(func() (any, error) { + s.init.prevNextInSection = init.Branch(func(context.Context) (any, error) { var sections page.Pages s.home.treeRef.m.collectSectionsRecursiveIncludingSelf(pageMapQuery{Prefix: s.home.treeRef.key}, func(n *contentNode) { sections = append(sections, n.p) @@ -311,12 +313,12 @@ func (s *Site) prepareInits() { return nil, nil }) - s.init.menus = init.Branch(func() (any, error) { + s.init.menus = init.Branch(func(context.Context) (any, error) { s.assembleMenus() return nil, nil }) - s.init.taxonomies = init.Branch(func() (any, error) { + s.init.taxonomies = init.Branch(func(context.Context) (any, error) { err := s.pageMap.assembleTaxonomies() return nil, err }) @@ -327,7 +329,7 @@ type siteRenderingContext struct { } func (s *Site) Menus() navigation.Menus { - s.init.menus.Do() + s.init.menus.Do(context.Background()) return s.menus } @@ -1821,7 +1823,9 @@ func (s *Site) renderForTemplate(name, outputFormat string, d any, w io.Writer, return nil } - if err = s.Tmpl().Execute(templ, w, d); err != nil { + ctx := context.Background() + + if err = s.Tmpl().ExecuteWithContext(ctx, templ, w, d); err != nil { return fmt.Errorf("render of %q failed: %w", name, err) } return diff --git a/hugolib/site_render.go b/hugolib/site_render.go index b572c443e..51d638dde 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -19,9 +19,8 @@ import ( "strings" "sync" - "github.com/gohugoio/hugo/tpl" - "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/tpl" "errors" diff --git a/hugolib/site_test.go b/hugolib/site_test.go index 8dac8fc92..a2ee56994 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -14,6 +14,7 @@ package hugolib import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -630,7 +631,7 @@ func TestOrderedPages(t *testing.T) { t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "Three", rbypubdate[0].Title()) } - bylength := s.RegularPages().ByLength() + bylength := s.RegularPages().ByLength(context.Background()) if bylength[0].Title() != "One" { t.Errorf("Pages in unexpected order. First should be '%s', got '%s'", "One", bylength[0].Title()) } @@ -662,7 +663,7 @@ func TestGroupedPages(t *testing.T) { writeSourcesToSource(t, "content", fs, groupedSources...) s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) - rbysection, err := s.RegularPages().GroupBy("Section", "desc") + rbysection, err := s.RegularPages().GroupBy(context.Background(), "Section", "desc") if err != nil { t.Fatalf("Unable to make PageGroup array: %s", err) } @@ -683,7 +684,7 @@ func TestGroupedPages(t *testing.T) { t.Errorf("PageGroup has unexpected number of pages. Third group should have '%d' pages, got '%d' pages", 2, len(rbysection[2].Pages)) } - bytype, err := s.RegularPages().GroupBy("Type", "asc") + bytype, err := s.RegularPages().GroupBy(context.Background(), "Type", "asc") if err != nil { t.Fatalf("Unable to make PageGroup array: %s", err) } diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index ca74e9340..89255c695 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -2,6 +2,7 @@ package hugolib import ( "bytes" + "context" "fmt" "image/jpeg" "io" @@ -1005,7 +1006,7 @@ func getPage(in page.Page, ref string) page.Page { } func content(c resource.ContentProvider) string { - cc, err := c.Content() + cc, err := c.Content(context.Background()) if err != nil { panic(err) } |