mirror of
https://github.com/gohugoio/hugo.git
synced 2025-08-29 22:29:56 +02:00
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:
@@ -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 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.
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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")
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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")
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user