mirror of
https://github.com/gohugoio/hugo.git
synced 2024-12-01 19:23:07 -05:00
90da7664bf
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
181 lines
5 KiB
Go
181 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()
|
|
}
|