hugo/hugolib/content_factory.go
Bjørn Erik Pedersen 90da7664bf 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
2023-02-21 17:56:41 +01:00

182 lines
5 KiB
Go

// Copyright 2021 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"context"
"fmt"
"io"
"path/filepath"
"strings"
"time"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/source"
"github.com/gohugoio/hugo/resources/page"
"github.com/spf13/afero"
)
// ContentFactory creates content files from archetype templates.
type ContentFactory struct {
h *HugoSites
// We parse the archetype templates as Go templates, so we need
// to replace any shortcode with a temporary placeholder.
shortcodeReplacerPre *strings.Replacer
shortcodeReplacerPost *strings.Replacer
}
// ApplyArchetypeFilename archetypeFilename to w as a template using the given Page p as the foundation for the data context.
func (f ContentFactory) ApplyArchetypeFilename(w io.Writer, p page.Page, archetypeKind, archetypeFilename string) error {
fi, err := f.h.SourceFilesystems.Archetypes.Fs.Stat(archetypeFilename)
if err != nil {
return err
}
if fi.IsDir() {
return fmt.Errorf("archetype directory (%q) not supported", archetypeFilename)
}
templateSource, err := afero.ReadFile(f.h.SourceFilesystems.Archetypes.Fs, archetypeFilename)
if err != nil {
return fmt.Errorf("failed to read archetype file %q: %s: %w", archetypeFilename, err, err)
}
return f.ApplyArchetypeTemplate(w, p, archetypeKind, string(templateSource))
}
// ApplyArchetypeTemplate templateSource to w as a template using the given Page p as the foundation for the data context.
func (f ContentFactory) ApplyArchetypeTemplate(w io.Writer, p page.Page, archetypeKind, templateSource string) error {
ps := p.(*pageState)
if archetypeKind == "" {
archetypeKind = p.Type()
}
d := &archetypeFileData{
Type: archetypeKind,
Date: htime.Now().Format(time.RFC3339),
Page: p,
File: p.File(),
}
templateSource = f.shortcodeReplacerPre.Replace(templateSource)
templ, err := ps.s.TextTmpl().Parse("archetype.md", string(templateSource))
if err != nil {
return fmt.Errorf("failed to parse archetype template: %s: %w", err, err)
}
result, err := executeToString(context.TODO(), ps.s.Tmpl(), templ, d)
if err != nil {
return fmt.Errorf("failed to execute archetype template: %s: %w", err, err)
}
_, err = io.WriteString(w, f.shortcodeReplacerPost.Replace(result))
return err
}
func (f ContentFactory) SectionFromFilename(filename string) (string, error) {
filename = filepath.Clean(filename)
rel, _, err := f.h.AbsProjectContentDir(filename)
if err != nil {
return "", err
}
parts := strings.Split(helpers.ToSlashTrimLeading(rel), "/")
if len(parts) < 2 {
return "", nil
}
return parts[0], nil
}
// CreateContentPlaceHolder creates a content placeholder file inside the
// best matching content directory.
func (f ContentFactory) CreateContentPlaceHolder(filename string, force bool) (string, error) {
filename = filepath.Clean(filename)
_, abs, err := f.h.AbsProjectContentDir(filename)
if err != nil {
return "", err
}
// This will be overwritten later, just write a placeholder to get
// the paths correct.
placeholder := `---
title: "Content Placeholder"
_build:
render: never
list: never
publishResources: false
---
`
if force {
return abs, afero.WriteReader(f.h.Fs.Source, abs, strings.NewReader(placeholder))
}
return abs, afero.SafeWriteReader(f.h.Fs.Source, abs, strings.NewReader(placeholder))
}
// NewContentFactory creates a new ContentFactory for h.
func NewContentFactory(h *HugoSites) ContentFactory {
return ContentFactory{
h: h,
shortcodeReplacerPre: strings.NewReplacer(
"{{<", "{x{<",
"{{%", "{x{%",
">}}", ">}x}",
"%}}", "%}x}"),
shortcodeReplacerPost: strings.NewReplacer(
"{x{<", "{{<",
"{x{%", "{{%",
">}x}", ">}}",
"%}x}", "%}}"),
}
}
// archetypeFileData represents the data available to an archetype template.
type archetypeFileData struct {
// The archetype content type, either given as --kind option or extracted
// from the target path's section, i.e. "blog/mypost.md" will resolve to
// "blog".
Type string
// The current date and time as a RFC3339 formatted string, suitable for use in front matter.
Date string
// The temporary page. Note that only the file path information is relevant at this stage.
Page page.Page
// File is the same as Page.File, embedded here for historic reasons.
// TODO(bep) make this a method.
source.File
}
func (f *archetypeFileData) Site() page.Site {
return f.Page.Site()
}
func (f *archetypeFileData) Name() string {
return f.Page.File().ContentBaseName()
}