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
--- /dev/null
+
+*.test
\ No newline at end of file
import (
"reflect"
+ "sort"
)
// Slicer defines a very generic way to create a typed slice. This is used
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
+}
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)
+
+}
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
+}
{{ 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.
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.
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
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=
package hugolib
import (
+ "context"
"fmt"
"io"
"path/filepath"
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)
}
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
package hugolib
import (
+ "context"
"encoding/json"
"fmt"
"html/template"
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)
}
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
}
}
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
}
}
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
}
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)
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
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)
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)
}
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
}
}
-func TestErrorNestedShortocde(t *testing.T) {
+func TestErrorNestedShortcode(t *testing.T) {
t.Parallel()
files := `
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"
package hugolib
import (
+ "context"
"fmt"
"os"
"path/filepath"
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/")
import (
"bytes"
+ "context"
"fmt"
"path"
"path/filepath"
"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"
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
}
// 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
}
// 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
}
// 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
package hugolib
import (
+ "context"
"fmt"
"github.com/gohugoio/hugo/output"
}
// 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 {
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
}
}
- return c
+ return c, hasVariants, nil
}
func (p pageContent) selfLayoutForOutput(f output.Format) string {
package hugolib
import (
+ "context"
"sync"
"github.com/gohugoio/hugo/navigation"
}
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)
}
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()
}
package hugolib
import (
+ "context"
"html/template"
"strings"
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
"context"
"fmt"
"html/template"
- "runtime/debug"
"strings"
"sync"
"unicode/utf8"
"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"
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
}
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,
)
} 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()
}
}
} 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
}
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)
p *pageState
// Lazy load dependencies
+ initToC *lazy.Init
initMain *lazy.Init
initPlain *lazy.Init
// 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
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")
}
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)
}
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)
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))
}
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,
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
package hugolib
import (
+ "context"
+
"github.com/gohugoio/hugo/lazy"
"github.com/gohugoio/hugo/resources/page"
)
}
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
}
package hugolib
import (
+ "context"
"fmt"
"html/template"
"os"
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)
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)
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")
}
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()))
}
}
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)
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()))
}
}
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()))
}
}
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()))
}
}
import (
"bytes"
+ "context"
"fmt"
"html/template"
"path"
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
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 {
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 {
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 {
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
}
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
}
}
// 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()
}
- 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 {
bp.PutBuffer(b)
}
- return result, hasVariants, err
+ return prerenderedShortcode{s: result, hasVariants: hasVariants}, err
}
func (s *shortcodeHandler) hasShortcodes() 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 {
// 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)
}
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) {
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)
}
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.
//
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
}
package hugolib
import (
+ "context"
"fmt"
"path/filepath"
"reflect"
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
}
{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
},
} {
- 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 {
package hugolib
import (
+ "context"
"fmt"
"html/template"
"io"
}
func (s *Site) Taxonomies() page.TaxonomyList {
- s.init.taxonomies.Do()
+ s.init.taxonomies.Do(context.Background())
return s.taxonomies
}
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))
}
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)
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)
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
})
}
func (s *Site) Menus() navigation.Menus {
- s.init.menus.Do()
+ s.init.menus.Do(context.Background())
return s.menus
}
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
"strings"
"sync"
- "github.com/gohugoio/hugo/tpl"
-
"github.com/gohugoio/hugo/config"
+ "github.com/gohugoio/hugo/tpl"
"errors"
package hugolib
import (
+ "context"
"encoding/json"
"fmt"
"io/ioutil"
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())
}
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)
}
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)
}
import (
"bytes"
+ "context"
"fmt"
"image/jpeg"
"io"
}
func content(c resource.ContentProvider) string {
- cc, err := c.Content()
+ cc, err := c.Content(context.Background())
if err != nil {
panic(err)
}
// Init holds a graph of lazily initialized dependencies.
type Init struct {
- // Used in tests
+ // Used mainly for testing.
initCount uint64
mu sync.Mutex
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()
}
// 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()
}
// 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")
}
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
}
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
}
}
-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()
}
}
-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)
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
}
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++ {
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)
return nil, nil
})
- _, err := init.Do()
+ _, err := init.Do(context.Background())
c.Assert(err, qt.IsNil)
}
return nil, nil
})
- _, err := init.Do()
+ _, err := init.Do(context.Background())
c.Assert(err, qt.Not(qt.IsNil))
return nil, errors.New("failed")
})
- _, err := init.Do()
+ _, err := init.Do(context.Background())
c.Assert(err, qt.Not(qt.IsNil))
}
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()
}
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)
}()
}
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)
}
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
}
// 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 {
}
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:]
}
// 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) {
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)
}
}
f(doc.FirstChild, -1, 0)
- return toc
+ return toc.Build()
}
func attr(node *html.Node, key string) string {
"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"
)
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\">")
}
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\">")
}
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\">")
}
// 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.
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
}
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
}
}
var (
- toc tableofcontents.Root
- tocHeading tableofcontents.Heading
+ toc tableofcontents.Builder
+ tocHeading = &tableofcontents.Heading{}
level int
row = -1
inHeading bool
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
}
return s, nil
})
- pc.Set(tocResultKey, toc)
+ pc.Set(tocResultKey, toc.Build())
}
type tocExtension struct {
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 {
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,
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
}
}
-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")
"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">
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">
</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)
+ }
+ })
+
+}
"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 --
[[related.indices]]
name = 'keywords'
weight = 70
+[[related.indices]]
+ name = 'pagerefs'
+ type = 'fragments'
+ weight = 30
-- layouts/_default/single.html --
{{ range site.RegularPages }}
{{ $tmp := .WordCount }}
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{}
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},
},
}
)
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
// 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.
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 {
// 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 {
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)
+ }
+ }
+ }
}
}
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) }
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)
}
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:
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]
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)
}
}
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
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
}
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
package related
import (
+ "context"
"fmt"
"math/rand"
"testing"
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)
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])
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])
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])
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])
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])
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++ {
for i := 0; i < b.N; i++ {
idx := NewInvertedIndex(cfg)
for _, doc := range pages {
- idx.Add(doc)
+ idx.Add(context.Background(), doc)
}
}
})
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
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)
}
}
}
package resources
import (
+ "context"
"image"
"github.com/gohugoio/hugo/common/hugio"
panic(e.ResourceError)
}
-func (e *errorResource) Content() (any, error) {
+func (e *errorResource) Content(context.Context) (any, error) {
panic(e.ResourceError)
}
package resources
import (
+ "context"
"fmt"
"image"
"image/gif"
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">`)
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"
// 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.
PageWithoutContent
}
+type PageFragment interface {
+ resource.ResourceLinksProvider
+ resource.ResourceMetaProvider
+}
+
// PageMetaProvider provides page metadata, typically provided via front matter.
type PageMetaProvider interface {
// The 4 page dates
// 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:
// 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.
// 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
}
// 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.
package page
import (
+ "context"
"html/template"
"github.com/gohugoio/hugo/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
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)
}
"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()
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
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,
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"
return ""
}
-func (p *nopPage) Content() (any, error) {
+func (p *nopPage) Content(context.Context) (any, error) {
return "", nil
}
return nil
}
-func (p *nopPage) FuzzyWordCount() int {
+func (p *nopPage) FuzzyWordCount(context.Context) int {
return 0
}
return
}
-func (p *nopPage) Len() int {
+func (p *nopPage) Len(context.Context) int {
return 0
}
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
}
return ""
}
-func (p *nopPage) ReadingTime() int {
+func (p *nopPage) ReadingTime(context.Context) int {
return 0
}
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
}
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 ""
}
return nil
}
-func (p *nopPage) Truncated() bool {
+func (p *nopPage) Truncated(context.Context) bool {
return false
}
return 0
}
-func (p *nopPage) WordCount() int {
+func (p *nopPage) WordCount(context.Context) int {
return 0
}
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
}
package page
import (
+ "context"
"errors"
"fmt"
"reflect"
// 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
}
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
package page
import (
+ "context"
"reflect"
"strings"
"testing"
{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)
}
{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)
}
{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)
}
{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)
}
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)
}
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)
}
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
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"
)
// 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
}
cache := d.GetRelatedDocsHandler()
- searchIndex, err := cache.getOrCreateIndex(p)
+ searchIndex, err := cache.getOrCreateIndex(ctx, p)
if err != nil {
return nil, err
}
}
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 {
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
}
}
package page
import (
+ "context"
"testing"
"time"
},
}
- 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")
package page
import (
+ "context"
"sort"
"github.com/gohugoio/hugo/common/collections"
// 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 {
return false
}
- return p1l.Len() < p2l.Len()
+ return p1l.Len(ctx) < p2l.Len(ctx)
}
pages, _ := spc.get(key, pageBy(length).Sort, p)
package page
import (
+ "context"
"fmt"
"testing"
"time"
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
{(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)
package page
import (
+ "context"
"fmt"
"html/template"
"testing"
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)
// 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 {
// 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 {
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)
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)
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
}
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)
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)
}
package page
import (
+ "context"
"fmt"
"html/template"
"path"
"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"
panic("not implemented")
}
-func (p *testPage) Content() (any, error) {
+func (p *testPage) Content(context.Context) (any, error) {
panic("not implemented")
}
panic("not implemented")
}
-func (p *testPage) FuzzyWordCount() int {
+func (p *testPage) FuzzyWordCount(context.Context) int {
return p.fuzzyWordCount
}
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)
}
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")
}
panic("not implemented")
}
-func (p *testPage) ReadingTime() int {
+func (p *testPage) ReadingTime(context.Context) int {
panic("not implemented")
}
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")
}
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")
}
panic("not implemented")
}
-func (p *testPage) Truncated() bool {
+func (p *testPage) Truncated(context.Context) bool {
panic("not implemented")
}
return p.weight
}
-func (p *testPage) WordCount() int {
+func (p *testPage) WordCount(context.Context) int {
panic("not implemented")
}
package postpub
import (
+ "context"
"fmt"
"reflect"
"strconv"
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
}
panic(r.fieldNotSupported("Params"))
}
-func (r *PostPublishResource) Content() (any, error) {
+func (r *PostPublishResource) Content(context.Context) (any, error) {
return r.field("Content"), nil
}
package resources
import (
+ "context"
"fmt"
"io"
"io/ioutil"
}
-func (l *genericResource) Content() (any, error) {
+func (l *genericResource) Content(context.Context) (any, error) {
if err := l.initContent(); err != nil {
return nil, err
}
package resource
import (
+ "context"
+
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/media"
// * 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)
// 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.
package integrity
import (
+ "context"
"html/template"
"testing"
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!")
}
package minifier
import (
+ "context"
"testing"
"github.com/gohugoio/hugo/resources/resource"
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>")
}
import (
"bytes"
+ "context"
"fmt"
"image"
"io"
*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 {
package resources
import (
+ "context"
"encoding/base64"
"fmt"
"io"
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")
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")
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))
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)
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")
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")
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")
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")
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))
type (
dataContextKeyType string
hasLockContextKeyType string
+ stackContextKeyType string
)
const (
package transform
import (
+ "context"
"html"
"html/template"
}
// 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
}
package transform_test
import (
+ "context"
"html/template"
"strings"
"testing"
{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))
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"))