]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Add page fragments support to Related
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 11 Feb 2023 15:20:24 +0000 (16:20 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 21 Feb 2023 16:56:41 +0000 (17:56 +0100)
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

66 files changed:
.gitignore [new file with mode: 0644]
common/collections/slice.go
common/collections/slice_test.go
compare/compare.go
docs/content/en/content-management/related.md
go.mod
go.sum
hugolib/content_factory.go
hugolib/content_map_page.go
hugolib/embedded_shortcodes_test.go
hugolib/hugo_sites.go
hugolib/hugo_sites_build.go
hugolib/hugo_sites_build_errors_test.go
hugolib/image_test.go
hugolib/language_content_dir_test.go
hugolib/page.go
hugolib/page__content.go
hugolib/page__menus.go
hugolib/page__new.go
hugolib/page__per_output.go
hugolib/page__position.go
hugolib/page_test.go
hugolib/shortcode.go
hugolib/shortcode_page.go
hugolib/shortcode_test.go
hugolib/site.go
hugolib/site_render.go
hugolib/site_test.go
hugolib/testhelpers_test.go
lazy/init.go
lazy/init_test.go
markup/asciidocext/convert.go
markup/asciidocext/convert_test.go
markup/converter/converter.go
markup/goldmark/convert.go
markup/goldmark/toc.go
markup/tableofcontents/tableofcontents.go
markup/tableofcontents/tableofcontents_test.go
related/integration_test.go
related/inverted_index.go
related/inverted_index_test.go
resources/errorResource.go
resources/image_test.go
resources/page/page.go
resources/page/page_lazy_contentprovider.go
resources/page/page_marshaljson.autogen.go
resources/page/page_nop.go
resources/page/pagegroup.go
resources/page/pagegroup_test.go
resources/page/pages.go
resources/page/pages_related.go
resources/page/pages_related_test.go
resources/page/pages_sort.go
resources/page/pages_sort_test.go
resources/page/pagination_test.go
resources/page/testhelpers_test.go
resources/postpub/postpub.go
resources/resource.go
resources/resource/resourcetypes.go
resources/resource_transformers/integrity/integrity_test.go
resources/resource_transformers/minifier/minify_test.go
resources/transform.go
resources/transform_test.go
tpl/internal/go_templates/texttemplate/hugo_template.go
tpl/transform/transform.go
tpl/transform/transform_test.go

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..00b5b2e
--- /dev/null
@@ -0,0 +1,2 @@
+
+*.test
\ No newline at end of file
index 51cb6ec1f80b5e4f75eff08fbaa6f29be78e342e..bf5c7b52b3061fa3c79914ab06198c078500dee8 100644 (file)
@@ -15,6 +15,7 @@ package collections
 
 import (
        "reflect"
+       "sort"
 )
 
 // Slicer defines a very generic way to create a typed slice. This is used
@@ -74,3 +75,22 @@ func StringSliceToInterfaceSlice(ss []string) []any {
        return result
 
 }
+
+type SortedStringSlice []string
+
+// Contains returns true if s is in ss.
+func (ss SortedStringSlice) Contains(s string) bool {
+       i := sort.SearchStrings(ss, s)
+       return i < len(ss) && ss[i] == s
+}
+
+// Count returns the number of times s is in ss.
+func (ss SortedStringSlice) Count(s string) int {
+       var count int
+       i := sort.SearchStrings(ss, s)
+       for i < len(ss) && ss[i] == s {
+               count++
+               i++
+       }
+       return count
+}
index 8e6553994da9afd4afbc0d6a12dd07c170678e02..5788b9161c8f58c105ecd1bf2bbe4757b1e785c0 100644 (file)
@@ -122,3 +122,18 @@ func TestSlice(t *testing.T) {
                c.Assert(test.expected, qt.DeepEquals, result, errMsg)
        }
 }
+
+func TestSortedStringSlice(t *testing.T) {
+       t.Parallel()
+       c := qt.New(t)
+
+       var s SortedStringSlice = []string{"a", "b", "b", "b", "c", "d"}
+
+       c.Assert(s.Contains("a"), qt.IsTrue)
+       c.Assert(s.Contains("b"), qt.IsTrue)
+       c.Assert(s.Contains("z"), qt.IsFalse)
+       c.Assert(s.Count("b"), qt.Equals, 3)
+       c.Assert(s.Count("z"), qt.Equals, 0)
+       c.Assert(s.Count("a"), qt.Equals, 1)
+
+}
index de97690c7d409a8ef457e97d1af800460c84cb71..67bb1c1256dd70f251a975c2255d03b23428434d 100644 (file)
@@ -36,3 +36,19 @@ type ProbablyEqer interface {
 type Comparer interface {
        Compare(other any) int
 }
+
+// Eq returns whether v1 is equal to v2.
+// It will use the Eqer interface if implemented, which
+// defines equals when two value are interchangeable
+// in the Hugo templates.
+func Eq(v1, v2 any) bool {
+       if v1 == nil || v2 == nil {
+               return v1 == v2
+       }
+
+       if eqer, ok := v1.(Eqer); ok {
+               return eqer.Eq(v2)
+       }
+
+       return v1 == v2
+}
index 2d2077c81c627ca7f7798efdb3543a01ca1d5072..bd3a5d466617aeedbc6e85cae7ec049e5562990d 100644 (file)
@@ -31,40 +31,82 @@ To list up to 5 related pages (which share the same _date_ or _keyword_ paramete
 {{ end }}
 {{< /code >}}
 
-### Methods
+The `Related` method takes one argument which may be a `Page` or a options map. The options map have these options:
 
-Here is the list of "Related" methods available on a page collection such `.RegularPages`.
+indices
+: The indices to search in.
 
-#### .Related PAGE
+document
+: The document to search for related content for.
 
-Returns a collection of pages related the given one.
+namedSlices
+: The keywords to search for.
+
+fragments
+: Fragments holds a a list of special keywords that is used for indices configured as type "fragments". This will match the fragment identifiers of the documents.
+
+A fictional example using all of the above options:
 
 ```go-html-template
-{{ $related := site.RegularPages.Related . }}
+{{ $page := . }}
+{{ $opts := 
+  "indices" (slice "tags" "keywords")
+  "document" $page
+  "namedSlices" (slice (keyVals "tags" "hugo" "rocks") (keyVals "date" $page.Date))
+  "fragments" (slice "heading-1" "heading-2")
+}}
 ```
 
-#### .RelatedIndices PAGE INDICE1 [INDICE2 ...]
+{{% note %}}
+We improved and simplified this feature in Hugo 0.111.0. Before this we had 3 different methods: `Related`, `RelatedTo` and `RelatedIndicies`. Now we have only one method: `Related`. The old methods are still available but deprecated. Also see [this blog article](https://regisphilibert.com/blog/2018/04/hugo-optmized-relashionships-with-related-content/) for a great explanation of more advanced usage of this feature.
+{{% /note %}}
+
+## Index Content Headings in Related Content
 
-Returns a collection of pages related to a given one restricted to a list of indices.
+{{< new-in "0.111.0" >}}
 
-```go-html-template
-{{ $related := site.RegularPages.RelatedIndices . "tags" "date" }}
-```
+Hugo can index the headings in your content and use this to find related content. You can enable this by adding a index of type `fragments` to your `related` configuration:
 
-#### .RelatedTo KEYVALS [KEYVALS2 ...]
 
-Returns a collection of pages related together by a set of indices and their match.
+```toml
+[related]
+threshold    = 20
+includeNewer = true
+toLower      = false
+[[related.indices]]
+name        = "fragmentrefs"
+type        = "fragments"
+applyFilter = false
+weight      = 80
+```
 
-In order to build those set and pass them as argument, one must use the `keyVals` function where the first argument would be the `indice` and the consecutive ones its potential `matches`.
+* The `name` maps to a optional front matter slice attribute that can be used to link from the page level down to the fragment/heading level.
+* If `applyFilter`is enabled, the `.HeadingsFiltered` on each page in the result will reflect the filtered headings. This is useful if you want to show the headings in the related content listing:
 
 ```go-html-template
-{{ $related := site.RegularPages.RelatedTo ( keyVals "tags" "hugo" "rocks")  ( keyVals "date" .Date ) }}
+{{ $related := .Site.RegularPages.Related . | first 5 }}
+{{ with $related }}
+  <h2>See Also</h2>
+  <ul>
+    {{ range . }}
+      <li>
+        <a href="{{ .RelPermalink }}">{{ .Title }}</a>
+        {{ with .HeadingsFiltered }}
+          <ul>
+            {{ range . }}
+              {{ $link := printf "%s#%s" $.RelPermalink .ID }}
+              <li>
+                <a href="{{ $link }}">{{ .Title }}</a>
+              </li>
+            {{ end }}
+          </ul>
+        {{ end }}
+      </li>
+    {{ end }}
+  </ul>
+{{ end }}
 ```
 
-{{% note %}}
-Read [this blog article](https://regisphilibert.com/blog/2018/04/hugo-optmized-relashionships-with-related-content/) for a great explanation of more advanced usage of this feature.
-{{% /note %}}
-
 ## Configure Related Content
 
 Hugo provides a sensible default configuration of Related Content, but you can fine-tune this in your configuration, on the global or language level if needed.
@@ -109,6 +151,12 @@ toLower
 name
 :  The index name. This value maps directly to a page param. Hugo supports string values (`author` in the example) and lists (`tags`, `keywords` etc.) and time and date objects.
 
+type
+: {{< new-in "0.111.0" >}}. One of `basic`(default) or `fragments`.
+
+applyFilter
+: {{< new-in "0.111.0" >}}. Apply a `type` specific filter to the result of a search. This is currently only used for the `fragments` type.
+
 weight
 : An integer weight that indicates _how important_ this parameter is relative to the other parameters.  It can be 0, which has the effect of turning this index off, or even negative. Test with different values to see what fits your content best.
 
diff --git a/go.mod b/go.mod
index d53a7a06e65e1bc2af07d06f2af2197ba7093908..5ae9efd269a4ef8fd827e7c71b4bf1ae8cec8535 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -61,6 +61,7 @@ require (
        github.com/yuin/goldmark v1.5.4
        go.uber.org/atomic v1.10.0
        gocloud.dev v0.28.0
+       golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
        golang.org/x/image v0.0.0-20211028202545-6944b10bf410
        golang.org/x/net v0.4.0
        golang.org/x/sync v0.1.0
diff --git a/go.sum b/go.sum
index 45d1f4ebeaca88da722bef39af2d3fa5aab66fd6..48c2ba125be93e23b458b0ac7f5611f7a526ccf0 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -2002,6 +2002,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE=
 golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
index 017a0bc979c656c15142d40f9d4b5728c973693d..e22f46513982f1dff387932903f02756fae3d097 100644 (file)
@@ -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)
        }
index d8f28286cdf030b6a311393f1742da24f997f59f..70c5d6a27a39da8b71569d5cdfde2490cbf7e2c8 100644 (file)
@@ -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
index 1707bcfa7e89105faff3b5e4d3adab473373ffce..1e06494bfa6ba012bcd624395087bcc0691467af 100644 (file)
@@ -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)
 
index 569c27be5c7363fd4ee14b4667687fb9b8bf7567..cdc5d97fb786357e9c8915de70cf1897388b6cea 100644 (file)
@@ -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)
index 5eee564aaf319f0fc87cbd69128a88b825c752f1..66abf4f1655ab84adf90e13941dbbb52e2372753 100644 (file)
@@ -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
        }
 
index ffbfe1c17e5af6b40c19a048c8591a339bd5493d..f42b4446172a8d6404e8b8ba4a12ae4645ab7e47 100644 (file)
@@ -396,7 +396,7 @@ line 4
 
 }
 
-func TestErrorNestedShortocde(t *testing.T) {
+func TestErrorNestedShortcode(t *testing.T) {
        t.Parallel()
 
        files := `
index ac18b9423d880b2ef118dc8d59b6f2fbc99f426f..db1707c2292b0f72db67f58f2d3dd14d3c4c2eb1 100644 (file)
 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"
index 57cdab67bc6b7e037940feec8c5850ba6278974d..23809f4dffe2264f3f8f427e2e3d450555099c30 100644 (file)
@@ -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/")
index 97f1ed351ba3b5b1fdad981da557248891d05fc4..40972d7c57dd50c43e7efe4a9ed7b416b291519d 100644 (file)
@@ -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
index a721d1fce979e1aac4b8d1773fb2c72af94856b7..89c38bd841b3a797a713d1fa0eeb86e10ce5a0b2 100644 (file)
@@ -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 {
index 49d392c2fc228c14409c8118a98df4234b15af8c..5bed2bc033df8438cb6b51e1f93872becd62c155 100644 (file)
@@ -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()
 }
index e52b9476b032abcd0c11614f926c80de9e80be72..3787cd2bd4f06104b30ee04ed7c67d716fce8355 100644 (file)
@@ -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
index 97e9cc46564d129b00e487ac579f7e12cca7f6a8..827a6b792fb7c0ea9d951d40d3c0fdf15f07b995 100644 (file)
@@ -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
index a087872cc0a383b0d84868356409eb997b8ffb7a..d977a705273cebd6226606105352512138170e12 100644 (file)
@@ -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
 }
 
index 939d06d414148980310aa7ab94d261ae19628e77..49617f17e2100727b9a90589e6dd381b6efc844e 100644 (file)
@@ -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()))
                }
        }
 
index 2951a143608c0ae4fb174adc9ca62b8bae0b6cbf..a82caff43cf5e73ab3e20276f5ab79f89577770b 100644 (file)
@@ -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)
        }
index 5a56e434f2f62877300bcf1eb3c011d3e4ab5cfb..3bc061bc08416092172d90319eb30d5dc6a90328 100644 (file)
 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
 }
index b5f27d6218cd62adbdc6a49f73f80c9536f9234f..2f285d0da4d7a1922cfd556765d22d657d6ecec7 100644 (file)
@@ -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 {
index 0ca7a81b448ae67b31877934b5b8fa769dedbcb1..e90fa41ff633a1aa68022bc977ef4e529baf1be5 100644 (file)
@@ -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
index b572c443e1b05bf363a7b48124919146534e22cc..51d638ddef0fec4a2b4ac25204084961187fa2f3 100644 (file)
@@ -19,9 +19,8 @@ import (
        "strings"
        "sync"
 
-       "github.com/gohugoio/hugo/tpl"
-
        "github.com/gohugoio/hugo/config"
+       "github.com/gohugoio/hugo/tpl"
 
        "errors"
 
index 8dac8fc92bd9b5b441e386221f728f2f4f0020d8..a2ee56994784006b24be011d9deb5aa58ca66da8 100644 (file)
@@ -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)
        }
index ca74e9340e29308640ef1502aede11ea36f7e6b3..89255c695ee7e48b66101e0bf37a5c66c7ac058e 100644 (file)
@@ -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)
        }
index b998d0305d32ec98c1c536919a4d7698cc464de1..4de2a83f74ff4407d3c88327372b16bb0e93575b 100644 (file)
@@ -29,7 +29,7 @@ func New() *Init {
 
 // Init holds a graph of lazily initialized dependencies.
 type Init struct {
-       // Used in tests
+       // Used mainly for testing.
        initCount uint64
 
        mu sync.Mutex
@@ -40,11 +40,11 @@ type Init struct {
        init onceMore
        out  any
        err  error
-       f    func() (any, error)
+       f    func(context.Context) (any, error)
 }
 
 // Add adds a func as a new child dependency.
-func (ini *Init) Add(initFn func() (any, error)) *Init {
+func (ini *Init) Add(initFn func(context.Context) (any, error)) *Init {
        if ini == nil {
                ini = New()
        }
@@ -59,14 +59,14 @@ func (ini *Init) InitCount() int {
 
 // AddWithTimeout is same as Add, but with a timeout that aborts initialization.
 func (ini *Init) AddWithTimeout(timeout time.Duration, f func(ctx context.Context) (any, error)) *Init {
-       return ini.Add(func() (any, error) {
-               return ini.withTimeout(timeout, f)
+       return ini.Add(func(ctx context.Context) (any, error) {
+               return ini.withTimeout(ctx, timeout, f)
        })
 }
 
 // Branch creates a new dependency branch based on an existing and adds
 // the given dependency as a child.
-func (ini *Init) Branch(initFn func() (any, error)) *Init {
+func (ini *Init) Branch(initFn func(context.Context) (any, error)) *Init {
        if ini == nil {
                ini = New()
        }
@@ -75,13 +75,13 @@ func (ini *Init) Branch(initFn func() (any, error)) *Init {
 
 // BranchdWithTimeout is same as Branch, but with a timeout.
 func (ini *Init) BranchWithTimeout(timeout time.Duration, f func(ctx context.Context) (any, error)) *Init {
-       return ini.Branch(func() (any, error) {
-               return ini.withTimeout(timeout, f)
+       return ini.Branch(func(ctx context.Context) (any, error) {
+               return ini.withTimeout(ctx, timeout, f)
        })
 }
 
 // Do initializes the entire dependency graph.
-func (ini *Init) Do() (any, error) {
+func (ini *Init) Do(ctx context.Context) (any, error) {
        if ini == nil {
                panic("init is nil")
        }
@@ -92,7 +92,7 @@ func (ini *Init) Do() (any, error) {
                if prev != nil {
                        // A branch. Initialize the ancestors.
                        if prev.shouldInitialize() {
-                               _, err := prev.Do()
+                               _, err := prev.Do(ctx)
                                if err != nil {
                                        ini.err = err
                                        return
@@ -105,12 +105,12 @@ func (ini *Init) Do() (any, error) {
                }
 
                if ini.f != nil {
-                       ini.out, ini.err = ini.f()
+                       ini.out, ini.err = ini.f(ctx)
                }
 
                for _, child := range ini.children {
                        if child.shouldInitialize() {
-                               _, err := child.Do()
+                               _, err := child.Do(ctx)
                                if err != nil {
                                        ini.err = err
                                        return
@@ -154,7 +154,7 @@ func (ini *Init) Reset() {
        }
 }
 
-func (ini *Init) add(branch bool, initFn func() (any, error)) *Init {
+func (ini *Init) add(branch bool, initFn func(context.Context) (any, error)) *Init {
        ini.mu.Lock()
        defer ini.mu.Unlock()
 
@@ -179,8 +179,8 @@ func (ini *Init) checkDone() {
        }
 }
 
-func (ini *Init) withTimeout(timeout time.Duration, f func(ctx context.Context) (any, error)) (any, error) {
-       ctx, cancel := context.WithTimeout(context.Background(), timeout)
+func (ini *Init) withTimeout(ctx context.Context, timeout time.Duration, f func(ctx context.Context) (any, error)) (any, error) {
+       ctx, cancel := context.WithTimeout(ctx, timeout)
        defer cancel()
        c := make(chan verr, 1)
 
index 4d871b937336b4c454b5428957965e7f9c5b7a93..499ea2cce7b5b1b58eef38cf0068fc7ce6b72484 100644 (file)
@@ -48,16 +48,16 @@ func TestInit(t *testing.T) {
 
        var result string
 
-       f1 := func(name string) func() (any, error) {
-               return func() (any, error) {
+       f1 := func(name string) func(context.Context) (any, error) {
+               return func(context.Context) (any, error) {
                        result += name + "|"
                        doWork()
                        return name, nil
                }
        }
 
-       f2 := func() func() (any, error) {
-               return func() (any, error) {
+       f2 := func() func(context.Context) (any, error) {
+               return func(context.Context) (any, error) {
                        doWork()
                        return nil, nil
                }
@@ -75,6 +75,8 @@ func TestInit(t *testing.T) {
 
        var wg sync.WaitGroup
 
+       ctx := context.Background()
+
        // Add some concurrency and randomness to verify thread safety and
        // init order.
        for i := 0; i < 100; i++ {
@@ -83,20 +85,20 @@ func TestInit(t *testing.T) {
                        defer wg.Done()
                        var err error
                        if rnd.Intn(10) < 5 {
-                               _, err = root.Do()
+                               _, err = root.Do(ctx)
                                c.Assert(err, qt.IsNil)
                        }
 
                        // Add a new branch on the fly.
                        if rnd.Intn(10) > 5 {
                                branch := branch1_2.Branch(f2())
-                               _, err = branch.Do()
+                               _, err = branch.Do(ctx)
                                c.Assert(err, qt.IsNil)
                        } else {
-                               _, err = branch1_2_1.Do()
+                               _, err = branch1_2_1.Do(ctx)
                                c.Assert(err, qt.IsNil)
                        }
-                       _, err = branch1_2.Do()
+                       _, err = branch1_2.Do(ctx)
                        c.Assert(err, qt.IsNil)
                }(i)
 
@@ -114,7 +116,7 @@ func TestInitAddWithTimeout(t *testing.T) {
                return nil, nil
        })
 
-       _, err := init.Do()
+       _, err := init.Do(context.Background())
 
        c.Assert(err, qt.IsNil)
 }
@@ -133,7 +135,7 @@ func TestInitAddWithTimeoutTimeout(t *testing.T) {
                return nil, nil
        })
 
-       _, err := init.Do()
+       _, err := init.Do(context.Background())
 
        c.Assert(err, qt.Not(qt.IsNil))
 
@@ -149,7 +151,7 @@ func TestInitAddWithTimeoutError(t *testing.T) {
                return nil, errors.New("failed")
        })
 
-       _, err := init.Do()
+       _, err := init.Do(context.Background())
 
        c.Assert(err, qt.Not(qt.IsNil))
 }
@@ -178,8 +180,8 @@ func TestInitBranchOrder(t *testing.T) {
 
        base := New()
 
-       work := func(size int, f func()) func() (any, error) {
-               return func() (any, error) {
+       work := func(size int, f func()) func(context.Context) (any, error) {
+               return func(context.Context) (any, error) {
                        doWorkOfSize(size)
                        if f != nil {
                                f()
@@ -205,13 +207,14 @@ func TestInitBranchOrder(t *testing.T) {
        }
 
        var wg sync.WaitGroup
+       ctx := context.Background()
 
        for _, v := range inits {
                v := v
                wg.Add(1)
                go func() {
                        defer wg.Done()
-                       _, err := v.Do()
+                       _, err := v.Do(ctx)
                        c.Assert(err, qt.IsNil)
                }()
        }
@@ -225,17 +228,17 @@ func TestInitBranchOrder(t *testing.T) {
 func TestResetError(t *testing.T) {
        c := qt.New(t)
        r := false
-       i := New().Add(func() (any, error) {
+       i := New().Add(func(context.Context) (any, error) {
                if r {
                        return nil, nil
                }
                return nil, errors.New("r is false")
        })
-       _, err := i.Do()
+       _, err := i.Do(context.Background())
        c.Assert(err, qt.IsNotNil)
        i.Reset()
        r = true
-       _, err = i.Do()
+       _, err = i.Do(context.Background())
        c.Assert(err, qt.IsNil)
 
 }
index 4c83e0e95b6b9fa24a21a8d3f3068286155b32b2..c9524778f6b9807e4baa5ebe379096707217f159 100644 (file)
@@ -53,10 +53,10 @@ func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error)
 
 type asciidocResult struct {
        converter.Result
-       toc tableofcontents.Root
+       toc *tableofcontents.Fragments
 }
 
-func (r asciidocResult) TableOfContents() tableofcontents.Root {
+func (r asciidocResult) TableOfContents() *tableofcontents.Fragments {
        return r.toc
 }
 
@@ -205,16 +205,16 @@ func hasAsciiDoc() bool {
 
 // extractTOC extracts the toc from the given src html.
 // It returns the html without the TOC, and the TOC data
-func (a *asciidocConverter) extractTOC(src []byte) ([]byte, tableofcontents.Root, error) {
+func (a *asciidocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fragments, error) {
        var buf bytes.Buffer
        buf.Write(src)
        node, err := html.Parse(&buf)
        if err != nil {
-               return nil, tableofcontents.Root{}, err
+               return nil, nil, err
        }
        var (
                f       func(*html.Node) bool
-               toc     tableofcontents.Root
+               toc     *tableofcontents.Fragments
                toVisit []*html.Node
        )
        f = func(n *html.Node) bool {
@@ -242,12 +242,12 @@ func (a *asciidocConverter) extractTOC(src []byte) ([]byte, tableofcontents.Root
        }
        f(node)
        if err != nil {
-               return nil, tableofcontents.Root{}, err
+               return nil, nil, err
        }
        buf.Reset()
        err = html.Render(&buf, node)
        if err != nil {
-               return nil, tableofcontents.Root{}, err
+               return nil, nil, err
        }
        // ltrim <html><head></head><body> and rtrim </body></html> which are added by html.Render
        res := buf.Bytes()[25:]
@@ -256,9 +256,9 @@ func (a *asciidocConverter) extractTOC(src []byte) ([]byte, tableofcontents.Root
 }
 
 // parseTOC returns a TOC root from the given toc Node
-func parseTOC(doc *html.Node) tableofcontents.Root {
+func parseTOC(doc *html.Node) *tableofcontents.Fragments {
        var (
-               toc tableofcontents.Root
+               toc tableofcontents.Builder
                f   func(*html.Node, int, int)
        )
        f = func(n *html.Node, row, level int) {
@@ -276,9 +276,9 @@ func parseTOC(doc *html.Node) tableofcontents.Root {
                                                continue
                                        }
                                        href := attr(c, "href")[1:]
-                                       toc.AddAt(tableofcontents.Heading{
-                                               Text: nodeContent(c),
-                                               ID:   href,
+                                       toc.AddAt(&tableofcontents.Heading{
+                                               Title: nodeContent(c),
+                                               ID:    href,
                                        }, row, level)
                                }
                                f(n.FirstChild, row, level)
@@ -289,7 +289,7 @@ func parseTOC(doc *html.Node) tableofcontents.Root {
                }
        }
        f(doc.FirstChild, -1, 0)
-       return toc
+       return toc.Build()
 }
 
 func attr(node *html.Node, key string) string {
index 3a350c5cea6f11fcb1be0ed0b32e819d1d4d4d4d..47208c0661609ef809e32d2fcc67feb706e8e1e2 100644 (file)
@@ -21,13 +21,13 @@ import (
        "path/filepath"
        "testing"
 
+       "github.com/gohugoio/hugo/common/collections"
        "github.com/gohugoio/hugo/common/hexec"
        "github.com/gohugoio/hugo/common/loggers"
        "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/config/security"
        "github.com/gohugoio/hugo/markup/converter"
        "github.com/gohugoio/hugo/markup/markup_config"
-       "github.com/gohugoio/hugo/markup/tableofcontents"
 
        qt "github.com/frankban/quicktest"
 )
@@ -343,49 +343,8 @@ testContent
        c.Assert(err, qt.IsNil)
        toc, ok := r.(converter.TableOfContentsProvider)
        c.Assert(ok, qt.Equals, true)
-       expected := tableofcontents.Root{
-               Headings: tableofcontents.Headings{
-                       {
-                               ID:   "",
-                               Text: "",
-                               Headings: tableofcontents.Headings{
-                                       {
-                                               ID:       "_introduction",
-                                               Text:     "Introduction",
-                                               Headings: nil,
-                                       },
-                                       {
-                                               ID:   "_section_1",
-                                               Text: "Section 1",
-                                               Headings: tableofcontents.Headings{
-                                                       {
-                                                               ID:   "_section_1_1",
-                                                               Text: "Section 1.1",
-                                                               Headings: tableofcontents.Headings{
-                                                                       {
-                                                                               ID:       "_section_1_1_1",
-                                                                               Text:     "Section 1.1.1",
-                                                                               Headings: nil,
-                                                                       },
-                                                               },
-                                                       },
-                                                       {
-                                                               ID:       "_section_1_2",
-                                                               Text:     "Section 1.2",
-                                                               Headings: nil,
-                                                       },
-                                               },
-                                       },
-                                       {
-                                               ID:       "_section_2",
-                                               Text:     "Section 2",
-                                               Headings: nil,
-                                       },
-                               },
-                       },
-               },
-       }
-       c.Assert(toc.TableOfContents(), qt.DeepEquals, expected)
+
+       c.Assert(toc.TableOfContents().Identifiers, qt.DeepEquals, collections.SortedStringSlice{"_introduction", "_section_1", "_section_1_1", "_section_1_1_1", "_section_1_2", "_section_2"})
        c.Assert(string(r.Bytes()), qt.Not(qt.Contains), "<div id=\"toc\" class=\"toc\">")
 }
 
@@ -404,22 +363,7 @@ func TestTableOfContentsWithCode(t *testing.T) {
        c.Assert(err, qt.IsNil)
        toc, ok := r.(converter.TableOfContentsProvider)
        c.Assert(ok, qt.Equals, true)
-       expected := tableofcontents.Root{
-               Headings: tableofcontents.Headings{
-                       {
-                               ID:   "",
-                               Text: "",
-                               Headings: tableofcontents.Headings{
-                                       {
-                                               ID:       "_some_code_in_the_title",
-                                               Text:     "Some <code>code</code> in the title",
-                                               Headings: nil,
-                                       },
-                               },
-                       },
-               },
-       }
-       c.Assert(toc.TableOfContents(), qt.DeepEquals, expected)
+       c.Assert(toc.TableOfContents().HeadingsMap["_some_code_in_the_title"].Title, qt.Equals, "Some <code>code</code> in the title")
        c.Assert(string(r.Bytes()), qt.Not(qt.Contains), "<div id=\"toc\" class=\"toc\">")
 }
 
@@ -443,21 +387,7 @@ func TestTableOfContentsPreserveTOC(t *testing.T) {
        c.Assert(err, qt.IsNil)
        toc, ok := r.(converter.TableOfContentsProvider)
        c.Assert(ok, qt.Equals, true)
-       expected := tableofcontents.Root{
-               Headings: tableofcontents.Headings{
-                       {
-                               ID:   "",
-                               Text: "",
-                               Headings: tableofcontents.Headings{
-                                       {
-                                               ID:       "some-title",
-                                               Text:     "Some title",
-                                               Headings: nil,
-                                       },
-                               },
-                       },
-               },
-       }
-       c.Assert(toc.TableOfContents(), qt.DeepEquals, expected)
+
+       c.Assert(toc.TableOfContents().Identifiers, qt.DeepEquals, collections.SortedStringSlice{"some-title"})
        c.Assert(string(r.Bytes()), qt.Contains, "<div id=\"toc\" class=\"toc\">")
 }
index c760381f405efdec808dfc970bd8a97def460619..7e5b56b0758e518cb49236b97322d6f742681ced 100644 (file)
@@ -101,7 +101,7 @@ type DocumentInfo interface {
 
 // TableOfContentsProvider provides the content as a ToC structure.
 type TableOfContentsProvider interface {
-       TableOfContents() tableofcontents.Root
+       TableOfContents() *tableofcontents.Fragments
 }
 
 // AnchorNameSanitizer tells how a converter sanitizes anchor names.
index a179cd2339a1a7009fe07295c69617e95851dc3a..6c1c7ad0a7cee493c2cd1754493ba8a51aa93b99 100644 (file)
@@ -160,11 +160,11 @@ var _ identity.IdentitiesProvider = (*converterResult)(nil)
 
 type converterResult struct {
        converter.Result
-       toc tableofcontents.Root
+       toc *tableofcontents.Fragments
        ids identity.Identities
 }
 
-func (c converterResult) TableOfContents() tableofcontents.Root {
+func (c converterResult) TableOfContents() *tableofcontents.Fragments {
        return c.toc
 }
 
@@ -228,9 +228,9 @@ type parserContext struct {
        parser.Context
 }
 
-func (p *parserContext) TableOfContents() tableofcontents.Root {
+func (p *parserContext) TableOfContents() *tableofcontents.Fragments {
        if v := p.Get(tocResultKey); v != nil {
-               return v.(tableofcontents.Root)
+               return v.(*tableofcontents.Fragments)
        }
-       return tableofcontents.Root{}
+       return nil
 }
index 396c1d07122af2a7c69f50f9200feec04c553454..ac5040e85373a63502a060944cb76d61dd9bef7a 100644 (file)
@@ -41,8 +41,8 @@ func (t *tocTransformer) Transform(n *ast.Document, reader text.Reader, pc parse
        }
 
        var (
-               toc         tableofcontents.Root
-               tocHeading  tableofcontents.Heading
+               toc         tableofcontents.Builder
+               tocHeading  = &tableofcontents.Heading{}
                level       int
                row         = -1
                inHeading   bool
@@ -53,10 +53,10 @@ func (t *tocTransformer) Transform(n *ast.Document, reader text.Reader, pc parse
                s := ast.WalkStatus(ast.WalkContinue)
                if n.Kind() == ast.KindHeading {
                        if inHeading && !entering {
-                               tocHeading.Text = headingText.String()
+                               tocHeading.Title = headingText.String()
                                headingText.Reset()
                                toc.AddAt(tocHeading, row, level-1)
-                               tocHeading = tableofcontents.Heading{}
+                               tocHeading = &tableofcontents.Heading{}
                                inHeading = false
                                return s, nil
                        }
@@ -106,7 +106,7 @@ func (t *tocTransformer) Transform(n *ast.Document, reader text.Reader, pc parse
                return s, nil
        })
 
-       pc.Set(tocResultKey, toc)
+       pc.Set(tocResultKey, toc.Build())
 }
 
 type tocExtension struct {
index 2e7f47d207fe70a867d0c71f30be7639bb915f34..bd0aaa8012ec80ad5f6782c6067821268571af5a 100644 (file)
 package tableofcontents
 
 import (
+       "sort"
        "strings"
+
+       "github.com/gohugoio/hugo/common/collections"
 )
 
+// Empty is an empty ToC.
+var Empty = &Fragments{
+       Headings:    Headings{},
+       HeadingsMap: map[string]*Heading{},
+}
+
+// Builder is used to build the ToC data structure.
+type Builder struct {
+       toc *Fragments
+}
+
+// Add adds the heading to the ToC.
+func (b *Builder) AddAt(h *Heading, row, level int) {
+       if b.toc == nil {
+               b.toc = &Fragments{}
+       }
+       b.toc.addAt(h, row, level)
+}
+
+// Build returns the ToC.
+func (b Builder) Build() *Fragments {
+       if b.toc == nil {
+               return Empty
+       }
+       b.toc.HeadingsMap = make(map[string]*Heading)
+       b.toc.walk(func(h *Heading) {
+               if h.ID != "" {
+                       b.toc.HeadingsMap[h.ID] = h
+                       b.toc.Identifiers = append(b.toc.Identifiers, h.ID)
+               }
+       })
+       sort.Strings(b.toc.Identifiers)
+       return b.toc
+}
+
 // Headings holds the top level headings.
-type Headings []Heading
+type Headings []*Heading
+
+// FilterBy returns a new Headings slice with all headings that matches the given predicate.
+// For internal use only.
+func (h Headings) FilterBy(fn func(*Heading) bool) Headings {
+       var out Headings
+
+       for _, h := range h {
+               h.walk(func(h *Heading) {
+                       if fn(h) {
+                               out = append(out, h)
+                       }
+               })
+       }
+       return out
+}
 
 // Heading holds the data about a heading and its children.
 type Heading struct {
-       ID   string
-       Text string
+       ID    string
+       Title string
 
        Headings Headings
 }
 
 // IsZero is true when no ID or Text is set.
 func (h Heading) IsZero() bool {
-       return h.ID == "" && h.Text == ""
+       return h.ID == "" && h.Title == ""
+}
+
+func (h *Heading) walk(fn func(*Heading)) {
+       fn(h)
+       for _, h := range h.Headings {
+               h.walk(fn)
+       }
 }
 
-// Root implements AddAt, which can be used to build the
-// data structure for the ToC.
-type Root struct {
+// Fragments holds the table of contents for a page.
+type Fragments struct {
+       // Headings holds the top level headings.
        Headings Headings
+
+       // Identifiers holds all the identifiers in the ToC as a sorted slice.
+       // Note that collections.SortedStringSlice has both a Contains and Count method
+       // that can be used to identify missing and duplicate IDs.
+       Identifiers collections.SortedStringSlice
+
+       // HeadingsMap holds all the headings in the ToC as a map.
+       // Note that with duplicate IDs, the last one will win.
+       HeadingsMap map[string]*Heading
 }
 
-// AddAt adds the heading into the given location.
-func (toc *Root) AddAt(h Heading, row, level int) {
+// addAt adds the heading into the given location.
+func (toc *Fragments) addAt(h *Heading, row, level int) {
        for i := len(toc.Headings); i <= row; i++ {
-               toc.Headings = append(toc.Headings, Heading{})
+               toc.Headings = append(toc.Headings, &Heading{})
        }
 
        if level == 0 {
@@ -50,19 +119,22 @@ func (toc *Root) AddAt(h Heading, row, level int) {
                return
        }
 
-       heading := &toc.Headings[row]
+       heading := toc.Headings[row]
 
        for i := 1; i < level; i++ {
                if len(heading.Headings) == 0 {
-                       heading.Headings = append(heading.Headings, Heading{})
+                       heading.Headings = append(heading.Headings, &Heading{})
                }
-               heading = &heading.Headings[len(heading.Headings)-1]
+               heading = heading.Headings[len(heading.Headings)-1]
        }
        heading.Headings = append(heading.Headings, h)
 }
 
 // ToHTML renders the ToC as HTML.
-func (toc Root) ToHTML(startLevel, stopLevel int, ordered bool) string {
+func (toc *Fragments) ToHTML(startLevel, stopLevel int, ordered bool) string {
+       if toc == nil {
+               return ""
+       }
        b := &tocBuilder{
                s:          strings.Builder{},
                h:          toc.Headings,
@@ -74,6 +146,12 @@ func (toc Root) ToHTML(startLevel, stopLevel int, ordered bool) string {
        return b.s.String()
 }
 
+func (toc Fragments) walk(fn func(*Heading)) {
+       for _, h := range toc.Headings {
+               h.walk(fn)
+       }
+}
+
 type tocBuilder struct {
        s strings.Builder
        h Headings
@@ -133,11 +211,11 @@ func (b *tocBuilder) writeHeadings(level, indent int, h Headings) {
        }
 }
 
-func (b *tocBuilder) writeHeading(level, indent int, h Heading) {
+func (b *tocBuilder) writeHeading(level, indent int, h *Heading) {
        b.indent(indent)
        b.s.WriteString("<li>")
        if !h.IsZero() {
-               b.s.WriteString("<a href=\"#" + h.ID + "\">" + h.Text + "</a>")
+               b.s.WriteString("<a href=\"#" + h.ID + "\">" + h.Title + "</a>")
        }
        b.writeHeadings(level, indent, h.Headings)
        b.s.WriteString("</li>\n")
index daeb9f991e9f800be1c4a952ffa99a0edde9e3f7..adbda4b00e4df561cde12140a97c5ddc813184ce 100644 (file)
@@ -17,18 +17,33 @@ import (
        "testing"
 
        qt "github.com/frankban/quicktest"
+       "github.com/gohugoio/hugo/common/collections"
 )
 
+var newTestTocBuilder = func() Builder {
+       var b Builder
+       b.AddAt(&Heading{Title: "Heading 1", ID: "h1-1"}, 0, 0)
+       b.AddAt(&Heading{Title: "1-H2-1", ID: "1-h2-1"}, 0, 1)
+       b.AddAt(&Heading{Title: "1-H2-2", ID: "1-h2-2"}, 0, 1)
+       b.AddAt(&Heading{Title: "1-H3-1", ID: "1-h2-2"}, 0, 2)
+       b.AddAt(&Heading{Title: "Heading 2", ID: "h1-2"}, 1, 0)
+       return b
+}
+
+var newTestToc = func() *Fragments {
+       return newTestTocBuilder().Build()
+}
+
 func TestToc(t *testing.T) {
        c := qt.New(t)
 
-       toc := &Root{}
+       toc := &Fragments{}
 
-       toc.AddAt(Heading{Text: "Heading 1", ID: "h1-1"}, 0, 0)
-       toc.AddAt(Heading{Text: "1-H2-1", ID: "1-h2-1"}, 0, 1)
-       toc.AddAt(Heading{Text: "1-H2-2", ID: "1-h2-2"}, 0, 1)
-       toc.AddAt(Heading{Text: "1-H3-1", ID: "1-h2-2"}, 0, 2)
-       toc.AddAt(Heading{Text: "Heading 2", ID: "h1-2"}, 1, 0)
+       toc.addAt(&Heading{Title: "Heading 1", ID: "h1-1"}, 0, 0)
+       toc.addAt(&Heading{Title: "1-H2-1", ID: "1-h2-1"}, 0, 1)
+       toc.addAt(&Heading{Title: "1-H2-2", ID: "1-h2-2"}, 0, 1)
+       toc.addAt(&Heading{Title: "1-H3-1", ID: "1-h2-2"}, 0, 2)
+       toc.addAt(&Heading{Title: "Heading 2", ID: "h1-2"}, 1, 0)
 
        got := toc.ToHTML(1, -1, false)
        c.Assert(got, qt.Equals, `<nav id="TableOfContents">
@@ -97,11 +112,11 @@ func TestToc(t *testing.T) {
 func TestTocMissingParent(t *testing.T) {
        c := qt.New(t)
 
-       toc := &Root{}
+       toc := &Fragments{}
 
-       toc.AddAt(Heading{Text: "H2", ID: "h2"}, 0, 1)
-       toc.AddAt(Heading{Text: "H3", ID: "h3"}, 1, 2)
-       toc.AddAt(Heading{Text: "H3", ID: "h3"}, 1, 2)
+       toc.addAt(&Heading{Title: "H2", ID: "h2"}, 0, 1)
+       toc.addAt(&Heading{Title: "H3", ID: "h3"}, 1, 2)
+       toc.addAt(&Heading{Title: "H3", ID: "h3"}, 1, 2)
 
        got := toc.ToHTML(1, -1, false)
        c.Assert(got, qt.Equals, `<nav id="TableOfContents">
@@ -153,3 +168,53 @@ func TestTocMissingParent(t *testing.T) {
   </ol>
 </nav>`, qt.Commentf(got))
 }
+
+func TestTocMisc(t *testing.T) {
+       c := qt.New(t)
+
+       c.Run("Identifiers", func(c *qt.C) {
+               toc := newTestToc()
+               c.Assert(toc.Identifiers, qt.DeepEquals, collections.SortedStringSlice{"1-h2-1", "1-h2-2", "1-h2-2", "h1-1", "h1-2"})
+       })
+
+       c.Run("HeadingsMap", func(c *qt.C) {
+               toc := newTestToc()
+               m := toc.HeadingsMap
+               c.Assert(m["h1-1"].Title, qt.Equals, "Heading 1")
+               c.Assert(m["doesnot exist"], qt.IsNil)
+       })
+}
+
+func BenchmarkToc(b *testing.B) {
+
+       newTocs := func(n int) []*Fragments {
+               var tocs []*Fragments
+               for i := 0; i < n; i++ {
+                       tocs = append(tocs, newTestToc())
+               }
+               return tocs
+       }
+
+       b.Run("Build", func(b *testing.B) {
+               var builders []Builder
+               for i := 0; i < b.N; i++ {
+                       builders = append(builders, newTestTocBuilder())
+               }
+               b.ResetTimer()
+
+               for i := 0; i < b.N; i++ {
+                       b := builders[i]
+                       b.Build()
+               }
+       })
+
+       b.Run("ToHTML", func(b *testing.B) {
+               tocs := newTocs(b.N)
+               b.ResetTimer()
+               for i := 0; i < b.N; i++ {
+                       toc := tocs[i]
+                       toc.ToHTML(1, -1, false)
+               }
+       })
+
+}
index 31af01614368aa49fce310bd305a477e20842d19..70875b2121433c2044e6957debf647afd0cb17af 100644 (file)
@@ -21,6 +21,123 @@ import (
        "github.com/gohugoio/hugo/hugolib"
 )
 
+func TestRelatedFragments(t *testing.T) {
+       t.Parallel()
+
+       files := `
+-- hugo.toml --
+baseURL = "http://example.com/"
+disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT"]
+[related]
+  includeNewer = false
+  threshold = 80
+  toLower = false
+[[related.indices]]
+  name = 'pagerefs'
+  type = 'fragments'
+  applyFilter = true
+  weight = 90
+[[related.indices]]
+  name = 'keywords'
+  weight = 80  
+-- content/p1.md --
+---
+title: p1
+pagerefs: ['ref1']
+---
+{{< see-also >}}
+
+## P1 title
+
+-- content/p2.md --
+---
+title: p2
+---
+
+## P2 title 1
+
+## P2 title 2
+
+## First title {#ref1}
+{{< see-also "ref1" >}}
+-- content/p3.md --
+---
+title: p3
+keywords: ['foo']
+---
+
+## P3 title 1
+
+## P3 title 2
+
+## Common p3, p4, p5
+-- content/p4.md --
+---
+title: p4
+---
+
+## Common p3, p4, p5
+
+## P4 title 1
+
+-- content/p5.md --
+---
+title: p5
+keywords: ['foo']
+---
+
+## P5 title 1
+
+## Common p3, p4, p5
+
+-- layouts/shortcodes/see-also.html --
+{{ $p1 := site.GetPage "p1" }}
+{{ $p2 := site.GetPage "p2" }}
+{{ $p3 := site.GetPage "p3" }}
+P1 Fragments: {{ $p1.Fragments.Identifiers }}
+P2 Fragments: {{ $p2.Fragments.Identifiers }}
+Contains ref1: {{ $p2.Fragments.Identifiers.Contains "ref1" }}
+Count ref1: {{ $p2.Fragments.Identifiers.Count "ref1" }}
+{{ $opts := dict "document" .Page "fragments" $.Params }}
+{{ $related1 := site.RegularPages.Related $opts }}
+{{ $related2 := site.RegularPages.Related $p3 }}
+Len Related 1: {{ len $related1 }}
+Len Related 2: {{ len $related2 }}
+Related 1: {{ template "list-related" $related1 }}
+Related 2: {{ template "list-related" $related2 }}
+
+{{ define "list-related" }}{{ range $i, $e := . }} {{ $i }}: {{ .Title }}: {{ with .HeadingsFiltered}}{{ range $i, $e := .}}h{{ $i }}: {{ .Title }}|{{ .ID }}|{{ end }}{{ end }}::END{{ end }}{{ end }}
+
+-- layouts/_default/single.html --
+Content: {{ .Content }}
+
+       
+`
+
+       b := hugolib.NewIntegrationTestBuilder(
+               hugolib.IntegrationTestConfig{
+                       T:           t,
+                       TxtarString: files,
+               }).Build()
+
+       expect := `
+P1 Fragments: [p1-title]       
+P2 Fragments: [p2-title-1 p2-title-2 ref1]
+Len Related 1: 1
+Related 2: 2
+`
+
+       for _, p := range []string{"p1", "p2"} {
+               b.AssertFileContent("public/"+p+"/index.html", expect)
+       }
+
+       b.AssertFileContent("public/p1/index.html",
+               "Related 1:  0: p2: h0: First title|ref1|::END",
+               "Related 2:  0: p5: h0: Common p3, p4, p5|common-p3-p4-p5|::END 1: p4: h0: Common p3, p4, p5|common-p3-p4-p5|::END",
+       )
+
+}
+
 func BenchmarkRelatedSite(b *testing.B) {
        files := `
 -- config.toml --
@@ -33,6 +150,10 @@ disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT"]
 [[related.indices]]
   name = 'keywords'
   weight = 70  
+[[related.indices]]
+  name = 'pagerefs'
+  type = 'fragments'
+  weight = 30  
 -- layouts/_default/single.html --
 {{ range site.RegularPages }}
 {{ $tmp := .WordCount }}
index 5502f9f11758b7611fe67141bb520b0e5a227c8e..eab97098ae01c895351700df3a25e5c3a81572b1 100644 (file)
 package related
 
 import (
+       "context"
        "errors"
        "fmt"
        "math"
        "sort"
        "strings"
+       "sync"
        "time"
 
+       xmaps "golang.org/x/exp/maps"
+
+       "github.com/gohugoio/hugo/common/collections"
        "github.com/gohugoio/hugo/common/maps"
+       "github.com/gohugoio/hugo/compare"
+       "github.com/gohugoio/hugo/markup/tableofcontents"
        "github.com/spf13/cast"
 
        "github.com/gohugoio/hugo/common/types"
        "github.com/mitchellh/mapstructure"
 )
 
+const (
+       TypeBasic     = "basic"
+       TypeFragments = "fragments"
+)
+
+var validTypes = map[string]bool{
+       TypeBasic:     true,
+       TypeFragments: true,
+}
+
 var (
        _        Keyword = (*StringKeyword)(nil)
        zeroDate         = time.Time{}
@@ -37,8 +54,8 @@ var (
        DefaultConfig = Config{
                Threshold: 80,
                Indices: IndexConfigs{
-                       IndexConfig{Name: "keywords", Weight: 100},
-                       IndexConfig{Name: "date", Weight: 10},
+                       IndexConfig{Name: "keywords", Weight: 100, Type: TypeBasic},
+                       IndexConfig{Name: "date", Weight: 10, Type: TypeBasic},
                },
        }
 )
@@ -84,6 +101,15 @@ func (c *Config) Add(index IndexConfig) {
        c.Indices = append(c.Indices, index)
 }
 
+func (c *Config) HasType(s string) bool {
+       for _, i := range c.Indices {
+               if i.Type == s {
+                       return true
+               }
+       }
+       return false
+}
+
 // IndexConfigs holds a set of index configurations.
 type IndexConfigs []IndexConfig
 
@@ -92,6 +118,13 @@ type IndexConfig struct {
        // The index name. This directly maps to a field or Param name.
        Name string
 
+       // The index type.
+       Type string
+
+       // Enable to apply a type specific filter to the results.
+       // This is currently only used for the "fragments" type.
+       ApplyFilter bool
+
        // Contextual pattern used to convert the Param value into a string.
        // Currently only used for dates. Can be used to, say, bump posts in the same
        // time frame when searching for related documents.
@@ -120,6 +153,14 @@ type Document interface {
        Name() string
 }
 
+// FragmentProvider is an optional interface that can be implemented by a Document.
+type FragmentProvider interface {
+       Fragments(context.Context) *tableofcontents.Fragments
+
+       // For internal use.
+       ApplyFilterToHeadings(context.Context, func(*tableofcontents.Heading) bool) Document
+}
+
 // InvertedIndex holds an inverted index, also sometimes named posting list, which
 // lists, for every possible search term, the documents that contain that term.
 type InvertedIndex struct {
@@ -160,7 +201,7 @@ func NewInvertedIndex(cfg Config) *InvertedIndex {
 
 // Add documents to the inverted index.
 // The value must support == and !=.
-func (idx *InvertedIndex) Add(docs ...Document) error {
+func (idx *InvertedIndex) Add(ctx context.Context, docs ...Document) error {
        var err error
        for _, config := range idx.cfg.Indices {
                if config.Weight == 0 {
@@ -179,6 +220,14 @@ func (idx *InvertedIndex) Add(docs ...Document) error {
                        for _, keyword := range words {
                                setm[keyword] = append(setm[keyword], doc)
                        }
+
+                       if config.Type == TypeFragments {
+                               if fp, ok := doc.(FragmentProvider); ok {
+                                       for _, fragment := range fp.Fragments(ctx).Identifiers {
+                                               setm[FragmentKeyword(fragment)] = append(setm[FragmentKeyword(fragment)], doc)
+                                       }
+                               }
+                       }
                }
        }
 
@@ -209,8 +258,22 @@ func (r *rank) addWeight(w int) {
        r.Matches++
 }
 
-func newRank(doc Document, weight int) *rank {
-       return &rank{Doc: doc, Weight: weight, Matches: 1}
+var rankPool = sync.Pool{
+       New: func() interface{} {
+               return &rank{}
+       },
+}
+
+func getRank(doc Document, weight int) *rank {
+       r := rankPool.Get().(*rank)
+       r.Doc = doc
+       r.Weight = weight
+       r.Matches = 1
+       return r
+}
+
+func putRank(r *rank) {
+       rankPool.Put(r)
 }
 
 func (r ranks) Len() int      { return len(r) }
@@ -225,22 +288,41 @@ func (r ranks) Less(i, j int) bool {
        return r[i].Weight > r[j].Weight
 }
 
-// SearchDoc finds the documents matching any of the keywords in the given indices
-// against the given document.
+// SearchOpts holds the options for a related search.
+type SearchOpts struct {
+       // The Document to search for related content for.
+       Document Document
+
+       // The keywords to search for.
+       NamedSlices []types.KeyValues
+
+       // The indices to search in.
+       Indices []string
+
+       // Fragments holds a a list of special keywords that is used
+       // for indices configured as type "fragments".
+       // This will match the fragment identifiers of the documents.
+       Fragments []string
+}
+
+// Search finds the documents matching any of the keywords in the given indices
+// against query options in opts.
 // The resulting document set will be sorted according to number of matches
 // and the index weights, and any matches with a rank below the configured
 // threshold (normalize to 0..100) will be removed.
 // If an index name is provided, only that index will be queried.
-func (idx *InvertedIndex) SearchDoc(doc Document, indices ...string) ([]Document, error) {
-       var q []queryElement
+func (idx *InvertedIndex) Search(ctx context.Context, opts SearchOpts) ([]Document, error) {
 
-       var configs IndexConfigs
+       var (
+               queryElements []queryElement
+               configs       IndexConfigs
+       )
 
-       if len(indices) == 0 {
+       if len(opts.Indices) == 0 {
                configs = idx.cfg.Indices
        } else {
-               configs = make(IndexConfigs, len(indices))
-               for i, indexName := range indices {
+               configs = make(IndexConfigs, len(opts.Indices))
+               for i, indexName := range opts.Indices {
                        cfg, found := idx.getIndexCfg(indexName)
                        if !found {
                                return nil, fmt.Errorf("index %q not found", indexName)
@@ -250,40 +332,78 @@ func (idx *InvertedIndex) SearchDoc(doc Document, indices ...string) ([]Document
        }
 
        for _, cfg := range configs {
-               keywords, err := doc.RelatedKeywords(cfg)
-               if err != nil {
-                       return nil, err
+               var keywords []Keyword
+               if opts.Document != nil {
+                       k, err := opts.Document.RelatedKeywords(cfg)
+                       if err != nil {
+                               return nil, err
+                       }
+                       keywords = append(keywords, k...)
+               }
+               if cfg.Type == TypeFragments {
+                       for _, fragment := range opts.Fragments {
+                               keywords = append(keywords, FragmentKeyword(fragment))
+                       }
+                       if opts.Document != nil {
+                               if fp, ok := opts.Document.(FragmentProvider); ok {
+                                       for _, fragment := range fp.Fragments(ctx).Identifiers {
+                                               keywords = append(keywords, FragmentKeyword(fragment))
+                                       }
+                               }
+                       }
+               }
+               queryElements = append(queryElements, newQueryElement(cfg.Name, keywords...))
+       }
+       for _, slice := range opts.NamedSlices {
+               var keywords []Keyword
+               key := slice.KeyString()
+               if key == "" {
+                       return nil, fmt.Errorf("index %q not valid", slice.Key)
+               }
+               conf, found := idx.getIndexCfg(key)
+               if !found {
+                       return nil, fmt.Errorf("index %q not found", key)
                }
 
-               q = append(q, newQueryElement(cfg.Name, keywords...))
+               for _, val := range slice.Values {
+                       k, err := conf.ToKeywords(val)
+                       if err != nil {
+                               return nil, err
+                       }
+                       keywords = append(keywords, k...)
+               }
+               queryElements = append(queryElements, newQueryElement(conf.Name, keywords...))
+       }
 
+       if opts.Document != nil {
+               return idx.searchDate(ctx, opts.Document, opts.Document.PublishDate(), queryElements...)
        }
+       return idx.search(ctx, queryElements...)
+}
 
-       return idx.searchDate(doc.PublishDate(), q...)
+func (cfg IndexConfig) stringToKeyword(s string) Keyword {
+       if cfg.ToLower {
+               s = strings.ToLower(s)
+       }
+       if cfg.Type == TypeFragments {
+               return FragmentKeyword(s)
+       }
+       return StringKeyword(s)
 }
 
 // ToKeywords returns a Keyword slice of the given input.
 func (cfg IndexConfig) ToKeywords(v any) ([]Keyword, error) {
-       var (
-               keywords []Keyword
-               toLower  = cfg.ToLower
-       )
+       var keywords []Keyword
+
        switch vv := v.(type) {
        case string:
-               if toLower {
-                       vv = strings.ToLower(vv)
-               }
-               keywords = append(keywords, StringKeyword(vv))
+               keywords = append(keywords, cfg.stringToKeyword(vv))
        case []string:
-               if toLower {
-                       vc := make([]string, len(vv))
-                       copy(vc, vv)
-                       for i := 0; i < len(vc); i++ {
-                               vc[i] = strings.ToLower(vc[i])
-                       }
-                       vv = vc
+               vvv := make([]Keyword, len(vv))
+               for i := 0; i < len(vvv); i++ {
+                       vvv[i] = cfg.stringToKeyword(vv[i])
                }
-               keywords = append(keywords, StringsToKeywords(vv...)...)
+               keywords = append(keywords, vvv...)
        case []any:
                return cfg.ToKeywords(cast.ToStringSlice(vv))
        case time.Time:
@@ -301,46 +421,20 @@ func (cfg IndexConfig) ToKeywords(v any) ([]Keyword, error) {
        return keywords, nil
 }
 
-// SearchKeyValues finds the documents matching any of the keywords in the given indices.
-// The resulting document set will be sorted according to number of matches
-// and the index weights, and any matches with a rank below the configured
-// threshold (normalize to 0..100) will be removed.
-func (idx *InvertedIndex) SearchKeyValues(args ...types.KeyValues) ([]Document, error) {
-       q := make([]queryElement, len(args))
-
-       for i, arg := range args {
-               var keywords []Keyword
-               key := arg.KeyString()
-               if key == "" {
-                       return nil, fmt.Errorf("index %q not valid", arg.Key)
-               }
-               conf, found := idx.getIndexCfg(key)
-               if !found {
-                       return nil, fmt.Errorf("index %q not found", key)
-               }
-
-               for _, val := range arg.Values {
-                       k, err := conf.ToKeywords(val)
-                       if err != nil {
-                               return nil, err
-                       }
-                       keywords = append(keywords, k...)
-               }
-
-               q[i] = newQueryElement(conf.Name, keywords...)
-
-       }
-
-       return idx.search(q...)
+func (idx *InvertedIndex) search(ctx context.Context, query ...queryElement) ([]Document, error) {
+       return idx.searchDate(ctx, nil, zeroDate, query...)
 }
 
-func (idx *InvertedIndex) search(query ...queryElement) ([]Document, error) {
-       return idx.searchDate(zeroDate, query...)
-}
-
-func (idx *InvertedIndex) searchDate(upperDate time.Time, query ...queryElement) ([]Document, error) {
+func (idx *InvertedIndex) searchDate(ctx context.Context, self Document, upperDate time.Time, query ...queryElement) ([]Document, error) {
        matchm := make(map[Document]*rank, 200)
+       defer func() {
+               for _, r := range matchm {
+                       putRank(r)
+               }
+       }()
+
        applyDateFilter := !idx.cfg.IncludeNewer && !upperDate.IsZero()
+       var fragmentsFilter collections.SortedStringSlice
 
        for _, el := range query {
                setm, found := idx.index[el.Index]
@@ -356,15 +450,27 @@ func (idx *InvertedIndex) searchDate(upperDate time.Time, query ...queryElement)
                for _, kw := range el.Keywords {
                        if docs, found := setm[kw]; found {
                                for _, doc := range docs {
+                                       if compare.Eq(doc, self) {
+                                               continue
+                                       }
+
                                        if applyDateFilter {
                                                // Exclude newer than the limit given
                                                if doc.PublishDate().After(upperDate) {
                                                        continue
                                                }
                                        }
+
+                                       if config.Type == TypeFragments && config.ApplyFilter {
+                                               if fkw, ok := kw.(FragmentKeyword); ok {
+                                                       fragmentsFilter = append(fragmentsFilter, string(fkw))
+                                               }
+                                       }
+
                                        r, found := matchm[doc]
                                        if !found {
-                                               matchm[doc] = newRank(doc, config.Weight)
+                                               r = getRank(doc, config.Weight)
+                                               matchm[doc] = r
                                        } else {
                                                r.addWeight(config.Weight)
                                        }
@@ -390,11 +496,19 @@ func (idx *InvertedIndex) searchDate(upperDate time.Time, query ...queryElement)
        }
 
        sort.Stable(matches)
+       sort.Strings(fragmentsFilter)
 
        result := make([]Document, len(matches))
 
        for i, m := range matches {
                result[i] = m.Doc
+               if len(fragmentsFilter) > 0 {
+                       if dp, ok := result[i].(FragmentProvider); ok {
+                               result[i] = dp.ApplyFilterToHeadings(ctx, func(h *tableofcontents.Heading) bool {
+                                       return fragmentsFilter.Contains(h.ID)
+                               })
+                       }
+               }
        }
 
        return result, nil
@@ -433,6 +547,14 @@ func DecodeConfig(m maps.Params) (Config, error) {
                        c.Indices[i].ToLower = true
                }
        }
+       for i := range c.Indices {
+               if c.Indices[i].Type == "" {
+                       c.Indices[i].Type = TypeBasic
+               }
+               if !validTypes[c.Indices[i].Type] {
+                       return c, fmt.Errorf("invalid index type %q. Must be one of %v", c.Indices[i].Type, xmaps.Keys(validTypes))
+               }
+       }
 
        return c, nil
 }
@@ -444,17 +566,24 @@ func (s StringKeyword) String() string {
        return string(s)
 }
 
+// FragmentKeyword represents a document fragment.
+type FragmentKeyword string
+
+func (f FragmentKeyword) String() string {
+       return string(f)
+}
+
 // Keyword is the interface a keyword in the search index must implement.
 type Keyword interface {
        String() string
 }
 
 // StringsToKeywords converts the given slice of strings to a slice of Keyword.
-func StringsToKeywords(s ...string) []Keyword {
+func (cfg IndexConfig) StringsToKeywords(s ...string) []Keyword {
        kw := make([]Keyword, len(s))
 
        for i := 0; i < len(s); i++ {
-               kw[i] = StringKeyword(s[i])
+               kw[i] = cfg.stringToKeyword(s[i])
        }
 
        return kw
index 7a3bd89c7864a3214b8a64a34af2e25757ecbc8b..d38a7f6eb183efc0daa75b6bad86201fc0c86dc0 100644 (file)
@@ -14,6 +14,7 @@
 package related
 
 import (
+       "context"
        "fmt"
        "math/rand"
        "testing"
@@ -105,7 +106,7 @@ func TestSearch(t *testing.T) {
                newTestDoc("tags", "g", "h").addKeywords("keywords", "a", "b"),
        }
 
-       idx.Add(docs...)
+       idx.Add(context.Background(), docs...)
 
        t.Run("count", func(t *testing.T) {
                c := qt.New(t)
@@ -122,7 +123,8 @@ func TestSearch(t *testing.T) {
 
        t.Run("search-tags", func(t *testing.T) {
                c := qt.New(t)
-               m, err := idx.search(newQueryElement("tags", StringsToKeywords("a", "b", "d", "z")...))
+               var cfg IndexConfig
+               m, err := idx.search(context.Background(), newQueryElement("tags", cfg.StringsToKeywords("a", "b", "d", "z")...))
                c.Assert(err, qt.IsNil)
                c.Assert(len(m), qt.Equals, 2)
                c.Assert(m[0], qt.Equals, docs[0])
@@ -131,9 +133,10 @@ func TestSearch(t *testing.T) {
 
        t.Run("search-tags-and-keywords", func(t *testing.T) {
                c := qt.New(t)
-               m, err := idx.search(
-                       newQueryElement("tags", StringsToKeywords("a", "b", "z")...),
-                       newQueryElement("keywords", StringsToKeywords("a", "b")...))
+               var cfg IndexConfig
+               m, err := idx.search(context.Background(),
+                       newQueryElement("tags", cfg.StringsToKeywords("a", "b", "z")...),
+                       newQueryElement("keywords", cfg.StringsToKeywords("a", "b")...))
                c.Assert(err, qt.IsNil)
                c.Assert(len(m), qt.Equals, 3)
                c.Assert(m[0], qt.Equals, docs[3])
@@ -144,7 +147,7 @@ func TestSearch(t *testing.T) {
        t.Run("searchdoc-all", func(t *testing.T) {
                c := qt.New(t)
                doc := newTestDoc("tags", "a").addKeywords("keywords", "a")
-               m, err := idx.SearchDoc(doc)
+               m, err := idx.Search(context.Background(), SearchOpts{Document: doc})
                c.Assert(err, qt.IsNil)
                c.Assert(len(m), qt.Equals, 2)
                c.Assert(m[0], qt.Equals, docs[3])
@@ -154,7 +157,7 @@ func TestSearch(t *testing.T) {
        t.Run("searchdoc-tags", func(t *testing.T) {
                c := qt.New(t)
                doc := newTestDoc("tags", "a", "b", "d", "z").addKeywords("keywords", "a", "b")
-               m, err := idx.SearchDoc(doc, "tags")
+               m, err := idx.Search(context.Background(), SearchOpts{Document: doc, Indices: []string{"tags"}})
                c.Assert(err, qt.IsNil)
                c.Assert(len(m), qt.Equals, 2)
                c.Assert(m[0], qt.Equals, docs[0])
@@ -166,9 +169,9 @@ func TestSearch(t *testing.T) {
                doc := newTestDoc("tags", "a", "b", "d", "z").addKeywords("keywords", "a", "b")
                // This will get a date newer than the others.
                newDoc := newTestDoc("keywords", "a", "b")
-               idx.Add(newDoc)
+               idx.Add(context.Background(), newDoc)
 
-               m, err := idx.SearchDoc(doc, "keywords")
+               m, err := idx.Search(context.Background(), SearchOpts{Document: doc, Indices: []string{"keywords"}})
                c.Assert(err, qt.IsNil)
                c.Assert(len(m), qt.Equals, 2)
                c.Assert(m[0], qt.Equals, docs[3])
@@ -186,10 +189,10 @@ func TestSearch(t *testing.T) {
                for i := 0; i < 10; i++ {
                        docc := *doc
                        docc.name = fmt.Sprintf("doc%d", i)
-                       idx.Add(&docc)
+                       idx.Add(context.Background(), &docc)
                }
 
-               m, err := idx.SearchDoc(doc, "keywords")
+               m, err := idx.Search(context.Background(), SearchOpts{Document: doc, Indices: []string{"keywords"}})
                c.Assert(err, qt.IsNil)
                c.Assert(len(m), qt.Equals, 10)
                for i := 0; i < 10; i++ {
@@ -265,7 +268,7 @@ func BenchmarkRelatedNewIndex(b *testing.B) {
                for i := 0; i < b.N; i++ {
                        idx := NewInvertedIndex(cfg)
                        for _, doc := range pages {
-                               idx.Add(doc)
+                               idx.Add(context.Background(), doc)
                        }
                }
        })
@@ -277,14 +280,15 @@ func BenchmarkRelatedNewIndex(b *testing.B) {
                        for i := 0; i < len(pages); i++ {
                                docs[i] = pages[i]
                        }
-                       idx.Add(docs...)
+                       idx.Add(context.Background(), docs...)
                }
        })
 }
 
 func BenchmarkRelatedMatchesIn(b *testing.B) {
-       q1 := newQueryElement("tags", StringsToKeywords("keyword2", "keyword5", "keyword32", "asdf")...)
-       q2 := newQueryElement("keywords", StringsToKeywords("keyword3", "keyword4")...)
+       var icfg IndexConfig
+       q1 := newQueryElement("tags", icfg.StringsToKeywords("keyword2", "keyword5", "keyword32", "asdf")...)
+       q2 := newQueryElement("keywords", icfg.StringsToKeywords("keyword3", "keyword4")...)
 
        docs := make([]*testDoc, 1000)
        numkeywords := 20
@@ -315,15 +319,16 @@ func BenchmarkRelatedMatchesIn(b *testing.B) {
                        index = "keywords"
                }
 
-               idx.Add(newTestDoc(index, allKeywords[start:end]...))
+               idx.Add(context.Background(), newTestDoc(index, allKeywords[start:end]...))
        }
 
        b.ResetTimer()
+       ctx := context.Background()
        for i := 0; i < b.N; i++ {
                if i%10 == 0 {
-                       idx.search(q2)
+                       idx.search(ctx, q2)
                } else {
-                       idx.search(q1)
+                       idx.search(ctx, q1)
                }
        }
 }
index e9411c3db0f1aa8c5fc820631353196b0eef8412..42edb0bd0a75be5c548e9741587254f6f0935d3a 100644 (file)
@@ -14,6 +14,7 @@
 package resources
 
 import (
+       "context"
        "image"
 
        "github.com/gohugoio/hugo/common/hugio"
@@ -55,7 +56,7 @@ func (e *errorResource) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
        panic(e.ResourceError)
 }
 
-func (e *errorResource) Content() (any, error) {
+func (e *errorResource) Content(context.Context) (any, error) {
        panic(e.ResourceError)
 }
 
index 655454390006247d8715c420c69ef631fd9b2c91..3cb1089f4a113f635140deeb3e5ba8be13224e56 100644 (file)
@@ -14,6 +14,7 @@
 package resources
 
 import (
+       "context"
        "fmt"
        "image"
        "image/gif"
@@ -436,7 +437,7 @@ func TestSVGImageContent(t *testing.T) {
        svg := fetchResourceForSpec(spec, c, "circle.svg")
        c.Assert(svg, qt.Not(qt.IsNil))
 
-       content, err := svg.Content()
+       content, err := svg.Content(context.Background())
        c.Assert(err, qt.IsNil)
        c.Assert(content, hqt.IsSameType, "")
        c.Assert(content.(string), qt.Contains, `<svg height="100" width="100">`)
index eeb2cdb280961686af7e6d8fa8d21c5884a532ad..00e716e83fca313fb10ba2bc73eb21d9be138395 100644 (file)
 package page
 
 import (
+       "context"
        "html/template"
 
        "github.com/gohugoio/hugo/identity"
        "github.com/gohugoio/hugo/markup/converter"
+       "github.com/gohugoio/hugo/markup/tableofcontents"
 
        "github.com/gohugoio/hugo/config"
        "github.com/gohugoio/hugo/tpl"
@@ -76,40 +78,40 @@ type ChildCareProvider interface {
 
 // ContentProvider provides the content related values for a Page.
 type ContentProvider interface {
-       Content() (any, error)
+       Content(context.Context) (any, error)
 
        // Plain returns the Page Content stripped of HTML markup.
-       Plain() string
+       Plain(context.Context) string
 
        // PlainWords returns a string slice from splitting Plain using https://pkg.go.dev/strings#Fields.
-       PlainWords() []string
+       PlainWords(context.Context) []string
 
        // Summary returns a generated summary of the content.
        // The breakpoint can be set manually by inserting a summary separator in the source file.
-       Summary() template.HTML
+       Summary(context.Context) template.HTML
 
        // Truncated returns whether the Summary  is truncated or not.
-       Truncated() bool
+       Truncated(context.Context) bool
 
        // FuzzyWordCount returns the approximate number of words in the content.
-       FuzzyWordCount() int
+       FuzzyWordCount(context.Context) int
 
        // WordCount returns the number of words in the content.
-       WordCount() int
+       WordCount(context.Context) int
 
        // ReadingTime returns the reading time based on the length of plain text.
-       ReadingTime() int
+       ReadingTime(context.Context) int
 
        // Len returns the length of the content.
        // This is for internal use only.
-       Len() int
+       Len(context.Context) int
 }
 
 // ContentRenderer provides the content rendering methods for some content.
 type ContentRenderer interface {
        // RenderContent renders the given content.
        // For internal use only.
-       RenderContent(content []byte, renderTOC bool) (converter.Result, error)
+       RenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.Result, error)
 }
 
 // FileProvider provides the source file.
@@ -167,6 +169,11 @@ type Page interface {
        PageWithoutContent
 }
 
+type PageFragment interface {
+       resource.ResourceLinksProvider
+       resource.ResourceMetaProvider
+}
+
 // PageMetaProvider provides page metadata, typically provided via front matter.
 type PageMetaProvider interface {
        // The 4 page dates
@@ -252,7 +259,7 @@ type PageMetaProvider interface {
 // PageRenderProvider provides a way for a Page to render content.
 type PageRenderProvider interface {
        // Render renders the given layout with this Page as context.
-       Render(layout ...string) (template.HTML, error)
+       Render(ctx context.Context, layout ...string) (template.HTML, error)
        // RenderString renders the first value in args with tPaginatorhe content renderer defined
        // for this Page.
        // It takes an optional map as a second argument:
@@ -260,7 +267,7 @@ type PageRenderProvider interface {
        // display (“inline”):
        // - inline or block. If inline (default), surrounding <p></p> on short snippets will be trimmed.
        // markup (defaults to the Page’s markup)
-       RenderString(args ...any) (template.HTML, error)
+       RenderString(ctx context.Context, args ...any) (template.HTML, error)
 }
 
 // PageWithoutContent is the Page without any of the content methods.
@@ -323,6 +330,14 @@ type PageWithoutContent interface {
        // Used in change/dependency tracking.
        identity.Provider
 
+       // Fragments returns the fragments for this page.
+       Fragments(context.Context) *tableofcontents.Fragments
+
+       // Headings returns the headings for this page when a filter is set.
+       // This is currently only triggered with the Related content feature
+       // and the "fragments" type of index.
+       HeadingsFiltered(context.Context) tableofcontents.Headings
+
        DeprecatedWarningPageMethods
 }
 
@@ -387,7 +402,7 @@ type SitesProvider interface {
 // TableOfContentsProvider provides the table of contents for a Page.
 type TableOfContentsProvider interface {
        // TableOfContents returns the table of contents for the page rendered as HTML.
-       TableOfContents() template.HTML
+       TableOfContents(context.Context) template.HTML
 }
 
 // TranslationsProvider provides access to any translations.
index 2e4ddc3527235eb6a7cb7f4f2130020a130dea23..e497718f97ca03fa8a250f6b1401e580db27bb96 100644 (file)
@@ -14,6 +14,7 @@
 package page
 
 import (
+       "context"
        "html/template"
 
        "github.com/gohugoio/hugo/lazy"
@@ -57,7 +58,7 @@ func NewLazyContentProvider(f func() (OutputFormatContentProvider, error)) *Lazy
                init: lazy.New(),
                cp:   NopCPageContentRenderer,
        }
-       lcp.init.Add(func() (any, error) {
+       lcp.init.Add(func(context.Context) (any, error) {
                cp, err := f()
                if err != nil {
                        return nil, err
@@ -72,67 +73,67 @@ func (lcp *LazyContentProvider) Reset() {
        lcp.init.Reset()
 }
 
-func (lcp *LazyContentProvider) Content() (any, error) {
-       lcp.init.Do()
-       return lcp.cp.Content()
+func (lcp *LazyContentProvider) Content(ctx context.Context) (any, error) {
+       lcp.init.Do(ctx)
+       return lcp.cp.Content(ctx)
 }
 
-func (lcp *LazyContentProvider) Plain() string {
-       lcp.init.Do()
-       return lcp.cp.Plain()
+func (lcp *LazyContentProvider) Plain(ctx context.Context) string {
+       lcp.init.Do(ctx)
+       return lcp.cp.Plain(ctx)
 }
 
-func (lcp *LazyContentProvider) PlainWords() []string {
-       lcp.init.Do()
-       return lcp.cp.PlainWords()
+func (lcp *LazyContentProvider) PlainWords(ctx context.Context) []string {
+       lcp.init.Do(ctx)
+       return lcp.cp.PlainWords(ctx)
 }
 
-func (lcp *LazyContentProvider) Summary() template.HTML {
-       lcp.init.Do()
-       return lcp.cp.Summary()
+func (lcp *LazyContentProvider) Summary(ctx context.Context) template.HTML {
+       lcp.init.Do(ctx)
+       return lcp.cp.Summary(ctx)
 }
 
-func (lcp *LazyContentProvider) Truncated() bool {
-       lcp.init.Do()
-       return lcp.cp.Truncated()
+func (lcp *LazyContentProvider) Truncated(ctx context.Context) bool {
+       lcp.init.Do(ctx)
+       return lcp.cp.Truncated(ctx)
 }
 
-func (lcp *LazyContentProvider) FuzzyWordCount() int {
-       lcp.init.Do()
-       return lcp.cp.FuzzyWordCount()
+func (lcp *LazyContentProvider) FuzzyWordCount(ctx context.Context) int {
+       lcp.init.Do(ctx)
+       return lcp.cp.FuzzyWordCount(ctx)
 }
 
-func (lcp *LazyContentProvider) WordCount() int {
-       lcp.init.Do()
-       return lcp.cp.WordCount()
+func (lcp *LazyContentProvider) WordCount(ctx context.Context) int {
+       lcp.init.Do(ctx)
+       return lcp.cp.WordCount(ctx)
 }
 
-func (lcp *LazyContentProvider) ReadingTime() int {
-       lcp.init.Do()
-       return lcp.cp.ReadingTime()
+func (lcp *LazyContentProvider) ReadingTime(ctx context.Context) int {
+       lcp.init.Do(ctx)
+       return lcp.cp.ReadingTime(ctx)
 }
 
-func (lcp *LazyContentProvider) Len() int {
-       lcp.init.Do()
-       return lcp.cp.Len()
+func (lcp *LazyContentProvider) Len(ctx context.Context) int {
+       lcp.init.Do(ctx)
+       return lcp.cp.Len(ctx)
 }
 
-func (lcp *LazyContentProvider) Render(layout ...string) (template.HTML, error) {
-       lcp.init.Do()
-       return lcp.cp.Render(layout...)
+func (lcp *LazyContentProvider) Render(ctx context.Context, layout ...string) (template.HTML, error) {
+       lcp.init.Do(context.TODO())
+       return lcp.cp.Render(ctx, layout...)
 }
 
-func (lcp *LazyContentProvider) RenderString(args ...any) (template.HTML, error) {
-       lcp.init.Do()
-       return lcp.cp.RenderString(args...)
+func (lcp *LazyContentProvider) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
+       lcp.init.Do(ctx)
+       return lcp.cp.RenderString(ctx, args...)
 }
 
-func (lcp *LazyContentProvider) TableOfContents() template.HTML {
-       lcp.init.Do()
-       return lcp.cp.TableOfContents()
+func (lcp *LazyContentProvider) TableOfContents(ctx context.Context) template.HTML {
+       lcp.init.Do(ctx)
+       return lcp.cp.TableOfContents(ctx)
 }
 
-func (lcp *LazyContentProvider) RenderContent(content []byte, renderTOC bool) (converter.Result, error) {
-       lcp.init.Do()
-       return lcp.cp.RenderContent(content, renderTOC)
+func (lcp *LazyContentProvider) RenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.Result, error) {
+       lcp.init.Do(ctx)
+       return lcp.cp.RenderContent(ctx, content, renderTOC)
 }
index 373257878eb5fcc1a5fcce1126c12f8e60a52fa9..c3524ec3680a884ee787680d307b1c4c09e0e5db 100644 (file)
@@ -25,24 +25,10 @@ import (
        "github.com/gohugoio/hugo/media"
        "github.com/gohugoio/hugo/navigation"
        "github.com/gohugoio/hugo/source"
-       "html/template"
        "time"
 )
 
 func MarshalPageToJSON(p Page) ([]byte, error) {
-       content, err := p.Content()
-       if err != nil {
-               return nil, err
-       }
-       plain := p.Plain()
-       plainWords := p.PlainWords()
-       summary := p.Summary()
-       truncated := p.Truncated()
-       fuzzyWordCount := p.FuzzyWordCount()
-       wordCount := p.WordCount()
-       readingTime := p.ReadingTime()
-       length := p.Len()
-       tableOfContents := p.TableOfContents()
        rawContent := p.RawContent()
        resourceType := p.ResourceType()
        mediaType := p.MediaType()
@@ -93,16 +79,6 @@ func MarshalPageToJSON(p Page) ([]byte, error) {
        getIdentity := p.GetIdentity()
 
        s := struct {
-               Content                  interface{}
-               Plain                    string
-               PlainWords               []string
-               Summary                  template.HTML
-               Truncated                bool
-               FuzzyWordCount           int
-               WordCount                int
-               ReadingTime              int
-               Len                      int
-               TableOfContents          template.HTML
                RawContent               string
                ResourceType             string
                MediaType                media.Type
@@ -152,16 +128,6 @@ func MarshalPageToJSON(p Page) ([]byte, error) {
                Store                    *maps.Scratch
                GetIdentity              identity.Identity
        }{
-               Content:                  content,
-               Plain:                    plain,
-               PlainWords:               plainWords,
-               Summary:                  summary,
-               Truncated:                truncated,
-               FuzzyWordCount:           fuzzyWordCount,
-               WordCount:                wordCount,
-               ReadingTime:              readingTime,
-               Len:                      length,
-               TableOfContents:          tableOfContents,
                RawContent:               rawContent,
                ResourceType:             resourceType,
                MediaType:                mediaType,
index c4af3f554e0ae9b0fd680d067e5b9b6326d4cac6..8946926a281df04f81de9d485c846ce8bc60267b 100644 (file)
@@ -17,11 +17,13 @@ package page
 
 import (
        "bytes"
+       "context"
        "html/template"
        "time"
 
        "github.com/gohugoio/hugo/identity"
        "github.com/gohugoio/hugo/markup/converter"
+       "github.com/gohugoio/hugo/markup/tableofcontents"
 
        "github.com/gohugoio/hugo/hugofs/files"
        "github.com/gohugoio/hugo/tpl"
@@ -105,7 +107,7 @@ func (p *nopPage) BundleType() files.ContentClass {
        return ""
 }
 
-func (p *nopPage) Content() (any, error) {
+func (p *nopPage) Content(context.Context) (any, error) {
        return "", nil
 }
 
@@ -179,7 +181,7 @@ func (p *nopPage) FirstSection() Page {
        return nil
 }
 
-func (p *nopPage) FuzzyWordCount() int {
+func (p *nopPage) FuzzyWordCount(context.Context) int {
        return 0
 }
 
@@ -279,7 +281,7 @@ func (p *nopPage) Lastmod() (t time.Time) {
        return
 }
 
-func (p *nopPage) Len() int {
+func (p *nopPage) Len(context.Context) int {
        return 0
 }
 
@@ -363,11 +365,11 @@ func (p *nopPage) Permalink() string {
        return ""
 }
 
-func (p *nopPage) Plain() string {
+func (p *nopPage) Plain(context.Context) string {
        return ""
 }
 
-func (p *nopPage) PlainWords() []string {
+func (p *nopPage) PlainWords(context.Context) []string {
        return nil
 }
 
@@ -399,7 +401,7 @@ func (p *nopPage) RawContent() string {
        return ""
 }
 
-func (p *nopPage) ReadingTime() int {
+func (p *nopPage) ReadingTime(context.Context) int {
        return 0
 }
 
@@ -415,11 +417,11 @@ func (p *nopPage) RelRef(argsm map[string]any) (string, error) {
        return "", nil
 }
 
-func (p *nopPage) Render(layout ...string) (template.HTML, error) {
+func (p *nopPage) Render(ctx context.Context, layout ...string) (template.HTML, error) {
        return "", nil
 }
 
-func (p *nopPage) RenderString(args ...any) (template.HTML, error) {
+func (p *nopPage) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
        return "", nil
 }
 
@@ -475,11 +477,11 @@ func (p *nopPage) String() string {
        return "nopPage"
 }
 
-func (p *nopPage) Summary() template.HTML {
+func (p *nopPage) Summary(context.Context) template.HTML {
        return ""
 }
 
-func (p *nopPage) TableOfContents() template.HTML {
+func (p *nopPage) TableOfContents(context.Context) template.HTML {
        return ""
 }
 
@@ -499,7 +501,7 @@ func (p *nopPage) Translations() Pages {
        return nil
 }
 
-func (p *nopPage) Truncated() bool {
+func (p *nopPage) Truncated(context.Context) bool {
        return false
 }
 
@@ -519,7 +521,7 @@ func (p *nopPage) Weight() int {
        return 0
 }
 
-func (p *nopPage) WordCount() int {
+func (p *nopPage) WordCount(context.Context) int {
        return 0
 }
 
@@ -527,9 +529,16 @@ func (p *nopPage) GetIdentity() identity.Identity {
        return identity.NewPathIdentity("content", "foo/bar.md")
 }
 
+func (p *nopPage) Fragments(context.Context) *tableofcontents.Fragments {
+       return nil
+}
+func (p *nopPage) HeadingsFiltered(context.Context) tableofcontents.Headings {
+       return nil
+}
+
 type nopContentRenderer int
 
-func (r *nopContentRenderer) RenderContent(content []byte, renderTOC bool) (converter.Result, error) {
+func (r *nopContentRenderer) RenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.Result, error) {
        b := &bytes.Buffer{}
        return b, nil
 }
index 3b32a1fae2dbacb83615b52bbefa5e450d972e96..bac5d8327eacb405920b2586385c7a9b7ef92e8f 100644 (file)
@@ -14,6 +14,7 @@
 package page
 
 import (
+       "context"
        "errors"
        "fmt"
        "reflect"
@@ -110,7 +111,7 @@ var (
 
 // GroupBy groups by the value in the given field or method name and with the given order.
 // Valid values for order is asc, desc, rev and reverse.
-func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) {
+func (p Pages) GroupBy(ctx context.Context, key string, order ...string) (PagesGroup, error) {
        if len(p) < 1 {
                return nil, nil
        }
@@ -158,7 +159,12 @@ func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) {
                case reflect.StructField:
                        fv = ppv.Elem().FieldByName(key)
                case reflect.Method:
-                       fv = hreflect.GetMethodByName(ppv, key).Call([]reflect.Value{})[0]
+                       var args []reflect.Value
+                       fn := hreflect.GetMethodByName(ppv, key)
+                       if fn.Type().NumIn() > 0 && fn.Type().In(0).Implements(hreflect.ContextInterface) {
+                               args = []reflect.Value{reflect.ValueOf(ctx)}
+                       }
+                       fv = fn.Call(args)[0]
                }
                if !fv.IsValid() {
                        continue
index ef0d24471185b3cdd018406cefc14c9fe2e347f7..91f05b24a15a74e9a5795663c6b536f7c845fe49 100644 (file)
@@ -14,6 +14,7 @@
 package page
 
 import (
+       "context"
        "reflect"
        "strings"
        "testing"
@@ -68,7 +69,7 @@ func TestGroupByWithFieldNameArg(t *testing.T) {
                {Key: 3, Pages: Pages{pages[0], pages[1]}},
        }
 
-       groups, err := pages.GroupBy("Weight")
+       groups, err := pages.GroupBy(context.Background(), "Weight")
        if err != nil {
                t.Fatalf("Unable to make PagesGroup array: %s", err)
        }
@@ -85,7 +86,7 @@ func TestGroupByWithMethodNameArg(t *testing.T) {
                {Key: "section2", Pages: Pages{pages[3], pages[4]}},
        }
 
-       groups, err := pages.GroupBy("Type")
+       groups, err := pages.GroupBy(context.Background(), "Type")
        if err != nil {
                t.Fatalf("Unable to make PagesGroup array: %s", err)
        }
@@ -102,7 +103,7 @@ func TestGroupByWithSectionArg(t *testing.T) {
                {Key: "section2", Pages: Pages{pages[3], pages[4]}},
        }
 
-       groups, err := pages.GroupBy("Section")
+       groups, err := pages.GroupBy(context.Background(), "Section")
        if err != nil {
                t.Fatalf("Unable to make PagesGroup array: %s", err)
        }
@@ -120,7 +121,7 @@ func TestGroupByInReverseOrder(t *testing.T) {
                {Key: 1, Pages: Pages{pages[3], pages[4]}},
        }
 
-       groups, err := pages.GroupBy("Weight", "desc")
+       groups, err := pages.GroupBy(context.Background(), "Weight", "desc")
        if err != nil {
                t.Fatalf("Unable to make PagesGroup array: %s", err)
        }
@@ -132,7 +133,7 @@ func TestGroupByInReverseOrder(t *testing.T) {
 func TestGroupByCalledWithEmptyPages(t *testing.T) {
        t.Parallel()
        var pages Pages
-       groups, err := pages.GroupBy("Weight")
+       groups, err := pages.GroupBy(context.Background(), "Weight")
        if err != nil {
                t.Fatalf("Unable to make PagesGroup array: %s", err)
        }
@@ -154,12 +155,12 @@ func TestReverse(t *testing.T) {
        t.Parallel()
        pages := preparePageGroupTestPages(t)
 
-       groups1, err := pages.GroupBy("Weight", "desc")
+       groups1, err := pages.GroupBy(context.Background(), "Weight", "desc")
        if err != nil {
                t.Fatalf("Unable to make PagesGroup array: %s", err)
        }
 
-       groups2, err := pages.GroupBy("Weight")
+       groups2, err := pages.GroupBy(context.Background(), "Weight")
        if err != nil {
                t.Fatalf("Unable to make PagesGroup array: %s", err)
        }
index f47af5114758a06c06087a207d22d95d0d949e3a..77e56a0628b69edf44cdea40f8cda01ce953beeb 100644 (file)
@@ -132,21 +132,6 @@ func (pages Pages) ProbablyEq(other any) bool {
        return true
 }
 
-func (ps Pages) removeFirstIfFound(p Page) Pages {
-       ii := -1
-       for i, pp := range ps {
-               if p.Eq(pp) {
-                       ii = i
-                       break
-               }
-       }
-
-       if ii != -1 {
-               ps = append(ps[:ii], ps[ii+1:]...)
-       }
-       return ps
-}
-
 // PagesFactory somehow creates some Pages.
 // We do a lot of lazy Pages initialization in Hugo, so we need a type.
 type PagesFactory func() Pages
index 35bb2965a3d38dedd9b370a2b90b81980928cabf..74bb1713ef3bd8fd9ead6f9e05aa79cf5a421b30 100644 (file)
 package page
 
 import (
+       "context"
        "fmt"
        "sync"
 
        "github.com/gohugoio/hugo/common/types"
        "github.com/gohugoio/hugo/related"
+       "github.com/mitchellh/mapstructure"
        "github.com/spf13/cast"
 )
 
@@ -34,74 +36,90 @@ type PageGenealogist interface {
 
        // Template example:
        // {{ $related := .RegularPages.Related . }}
-       Related(doc related.Document) (Pages, error)
+       Related(ctx context.Context, opts any) (Pages, error)
 
        // Template example:
        // {{ $related := .RegularPages.RelatedIndices . "tags" "date" }}
-       RelatedIndices(doc related.Document, indices ...any) (Pages, error)
+       // Deprecated: Use Related instead.
+       RelatedIndices(ctx context.Context, doc related.Document, indices ...any) (Pages, error)
 
        // Template example:
        // {{ $related := .RegularPages.RelatedTo ( keyVals "tags" "hugo", "rocks")  ( keyVals "date" .Date ) }}
-       RelatedTo(args ...types.KeyValues) (Pages, error)
+       // Deprecated: Use Related instead.
+       RelatedTo(ctx context.Context, args ...types.KeyValues) (Pages, error)
 }
 
 // Related searches all the configured indices with the search keywords from the
 // supplied document.
-func (p Pages) Related(doc related.Document) (Pages, error) {
-       result, err := p.searchDoc(doc)
-       if err != nil {
-               return nil, err
+func (p Pages) Related(ctx context.Context, optsv any) (Pages, error) {
+       if len(p) == 0 {
+               return nil, nil
+       }
+
+       var opts related.SearchOpts
+       switch v := optsv.(type) {
+       case related.Document:
+               opts.Document = v
+       case map[string]any:
+               if err := mapstructure.WeakDecode(v, &opts); err != nil {
+                       return nil, err
+               }
+       default:
+               return nil, fmt.Errorf("invalid argument type %T", optsv)
        }
 
-       if page, ok := doc.(Page); ok {
-               return result.removeFirstIfFound(page), nil
+       result, err := p.search(ctx, opts)
+       if err != nil {
+               return nil, err
        }
 
        return result, nil
+
 }
 
 // RelatedIndices searches the given indices with the search keywords from the
 // supplied document.
-func (p Pages) RelatedIndices(doc related.Document, indices ...any) (Pages, error) {
+// Deprecated: Use Related instead.
+func (p Pages) RelatedIndices(ctx context.Context, doc related.Document, indices ...any) (Pages, error) {
        indicesStr, err := cast.ToStringSliceE(indices)
        if err != nil {
                return nil, err
        }
 
-       result, err := p.searchDoc(doc, indicesStr...)
-       if err != nil {
-               return nil, err
+       opts := related.SearchOpts{
+               Document: doc,
+               Indices:  indicesStr,
        }
 
-       if page, ok := doc.(Page); ok {
-               return result.removeFirstIfFound(page), nil
+       result, err := p.search(ctx, opts)
+       if err != nil {
+               return nil, err
        }
 
        return result, nil
 }
 
 // RelatedTo searches the given indices with the corresponding values.
-func (p Pages) RelatedTo(args ...types.KeyValues) (Pages, error) {
+// Deprecated: Use Related instead.
+func (p Pages) RelatedTo(ctx context.Context, args ...types.KeyValues) (Pages, error) {
        if len(p) == 0 {
                return nil, nil
        }
 
-       return p.search(args...)
-}
+       opts := related.SearchOpts{
+               NamedSlices: args,
+       }
 
-func (p Pages) search(args ...types.KeyValues) (Pages, error) {
-       return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) {
-               return idx.SearchKeyValues(args...)
-       })
+       return p.search(ctx, opts)
 }
 
-func (p Pages) searchDoc(doc related.Document, indices ...string) (Pages, error) {
-       return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) {
-               return idx.SearchDoc(doc, indices...)
+func (p Pages) search(ctx context.Context, opts related.SearchOpts) (Pages, error) {
+       return p.withInvertedIndex(ctx, func(idx *related.InvertedIndex) ([]related.Document, error) {
+               return idx.Search(ctx, opts)
        })
 }
 
-func (p Pages) withInvertedIndex(search func(idx *related.InvertedIndex) ([]related.Document, error)) (Pages, error) {
+func (p Pages) withInvertedIndex(ctx context.Context, search func(idx *related.InvertedIndex) ([]related.Document, error)) (Pages, error) {
        if len(p) == 0 {
                return nil, nil
        }
@@ -113,7 +131,7 @@ func (p Pages) withInvertedIndex(search func(idx *related.InvertedIndex) ([]rela
 
        cache := d.GetRelatedDocsHandler()
 
-       searchIndex, err := cache.getOrCreateIndex(p)
+       searchIndex, err := cache.getOrCreateIndex(ctx, p)
        if err != nil {
                return nil, err
        }
@@ -164,8 +182,7 @@ func (s *RelatedDocsHandler) getIndex(p Pages) *related.InvertedIndex {
        }
        return nil
 }
-
-func (s *RelatedDocsHandler) getOrCreateIndex(p Pages) (*related.InvertedIndex, error) {
+func (s *RelatedDocsHandler) getOrCreateIndex(ctx context.Context, p Pages) (*related.InvertedIndex, error) {
        s.mu.RLock()
        cachedIndex := s.getIndex(p)
        if cachedIndex != nil {
@@ -184,7 +201,7 @@ func (s *RelatedDocsHandler) getOrCreateIndex(p Pages) (*related.InvertedIndex,
        searchIndex := related.NewInvertedIndex(s.cfg)
 
        for _, page := range p {
-               if err := searchIndex.Add(page); err != nil {
+               if err := searchIndex.Add(ctx, page); err != nil {
                        return nil, err
                }
        }
index 3c5780a9a36a349ca521fffc9536110218d32174..75ab7ecb98e50919dc4268d7699250498ce6e310 100644 (file)
@@ -14,6 +14,7 @@
 package page
 
 import (
+       "context"
        "testing"
        "time"
 
@@ -51,26 +52,42 @@ func TestRelated(t *testing.T) {
                },
        }
 
-       result, err := pages.RelatedTo(types.NewKeyValuesStrings("keywords", "hugo", "rocks"))
+       ctx := context.Background()
+       opts := map[string]any{
+               "namedSlices": types.NewKeyValuesStrings("keywords", "hugo", "rocks"),
+       }
+       result, err := pages.Related(ctx, opts)
 
        c.Assert(err, qt.IsNil)
        c.Assert(len(result), qt.Equals, 2)
        c.Assert(result[0].Title(), qt.Equals, "Page 2")
        c.Assert(result[1].Title(), qt.Equals, "Page 1")
 
-       result, err = pages.Related(pages[0])
+       result, err = pages.Related(ctx, pages[0])
        c.Assert(err, qt.IsNil)
        c.Assert(len(result), qt.Equals, 2)
        c.Assert(result[0].Title(), qt.Equals, "Page 2")
        c.Assert(result[1].Title(), qt.Equals, "Page 3")
 
-       result, err = pages.RelatedIndices(pages[0], "keywords")
+       opts = map[string]any{
+               "document": pages[0],
+               "indices":  []string{"keywords"},
+       }
+       result, err = pages.Related(ctx, opts)
        c.Assert(err, qt.IsNil)
        c.Assert(len(result), qt.Equals, 2)
        c.Assert(result[0].Title(), qt.Equals, "Page 2")
        c.Assert(result[1].Title(), qt.Equals, "Page 3")
 
-       result, err = pages.RelatedTo(types.NewKeyValuesStrings("keywords", "bep", "rocks"))
+       opts = map[string]any{
+               "namedSlices": []types.KeyValues{
+                       {
+                               Key:    "keywords",
+                               Values: []any{"bep", "rocks"},
+                       },
+               },
+       }
+       result, err = pages.Related(context.Background(), opts)
        c.Assert(err, qt.IsNil)
        c.Assert(len(result), qt.Equals, 2)
        c.Assert(result[0].Title(), qt.Equals, "Page 2")
index 08cb34a324254ac3ffcfd88fe962122b6f8fb82b..b9b905cc22742df613b5480a41d9bb1b35e7e4fc 100644 (file)
@@ -14,6 +14,7 @@
 package page
 
 import (
+       "context"
        "sort"
 
        "github.com/gohugoio/hugo/common/collections"
@@ -299,7 +300,7 @@ func (p Pages) ByLastmod() Pages {
 // Adjacent invocations on the same receiver will return a cached result.
 //
 // This may safely be executed  in parallel.
-func (p Pages) ByLength() Pages {
+func (p Pages) ByLength(ctx context.Context) Pages {
        const key = "pageSort.ByLength"
 
        length := func(p1, p2 Page) bool {
@@ -314,7 +315,7 @@ func (p Pages) ByLength() Pages {
                        return false
                }
 
-               return p1l.Len() < p2l.Len()
+               return p1l.Len(ctx) < p2l.Len(ctx)
        }
 
        pages, _ := spc.get(key, pageBy(length).Sort, p)
index cf4e339eec40de9a79ff476489e5ba1e122f4140..72823723037a4e1c3df4fc5c890f50f1dc0372b5 100644 (file)
@@ -14,6 +14,7 @@
 package page
 
 import (
+       "context"
        "fmt"
        "testing"
        "time"
@@ -104,6 +105,12 @@ func TestSortByN(t *testing.T) {
        d4 := d1.Add(-20 * time.Hour)
 
        p := createSortTestPages(4)
+       ctx := context.Background()
+
+       byLen := func(p Pages) Pages {
+               return p.ByLength(ctx)
+
+       }
 
        for i, this := range []struct {
                sortFunc   func(p Pages) Pages
@@ -116,7 +123,7 @@ func TestSortByN(t *testing.T) {
                {(Pages).ByPublishDate, func(p Pages) bool { return p[0].PublishDate() == d4 }},
                {(Pages).ByExpiryDate, func(p Pages) bool { return p[0].ExpiryDate() == d4 }},
                {(Pages).ByLastmod, func(p Pages) bool { return p[1].Lastmod() == d3 }},
-               {(Pages).ByLength, func(p Pages) bool { return p[0].(resource.LengthProvider).Len() == len(p[0].(*testPage).content) }},
+               {byLen, func(p Pages) bool { return p[0].(resource.LengthProvider).Len(ctx) == len(p[0].(*testPage).content) }},
        } {
                setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "ab", "cde", "fg"}, [4]int{0, 3, 2, 1}, p)
 
index e379f9b6bc47f9fb7d2f97327d7b88df7c1dbba7..2686d392035d5f911b9964e4dc313383a0197c8f 100644 (file)
@@ -14,6 +14,7 @@
 package page
 
 import (
+       "context"
        "fmt"
        "html/template"
        "testing"
@@ -43,7 +44,7 @@ func TestSplitPageGroups(t *testing.T) {
        t.Parallel()
        c := qt.New(t)
        pages := createTestPages(21)
-       groups, _ := pages.GroupBy("Weight", "desc")
+       groups, _ := pages.GroupBy(context.Background(), "Weight", "desc")
        chunks := splitPageGroups(groups, 5)
        c.Assert(len(chunks), qt.Equals, 5)
 
@@ -56,7 +57,7 @@ func TestSplitPageGroups(t *testing.T) {
                        // first group 10 in weight
                        c.Assert(pg.Key, qt.Equals, 10)
                        for _, p := range pg.Pages {
-                               c.Assert(p.FuzzyWordCount()%2 == 0, qt.Equals, true) // magic test
+                               c.Assert(p.FuzzyWordCount(context.Background())%2 == 0, qt.Equals, true) // magic test
                        }
                }
        } else {
@@ -71,7 +72,7 @@ func TestSplitPageGroups(t *testing.T) {
                        // last should have 5 in weight
                        c.Assert(pg.Key, qt.Equals, 5)
                        for _, p := range pg.Pages {
-                               c.Assert(p.FuzzyWordCount()%2 != 0, qt.Equals, true) // magic test
+                               c.Assert(p.FuzzyWordCount(context.Background())%2 != 0, qt.Equals, true) // magic test
                        }
                }
        } else {
@@ -83,7 +84,7 @@ func TestPager(t *testing.T) {
        t.Parallel()
        c := qt.New(t)
        pages := createTestPages(21)
-       groups, _ := pages.GroupBy("Weight", "desc")
+       groups, _ := pages.GroupBy(context.Background(), "Weight", "desc")
 
        urlFactory := func(page int) string {
                return fmt.Sprintf("page/%d/", page)
@@ -149,7 +150,7 @@ func TestPagerNoPages(t *testing.T) {
        t.Parallel()
        c := qt.New(t)
        pages := createTestPages(0)
-       groups, _ := pages.GroupBy("Weight", "desc")
+       groups, _ := pages.GroupBy(context.Background(), "Weight", "desc")
 
        urlFactory := func(page int) string {
                return fmt.Sprintf("page/%d/", page)
@@ -249,9 +250,9 @@ func TestProbablyEqualPageLists(t *testing.T) {
        t.Parallel()
        fivePages := createTestPages(5)
        zeroPages := createTestPages(0)
-       zeroPagesByWeight, _ := createTestPages(0).GroupBy("Weight", "asc")
-       fivePagesByWeight, _ := createTestPages(5).GroupBy("Weight", "asc")
-       ninePagesByWeight, _ := createTestPages(9).GroupBy("Weight", "asc")
+       zeroPagesByWeight, _ := createTestPages(0).GroupBy(context.Background(), "Weight", "asc")
+       fivePagesByWeight, _ := createTestPages(5).GroupBy(context.Background(), "Weight", "asc")
+       ninePagesByWeight, _ := createTestPages(9).GroupBy(context.Background(), "Weight", "asc")
 
        for i, this := range []struct {
                v1     any
@@ -287,7 +288,7 @@ func TestPaginationPage(t *testing.T) {
        }
 
        fivePages := createTestPages(7)
-       fivePagesFuzzyWordCount, _ := createTestPages(7).GroupBy("FuzzyWordCount", "asc")
+       fivePagesFuzzyWordCount, _ := createTestPages(7).GroupBy(context.Background(), "FuzzyWordCount", "asc")
 
        p1, _ := newPaginatorFromPages(fivePages, 2, urlFactory)
        p2, _ := newPaginatorFromPageGroups(fivePagesFuzzyWordCount, 2, urlFactory)
@@ -301,10 +302,10 @@ func TestPaginationPage(t *testing.T) {
        page21, _ := f2.page(1)
        page2Nil, _ := f2.page(3)
 
-       c.Assert(page11.FuzzyWordCount(), qt.Equals, 3)
+       c.Assert(page11.FuzzyWordCount(context.Background()), qt.Equals, 3)
        c.Assert(page1Nil, qt.IsNil)
 
        c.Assert(page21, qt.Not(qt.IsNil))
-       c.Assert(page21.FuzzyWordCount(), qt.Equals, 3)
+       c.Assert(page21.FuzzyWordCount(context.Background()), qt.Equals, 3)
        c.Assert(page2Nil, qt.IsNil)
 }
index e8275ba4050e5f9714acaedaa17f07eeef7326d8..72f62ee8d323436e120adc429e603fd9deb1b744 100644 (file)
@@ -14,6 +14,7 @@
 package page
 
 import (
+       "context"
        "fmt"
        "html/template"
        "path"
@@ -22,6 +23,7 @@ import (
 
        "github.com/gohugoio/hugo/hugofs/files"
        "github.com/gohugoio/hugo/identity"
+       "github.com/gohugoio/hugo/markup/tableofcontents"
        "github.com/gohugoio/hugo/tpl"
 
        "github.com/gohugoio/hugo/modules"
@@ -153,7 +155,7 @@ func (p *testPage) BundleType() files.ContentClass {
        panic("not implemented")
 }
 
-func (p *testPage) Content() (any, error) {
+func (p *testPage) Content(context.Context) (any, error) {
        panic("not implemented")
 }
 
@@ -225,7 +227,7 @@ func (p *testPage) FirstSection() Page {
        panic("not implemented")
 }
 
-func (p *testPage) FuzzyWordCount() int {
+func (p *testPage) FuzzyWordCount(context.Context) int {
        return p.fuzzyWordCount
 }
 
@@ -329,11 +331,19 @@ func (p *testPage) LanguagePrefix() string {
        return ""
 }
 
+func (p *testPage) Fragments(context.Context) *tableofcontents.Fragments {
+       return nil
+}
+
+func (p *testPage) HeadingsFiltered(context.Context) tableofcontents.Headings {
+       return nil
+}
+
 func (p *testPage) Lastmod() time.Time {
        return p.lastMod
 }
 
-func (p *testPage) Len() int {
+func (p *testPage) Len(context.Context) int {
        return len(p.content)
 }
 
@@ -431,11 +441,11 @@ func (p *testPage) Permalink() string {
        panic("not implemented")
 }
 
-func (p *testPage) Plain() string {
+func (p *testPage) Plain(context.Context) string {
        panic("not implemented")
 }
 
-func (p *testPage) PlainWords() []string {
+func (p *testPage) PlainWords(context.Context) []string {
        panic("not implemented")
 }
 
@@ -463,7 +473,7 @@ func (p *testPage) RawContent() string {
        panic("not implemented")
 }
 
-func (p *testPage) ReadingTime() int {
+func (p *testPage) ReadingTime(context.Context) int {
        panic("not implemented")
 }
 
@@ -487,11 +497,11 @@ func (p *testPage) RelRefFrom(argsm map[string]any, source any) (string, error)
        return "", nil
 }
 
-func (p *testPage) Render(layout ...string) (template.HTML, error) {
+func (p *testPage) Render(ctx context.Context, layout ...string) (template.HTML, error) {
        panic("not implemented")
 }
 
-func (p *testPage) RenderString(args ...any) (template.HTML, error) {
+func (p *testPage) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
        panic("not implemented")
 }
 
@@ -552,11 +562,11 @@ func (p *testPage) String() string {
        return p.path
 }
 
-func (p *testPage) Summary() template.HTML {
+func (p *testPage) Summary(context.Context) template.HTML {
        panic("not implemented")
 }
 
-func (p *testPage) TableOfContents() template.HTML {
+func (p *testPage) TableOfContents(context.Context) template.HTML {
        panic("not implemented")
 }
 
@@ -576,7 +586,7 @@ func (p *testPage) Translations() Pages {
        panic("not implemented")
 }
 
-func (p *testPage) Truncated() bool {
+func (p *testPage) Truncated(context.Context) bool {
        panic("not implemented")
 }
 
@@ -596,7 +606,7 @@ func (p *testPage) Weight() int {
        return p.weight
 }
 
-func (p *testPage) WordCount() int {
+func (p *testPage) WordCount(context.Context) int {
        panic("not implemented")
 }
 
index 400e00aa435d154ed0519a6e6579781de859efa0..5911362ec11685059c0731936b24adde4c501052 100644 (file)
@@ -14,6 +14,7 @@
 package postpub
 
 import (
+       "context"
        "fmt"
        "reflect"
        "strconv"
@@ -101,7 +102,7 @@ func (r *PostPublishResource) GetFieldString(pattern string) (string, bool) {
        case fieldAccessor == "ResourceType":
                return d.ResourceType(), true
        case fieldAccessor == "Content":
-               content, err := d.(resource.ContentProvider).Content()
+               content, err := d.(resource.ContentProvider).Content(context.Background())
                if err != nil {
                        return "", true
                }
@@ -172,7 +173,7 @@ func (r *PostPublishResource) Params() maps.Params {
        panic(r.fieldNotSupported("Params"))
 }
 
-func (r *PostPublishResource) Content() (any, error) {
+func (r *PostPublishResource) Content(context.Context) (any, error) {
        return r.field("Content"), nil
 }
 
index 0d7d1d85a5799dfb353a6eadba2e69b5489a7f7f..8a524247ae3f1a06b52787c86a61e706c47179a1 100644 (file)
@@ -14,6 +14,7 @@
 package resources
 
 import (
+       "context"
        "fmt"
        "io"
        "io/ioutil"
@@ -256,7 +257,7 @@ func (l *genericResource) cloneTo(targetPath string) resource.Resource {
 
 }
 
-func (l *genericResource) Content() (any, error) {
+func (l *genericResource) Content(context.Context) (any, error) {
        if err := l.initContent(); err != nil {
                return nil, err
        }
index 43761b27d3efa38faf9cd2cf24cf4b5d6e30d8b0..237bee0c8a1f40e1cdf1af2dab4ed30b6de49604 100644 (file)
@@ -14,6 +14,8 @@
 package resource
 
 import (
+       "context"
+
        "github.com/gohugoio/hugo/common/maps"
        "github.com/gohugoio/hugo/langs"
        "github.com/gohugoio/hugo/media"
@@ -162,7 +164,7 @@ type ContentProvider interface {
        // * Page: template.HTML
        // * JSON: String
        // * Etc.
-       Content() (any, error)
+       Content(context.Context) (any, error)
 }
 
 // OpenReadSeekCloser allows setting some other way (than reading from a filesystem)
@@ -178,7 +180,7 @@ type ReadSeekCloserResource interface {
 // LengthProvider is a Resource that provides a length
 // (typically the length of the content).
 type LengthProvider interface {
-       Len() int
+       Len(context.Context) int
 }
 
 // LanguageProvider is a Resource in a language.
index cba993d1eb08f61b62de6e22e7c62c4021000084..ef3f13a558b8a96ad00ba5f20781e39f84f672b2 100644 (file)
@@ -14,6 +14,7 @@
 package integrity
 
 import (
+       "context"
        "html/template"
        "testing"
 
@@ -63,7 +64,7 @@ func TestTransform(t *testing.T) {
        c.Assert(err, qt.IsNil)
        c.Assert(transformed.RelPermalink(), qt.Equals, "/hugo.a5ad1c6961214a55de53c1ce6e60d27b6b761f54851fa65e33066460dfa6a0db.txt")
        c.Assert(transformed.Data(), qt.DeepEquals, map[string]any{"Integrity": template.HTMLAttr("sha256-pa0caWEhSlXeU8HObmDSe2t2H1SFH6ZeMwZkYN+moNs=")})
-       content, err := transformed.(resource.ContentProvider).Content()
+       content, err := transformed.(resource.ContentProvider).Content(context.Background())
        c.Assert(err, qt.IsNil)
        c.Assert(content, qt.Equals, "Hugo Rocks!")
 }
index b0ebe3171acbc96167b33231adbb780ab6673187..b2d8ed734d5a81bc9b98e97cc98946d28d2cffc0 100644 (file)
@@ -14,6 +14,7 @@
 package minifier
 
 import (
+       "context"
        "testing"
 
        "github.com/gohugoio/hugo/resources/resource"
@@ -36,7 +37,7 @@ func TestTransform(t *testing.T) {
        c.Assert(err, qt.IsNil)
 
        c.Assert(transformed.RelPermalink(), qt.Equals, "/hugo.min.html")
-       content, err := transformed.(resource.ContentProvider).Content()
+       content, err := transformed.(resource.ContentProvider).Content(context.Background())
        c.Assert(err, qt.IsNil)
        c.Assert(content, qt.Equals, "<h1>Hugo Rocks!</h1>")
 }
index a37011acd25cc5ac6305cb9c40420163db71a036..3477c710f34b7d09b3548d368a0776b812610203 100644 (file)
@@ -15,6 +15,7 @@ package resources
 
 import (
        "bytes"
+       "context"
        "fmt"
        "image"
        "io"
@@ -159,12 +160,12 @@ type resourceAdapter struct {
        *resourceAdapterInner
 }
 
-func (r *resourceAdapter) Content() (any, error) {
+func (r *resourceAdapter) Content(context.Context) (any, error) {
        r.init(false, true)
        if r.transformationsErr != nil {
                return nil, r.transformationsErr
        }
-       return r.target.Content()
+       return r.target.Content(context.Background())
 }
 
 func (r *resourceAdapter) Err() resource.ResourceError {
index 1bd8302d29b30b36f4968ef5e203b24b6cfeb942..c883e2593f4cb634235a374421782dfce1663c86 100644 (file)
@@ -14,6 +14,7 @@
 package resources
 
 import (
+       "context"
        "encoding/base64"
        "fmt"
        "io"
@@ -115,7 +116,7 @@ func TestTransform(t *testing.T) {
 
                tr, err := r.Transform(transformation)
                c.Assert(err, qt.IsNil)
-               content, err := tr.(resource.ContentProvider).Content()
+               content, err := tr.(resource.ContentProvider).Content(context.Background())
                c.Assert(err, qt.IsNil)
 
                c.Assert(content, qt.Equals, "color is green")
@@ -149,7 +150,7 @@ func TestTransform(t *testing.T) {
 
                tr, err := r.Transform(transformation)
                c.Assert(err, qt.IsNil)
-               content, err := tr.(resource.ContentProvider).Content()
+               content, err := tr.(resource.ContentProvider).Content(context.Background())
                c.Assert(err, qt.IsNil)
 
                c.Assert(content, qt.Equals, "color is blue")
@@ -184,7 +185,7 @@ func TestTransform(t *testing.T) {
                for i, transformation := range []ResourceTransformation{t1, t2} {
                        r := createTransformer(spec, "f1.txt", "color is blue")
                        tr, _ := r.Transform(transformation)
-                       content, err := tr.(resource.ContentProvider).Content()
+                       content, err := tr.(resource.ContentProvider).Content(context.Background())
                        c.Assert(err, qt.IsNil)
                        c.Assert(content, qt.Equals, "color is green", qt.Commentf("i=%d", i))
 
@@ -237,7 +238,7 @@ func TestTransform(t *testing.T) {
 
                        tr, _ := r.Transform(transformation)
                        c.Assert(tr.RelPermalink(), qt.Equals, "/f1.cached.txt", msg)
-                       content, err := tr.(resource.ContentProvider).Content()
+                       content, err := tr.(resource.ContentProvider).Content(context.Background())
                        c.Assert(err, qt.IsNil)
                        c.Assert(content, qt.Equals, "color is green", msg)
                        c.Assert(tr.MediaType(), eq, media.CSVType)
@@ -264,7 +265,7 @@ func TestTransform(t *testing.T) {
 
                relPermalink := tr.RelPermalink()
 
-               content, err := tr.(resource.ContentProvider).Content()
+               content, err := tr.(resource.ContentProvider).Content(context.Background())
                c.Assert(err, qt.IsNil)
 
                c.Assert(relPermalink, qt.Equals, "/f1.t1.txt")
@@ -286,7 +287,7 @@ func TestTransform(t *testing.T) {
                r := createTransformer(spec, "f1.txt", "color is blue")
 
                tr, _ := r.Transform(t1, t2)
-               content, err := tr.(resource.ContentProvider).Content()
+               content, err := tr.(resource.ContentProvider).Content(context.Background())
                c.Assert(err, qt.IsNil)
 
                c.Assert(content, qt.Equals, "car is green")
@@ -308,9 +309,9 @@ func TestTransform(t *testing.T) {
                tr1, _ := r.Transform(t1)
                tr2, _ := tr1.Transform(t2)
 
-               content1, err := tr1.(resource.ContentProvider).Content()
+               content1, err := tr1.(resource.ContentProvider).Content(context.Background())
                c.Assert(err, qt.IsNil)
-               content2, err := tr2.(resource.ContentProvider).Content()
+               content2, err := tr2.(resource.ContentProvider).Content(context.Background())
                c.Assert(err, qt.IsNil)
 
                c.Assert(content1, qt.Equals, "color is green")
@@ -339,7 +340,7 @@ func TestTransform(t *testing.T) {
                r := createTransformer(spec, "f1.txt", countstr.String())
 
                tr, _ := r.Transform(transformations...)
-               content, err := tr.(resource.ContentProvider).Content()
+               content, err := tr.(resource.ContentProvider).Content(context.Background())
                c.Assert(err, qt.IsNil)
 
                c.Assert(content, qt.Equals, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
@@ -417,7 +418,7 @@ func TestTransform(t *testing.T) {
                                        id := (i + j) % 10
                                        tr, err := transformers[id].Transform(transformations[id])
                                        c.Assert(err, qt.IsNil)
-                                       content, err := tr.(resource.ContentProvider).Content()
+                                       content, err := tr.(resource.ContentProvider).Content(context.Background())
                                        c.Assert(err, qt.IsNil)
                                        c.Assert(content, qt.Equals, "color is blue")
                                        c.Assert(tr.RelPermalink(), qt.Equals, fmt.Sprintf("/f%d.test.txt", id))
index dab5a05a3d034b090402bfa8287e92573300c307..96f526005ed06a8c78f9ec439817b095d06ed7cc 100644 (file)
@@ -62,6 +62,7 @@ func NewExecuter(helper ExecHelper) Executer {
 type (
        dataContextKeyType    string
        hasLockContextKeyType string
+       stackContextKeyType   string
 )
 
 const (
index 039d674c4248c2e6acdc6f5cc2f265e765617ac9..0f9ad61d4962ef6eb51de35317d93ee3da268e7a 100644 (file)
@@ -15,6 +15,7 @@
 package transform
 
 import (
+       "context"
        "html"
        "html/template"
 
@@ -118,13 +119,13 @@ func (ns *Namespace) HTMLUnescape(s any) (string, error) {
 }
 
 // Markdownify renders s from Markdown to HTML.
-func (ns *Namespace) Markdownify(s any) (template.HTML, error) {
+func (ns *Namespace) Markdownify(ctx context.Context, s any) (template.HTML, error) {
 
        home := ns.deps.Site.Home()
        if home == nil {
                panic("home must not be nil")
        }
-       ss, err := home.RenderString(s)
+       ss, err := home.RenderString(ctx, s)
        if err != nil {
                return "", err
        }
index edef4e1bde504d3d66ff7df042b7ab05780ed06f..86ddb1259895531ce0a0b71170810cc6f4d12a30 100644 (file)
@@ -14,6 +14,7 @@
 package transform_test
 
 import (
+       "context"
        "html/template"
        "strings"
        "testing"
@@ -185,7 +186,7 @@ func TestMarkdownify(t *testing.T) {
                {tstNoStringer{}, false},
        } {
 
-               result, err := ns.Markdownify(test.s)
+               result, err := ns.Markdownify(context.Background(), test.s)
 
                if bb, ok := test.expect.(bool); ok && !bb {
                        b.Assert(err, qt.Not(qt.IsNil))
@@ -218,7 +219,7 @@ This is some more text.
 And then some.
 `
 
-       result, err := ns.Markdownify(text)
+       result, err := ns.Markdownify(context.Background(), text)
        b.Assert(err, qt.IsNil)
        b.Assert(result, qt.Equals, template.HTML(
                "<p>#First</p>\n<p>This is some <em>bold</em> text.</p>\n<h2 id=\"second\">Second</h2>\n<p>This is some more text.</p>\n<p>And then some.</p>\n"))