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

@@ -15,6 +15,7 @@ package hugolib
import (
"bytes"
"context"
"fmt"
"html/template"
"path"
@@ -302,13 +303,44 @@ const (
innerCleanupExpand = "$1"
)
func renderShortcode(
func prepareShortcode(
ctx context.Context,
level int,
s *Site,
tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
p *pageState) (string, bool, error) {
p *pageState) (shortcodeRenderer, error) {
toParseErr := func(err error) error {
return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), p.source.parsed.Input(), sc.pos)
}
// Allow the caller to delay the rendering of the shortcode if needed.
var fn shortcodeRenderFunc = func(ctx context.Context) ([]byte, bool, error) {
r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p)
if err != nil {
return nil, false, toParseErr(err)
}
b, hasVariants, err := r.renderShortcode(ctx)
if err != nil {
return nil, false, toParseErr(err)
}
return b, hasVariants, nil
}
return fn, nil
}
func doRenderShortcode(
ctx context.Context,
level int,
s *Site,
tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
p *pageState) (shortcodeRenderer, error) {
var tmpl tpl.Template
// Tracks whether this shortcode or any of its children has template variations
@@ -319,7 +351,7 @@ func renderShortcode(
if sc.isInline {
if !p.s.ExecHelper.Sec().EnableInlineShortcodes {
return "", false, nil
return zeroShortcode, nil
}
templName := path.Join("_inline_shortcode", p.File().Path(), sc.name)
if sc.isClosing {
@@ -332,7 +364,7 @@ func renderShortcode(
pos := fe.Position()
pos.LineNumber += p.posOffset(sc.pos).LineNumber
fe = fe.UpdatePosition(pos)
return "", false, p.wrapError(fe)
return zeroShortcode, p.wrapError(fe)
}
} else {
@@ -340,7 +372,7 @@ func renderShortcode(
var found bool
tmpl, found = s.TextTmpl().Lookup(templName)
if !found {
return "", false, fmt.Errorf("no earlier definition of shortcode %q found", sc.name)
return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name)
}
}
} else {
@@ -348,7 +380,7 @@ func renderShortcode(
tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants)
if !found {
s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
return "", false, nil
return zeroShortcode, nil
}
hasVariants = hasVariants || more
}
@@ -365,16 +397,20 @@ func renderShortcode(
case string:
inner += innerData
case *shortcode:
s, more, err := renderShortcode(level+1, s, tplVariants, innerData, data, p)
s, err := prepareShortcode(ctx, level+1, s, tplVariants, innerData, data, p)
if err != nil {
return "", false, err
return zeroShortcode, err
}
ss, more, err := s.renderShortcodeString(ctx)
hasVariants = hasVariants || more
inner += s
if err != nil {
return zeroShortcode, err
}
inner += ss
default:
s.Log.Errorf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ",
sc.name, p.File().Path(), reflect.TypeOf(innerData))
return "", false, nil
return zeroShortcode, nil
}
}
@@ -382,9 +418,9 @@ func renderShortcode(
// shortcode.
if sc.doMarkup && (level > 0 || sc.configVersion() == 1) {
var err error
b, err := p.pageOutput.contentRenderer.RenderContent([]byte(inner), false)
b, err := p.pageOutput.contentRenderer.RenderContent(ctx, []byte(inner), false)
if err != nil {
return "", false, err
return zeroShortcode, err
}
newInner := b.Bytes()
@@ -418,14 +454,14 @@ func renderShortcode(
}
result, err := renderShortcodeWithPage(s.Tmpl(), tmpl, data)
result, err := renderShortcodeWithPage(ctx, s.Tmpl(), tmpl, data)
if err != nil && sc.isInline {
fe := herrors.NewFileErrorFromName(err, p.File().Filename())
pos := fe.Position()
pos.LineNumber += p.posOffset(sc.pos).LineNumber
fe = fe.UpdatePosition(pos)
return "", false, fe
return zeroShortcode, fe
}
if len(sc.inner) == 0 && len(sc.indentation) > 0 {
@@ -444,7 +480,7 @@ func renderShortcode(
bp.PutBuffer(b)
}
return result, hasVariants, err
return prerenderedShortcode{s: result, hasVariants: hasVariants}, err
}
func (s *shortcodeHandler) hasShortcodes() bool {
@@ -473,28 +509,24 @@ func (s *shortcodeHandler) hasName(name string) bool {
return ok
}
func (s *shortcodeHandler) renderShortcodesForPage(p *pageState, f output.Format) (map[string]string, bool, error) {
rendered := make(map[string]string)
func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, p *pageState, f output.Format) (map[string]shortcodeRenderer, error) {
rendered := make(map[string]shortcodeRenderer)
tplVariants := tpl.TemplateVariants{
Language: p.Language().Lang,
OutputFormat: f,
}
var hasVariants bool
for _, v := range s.shortcodes {
s, more, err := renderShortcode(0, s.s, tplVariants, v, nil, p)
s, err := prepareShortcode(ctx, 0, s.s, tplVariants, v, nil, p)
if err != nil {
err = p.parseError(fmt.Errorf("failed to render shortcode %q: %w", v.name, err), p.source.parsed.Input(), v.pos)
return nil, false, err
return nil, err
}
hasVariants = hasVariants || more
rendered[v.placeholder] = s
}
return rendered, hasVariants, nil
return rendered, nil
}
func (s *shortcodeHandler) parseError(err error, input []byte, pos int) error {
@@ -668,11 +700,11 @@ Loop:
// Replace prefixed shortcode tokens with the real content.
// Note: This function will rewrite the input slice.
func replaceShortcodeTokens(source []byte, replacements map[string]string) ([]byte, error) {
if len(replacements) == 0 {
return source, nil
}
func expandShortcodeTokens(
ctx context.Context,
source []byte,
tokenHandler func(ctx context.Context, token string) ([]byte, error),
) ([]byte, error) {
start := 0
pre := []byte(shortcodePlaceholderPrefix)
@@ -691,8 +723,11 @@ func replaceShortcodeTokens(source []byte, replacements map[string]string) ([]by
}
end := j + postIdx + 4
newVal := []byte(replacements[string(source[j:end])])
key := string(source[j:end])
newVal, err := tokenHandler(ctx, key)
if err != nil {
return nil, err
}
// Issue #1148: Check for wrapping p-tags <p>
if j >= 3 && bytes.Equal(source[j-3:j], pStart) {
@@ -712,11 +747,11 @@ func replaceShortcodeTokens(source []byte, replacements map[string]string) ([]by
return source, nil
}
func renderShortcodeWithPage(h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
func renderShortcodeWithPage(ctx context.Context, h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
buffer := bp.GetBuffer()
defer bp.PutBuffer(buffer)
err := h.Execute(tmpl, buffer, data)
err := h.ExecuteWithContext(ctx, tmpl, buffer, data)
if err != nil {
return "", fmt.Errorf("failed to process shortcode: %w", err)
}