From 90da7664bf1f3a0ca2e18144b5deacf532c6e3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sat, 11 Feb 2023 16:20:24 +0100 Subject: Add page fragments support to Related The main topic of this commit is that you can now index fragments (content heading identifiers) when calling `.Related`. You can do this by: * Configure one or more indices with type `fragments` * The name of those index configurations maps to an (optional) front matter slice with fragment references. This allows you to link page<->fragment and page<->page. * This also will index all the fragments (heading identifiers) of the pages. It's also possible to use type `fragments` indices in shortcode, e.g.: ``` {{ $related := site.RegularPages.Related .Page }} ``` But, and this is important, you need to include the shortcode using the `{{<` delimiter. Not doing so will create infinite loops and timeouts. This commit also: * Adds two new methods to Page: Fragments (can also be used to build ToC) and HeadingsFiltered (this is only used in Related Content with index type `fragments` and `enableFilter` set to true. * Consolidates all `.Related*` methods into one, which takes either a `Page` or an options map as its only argument. * Add `context.Context` to all of the content related Page API. Turns out it wasn't strictly needed for this particular feature, but it will soon become usefil, e.g. in #9339. Closes #10711 Updates #9339 Updates #10725 --- resources/errorResource.go | 3 +- resources/image_test.go | 3 +- resources/page/page.go | 41 +++++++---- resources/page/page_lazy_contentprovider.go | 81 +++++++++++----------- resources/page/page_marshaljson.autogen.go | 34 --------- resources/page/page_nop.go | 35 ++++++---- resources/page/pagegroup.go | 10 ++- resources/page/pagegroup_test.go | 15 ++-- resources/page/pages.go | 15 ---- resources/page/pages_related.go | 77 ++++++++++++-------- resources/page/pages_related_test.go | 25 +++++-- resources/page/pages_sort.go | 5 +- resources/page/pages_sort_test.go | 9 ++- resources/page/pagination_test.go | 23 +++--- resources/page/testhelpers_test.go | 34 +++++---- resources/postpub/postpub.go | 5 +- resources/resource.go | 3 +- resources/resource/resourcetypes.go | 6 +- .../integrity/integrity_test.go | 3 +- .../resource_transformers/minifier/minify_test.go | 3 +- resources/transform.go | 5 +- resources/transform_test.go | 21 +++--- 22 files changed, 251 insertions(+), 205 deletions(-) (limited to 'resources') diff --git a/resources/errorResource.go b/resources/errorResource.go index e9411c3db..42edb0bd0 100644 --- a/resources/errorResource.go +++ b/resources/errorResource.go @@ -14,6 +14,7 @@ package resources import ( + "context" "image" "github.com/gohugoio/hugo/common/hugio" @@ -55,7 +56,7 @@ func (e *errorResource) ReadSeekCloser() (hugio.ReadSeekCloser, error) { panic(e.ResourceError) } -func (e *errorResource) Content() (any, error) { +func (e *errorResource) Content(context.Context) (any, error) { panic(e.ResourceError) } diff --git a/resources/image_test.go b/resources/image_test.go index 655454390..3cb1089f4 100644 --- a/resources/image_test.go +++ b/resources/image_test.go @@ -14,6 +14,7 @@ package resources import ( + "context" "fmt" "image" "image/gif" @@ -436,7 +437,7 @@ func TestSVGImageContent(t *testing.T) { svg := fetchResourceForSpec(spec, c, "circle.svg") c.Assert(svg, qt.Not(qt.IsNil)) - content, err := svg.Content() + content, err := svg.Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content, hqt.IsSameType, "") c.Assert(content.(string), qt.Contains, ``) diff --git a/resources/page/page.go b/resources/page/page.go index eeb2cdb28..00e716e83 100644 --- a/resources/page/page.go +++ b/resources/page/page.go @@ -16,10 +16,12 @@ package page import ( + "context" "html/template" "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/tpl" @@ -76,40 +78,40 @@ type ChildCareProvider interface { // ContentProvider provides the content related values for a Page. type ContentProvider interface { - Content() (any, error) + Content(context.Context) (any, error) // Plain returns the Page Content stripped of HTML markup. - Plain() string + Plain(context.Context) string // PlainWords returns a string slice from splitting Plain using https://pkg.go.dev/strings#Fields. - PlainWords() []string + PlainWords(context.Context) []string // Summary returns a generated summary of the content. // The breakpoint can be set manually by inserting a summary separator in the source file. - Summary() template.HTML + Summary(context.Context) template.HTML // Truncated returns whether the Summary is truncated or not. - Truncated() bool + Truncated(context.Context) bool // FuzzyWordCount returns the approximate number of words in the content. - FuzzyWordCount() int + FuzzyWordCount(context.Context) int // WordCount returns the number of words in the content. - WordCount() int + WordCount(context.Context) int // ReadingTime returns the reading time based on the length of plain text. - ReadingTime() int + ReadingTime(context.Context) int // Len returns the length of the content. // This is for internal use only. - Len() int + Len(context.Context) int } // ContentRenderer provides the content rendering methods for some content. type ContentRenderer interface { // RenderContent renders the given content. // For internal use only. - RenderContent(content []byte, renderTOC bool) (converter.Result, error) + RenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.Result, error) } // FileProvider provides the source file. @@ -167,6 +169,11 @@ type Page interface { PageWithoutContent } +type PageFragment interface { + resource.ResourceLinksProvider + resource.ResourceMetaProvider +} + // PageMetaProvider provides page metadata, typically provided via front matter. type PageMetaProvider interface { // The 4 page dates @@ -252,7 +259,7 @@ type PageMetaProvider interface { // PageRenderProvider provides a way for a Page to render content. type PageRenderProvider interface { // Render renders the given layout with this Page as context. - Render(layout ...string) (template.HTML, error) + Render(ctx context.Context, layout ...string) (template.HTML, error) // RenderString renders the first value in args with tPaginatorhe content renderer defined // for this Page. // It takes an optional map as a second argument: @@ -260,7 +267,7 @@ type PageRenderProvider interface { // display (“inline”): // - inline or block. If inline (default), surrounding

on short snippets will be trimmed. // markup (defaults to the Page’s markup) - RenderString(args ...any) (template.HTML, error) + RenderString(ctx context.Context, args ...any) (template.HTML, error) } // PageWithoutContent is the Page without any of the content methods. @@ -323,6 +330,14 @@ type PageWithoutContent interface { // Used in change/dependency tracking. identity.Provider + // Fragments returns the fragments for this page. + Fragments(context.Context) *tableofcontents.Fragments + + // Headings returns the headings for this page when a filter is set. + // This is currently only triggered with the Related content feature + // and the "fragments" type of index. + HeadingsFiltered(context.Context) tableofcontents.Headings + DeprecatedWarningPageMethods } @@ -387,7 +402,7 @@ type SitesProvider interface { // TableOfContentsProvider provides the table of contents for a Page. type TableOfContentsProvider interface { // TableOfContents returns the table of contents for the page rendered as HTML. - TableOfContents() template.HTML + TableOfContents(context.Context) template.HTML } // TranslationsProvider provides access to any translations. diff --git a/resources/page/page_lazy_contentprovider.go b/resources/page/page_lazy_contentprovider.go index 2e4ddc352..e497718f9 100644 --- a/resources/page/page_lazy_contentprovider.go +++ b/resources/page/page_lazy_contentprovider.go @@ -14,6 +14,7 @@ package page import ( + "context" "html/template" "github.com/gohugoio/hugo/lazy" @@ -57,7 +58,7 @@ func NewLazyContentProvider(f func() (OutputFormatContentProvider, error)) *Lazy init: lazy.New(), cp: NopCPageContentRenderer, } - lcp.init.Add(func() (any, error) { + lcp.init.Add(func(context.Context) (any, error) { cp, err := f() if err != nil { return nil, err @@ -72,67 +73,67 @@ func (lcp *LazyContentProvider) Reset() { lcp.init.Reset() } -func (lcp *LazyContentProvider) Content() (any, error) { - lcp.init.Do() - return lcp.cp.Content() +func (lcp *LazyContentProvider) Content(ctx context.Context) (any, error) { + lcp.init.Do(ctx) + return lcp.cp.Content(ctx) } -func (lcp *LazyContentProvider) Plain() string { - lcp.init.Do() - return lcp.cp.Plain() +func (lcp *LazyContentProvider) Plain(ctx context.Context) string { + lcp.init.Do(ctx) + return lcp.cp.Plain(ctx) } -func (lcp *LazyContentProvider) PlainWords() []string { - lcp.init.Do() - return lcp.cp.PlainWords() +func (lcp *LazyContentProvider) PlainWords(ctx context.Context) []string { + lcp.init.Do(ctx) + return lcp.cp.PlainWords(ctx) } -func (lcp *LazyContentProvider) Summary() template.HTML { - lcp.init.Do() - return lcp.cp.Summary() +func (lcp *LazyContentProvider) Summary(ctx context.Context) template.HTML { + lcp.init.Do(ctx) + return lcp.cp.Summary(ctx) } -func (lcp *LazyContentProvider) Truncated() bool { - lcp.init.Do() - return lcp.cp.Truncated() +func (lcp *LazyContentProvider) Truncated(ctx context.Context) bool { + lcp.init.Do(ctx) + return lcp.cp.Truncated(ctx) } -func (lcp *LazyContentProvider) FuzzyWordCount() int { - lcp.init.Do() - return lcp.cp.FuzzyWordCount() +func (lcp *LazyContentProvider) FuzzyWordCount(ctx context.Context) int { + lcp.init.Do(ctx) + return lcp.cp.FuzzyWordCount(ctx) } -func (lcp *LazyContentProvider) WordCount() int { - lcp.init.Do() - return lcp.cp.WordCount() +func (lcp *LazyContentProvider) WordCount(ctx context.Context) int { + lcp.init.Do(ctx) + return lcp.cp.WordCount(ctx) } -func (lcp *LazyContentProvider) ReadingTime() int { - lcp.init.Do() - return lcp.cp.ReadingTime() +func (lcp *LazyContentProvider) ReadingTime(ctx context.Context) int { + lcp.init.Do(ctx) + return lcp.cp.ReadingTime(ctx) } -func (lcp *LazyContentProvider) Len() int { - lcp.init.Do() - return lcp.cp.Len() +func (lcp *LazyContentProvider) Len(ctx context.Context) int { + lcp.init.Do(ctx) + return lcp.cp.Len(ctx) } -func (lcp *LazyContentProvider) Render(layout ...string) (template.HTML, error) { - lcp.init.Do() - return lcp.cp.Render(layout...) +func (lcp *LazyContentProvider) Render(ctx context.Context, layout ...string) (template.HTML, error) { + lcp.init.Do(context.TODO()) + return lcp.cp.Render(ctx, layout...) } -func (lcp *LazyContentProvider) RenderString(args ...any) (template.HTML, error) { - lcp.init.Do() - return lcp.cp.RenderString(args...) +func (lcp *LazyContentProvider) RenderString(ctx context.Context, args ...any) (template.HTML, error) { + lcp.init.Do(ctx) + return lcp.cp.RenderString(ctx, args...) } -func (lcp *LazyContentProvider) TableOfContents() template.HTML { - lcp.init.Do() - return lcp.cp.TableOfContents() +func (lcp *LazyContentProvider) TableOfContents(ctx context.Context) template.HTML { + lcp.init.Do(ctx) + return lcp.cp.TableOfContents(ctx) } -func (lcp *LazyContentProvider) RenderContent(content []byte, renderTOC bool) (converter.Result, error) { - lcp.init.Do() - return lcp.cp.RenderContent(content, renderTOC) +func (lcp *LazyContentProvider) RenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.Result, error) { + lcp.init.Do(ctx) + return lcp.cp.RenderContent(ctx, content, renderTOC) } diff --git a/resources/page/page_marshaljson.autogen.go b/resources/page/page_marshaljson.autogen.go index 373257878..c3524ec36 100644 --- a/resources/page/page_marshaljson.autogen.go +++ b/resources/page/page_marshaljson.autogen.go @@ -25,24 +25,10 @@ import ( "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/navigation" "github.com/gohugoio/hugo/source" - "html/template" "time" ) func MarshalPageToJSON(p Page) ([]byte, error) { - content, err := p.Content() - if err != nil { - return nil, err - } - plain := p.Plain() - plainWords := p.PlainWords() - summary := p.Summary() - truncated := p.Truncated() - fuzzyWordCount := p.FuzzyWordCount() - wordCount := p.WordCount() - readingTime := p.ReadingTime() - length := p.Len() - tableOfContents := p.TableOfContents() rawContent := p.RawContent() resourceType := p.ResourceType() mediaType := p.MediaType() @@ -93,16 +79,6 @@ func MarshalPageToJSON(p Page) ([]byte, error) { getIdentity := p.GetIdentity() s := struct { - Content interface{} - Plain string - PlainWords []string - Summary template.HTML - Truncated bool - FuzzyWordCount int - WordCount int - ReadingTime int - Len int - TableOfContents template.HTML RawContent string ResourceType string MediaType media.Type @@ -152,16 +128,6 @@ func MarshalPageToJSON(p Page) ([]byte, error) { Store *maps.Scratch GetIdentity identity.Identity }{ - Content: content, - Plain: plain, - PlainWords: plainWords, - Summary: summary, - Truncated: truncated, - FuzzyWordCount: fuzzyWordCount, - WordCount: wordCount, - ReadingTime: readingTime, - Len: length, - TableOfContents: tableOfContents, RawContent: rawContent, ResourceType: resourceType, MediaType: mediaType, diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go index c4af3f554..8946926a2 100644 --- a/resources/page/page_nop.go +++ b/resources/page/page_nop.go @@ -17,11 +17,13 @@ package page import ( "bytes" + "context" "html/template" "time" "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/tpl" @@ -105,7 +107,7 @@ func (p *nopPage) BundleType() files.ContentClass { return "" } -func (p *nopPage) Content() (any, error) { +func (p *nopPage) Content(context.Context) (any, error) { return "", nil } @@ -179,7 +181,7 @@ func (p *nopPage) FirstSection() Page { return nil } -func (p *nopPage) FuzzyWordCount() int { +func (p *nopPage) FuzzyWordCount(context.Context) int { return 0 } @@ -279,7 +281,7 @@ func (p *nopPage) Lastmod() (t time.Time) { return } -func (p *nopPage) Len() int { +func (p *nopPage) Len(context.Context) int { return 0 } @@ -363,11 +365,11 @@ func (p *nopPage) Permalink() string { return "" } -func (p *nopPage) Plain() string { +func (p *nopPage) Plain(context.Context) string { return "" } -func (p *nopPage) PlainWords() []string { +func (p *nopPage) PlainWords(context.Context) []string { return nil } @@ -399,7 +401,7 @@ func (p *nopPage) RawContent() string { return "" } -func (p *nopPage) ReadingTime() int { +func (p *nopPage) ReadingTime(context.Context) int { return 0 } @@ -415,11 +417,11 @@ func (p *nopPage) RelRef(argsm map[string]any) (string, error) { return "", nil } -func (p *nopPage) Render(layout ...string) (template.HTML, error) { +func (p *nopPage) Render(ctx context.Context, layout ...string) (template.HTML, error) { return "", nil } -func (p *nopPage) RenderString(args ...any) (template.HTML, error) { +func (p *nopPage) RenderString(ctx context.Context, args ...any) (template.HTML, error) { return "", nil } @@ -475,11 +477,11 @@ func (p *nopPage) String() string { return "nopPage" } -func (p *nopPage) Summary() template.HTML { +func (p *nopPage) Summary(context.Context) template.HTML { return "" } -func (p *nopPage) TableOfContents() template.HTML { +func (p *nopPage) TableOfContents(context.Context) template.HTML { return "" } @@ -499,7 +501,7 @@ func (p *nopPage) Translations() Pages { return nil } -func (p *nopPage) Truncated() bool { +func (p *nopPage) Truncated(context.Context) bool { return false } @@ -519,7 +521,7 @@ func (p *nopPage) Weight() int { return 0 } -func (p *nopPage) WordCount() int { +func (p *nopPage) WordCount(context.Context) int { return 0 } @@ -527,9 +529,16 @@ func (p *nopPage) GetIdentity() identity.Identity { return identity.NewPathIdentity("content", "foo/bar.md") } +func (p *nopPage) Fragments(context.Context) *tableofcontents.Fragments { + return nil +} +func (p *nopPage) HeadingsFiltered(context.Context) tableofcontents.Headings { + return nil +} + type nopContentRenderer int -func (r *nopContentRenderer) RenderContent(content []byte, renderTOC bool) (converter.Result, error) { +func (r *nopContentRenderer) RenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.Result, error) { b := &bytes.Buffer{} return b, nil } diff --git a/resources/page/pagegroup.go b/resources/page/pagegroup.go index 3b32a1fae..bac5d8327 100644 --- a/resources/page/pagegroup.go +++ b/resources/page/pagegroup.go @@ -14,6 +14,7 @@ package page import ( + "context" "errors" "fmt" "reflect" @@ -110,7 +111,7 @@ var ( // GroupBy groups by the value in the given field or method name and with the given order. // Valid values for order is asc, desc, rev and reverse. -func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) { +func (p Pages) GroupBy(ctx context.Context, key string, order ...string) (PagesGroup, error) { if len(p) < 1 { return nil, nil } @@ -158,7 +159,12 @@ func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) { case reflect.StructField: fv = ppv.Elem().FieldByName(key) case reflect.Method: - fv = hreflect.GetMethodByName(ppv, key).Call([]reflect.Value{})[0] + var args []reflect.Value + fn := hreflect.GetMethodByName(ppv, key) + if fn.Type().NumIn() > 0 && fn.Type().In(0).Implements(hreflect.ContextInterface) { + args = []reflect.Value{reflect.ValueOf(ctx)} + } + fv = fn.Call(args)[0] } if !fv.IsValid() { continue diff --git a/resources/page/pagegroup_test.go b/resources/page/pagegroup_test.go index ef0d24471..91f05b24a 100644 --- a/resources/page/pagegroup_test.go +++ b/resources/page/pagegroup_test.go @@ -14,6 +14,7 @@ package page import ( + "context" "reflect" "strings" "testing" @@ -68,7 +69,7 @@ func TestGroupByWithFieldNameArg(t *testing.T) { {Key: 3, Pages: Pages{pages[0], pages[1]}}, } - groups, err := pages.GroupBy("Weight") + groups, err := pages.GroupBy(context.Background(), "Weight") if err != nil { t.Fatalf("Unable to make PagesGroup array: %s", err) } @@ -85,7 +86,7 @@ func TestGroupByWithMethodNameArg(t *testing.T) { {Key: "section2", Pages: Pages{pages[3], pages[4]}}, } - groups, err := pages.GroupBy("Type") + groups, err := pages.GroupBy(context.Background(), "Type") if err != nil { t.Fatalf("Unable to make PagesGroup array: %s", err) } @@ -102,7 +103,7 @@ func TestGroupByWithSectionArg(t *testing.T) { {Key: "section2", Pages: Pages{pages[3], pages[4]}}, } - groups, err := pages.GroupBy("Section") + groups, err := pages.GroupBy(context.Background(), "Section") if err != nil { t.Fatalf("Unable to make PagesGroup array: %s", err) } @@ -120,7 +121,7 @@ func TestGroupByInReverseOrder(t *testing.T) { {Key: 1, Pages: Pages{pages[3], pages[4]}}, } - groups, err := pages.GroupBy("Weight", "desc") + groups, err := pages.GroupBy(context.Background(), "Weight", "desc") if err != nil { t.Fatalf("Unable to make PagesGroup array: %s", err) } @@ -132,7 +133,7 @@ func TestGroupByInReverseOrder(t *testing.T) { func TestGroupByCalledWithEmptyPages(t *testing.T) { t.Parallel() var pages Pages - groups, err := pages.GroupBy("Weight") + groups, err := pages.GroupBy(context.Background(), "Weight") if err != nil { t.Fatalf("Unable to make PagesGroup array: %s", err) } @@ -154,12 +155,12 @@ func TestReverse(t *testing.T) { t.Parallel() pages := preparePageGroupTestPages(t) - groups1, err := pages.GroupBy("Weight", "desc") + groups1, err := pages.GroupBy(context.Background(), "Weight", "desc") if err != nil { t.Fatalf("Unable to make PagesGroup array: %s", err) } - groups2, err := pages.GroupBy("Weight") + groups2, err := pages.GroupBy(context.Background(), "Weight") if err != nil { t.Fatalf("Unable to make PagesGroup array: %s", err) } diff --git a/resources/page/pages.go b/resources/page/pages.go index f47af5114..77e56a062 100644 --- a/resources/page/pages.go +++ b/resources/page/pages.go @@ -132,21 +132,6 @@ func (pages Pages) ProbablyEq(other any) bool { return true } -func (ps Pages) removeFirstIfFound(p Page) Pages { - ii := -1 - for i, pp := range ps { - if p.Eq(pp) { - ii = i - break - } - } - - if ii != -1 { - ps = append(ps[:ii], ps[ii+1:]...) - } - return ps -} - // PagesFactory somehow creates some Pages. // We do a lot of lazy Pages initialization in Hugo, so we need a type. type PagesFactory func() Pages diff --git a/resources/page/pages_related.go b/resources/page/pages_related.go index 35bb2965a..74bb1713e 100644 --- a/resources/page/pages_related.go +++ b/resources/page/pages_related.go @@ -14,11 +14,13 @@ package page import ( + "context" "fmt" "sync" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/related" + "github.com/mitchellh/mapstructure" "github.com/spf13/cast" ) @@ -34,74 +36,90 @@ type PageGenealogist interface { // Template example: // {{ $related := .RegularPages.Related . }} - Related(doc related.Document) (Pages, error) + Related(ctx context.Context, opts any) (Pages, error) // Template example: // {{ $related := .RegularPages.RelatedIndices . "tags" "date" }} - RelatedIndices(doc related.Document, indices ...any) (Pages, error) + // Deprecated: Use Related instead. + RelatedIndices(ctx context.Context, doc related.Document, indices ...any) (Pages, error) // Template example: // {{ $related := .RegularPages.RelatedTo ( keyVals "tags" "hugo", "rocks") ( keyVals "date" .Date ) }} - RelatedTo(args ...types.KeyValues) (Pages, error) + // Deprecated: Use Related instead. + RelatedTo(ctx context.Context, args ...types.KeyValues) (Pages, error) } // Related searches all the configured indices with the search keywords from the // supplied document. -func (p Pages) Related(doc related.Document) (Pages, error) { - result, err := p.searchDoc(doc) - if err != nil { - return nil, err +func (p Pages) Related(ctx context.Context, optsv any) (Pages, error) { + if len(p) == 0 { + return nil, nil + } + + var opts related.SearchOpts + switch v := optsv.(type) { + case related.Document: + opts.Document = v + case map[string]any: + if err := mapstructure.WeakDecode(v, &opts); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("invalid argument type %T", optsv) } - if page, ok := doc.(Page); ok { - return result.removeFirstIfFound(page), nil + result, err := p.search(ctx, opts) + if err != nil { + return nil, err } return result, nil + } // RelatedIndices searches the given indices with the search keywords from the // supplied document. -func (p Pages) RelatedIndices(doc related.Document, indices ...any) (Pages, error) { +// Deprecated: Use Related instead. +func (p Pages) RelatedIndices(ctx context.Context, doc related.Document, indices ...any) (Pages, error) { indicesStr, err := cast.ToStringSliceE(indices) if err != nil { return nil, err } - result, err := p.searchDoc(doc, indicesStr...) - if err != nil { - return nil, err + opts := related.SearchOpts{ + Document: doc, + Indices: indicesStr, } - if page, ok := doc.(Page); ok { - return result.removeFirstIfFound(page), nil + result, err := p.search(ctx, opts) + if err != nil { + return nil, err } return result, nil } // RelatedTo searches the given indices with the corresponding values. -func (p Pages) RelatedTo(args ...types.KeyValues) (Pages, error) { +// Deprecated: Use Related instead. +func (p Pages) RelatedTo(ctx context.Context, args ...types.KeyValues) (Pages, error) { if len(p) == 0 { return nil, nil } - return p.search(args...) -} + opts := related.SearchOpts{ + NamedSlices: args, + } -func (p Pages) search(args ...types.KeyValues) (Pages, error) { - return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) { - return idx.SearchKeyValues(args...) - }) + return p.search(ctx, opts) } -func (p Pages) searchDoc(doc related.Document, indices ...string) (Pages, error) { - return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) { - return idx.SearchDoc(doc, indices...) +func (p Pages) search(ctx context.Context, opts related.SearchOpts) (Pages, error) { + return p.withInvertedIndex(ctx, func(idx *related.InvertedIndex) ([]related.Document, error) { + return idx.Search(ctx, opts) }) } -func (p Pages) withInvertedIndex(search func(idx *related.InvertedIndex) ([]related.Document, error)) (Pages, error) { +func (p Pages) withInvertedIndex(ctx context.Context, search func(idx *related.InvertedIndex) ([]related.Document, error)) (Pages, error) { if len(p) == 0 { return nil, nil } @@ -113,7 +131,7 @@ func (p Pages) withInvertedIndex(search func(idx *related.InvertedIndex) ([]rela cache := d.GetRelatedDocsHandler() - searchIndex, err := cache.getOrCreateIndex(p) + searchIndex, err := cache.getOrCreateIndex(ctx, p) if err != nil { return nil, err } @@ -164,8 +182,7 @@ func (s *RelatedDocsHandler) getIndex(p Pages) *related.InvertedIndex { } return nil } - -func (s *RelatedDocsHandler) getOrCreateIndex(p Pages) (*related.InvertedIndex, error) { +func (s *RelatedDocsHandler) getOrCreateIndex(ctx context.Context, p Pages) (*related.InvertedIndex, error) { s.mu.RLock() cachedIndex := s.getIndex(p) if cachedIndex != nil { @@ -184,7 +201,7 @@ func (s *RelatedDocsHandler) getOrCreateIndex(p Pages) (*related.InvertedIndex, searchIndex := related.NewInvertedIndex(s.cfg) for _, page := range p { - if err := searchIndex.Add(page); err != nil { + if err := searchIndex.Add(ctx, page); err != nil { return nil, err } } diff --git a/resources/page/pages_related_test.go b/resources/page/pages_related_test.go index 3c5780a9a..75ab7ecb9 100644 --- a/resources/page/pages_related_test.go +++ b/resources/page/pages_related_test.go @@ -14,6 +14,7 @@ package page import ( + "context" "testing" "time" @@ -51,26 +52,42 @@ func TestRelated(t *testing.T) { }, } - result, err := pages.RelatedTo(types.NewKeyValuesStrings("keywords", "hugo", "rocks")) + ctx := context.Background() + opts := map[string]any{ + "namedSlices": types.NewKeyValuesStrings("keywords", "hugo", "rocks"), + } + result, err := pages.Related(ctx, opts) c.Assert(err, qt.IsNil) c.Assert(len(result), qt.Equals, 2) c.Assert(result[0].Title(), qt.Equals, "Page 2") c.Assert(result[1].Title(), qt.Equals, "Page 1") - result, err = pages.Related(pages[0]) + result, err = pages.Related(ctx, pages[0]) c.Assert(err, qt.IsNil) c.Assert(len(result), qt.Equals, 2) c.Assert(result[0].Title(), qt.Equals, "Page 2") c.Assert(result[1].Title(), qt.Equals, "Page 3") - result, err = pages.RelatedIndices(pages[0], "keywords") + opts = map[string]any{ + "document": pages[0], + "indices": []string{"keywords"}, + } + result, err = pages.Related(ctx, opts) c.Assert(err, qt.IsNil) c.Assert(len(result), qt.Equals, 2) c.Assert(result[0].Title(), qt.Equals, "Page 2") c.Assert(result[1].Title(), qt.Equals, "Page 3") - result, err = pages.RelatedTo(types.NewKeyValuesStrings("keywords", "bep", "rocks")) + opts = map[string]any{ + "namedSlices": []types.KeyValues{ + { + Key: "keywords", + Values: []any{"bep", "rocks"}, + }, + }, + } + result, err = pages.Related(context.Background(), opts) c.Assert(err, qt.IsNil) c.Assert(len(result), qt.Equals, 2) c.Assert(result[0].Title(), qt.Equals, "Page 2") diff --git a/resources/page/pages_sort.go b/resources/page/pages_sort.go index 08cb34a32..b9b905cc2 100644 --- a/resources/page/pages_sort.go +++ b/resources/page/pages_sort.go @@ -14,6 +14,7 @@ package page import ( + "context" "sort" "github.com/gohugoio/hugo/common/collections" @@ -299,7 +300,7 @@ func (p Pages) ByLastmod() Pages { // Adjacent invocations on the same receiver will return a cached result. // // This may safely be executed in parallel. -func (p Pages) ByLength() Pages { +func (p Pages) ByLength(ctx context.Context) Pages { const key = "pageSort.ByLength" length := func(p1, p2 Page) bool { @@ -314,7 +315,7 @@ func (p Pages) ByLength() Pages { return false } - return p1l.Len() < p2l.Len() + return p1l.Len(ctx) < p2l.Len(ctx) } pages, _ := spc.get(key, pageBy(length).Sort, p) diff --git a/resources/page/pages_sort_test.go b/resources/page/pages_sort_test.go index cf4e339ee..728237230 100644 --- a/resources/page/pages_sort_test.go +++ b/resources/page/pages_sort_test.go @@ -14,6 +14,7 @@ package page import ( + "context" "fmt" "testing" "time" @@ -104,6 +105,12 @@ func TestSortByN(t *testing.T) { d4 := d1.Add(-20 * time.Hour) p := createSortTestPages(4) + ctx := context.Background() + + byLen := func(p Pages) Pages { + return p.ByLength(ctx) + + } for i, this := range []struct { sortFunc func(p Pages) Pages @@ -116,7 +123,7 @@ func TestSortByN(t *testing.T) { {(Pages).ByPublishDate, func(p Pages) bool { return p[0].PublishDate() == d4 }}, {(Pages).ByExpiryDate, func(p Pages) bool { return p[0].ExpiryDate() == d4 }}, {(Pages).ByLastmod, func(p Pages) bool { return p[1].Lastmod() == d3 }}, - {(Pages).ByLength, func(p Pages) bool { return p[0].(resource.LengthProvider).Len() == len(p[0].(*testPage).content) }}, + {byLen, func(p Pages) bool { return p[0].(resource.LengthProvider).Len(ctx) == len(p[0].(*testPage).content) }}, } { setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "ab", "cde", "fg"}, [4]int{0, 3, 2, 1}, p) diff --git a/resources/page/pagination_test.go b/resources/page/pagination_test.go index e379f9b6b..2686d3920 100644 --- a/resources/page/pagination_test.go +++ b/resources/page/pagination_test.go @@ -14,6 +14,7 @@ package page import ( + "context" "fmt" "html/template" "testing" @@ -43,7 +44,7 @@ func TestSplitPageGroups(t *testing.T) { t.Parallel() c := qt.New(t) pages := createTestPages(21) - groups, _ := pages.GroupBy("Weight", "desc") + groups, _ := pages.GroupBy(context.Background(), "Weight", "desc") chunks := splitPageGroups(groups, 5) c.Assert(len(chunks), qt.Equals, 5) @@ -56,7 +57,7 @@ func TestSplitPageGroups(t *testing.T) { // first group 10 in weight c.Assert(pg.Key, qt.Equals, 10) for _, p := range pg.Pages { - c.Assert(p.FuzzyWordCount()%2 == 0, qt.Equals, true) // magic test + c.Assert(p.FuzzyWordCount(context.Background())%2 == 0, qt.Equals, true) // magic test } } } else { @@ -71,7 +72,7 @@ func TestSplitPageGroups(t *testing.T) { // last should have 5 in weight c.Assert(pg.Key, qt.Equals, 5) for _, p := range pg.Pages { - c.Assert(p.FuzzyWordCount()%2 != 0, qt.Equals, true) // magic test + c.Assert(p.FuzzyWordCount(context.Background())%2 != 0, qt.Equals, true) // magic test } } } else { @@ -83,7 +84,7 @@ func TestPager(t *testing.T) { t.Parallel() c := qt.New(t) pages := createTestPages(21) - groups, _ := pages.GroupBy("Weight", "desc") + groups, _ := pages.GroupBy(context.Background(), "Weight", "desc") urlFactory := func(page int) string { return fmt.Sprintf("page/%d/", page) @@ -149,7 +150,7 @@ func TestPagerNoPages(t *testing.T) { t.Parallel() c := qt.New(t) pages := createTestPages(0) - groups, _ := pages.GroupBy("Weight", "desc") + groups, _ := pages.GroupBy(context.Background(), "Weight", "desc") urlFactory := func(page int) string { return fmt.Sprintf("page/%d/", page) @@ -249,9 +250,9 @@ func TestProbablyEqualPageLists(t *testing.T) { t.Parallel() fivePages := createTestPages(5) zeroPages := createTestPages(0) - zeroPagesByWeight, _ := createTestPages(0).GroupBy("Weight", "asc") - fivePagesByWeight, _ := createTestPages(5).GroupBy("Weight", "asc") - ninePagesByWeight, _ := createTestPages(9).GroupBy("Weight", "asc") + zeroPagesByWeight, _ := createTestPages(0).GroupBy(context.Background(), "Weight", "asc") + fivePagesByWeight, _ := createTestPages(5).GroupBy(context.Background(), "Weight", "asc") + ninePagesByWeight, _ := createTestPages(9).GroupBy(context.Background(), "Weight", "asc") for i, this := range []struct { v1 any @@ -287,7 +288,7 @@ func TestPaginationPage(t *testing.T) { } fivePages := createTestPages(7) - fivePagesFuzzyWordCount, _ := createTestPages(7).GroupBy("FuzzyWordCount", "asc") + fivePagesFuzzyWordCount, _ := createTestPages(7).GroupBy(context.Background(), "FuzzyWordCount", "asc") p1, _ := newPaginatorFromPages(fivePages, 2, urlFactory) p2, _ := newPaginatorFromPageGroups(fivePagesFuzzyWordCount, 2, urlFactory) @@ -301,10 +302,10 @@ func TestPaginationPage(t *testing.T) { page21, _ := f2.page(1) page2Nil, _ := f2.page(3) - c.Assert(page11.FuzzyWordCount(), qt.Equals, 3) + c.Assert(page11.FuzzyWordCount(context.Background()), qt.Equals, 3) c.Assert(page1Nil, qt.IsNil) c.Assert(page21, qt.Not(qt.IsNil)) - c.Assert(page21.FuzzyWordCount(), qt.Equals, 3) + c.Assert(page21.FuzzyWordCount(context.Background()), qt.Equals, 3) c.Assert(page2Nil, qt.IsNil) } diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index e8275ba40..72f62ee8d 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -14,6 +14,7 @@ package page import ( + "context" "fmt" "html/template" "path" @@ -22,6 +23,7 @@ import ( "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/modules" @@ -153,7 +155,7 @@ func (p *testPage) BundleType() files.ContentClass { panic("not implemented") } -func (p *testPage) Content() (any, error) { +func (p *testPage) Content(context.Context) (any, error) { panic("not implemented") } @@ -225,7 +227,7 @@ func (p *testPage) FirstSection() Page { panic("not implemented") } -func (p *testPage) FuzzyWordCount() int { +func (p *testPage) FuzzyWordCount(context.Context) int { return p.fuzzyWordCount } @@ -329,11 +331,19 @@ func (p *testPage) LanguagePrefix() string { return "" } +func (p *testPage) Fragments(context.Context) *tableofcontents.Fragments { + return nil +} + +func (p *testPage) HeadingsFiltered(context.Context) tableofcontents.Headings { + return nil +} + func (p *testPage) Lastmod() time.Time { return p.lastMod } -func (p *testPage) Len() int { +func (p *testPage) Len(context.Context) int { return len(p.content) } @@ -431,11 +441,11 @@ func (p *testPage) Permalink() string { panic("not implemented") } -func (p *testPage) Plain() string { +func (p *testPage) Plain(context.Context) string { panic("not implemented") } -func (p *testPage) PlainWords() []string { +func (p *testPage) PlainWords(context.Context) []string { panic("not implemented") } @@ -463,7 +473,7 @@ func (p *testPage) RawContent() string { panic("not implemented") } -func (p *testPage) ReadingTime() int { +func (p *testPage) ReadingTime(context.Context) int { panic("not implemented") } @@ -487,11 +497,11 @@ func (p *testPage) RelRefFrom(argsm map[string]any, source any) (string, error) return "", nil } -func (p *testPage) Render(layout ...string) (template.HTML, error) { +func (p *testPage) Render(ctx context.Context, layout ...string) (template.HTML, error) { panic("not implemented") } -func (p *testPage) RenderString(args ...any) (template.HTML, error) { +func (p *testPage) RenderString(ctx context.Context, args ...any) (template.HTML, error) { panic("not implemented") } @@ -552,11 +562,11 @@ func (p *testPage) String() string { return p.path } -func (p *testPage) Summary() template.HTML { +func (p *testPage) Summary(context.Context) template.HTML { panic("not implemented") } -func (p *testPage) TableOfContents() template.HTML { +func (p *testPage) TableOfContents(context.Context) template.HTML { panic("not implemented") } @@ -576,7 +586,7 @@ func (p *testPage) Translations() Pages { panic("not implemented") } -func (p *testPage) Truncated() bool { +func (p *testPage) Truncated(context.Context) bool { panic("not implemented") } @@ -596,7 +606,7 @@ func (p *testPage) Weight() int { return p.weight } -func (p *testPage) WordCount() int { +func (p *testPage) WordCount(context.Context) int { panic("not implemented") } diff --git a/resources/postpub/postpub.go b/resources/postpub/postpub.go index 400e00aa4..5911362ec 100644 --- a/resources/postpub/postpub.go +++ b/resources/postpub/postpub.go @@ -14,6 +14,7 @@ package postpub import ( + "context" "fmt" "reflect" "strconv" @@ -101,7 +102,7 @@ func (r *PostPublishResource) GetFieldString(pattern string) (string, bool) { case fieldAccessor == "ResourceType": return d.ResourceType(), true case fieldAccessor == "Content": - content, err := d.(resource.ContentProvider).Content() + content, err := d.(resource.ContentProvider).Content(context.Background()) if err != nil { return "", true } @@ -172,7 +173,7 @@ func (r *PostPublishResource) Params() maps.Params { panic(r.fieldNotSupported("Params")) } -func (r *PostPublishResource) Content() (any, error) { +func (r *PostPublishResource) Content(context.Context) (any, error) { return r.field("Content"), nil } diff --git a/resources/resource.go b/resources/resource.go index 0d7d1d85a..8a524247a 100644 --- a/resources/resource.go +++ b/resources/resource.go @@ -14,6 +14,7 @@ package resources import ( + "context" "fmt" "io" "io/ioutil" @@ -256,7 +257,7 @@ func (l *genericResource) cloneTo(targetPath string) resource.Resource { } -func (l *genericResource) Content() (any, error) { +func (l *genericResource) Content(context.Context) (any, error) { if err := l.initContent(); err != nil { return nil, err } diff --git a/resources/resource/resourcetypes.go b/resources/resource/resourcetypes.go index 43761b27d..237bee0c8 100644 --- a/resources/resource/resourcetypes.go +++ b/resources/resource/resourcetypes.go @@ -14,6 +14,8 @@ package resource import ( + "context" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/media" @@ -162,7 +164,7 @@ type ContentProvider interface { // * Page: template.HTML // * JSON: String // * Etc. - Content() (any, error) + Content(context.Context) (any, error) } // OpenReadSeekCloser allows setting some other way (than reading from a filesystem) @@ -178,7 +180,7 @@ type ReadSeekCloserResource interface { // LengthProvider is a Resource that provides a length // (typically the length of the content). type LengthProvider interface { - Len() int + Len(context.Context) int } // LanguageProvider is a Resource in a language. diff --git a/resources/resource_transformers/integrity/integrity_test.go b/resources/resource_transformers/integrity/integrity_test.go index cba993d1e..ef3f13a55 100644 --- a/resources/resource_transformers/integrity/integrity_test.go +++ b/resources/resource_transformers/integrity/integrity_test.go @@ -14,6 +14,7 @@ package integrity import ( + "context" "html/template" "testing" @@ -63,7 +64,7 @@ func TestTransform(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(transformed.RelPermalink(), qt.Equals, "/hugo.a5ad1c6961214a55de53c1ce6e60d27b6b761f54851fa65e33066460dfa6a0db.txt") c.Assert(transformed.Data(), qt.DeepEquals, map[string]any{"Integrity": template.HTMLAttr("sha256-pa0caWEhSlXeU8HObmDSe2t2H1SFH6ZeMwZkYN+moNs=")}) - content, err := transformed.(resource.ContentProvider).Content() + content, err := transformed.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "Hugo Rocks!") } diff --git a/resources/resource_transformers/minifier/minify_test.go b/resources/resource_transformers/minifier/minify_test.go index b0ebe3171..b2d8ed734 100644 --- a/resources/resource_transformers/minifier/minify_test.go +++ b/resources/resource_transformers/minifier/minify_test.go @@ -14,6 +14,7 @@ package minifier import ( + "context" "testing" "github.com/gohugoio/hugo/resources/resource" @@ -36,7 +37,7 @@ func TestTransform(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(transformed.RelPermalink(), qt.Equals, "/hugo.min.html") - content, err := transformed.(resource.ContentProvider).Content() + content, err := transformed.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "

Hugo Rocks!

") } diff --git a/resources/transform.go b/resources/transform.go index a37011acd..3477c710f 100644 --- a/resources/transform.go +++ b/resources/transform.go @@ -15,6 +15,7 @@ package resources import ( "bytes" + "context" "fmt" "image" "io" @@ -159,12 +160,12 @@ type resourceAdapter struct { *resourceAdapterInner } -func (r *resourceAdapter) Content() (any, error) { +func (r *resourceAdapter) Content(context.Context) (any, error) { r.init(false, true) if r.transformationsErr != nil { return nil, r.transformationsErr } - return r.target.Content() + return r.target.Content(context.Background()) } func (r *resourceAdapter) Err() resource.ResourceError { diff --git a/resources/transform_test.go b/resources/transform_test.go index 1bd8302d2..c883e2593 100644 --- a/resources/transform_test.go +++ b/resources/transform_test.go @@ -14,6 +14,7 @@ package resources import ( + "context" "encoding/base64" "fmt" "io" @@ -115,7 +116,7 @@ func TestTransform(t *testing.T) { tr, err := r.Transform(transformation) c.Assert(err, qt.IsNil) - content, err := tr.(resource.ContentProvider).Content() + content, err := tr.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "color is green") @@ -149,7 +150,7 @@ func TestTransform(t *testing.T) { tr, err := r.Transform(transformation) c.Assert(err, qt.IsNil) - content, err := tr.(resource.ContentProvider).Content() + content, err := tr.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "color is blue") @@ -184,7 +185,7 @@ func TestTransform(t *testing.T) { for i, transformation := range []ResourceTransformation{t1, t2} { r := createTransformer(spec, "f1.txt", "color is blue") tr, _ := r.Transform(transformation) - content, err := tr.(resource.ContentProvider).Content() + content, err := tr.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "color is green", qt.Commentf("i=%d", i)) @@ -237,7 +238,7 @@ func TestTransform(t *testing.T) { tr, _ := r.Transform(transformation) c.Assert(tr.RelPermalink(), qt.Equals, "/f1.cached.txt", msg) - content, err := tr.(resource.ContentProvider).Content() + content, err := tr.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "color is green", msg) c.Assert(tr.MediaType(), eq, media.CSVType) @@ -264,7 +265,7 @@ func TestTransform(t *testing.T) { relPermalink := tr.RelPermalink() - content, err := tr.(resource.ContentProvider).Content() + content, err := tr.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(relPermalink, qt.Equals, "/f1.t1.txt") @@ -286,7 +287,7 @@ func TestTransform(t *testing.T) { r := createTransformer(spec, "f1.txt", "color is blue") tr, _ := r.Transform(t1, t2) - content, err := tr.(resource.ContentProvider).Content() + content, err := tr.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "car is green") @@ -308,9 +309,9 @@ func TestTransform(t *testing.T) { tr1, _ := r.Transform(t1) tr2, _ := tr1.Transform(t2) - content1, err := tr1.(resource.ContentProvider).Content() + content1, err := tr1.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) - content2, err := tr2.(resource.ContentProvider).Content() + content2, err := tr2.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content1, qt.Equals, "color is green") @@ -339,7 +340,7 @@ func TestTransform(t *testing.T) { r := createTransformer(spec, "f1.txt", countstr.String()) tr, _ := r.Transform(transformations...) - content, err := tr.(resource.ContentProvider).Content() + content, err := tr.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") @@ -417,7 +418,7 @@ func TestTransform(t *testing.T) { id := (i + j) % 10 tr, err := transformers[id].Transform(transformations[id]) c.Assert(err, qt.IsNil) - content, err := tr.(resource.ContentProvider).Content() + content, err := tr.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "color is blue") c.Assert(tr.RelPermalink(), qt.Equals, fmt.Sprintf("/f%d.test.txt", id)) -- cgit v1.2.3