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
This commit is contained in:
Bjørn Erik Pedersen
2023-02-11 16:20:24 +01:00
parent 0afec0a9f4
commit 90da7664bf
66 changed files with 1363 additions and 829 deletions

View File

@@ -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 <p></p> on short snippets will be trimmed.
// markup (defaults to the Pages 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.

View File

@@ -14,6 +14,7 @@
package page
import (
"context"
"html/template"
"github.com/gohugoio/hugo/lazy"
@@ -57,7 +58,7 @@ func NewLazyContentProvider(f func() (OutputFormatContentProvider, error)) *Lazy
init: lazy.New(),
cp: NopCPageContentRenderer,
}
lcp.init.Add(func() (any, error) {
lcp.init.Add(func(context.Context) (any, error) {
cp, err := f()
if err != nil {
return nil, err
@@ -72,67 +73,67 @@ func (lcp *LazyContentProvider) Reset() {
lcp.init.Reset()
}
func (lcp *LazyContentProvider) Content() (any, error) {
lcp.init.Do()
return lcp.cp.Content()
func (lcp *LazyContentProvider) Content(ctx context.Context) (any, error) {
lcp.init.Do(ctx)
return lcp.cp.Content(ctx)
}
func (lcp *LazyContentProvider) Plain() string {
lcp.init.Do()
return lcp.cp.Plain()
func (lcp *LazyContentProvider) Plain(ctx context.Context) string {
lcp.init.Do(ctx)
return lcp.cp.Plain(ctx)
}
func (lcp *LazyContentProvider) PlainWords() []string {
lcp.init.Do()
return lcp.cp.PlainWords()
func (lcp *LazyContentProvider) PlainWords(ctx context.Context) []string {
lcp.init.Do(ctx)
return lcp.cp.PlainWords(ctx)
}
func (lcp *LazyContentProvider) Summary() template.HTML {
lcp.init.Do()
return lcp.cp.Summary()
func (lcp *LazyContentProvider) Summary(ctx context.Context) template.HTML {
lcp.init.Do(ctx)
return lcp.cp.Summary(ctx)
}
func (lcp *LazyContentProvider) Truncated() bool {
lcp.init.Do()
return lcp.cp.Truncated()
func (lcp *LazyContentProvider) Truncated(ctx context.Context) bool {
lcp.init.Do(ctx)
return lcp.cp.Truncated(ctx)
}
func (lcp *LazyContentProvider) FuzzyWordCount() int {
lcp.init.Do()
return lcp.cp.FuzzyWordCount()
func (lcp *LazyContentProvider) FuzzyWordCount(ctx context.Context) int {
lcp.init.Do(ctx)
return lcp.cp.FuzzyWordCount(ctx)
}
func (lcp *LazyContentProvider) WordCount() int {
lcp.init.Do()
return lcp.cp.WordCount()
func (lcp *LazyContentProvider) WordCount(ctx context.Context) int {
lcp.init.Do(ctx)
return lcp.cp.WordCount(ctx)
}
func (lcp *LazyContentProvider) ReadingTime() int {
lcp.init.Do()
return lcp.cp.ReadingTime()
func (lcp *LazyContentProvider) ReadingTime(ctx context.Context) int {
lcp.init.Do(ctx)
return lcp.cp.ReadingTime(ctx)
}
func (lcp *LazyContentProvider) Len() int {
lcp.init.Do()
return lcp.cp.Len()
func (lcp *LazyContentProvider) Len(ctx context.Context) int {
lcp.init.Do(ctx)
return lcp.cp.Len(ctx)
}
func (lcp *LazyContentProvider) Render(layout ...string) (template.HTML, error) {
lcp.init.Do()
return lcp.cp.Render(layout...)
func (lcp *LazyContentProvider) Render(ctx context.Context, layout ...string) (template.HTML, error) {
lcp.init.Do(context.TODO())
return lcp.cp.Render(ctx, layout...)
}
func (lcp *LazyContentProvider) RenderString(args ...any) (template.HTML, error) {
lcp.init.Do()
return lcp.cp.RenderString(args...)
func (lcp *LazyContentProvider) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
lcp.init.Do(ctx)
return lcp.cp.RenderString(ctx, args...)
}
func (lcp *LazyContentProvider) TableOfContents() template.HTML {
lcp.init.Do()
return lcp.cp.TableOfContents()
func (lcp *LazyContentProvider) TableOfContents(ctx context.Context) template.HTML {
lcp.init.Do(ctx)
return lcp.cp.TableOfContents(ctx)
}
func (lcp *LazyContentProvider) RenderContent(content []byte, renderTOC bool) (converter.Result, error) {
lcp.init.Do()
return lcp.cp.RenderContent(content, renderTOC)
func (lcp *LazyContentProvider) RenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.Result, error) {
lcp.init.Do(ctx)
return lcp.cp.RenderContent(ctx, content, renderTOC)
}

View File

@@ -25,24 +25,10 @@ import (
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/navigation"
"github.com/gohugoio/hugo/source"
"html/template"
"time"
)
func MarshalPageToJSON(p Page) ([]byte, error) {
content, err := p.Content()
if err != nil {
return nil, err
}
plain := p.Plain()
plainWords := p.PlainWords()
summary := p.Summary()
truncated := p.Truncated()
fuzzyWordCount := p.FuzzyWordCount()
wordCount := p.WordCount()
readingTime := p.ReadingTime()
length := p.Len()
tableOfContents := p.TableOfContents()
rawContent := p.RawContent()
resourceType := p.ResourceType()
mediaType := p.MediaType()
@@ -93,16 +79,6 @@ func MarshalPageToJSON(p Page) ([]byte, error) {
getIdentity := p.GetIdentity()
s := struct {
Content interface{}
Plain string
PlainWords []string
Summary template.HTML
Truncated bool
FuzzyWordCount int
WordCount int
ReadingTime int
Len int
TableOfContents template.HTML
RawContent string
ResourceType string
MediaType media.Type
@@ -152,16 +128,6 @@ func MarshalPageToJSON(p Page) ([]byte, error) {
Store *maps.Scratch
GetIdentity identity.Identity
}{
Content: content,
Plain: plain,
PlainWords: plainWords,
Summary: summary,
Truncated: truncated,
FuzzyWordCount: fuzzyWordCount,
WordCount: wordCount,
ReadingTime: readingTime,
Len: length,
TableOfContents: tableOfContents,
RawContent: rawContent,
ResourceType: resourceType,
MediaType: mediaType,

View File

@@ -17,11 +17,13 @@ package page
import (
"bytes"
"context"
"html/template"
"time"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/tpl"
@@ -105,7 +107,7 @@ func (p *nopPage) BundleType() files.ContentClass {
return ""
}
func (p *nopPage) Content() (any, error) {
func (p *nopPage) Content(context.Context) (any, error) {
return "", nil
}
@@ -179,7 +181,7 @@ func (p *nopPage) FirstSection() Page {
return nil
}
func (p *nopPage) FuzzyWordCount() int {
func (p *nopPage) FuzzyWordCount(context.Context) int {
return 0
}
@@ -279,7 +281,7 @@ func (p *nopPage) Lastmod() (t time.Time) {
return
}
func (p *nopPage) Len() int {
func (p *nopPage) Len(context.Context) int {
return 0
}
@@ -363,11 +365,11 @@ func (p *nopPage) Permalink() string {
return ""
}
func (p *nopPage) Plain() string {
func (p *nopPage) Plain(context.Context) string {
return ""
}
func (p *nopPage) PlainWords() []string {
func (p *nopPage) PlainWords(context.Context) []string {
return nil
}
@@ -399,7 +401,7 @@ func (p *nopPage) RawContent() string {
return ""
}
func (p *nopPage) ReadingTime() int {
func (p *nopPage) ReadingTime(context.Context) int {
return 0
}
@@ -415,11 +417,11 @@ func (p *nopPage) RelRef(argsm map[string]any) (string, error) {
return "", nil
}
func (p *nopPage) Render(layout ...string) (template.HTML, error) {
func (p *nopPage) Render(ctx context.Context, layout ...string) (template.HTML, error) {
return "", nil
}
func (p *nopPage) RenderString(args ...any) (template.HTML, error) {
func (p *nopPage) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
return "", nil
}
@@ -475,11 +477,11 @@ func (p *nopPage) String() string {
return "nopPage"
}
func (p *nopPage) Summary() template.HTML {
func (p *nopPage) Summary(context.Context) template.HTML {
return ""
}
func (p *nopPage) TableOfContents() template.HTML {
func (p *nopPage) TableOfContents(context.Context) template.HTML {
return ""
}
@@ -499,7 +501,7 @@ func (p *nopPage) Translations() Pages {
return nil
}
func (p *nopPage) Truncated() bool {
func (p *nopPage) Truncated(context.Context) bool {
return false
}
@@ -519,7 +521,7 @@ func (p *nopPage) Weight() int {
return 0
}
func (p *nopPage) WordCount() int {
func (p *nopPage) WordCount(context.Context) int {
return 0
}
@@ -527,9 +529,16 @@ func (p *nopPage) GetIdentity() identity.Identity {
return identity.NewPathIdentity("content", "foo/bar.md")
}
func (p *nopPage) Fragments(context.Context) *tableofcontents.Fragments {
return nil
}
func (p *nopPage) HeadingsFiltered(context.Context) tableofcontents.Headings {
return nil
}
type nopContentRenderer int
func (r *nopContentRenderer) RenderContent(content []byte, renderTOC bool) (converter.Result, error) {
func (r *nopContentRenderer) RenderContent(ctx context.Context, content []byte, renderTOC bool) (converter.Result, error) {
b := &bytes.Buffer{}
return b, nil
}

View File

@@ -14,6 +14,7 @@
package page
import (
"context"
"errors"
"fmt"
"reflect"
@@ -110,7 +111,7 @@ var (
// GroupBy groups by the value in the given field or method name and with the given order.
// Valid values for order is asc, desc, rev and reverse.
func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) {
func (p Pages) GroupBy(ctx context.Context, key string, order ...string) (PagesGroup, error) {
if len(p) < 1 {
return nil, nil
}
@@ -158,7 +159,12 @@ func (p Pages) GroupBy(key string, order ...string) (PagesGroup, error) {
case reflect.StructField:
fv = ppv.Elem().FieldByName(key)
case reflect.Method:
fv = hreflect.GetMethodByName(ppv, key).Call([]reflect.Value{})[0]
var args []reflect.Value
fn := hreflect.GetMethodByName(ppv, key)
if fn.Type().NumIn() > 0 && fn.Type().In(0).Implements(hreflect.ContextInterface) {
args = []reflect.Value{reflect.ValueOf(ctx)}
}
fv = fn.Call(args)[0]
}
if !fv.IsValid() {
continue

View File

@@ -14,6 +14,7 @@
package page
import (
"context"
"reflect"
"strings"
"testing"
@@ -68,7 +69,7 @@ func TestGroupByWithFieldNameArg(t *testing.T) {
{Key: 3, Pages: Pages{pages[0], pages[1]}},
}
groups, err := pages.GroupBy("Weight")
groups, err := pages.GroupBy(context.Background(), "Weight")
if err != nil {
t.Fatalf("Unable to make PagesGroup array: %s", err)
}
@@ -85,7 +86,7 @@ func TestGroupByWithMethodNameArg(t *testing.T) {
{Key: "section2", Pages: Pages{pages[3], pages[4]}},
}
groups, err := pages.GroupBy("Type")
groups, err := pages.GroupBy(context.Background(), "Type")
if err != nil {
t.Fatalf("Unable to make PagesGroup array: %s", err)
}
@@ -102,7 +103,7 @@ func TestGroupByWithSectionArg(t *testing.T) {
{Key: "section2", Pages: Pages{pages[3], pages[4]}},
}
groups, err := pages.GroupBy("Section")
groups, err := pages.GroupBy(context.Background(), "Section")
if err != nil {
t.Fatalf("Unable to make PagesGroup array: %s", err)
}
@@ -120,7 +121,7 @@ func TestGroupByInReverseOrder(t *testing.T) {
{Key: 1, Pages: Pages{pages[3], pages[4]}},
}
groups, err := pages.GroupBy("Weight", "desc")
groups, err := pages.GroupBy(context.Background(), "Weight", "desc")
if err != nil {
t.Fatalf("Unable to make PagesGroup array: %s", err)
}
@@ -132,7 +133,7 @@ func TestGroupByInReverseOrder(t *testing.T) {
func TestGroupByCalledWithEmptyPages(t *testing.T) {
t.Parallel()
var pages Pages
groups, err := pages.GroupBy("Weight")
groups, err := pages.GroupBy(context.Background(), "Weight")
if err != nil {
t.Fatalf("Unable to make PagesGroup array: %s", err)
}
@@ -154,12 +155,12 @@ func TestReverse(t *testing.T) {
t.Parallel()
pages := preparePageGroupTestPages(t)
groups1, err := pages.GroupBy("Weight", "desc")
groups1, err := pages.GroupBy(context.Background(), "Weight", "desc")
if err != nil {
t.Fatalf("Unable to make PagesGroup array: %s", err)
}
groups2, err := pages.GroupBy("Weight")
groups2, err := pages.GroupBy(context.Background(), "Weight")
if err != nil {
t.Fatalf("Unable to make PagesGroup array: %s", err)
}

View File

@@ -132,21 +132,6 @@ func (pages Pages) ProbablyEq(other any) bool {
return true
}
func (ps Pages) removeFirstIfFound(p Page) Pages {
ii := -1
for i, pp := range ps {
if p.Eq(pp) {
ii = i
break
}
}
if ii != -1 {
ps = append(ps[:ii], ps[ii+1:]...)
}
return ps
}
// PagesFactory somehow creates some Pages.
// We do a lot of lazy Pages initialization in Hugo, so we need a type.
type PagesFactory func() Pages

View File

@@ -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)
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)
}
result, err := p.search(ctx, opts)
if err != nil {
return nil, err
}
if page, ok := doc.(Page); ok {
return result.removeFirstIfFound(page), nil
}
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,
}
return p.search(ctx, opts)
}
func (p Pages) search(args ...types.KeyValues) (Pages, error) {
return p.withInvertedIndex(func(idx *related.InvertedIndex) ([]related.Document, error) {
return idx.SearchKeyValues(args...)
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) 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) 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
}
}

View File

@@ -14,6 +14,7 @@
package page
import (
"context"
"testing"
"time"
@@ -51,26 +52,42 @@ func TestRelated(t *testing.T) {
},
}
result, err := pages.RelatedTo(types.NewKeyValuesStrings("keywords", "hugo", "rocks"))
ctx := context.Background()
opts := map[string]any{
"namedSlices": types.NewKeyValuesStrings("keywords", "hugo", "rocks"),
}
result, err := pages.Related(ctx, opts)
c.Assert(err, qt.IsNil)
c.Assert(len(result), qt.Equals, 2)
c.Assert(result[0].Title(), qt.Equals, "Page 2")
c.Assert(result[1].Title(), qt.Equals, "Page 1")
result, err = pages.Related(pages[0])
result, err = pages.Related(ctx, pages[0])
c.Assert(err, qt.IsNil)
c.Assert(len(result), qt.Equals, 2)
c.Assert(result[0].Title(), qt.Equals, "Page 2")
c.Assert(result[1].Title(), qt.Equals, "Page 3")
result, err = pages.RelatedIndices(pages[0], "keywords")
opts = map[string]any{
"document": pages[0],
"indices": []string{"keywords"},
}
result, err = pages.Related(ctx, opts)
c.Assert(err, qt.IsNil)
c.Assert(len(result), qt.Equals, 2)
c.Assert(result[0].Title(), qt.Equals, "Page 2")
c.Assert(result[1].Title(), qt.Equals, "Page 3")
result, err = pages.RelatedTo(types.NewKeyValuesStrings("keywords", "bep", "rocks"))
opts = map[string]any{
"namedSlices": []types.KeyValues{
{
Key: "keywords",
Values: []any{"bep", "rocks"},
},
},
}
result, err = pages.Related(context.Background(), opts)
c.Assert(err, qt.IsNil)
c.Assert(len(result), qt.Equals, 2)
c.Assert(result[0].Title(), qt.Equals, "Page 2")

View File

@@ -14,6 +14,7 @@
package page
import (
"context"
"sort"
"github.com/gohugoio/hugo/common/collections"
@@ -299,7 +300,7 @@ func (p Pages) ByLastmod() Pages {
// Adjacent invocations on the same receiver will return a cached result.
//
// This may safely be executed in parallel.
func (p Pages) ByLength() Pages {
func (p Pages) ByLength(ctx context.Context) Pages {
const key = "pageSort.ByLength"
length := func(p1, p2 Page) bool {
@@ -314,7 +315,7 @@ func (p Pages) ByLength() Pages {
return false
}
return p1l.Len() < p2l.Len()
return p1l.Len(ctx) < p2l.Len(ctx)
}
pages, _ := spc.get(key, pageBy(length).Sort, p)

View File

@@ -14,6 +14,7 @@
package page
import (
"context"
"fmt"
"testing"
"time"
@@ -104,6 +105,12 @@ func TestSortByN(t *testing.T) {
d4 := d1.Add(-20 * time.Hour)
p := createSortTestPages(4)
ctx := context.Background()
byLen := func(p Pages) Pages {
return p.ByLength(ctx)
}
for i, this := range []struct {
sortFunc func(p Pages) Pages
@@ -116,7 +123,7 @@ func TestSortByN(t *testing.T) {
{(Pages).ByPublishDate, func(p Pages) bool { return p[0].PublishDate() == d4 }},
{(Pages).ByExpiryDate, func(p Pages) bool { return p[0].ExpiryDate() == d4 }},
{(Pages).ByLastmod, func(p Pages) bool { return p[1].Lastmod() == d3 }},
{(Pages).ByLength, func(p Pages) bool { return p[0].(resource.LengthProvider).Len() == len(p[0].(*testPage).content) }},
{byLen, func(p Pages) bool { return p[0].(resource.LengthProvider).Len(ctx) == len(p[0].(*testPage).content) }},
} {
setSortVals([4]time.Time{d1, d2, d3, d4}, [4]string{"b", "ab", "cde", "fg"}, [4]int{0, 3, 2, 1}, p)

View File

@@ -14,6 +14,7 @@
package page
import (
"context"
"fmt"
"html/template"
"testing"
@@ -43,7 +44,7 @@ func TestSplitPageGroups(t *testing.T) {
t.Parallel()
c := qt.New(t)
pages := createTestPages(21)
groups, _ := pages.GroupBy("Weight", "desc")
groups, _ := pages.GroupBy(context.Background(), "Weight", "desc")
chunks := splitPageGroups(groups, 5)
c.Assert(len(chunks), qt.Equals, 5)
@@ -56,7 +57,7 @@ func TestSplitPageGroups(t *testing.T) {
// first group 10 in weight
c.Assert(pg.Key, qt.Equals, 10)
for _, p := range pg.Pages {
c.Assert(p.FuzzyWordCount()%2 == 0, qt.Equals, true) // magic test
c.Assert(p.FuzzyWordCount(context.Background())%2 == 0, qt.Equals, true) // magic test
}
}
} else {
@@ -71,7 +72,7 @@ func TestSplitPageGroups(t *testing.T) {
// last should have 5 in weight
c.Assert(pg.Key, qt.Equals, 5)
for _, p := range pg.Pages {
c.Assert(p.FuzzyWordCount()%2 != 0, qt.Equals, true) // magic test
c.Assert(p.FuzzyWordCount(context.Background())%2 != 0, qt.Equals, true) // magic test
}
}
} else {
@@ -83,7 +84,7 @@ func TestPager(t *testing.T) {
t.Parallel()
c := qt.New(t)
pages := createTestPages(21)
groups, _ := pages.GroupBy("Weight", "desc")
groups, _ := pages.GroupBy(context.Background(), "Weight", "desc")
urlFactory := func(page int) string {
return fmt.Sprintf("page/%d/", page)
@@ -149,7 +150,7 @@ func TestPagerNoPages(t *testing.T) {
t.Parallel()
c := qt.New(t)
pages := createTestPages(0)
groups, _ := pages.GroupBy("Weight", "desc")
groups, _ := pages.GroupBy(context.Background(), "Weight", "desc")
urlFactory := func(page int) string {
return fmt.Sprintf("page/%d/", page)
@@ -249,9 +250,9 @@ func TestProbablyEqualPageLists(t *testing.T) {
t.Parallel()
fivePages := createTestPages(5)
zeroPages := createTestPages(0)
zeroPagesByWeight, _ := createTestPages(0).GroupBy("Weight", "asc")
fivePagesByWeight, _ := createTestPages(5).GroupBy("Weight", "asc")
ninePagesByWeight, _ := createTestPages(9).GroupBy("Weight", "asc")
zeroPagesByWeight, _ := createTestPages(0).GroupBy(context.Background(), "Weight", "asc")
fivePagesByWeight, _ := createTestPages(5).GroupBy(context.Background(), "Weight", "asc")
ninePagesByWeight, _ := createTestPages(9).GroupBy(context.Background(), "Weight", "asc")
for i, this := range []struct {
v1 any
@@ -287,7 +288,7 @@ func TestPaginationPage(t *testing.T) {
}
fivePages := createTestPages(7)
fivePagesFuzzyWordCount, _ := createTestPages(7).GroupBy("FuzzyWordCount", "asc")
fivePagesFuzzyWordCount, _ := createTestPages(7).GroupBy(context.Background(), "FuzzyWordCount", "asc")
p1, _ := newPaginatorFromPages(fivePages, 2, urlFactory)
p2, _ := newPaginatorFromPageGroups(fivePagesFuzzyWordCount, 2, urlFactory)
@@ -301,10 +302,10 @@ func TestPaginationPage(t *testing.T) {
page21, _ := f2.page(1)
page2Nil, _ := f2.page(3)
c.Assert(page11.FuzzyWordCount(), qt.Equals, 3)
c.Assert(page11.FuzzyWordCount(context.Background()), qt.Equals, 3)
c.Assert(page1Nil, qt.IsNil)
c.Assert(page21, qt.Not(qt.IsNil))
c.Assert(page21.FuzzyWordCount(), qt.Equals, 3)
c.Assert(page21.FuzzyWordCount(context.Background()), qt.Equals, 3)
c.Assert(page2Nil, qt.IsNil)
}

View File

@@ -14,6 +14,7 @@
package page
import (
"context"
"fmt"
"html/template"
"path"
@@ -22,6 +23,7 @@ import (
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/modules"
@@ -153,7 +155,7 @@ func (p *testPage) BundleType() files.ContentClass {
panic("not implemented")
}
func (p *testPage) Content() (any, error) {
func (p *testPage) Content(context.Context) (any, error) {
panic("not implemented")
}
@@ -225,7 +227,7 @@ func (p *testPage) FirstSection() Page {
panic("not implemented")
}
func (p *testPage) FuzzyWordCount() int {
func (p *testPage) FuzzyWordCount(context.Context) int {
return p.fuzzyWordCount
}
@@ -329,11 +331,19 @@ func (p *testPage) LanguagePrefix() string {
return ""
}
func (p *testPage) Fragments(context.Context) *tableofcontents.Fragments {
return nil
}
func (p *testPage) HeadingsFiltered(context.Context) tableofcontents.Headings {
return nil
}
func (p *testPage) Lastmod() time.Time {
return p.lastMod
}
func (p *testPage) Len() int {
func (p *testPage) Len(context.Context) int {
return len(p.content)
}
@@ -431,11 +441,11 @@ func (p *testPage) Permalink() string {
panic("not implemented")
}
func (p *testPage) Plain() string {
func (p *testPage) Plain(context.Context) string {
panic("not implemented")
}
func (p *testPage) PlainWords() []string {
func (p *testPage) PlainWords(context.Context) []string {
panic("not implemented")
}
@@ -463,7 +473,7 @@ func (p *testPage) RawContent() string {
panic("not implemented")
}
func (p *testPage) ReadingTime() int {
func (p *testPage) ReadingTime(context.Context) int {
panic("not implemented")
}
@@ -487,11 +497,11 @@ func (p *testPage) RelRefFrom(argsm map[string]any, source any) (string, error)
return "", nil
}
func (p *testPage) Render(layout ...string) (template.HTML, error) {
func (p *testPage) Render(ctx context.Context, layout ...string) (template.HTML, error) {
panic("not implemented")
}
func (p *testPage) RenderString(args ...any) (template.HTML, error) {
func (p *testPage) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
panic("not implemented")
}
@@ -552,11 +562,11 @@ func (p *testPage) String() string {
return p.path
}
func (p *testPage) Summary() template.HTML {
func (p *testPage) Summary(context.Context) template.HTML {
panic("not implemented")
}
func (p *testPage) TableOfContents() template.HTML {
func (p *testPage) TableOfContents(context.Context) template.HTML {
panic("not implemented")
}
@@ -576,7 +586,7 @@ func (p *testPage) Translations() Pages {
panic("not implemented")
}
func (p *testPage) Truncated() bool {
func (p *testPage) Truncated(context.Context) bool {
panic("not implemented")
}
@@ -596,7 +606,7 @@ func (p *testPage) Weight() int {
return p.weight
}
func (p *testPage) WordCount() int {
func (p *testPage) WordCount(context.Context) int {
panic("not implemented")
}