mirror of https://github.com/gohugoio/hugo
1989 lines
48 KiB
Go
1989 lines
48 KiB
Go
// Copyright 2025 The Hugo Authors. All rights reserved.
|
|
//
|
|
// Portions Copyright The Go Authors.
|
|
|
|
// 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 tplimpl
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"embed"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"iter"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gohugoio/hugo/common/herrors"
|
|
"github.com/gohugoio/hugo/common/loggers"
|
|
"github.com/gohugoio/hugo/common/maps"
|
|
"github.com/gohugoio/hugo/common/paths"
|
|
"github.com/gohugoio/hugo/helpers"
|
|
"github.com/gohugoio/hugo/hugofs"
|
|
"github.com/gohugoio/hugo/hugofs/files"
|
|
"github.com/gohugoio/hugo/hugolib/doctree"
|
|
"github.com/gohugoio/hugo/identity"
|
|
"github.com/gohugoio/hugo/media"
|
|
"github.com/gohugoio/hugo/metrics"
|
|
"github.com/gohugoio/hugo/output"
|
|
"github.com/gohugoio/hugo/resources/kinds"
|
|
"github.com/gohugoio/hugo/resources/page"
|
|
"github.com/gohugoio/hugo/tpl"
|
|
htmltemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/htmltemplate"
|
|
texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
|
|
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
const (
|
|
CategoryLayout Category = iota + 1
|
|
CategoryBaseof
|
|
CategoryMarkup
|
|
CategoryShortcode
|
|
CategoryPartial
|
|
// Internal categories
|
|
CategoryServer
|
|
CategoryHugo
|
|
)
|
|
|
|
const (
|
|
SubCategoryMain SubCategory = iota
|
|
SubCategoryEmbedded // Internal Hugo templates
|
|
SubCategoryInline // Inline partials
|
|
)
|
|
|
|
const (
|
|
containerMarkup = "_markup"
|
|
containerShortcodes = "_shortcodes"
|
|
shortcodesPathIdentifier = "/_shortcodes/"
|
|
containerPartials = "_partials"
|
|
)
|
|
|
|
const (
|
|
layoutAll = "all"
|
|
layoutList = "list"
|
|
layoutSingle = "single"
|
|
)
|
|
|
|
var (
|
|
_ identity.IdentityProvider = (*TemplInfo)(nil)
|
|
_ identity.IsProbablyDependentProvider = (*TemplInfo)(nil)
|
|
_ identity.IsProbablyDependencyProvider = (*TemplInfo)(nil)
|
|
)
|
|
|
|
const (
|
|
processingStateInitial processingState = iota
|
|
processingStateTransformed
|
|
)
|
|
|
|
// The identifiers may be truncated in the log, e.g.
|
|
// "executing "main" at <$scaled.SRelPermalin...>: can't evaluate field SRelPermalink in type *resource.Image"
|
|
// We need this to identify position in templates with base templates applied.
|
|
var identifiersRe = regexp.MustCompile(`at \<(.*?)(\.{3})?\>:`)
|
|
|
|
var weightNoMatch = weight{w1: -1}
|
|
|
|
//
|
|
//go:embed all:embedded/templates/*
|
|
var embeddedTemplatesFs embed.FS
|
|
|
|
func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
|
|
html, ok := opts.OutputFormats.GetByName("html")
|
|
if !ok {
|
|
panic("HTML output format not found")
|
|
}
|
|
s := &TemplateStore{
|
|
opts: opts,
|
|
siteOpts: siteOpts,
|
|
optsOrig: opts,
|
|
siteOptsOrig: siteOpts,
|
|
htmlFormat: html,
|
|
storeSite: configureSiteStorage(siteOpts, opts.Watching),
|
|
treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
|
|
treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
|
|
templatesByPath: maps.NewCache[string, *TemplInfo](),
|
|
shortcodesByName: maps.NewCache[string, *TemplInfo](),
|
|
cacheLookupPartials: maps.NewCache[string, *TemplInfo](),
|
|
|
|
// Note that the funcs passed below is just for name validation.
|
|
tns: newTemplateNamespace(siteOpts.TemplateFuncs),
|
|
|
|
dh: descriptorHandler{
|
|
opts: opts,
|
|
},
|
|
}
|
|
|
|
if err := s.init(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.insertTemplates(nil, false); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.insertEmbedded(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.parseTemplates(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.extractInlinePartials(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.transformTemplates(); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.tns.createPrototypes(true); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.prepareTemplates(); err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
//go:generate stringer -type Category
|
|
|
|
type Category int
|
|
|
|
type SiteOptions struct {
|
|
Site page.Site
|
|
TemplateFuncs map[string]any
|
|
}
|
|
|
|
type StoreOptions struct {
|
|
// The filesystem to use.
|
|
Fs afero.Fs
|
|
|
|
// The logger to use.
|
|
Log loggers.Logger
|
|
|
|
// The path parser to use.
|
|
PathParser *paths.PathParser
|
|
|
|
// Set when --enableTemplateMetrics is set.
|
|
Metrics metrics.Provider
|
|
|
|
// All configured output formats.
|
|
OutputFormats output.Formats
|
|
|
|
// All configured media types.
|
|
MediaTypes media.Types
|
|
|
|
// The default content language.
|
|
DefaultContentLanguage string
|
|
|
|
// The default output format.
|
|
DefaultOutputFormat string
|
|
|
|
// Taxonomy config.
|
|
TaxonomySingularPlural map[string]string
|
|
|
|
// Whether we are in watch or server mode.
|
|
Watching bool
|
|
|
|
// compiled.
|
|
legacyMappingTaxonomy map[string]legacyOrdinalMapping
|
|
legacyMappingTerm map[string]legacyOrdinalMapping
|
|
legacyMappingSection map[string]legacyOrdinalMapping
|
|
}
|
|
|
|
//go:generate stringer -type SubCategory
|
|
|
|
type SubCategory int
|
|
|
|
type TemplInfo struct {
|
|
// The category of this template.
|
|
category Category
|
|
|
|
subCategory SubCategory
|
|
|
|
// PathInfo info.
|
|
PathInfo *paths.Path
|
|
|
|
// Set when backed by a file.
|
|
Fi hugofs.FileMetaInfo
|
|
|
|
// The template content with any leading BOM removed.
|
|
content string
|
|
|
|
// The parsed template.
|
|
// Note that any baseof template will be applied later.
|
|
Template tpl.Template
|
|
|
|
// If no baseof is needed, this will be set to true.
|
|
// E.g. shortcode templates do not need a baseof.
|
|
noBaseOf bool
|
|
|
|
// If NoBaseOf is false, we will look for the final template in this tree.
|
|
baseVariants *doctree.SimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]
|
|
|
|
// The template variants that are based on this template.
|
|
overlays []*TemplInfo
|
|
|
|
// The base template used, if any.
|
|
base *TemplInfo
|
|
|
|
// The descriptior that this template represents.
|
|
D TemplateDescriptor
|
|
|
|
// Parser state.
|
|
ParseInfo ParseInfo
|
|
|
|
// The execution counter for this template.
|
|
executionCounter atomic.Uint64
|
|
|
|
// processing state.
|
|
state processingState
|
|
isLegacyMapped bool
|
|
}
|
|
|
|
func (ti *TemplInfo) SubCategory() SubCategory {
|
|
return ti.subCategory
|
|
}
|
|
|
|
func (ti *TemplInfo) BaseVariantsSeq() iter.Seq[*TemplWithBaseApplied] {
|
|
return func(yield func(*TemplWithBaseApplied) bool) {
|
|
ti.baseVariants.Walk(func(key string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
|
|
for _, vv := range v {
|
|
if !yield(vv) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
})
|
|
}
|
|
}
|
|
|
|
func (t *TemplInfo) IdentifierBase() string {
|
|
if t.PathInfo == nil {
|
|
return t.Name()
|
|
}
|
|
return t.PathInfo.IdentifierBase()
|
|
}
|
|
|
|
func (t *TemplInfo) GetIdentity() identity.Identity {
|
|
return t
|
|
}
|
|
|
|
func (ti *TemplInfo) Name() string {
|
|
if ti.Template == nil {
|
|
if ti.PathInfo != nil {
|
|
return ti.PathInfo.PathNoLeadingSlash()
|
|
}
|
|
}
|
|
return ti.Template.Name()
|
|
}
|
|
|
|
func (ti *TemplInfo) Prepare() (*texttemplate.Template, error) {
|
|
return ti.Template.Prepare()
|
|
}
|
|
|
|
func (t *TemplInfo) IsProbablyDependency(other identity.Identity) bool {
|
|
return t.isProbablyTheSameIDAs(other)
|
|
}
|
|
|
|
func (t *TemplInfo) IsProbablyDependent(other identity.Identity) bool {
|
|
for _, overlay := range t.overlays {
|
|
if overlay.isProbablyTheSameIDAs(other) {
|
|
return true
|
|
}
|
|
}
|
|
return t.isProbablyTheSameIDAs(other)
|
|
}
|
|
|
|
func (ti *TemplInfo) String() string {
|
|
if ti == nil {
|
|
return "<nil>"
|
|
}
|
|
return ti.PathInfo.String()
|
|
}
|
|
|
|
func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, d1 TemplateDescriptor, k1 string, slashCountK1 int, best *bestMatch) {
|
|
if ti.baseVariants == nil {
|
|
return
|
|
}
|
|
|
|
ti.baseVariants.WalkPath(k1, func(k2 string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
|
|
slashCountK2 := strings.Count(k2, "/")
|
|
distance := slashCountK1 - slashCountK2
|
|
|
|
for d2, vv := range v {
|
|
weight := s.dh.compareDescriptors(CategoryBaseof, false, d1, d2)
|
|
weight.distance = distance
|
|
if best.isBetter(weight, vv.Template) {
|
|
best.updateValues(weight, k2, d2, vv.Template)
|
|
}
|
|
}
|
|
return false, nil
|
|
})
|
|
}
|
|
|
|
func (t *TemplInfo) isProbablyTheSameIDAs(other identity.Identity) bool {
|
|
if t.IdentifierBase() == other.IdentifierBase() {
|
|
return true
|
|
}
|
|
|
|
if t.Fi != nil && t.Fi.Meta().PathInfo != t.PathInfo {
|
|
return other.IdentifierBase() == t.Fi.Meta().PathInfo.IdentifierBase()
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Implements the additional methods in tpl.CurrentTemplateInfoOps.
|
|
func (ti *TemplInfo) Base() tpl.CurrentTemplateInfoCommonOps {
|
|
return ti.base
|
|
}
|
|
|
|
func (ti *TemplInfo) Filename() string {
|
|
if ti.Fi == nil {
|
|
return ""
|
|
}
|
|
return ti.Fi.Meta().Filename
|
|
}
|
|
|
|
type TemplWithBaseApplied struct {
|
|
// The template that's overlaid on top of the base template.
|
|
Overlay *TemplInfo
|
|
// The base template.
|
|
Base *TemplInfo
|
|
// This is the final template that can be used to render a page.
|
|
Template *TemplInfo
|
|
}
|
|
|
|
// TemplateQuery is used in LookupPagesLayout to find the best matching template.
|
|
type TemplateQuery struct {
|
|
// The path to walk down to.
|
|
Path string
|
|
|
|
// The name to look for. Used for shortcode queries.
|
|
Name string
|
|
|
|
// The category to look in.
|
|
Category Category
|
|
|
|
// The template descriptor to match against.
|
|
Desc TemplateDescriptor
|
|
|
|
// Whether to even consider this candidate.
|
|
Consider func(candidate *TemplInfo) bool
|
|
}
|
|
|
|
func (q *TemplateQuery) init() {
|
|
if q.Desc.Kind == kinds.KindTemporary {
|
|
q.Desc.Kind = ""
|
|
} else if kinds.GetKindMain(q.Desc.Kind) == "" {
|
|
q.Desc.Kind = ""
|
|
}
|
|
if q.Desc.LayoutFromTemplate == "" && q.Desc.Kind != "" {
|
|
if q.Desc.Kind == kinds.KindPage {
|
|
q.Desc.LayoutFromTemplate = layoutSingle
|
|
} else {
|
|
q.Desc.LayoutFromTemplate = layoutList
|
|
}
|
|
}
|
|
|
|
if q.Consider == nil {
|
|
q.Consider = func(match *TemplInfo) bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
q.Name = strings.ToLower(q.Name)
|
|
|
|
if q.Category == 0 {
|
|
panic("category not set")
|
|
}
|
|
}
|
|
|
|
type TemplateStore struct {
|
|
opts StoreOptions
|
|
siteOpts SiteOptions
|
|
htmlFormat output.Format
|
|
|
|
treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
|
|
treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
|
|
templatesByPath *maps.Cache[string, *TemplInfo]
|
|
shortcodesByName *maps.Cache[string, *TemplInfo]
|
|
|
|
dh descriptorHandler
|
|
|
|
// The template namespace.
|
|
tns *templateNamespace
|
|
|
|
// Site specific state.
|
|
// All above this is reused.
|
|
storeSite *storeSite
|
|
|
|
// For testing benchmarking.
|
|
optsOrig StoreOptions
|
|
siteOptsOrig SiteOptions
|
|
|
|
// caches. These need to be refreshed when the templates are refreshed.
|
|
cacheLookupPartials *maps.Cache[string, *TemplInfo]
|
|
}
|
|
|
|
// NewFromOpts creates a new store with the same configuration as the original.
|
|
// Used for testing/benchmarking.
|
|
func (s *TemplateStore) NewFromOpts() (*TemplateStore, error) {
|
|
return NewStore(s.optsOrig, s.siteOptsOrig)
|
|
}
|
|
|
|
// In the previous implementation of base templates in Hugo, we parsed and applied these base templates on
|
|
// request, e.g. in the middle of rendering. The idea was that we coulnd't know upfront which layoyt/base template
|
|
// combination that would be used.
|
|
// This, however, added a lot of complexity involving a careful dance of template cloning and parsing
|
|
// (Go HTML tenplates cannot be parsed after any of the templates in the tree have been executed).
|
|
// FindAllBaseTemplateCandidates finds all base template candidates for the given descriptor so we can apply them upfront.
|
|
// In this setup we may end up with unused base templates, but not having to do the cloning should more than make up for that.
|
|
func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc TemplateDescriptor) []keyTemplateInfo {
|
|
var result []keyTemplateInfo
|
|
descBaseof := desc
|
|
s.treeMain.Walk(func(k string, v map[nodeKey]*TemplInfo) (bool, error) {
|
|
for _, vv := range v {
|
|
if vv.category != CategoryBaseof {
|
|
continue
|
|
}
|
|
|
|
if vv.D.isKindInLayout(desc.LayoutFromTemplate) && s.dh.compareDescriptors(CategoryBaseof, false, descBaseof, vv.D).w1 > 0 {
|
|
result = append(result, keyTemplateInfo{Key: k, Info: vv})
|
|
}
|
|
}
|
|
return false, nil
|
|
})
|
|
|
|
return result
|
|
}
|
|
|
|
func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error {
|
|
defer func() {
|
|
ti.executionCounter.Add(1)
|
|
if ti.base != nil {
|
|
ti.base.executionCounter.Add(1)
|
|
}
|
|
}()
|
|
|
|
templ := ti.Template
|
|
|
|
currentTi := &tpl.CurrentTemplateInfo{
|
|
Parent: tpl.Context.CurrentTemplate.Get(ctx),
|
|
CurrentTemplateInfoOps: ti,
|
|
}
|
|
|
|
ctx = tpl.Context.CurrentTemplate.Set(ctx, currentTi)
|
|
|
|
if t.opts.Metrics != nil {
|
|
defer t.opts.Metrics.MeasureSince(templ.Name(), time.Now())
|
|
}
|
|
|
|
execErr := t.storeSite.executer.ExecuteWithContext(ctx, ti, wr, data)
|
|
if execErr != nil {
|
|
return t.addFileContext(ti, "execute of template failed", execErr)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (t *TemplateStore) GetFunc(name string) (reflect.Value, bool) {
|
|
v, found := t.storeSite.execHelper.funcs[name]
|
|
return v, found
|
|
}
|
|
|
|
func (s *TemplateStore) GetIdentity(p string) identity.Identity {
|
|
p = paths.AddLeadingSlash(p)
|
|
v, found := s.templatesByPath.Get(p)
|
|
if !found {
|
|
return nil
|
|
}
|
|
return v.GetIdentity()
|
|
}
|
|
|
|
func (t *TemplateStore) LookupByPath(templatePath string) *TemplInfo {
|
|
v, _ := t.templatesByPath.Get(templatePath)
|
|
return v
|
|
}
|
|
|
|
var bestPool = sync.Pool{
|
|
New: func() any {
|
|
return &bestMatch{}
|
|
},
|
|
}
|
|
|
|
func (s *TemplateStore) getBest() *bestMatch {
|
|
v := bestPool.Get()
|
|
b := v.(*bestMatch)
|
|
b.defaultOutputformat = s.opts.DefaultOutputFormat
|
|
return b
|
|
}
|
|
|
|
func (s *TemplateStore) putBest(b *bestMatch) {
|
|
b.reset()
|
|
bestPool.Put(b)
|
|
}
|
|
|
|
func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo {
|
|
q.init()
|
|
key := s.key(q.Path)
|
|
|
|
slashCountKey := strings.Count(key, "/")
|
|
best1 := s.getBest()
|
|
defer s.putBest(best1)
|
|
s.findBestMatchWalkPath(q, key, slashCountKey, best1)
|
|
if best1.w.w1 <= 0 {
|
|
return nil
|
|
}
|
|
m := best1.templ
|
|
if m.noBaseOf {
|
|
return m
|
|
}
|
|
best1.reset()
|
|
m.findBestMatchBaseof(s, q.Desc, key, slashCountKey, best1)
|
|
if best1.w.w1 <= 0 {
|
|
return nil
|
|
}
|
|
return best1.templ
|
|
}
|
|
|
|
func (s *TemplateStore) LookupPartial(pth string) *TemplInfo {
|
|
ti, _ := s.cacheLookupPartials.GetOrCreate(pth, func() (*TemplInfo, error) {
|
|
pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth).ForType(paths.TypePartial)
|
|
k1, _, _, desc, err := s.toKeyCategoryAndDescriptor(pi)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if desc.OutputFormat == "" && desc.MediaType == "" {
|
|
// Assume HTML.
|
|
desc.OutputFormat = s.htmlFormat.Name
|
|
desc.MediaType = s.htmlFormat.MediaType.Type
|
|
desc.IsPlainText = s.htmlFormat.IsPlainText
|
|
}
|
|
|
|
best := s.getBest()
|
|
defer s.putBest(best)
|
|
s.findBestMatchGet(s.key(path.Join(containerPartials, k1)), CategoryPartial, nil, desc, best)
|
|
return best.templ, nil
|
|
})
|
|
|
|
return ti
|
|
}
|
|
|
|
func (s *TemplateStore) LookupShortcodeByName(name string) *TemplInfo {
|
|
name = strings.ToLower(name)
|
|
ti, _ := s.shortcodesByName.Get(name)
|
|
if ti == nil {
|
|
return nil
|
|
}
|
|
return ti
|
|
}
|
|
|
|
func (s *TemplateStore) LookupShortcode(q TemplateQuery) *TemplInfo {
|
|
q.init()
|
|
k1 := s.key(q.Path)
|
|
|
|
slashCountK1 := strings.Count(k1, "/")
|
|
|
|
best := s.getBest()
|
|
defer s.putBest(best)
|
|
|
|
s.treeShortcodes.WalkPath(k1, func(k2 string, m map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
|
|
slashCountK2 := strings.Count(k2, "/")
|
|
distance := slashCountK1 - slashCountK2
|
|
|
|
v, found := m[q.Name]
|
|
if !found {
|
|
return false, nil
|
|
}
|
|
|
|
for k, vv := range v {
|
|
if !q.Consider(vv) {
|
|
continue
|
|
}
|
|
|
|
weight := s.dh.compareDescriptors(q.Category, vv.subCategory == SubCategoryEmbedded, q.Desc, k)
|
|
weight.distance = distance
|
|
if best.isBetter(weight, vv) {
|
|
best.updateValues(weight, k2, k, vv)
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
})
|
|
|
|
// Any match will do.
|
|
return best.templ
|
|
}
|
|
|
|
// PrintDebug is for testing/debugging only.
|
|
func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer) {
|
|
if w == nil {
|
|
w = os.Stdout
|
|
}
|
|
|
|
printOne := func(key string, vv *TemplInfo) {
|
|
level := strings.Count(key, "/")
|
|
if category != vv.category {
|
|
return
|
|
}
|
|
s := strings.ReplaceAll(strings.TrimSpace(vv.content), "\n", " ")
|
|
ts := fmt.Sprintf("kind: %q layout: %q content: %.30s", vv.D.Kind, vv.D.LayoutFromTemplate, s)
|
|
fmt.Fprintf(w, "%s%s %s\n", strings.Repeat(" ", level), key, ts)
|
|
}
|
|
s.treeMain.WalkPrefix(prefix, func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
|
|
for _, vv := range v {
|
|
printOne(key, vv)
|
|
}
|
|
return false, nil
|
|
})
|
|
s.treeShortcodes.WalkPrefix(prefix, func(key string, v map[string]map[TemplateDescriptor]*TemplInfo) (bool, error) {
|
|
for _, vv := range v {
|
|
for _, vv2 := range vv {
|
|
printOne(key, vv2)
|
|
}
|
|
}
|
|
return false, nil
|
|
})
|
|
}
|
|
|
|
func (s *TemplateStore) clearCaches() {
|
|
s.cacheLookupPartials.Reset()
|
|
}
|
|
|
|
// RefreshFiles refreshes this store for the files matching the given predicate.
|
|
func (s *TemplateStore) RefreshFiles(include func(fi hugofs.FileMetaInfo) bool) error {
|
|
s.clearCaches()
|
|
|
|
if err := s.tns.createPrototypesParse(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.insertTemplates(include, true); err != nil {
|
|
return err
|
|
}
|
|
if err := s.parseTemplates(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.extractInlinePartials(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.transformTemplates(); err != nil {
|
|
return err
|
|
}
|
|
if err := s.tns.createPrototypes(false); err != nil {
|
|
return err
|
|
}
|
|
if err := s.prepareTemplates(); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *TemplateStore) HasTemplate(templatePath string) bool {
|
|
templatePath = paths.AddLeadingSlash(templatePath)
|
|
return s.templatesByPath.Contains(templatePath)
|
|
}
|
|
|
|
func (t *TemplateStore) TextLookup(name string) *TemplInfo {
|
|
templ := t.tns.standaloneText.Lookup(name)
|
|
if templ == nil {
|
|
return nil
|
|
}
|
|
return &TemplInfo{
|
|
Template: templ,
|
|
}
|
|
}
|
|
|
|
func (t *TemplateStore) TextParse(name, tpl string) (*TemplInfo, error) {
|
|
templ, err := t.tns.standaloneText.New(name).Parse(tpl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &TemplInfo{
|
|
Template: templ,
|
|
}, nil
|
|
}
|
|
|
|
func (t *TemplateStore) UnusedTemplates() []*TemplInfo {
|
|
var unused []*TemplInfo
|
|
|
|
for vv := range t.templates() {
|
|
if vv.subCategory != SubCategoryMain || vv.isLegacyMapped {
|
|
// Skip inline partials and internal templates.
|
|
continue
|
|
}
|
|
if vv.noBaseOf {
|
|
if vv.executionCounter.Load() == 0 {
|
|
unused = append(unused, vv)
|
|
}
|
|
} else {
|
|
for vvv := range vv.BaseVariantsSeq() {
|
|
if vvv.Template.executionCounter.Load() == 0 {
|
|
unused = append(unused, vvv.Template)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Sort(byPath(unused))
|
|
return unused
|
|
}
|
|
|
|
// WithSiteOpts creates a new store with the given site options.
|
|
// This is used to create per site template store, all sharing the same templates,
|
|
// but with a different template function execution context.
|
|
func (s TemplateStore) WithSiteOpts(opts SiteOptions) *TemplateStore {
|
|
s.siteOpts = opts
|
|
s.storeSite = configureSiteStorage(opts, s.opts.Watching)
|
|
return &s
|
|
}
|
|
|
|
func (s *TemplateStore) findBestMatchGet(key string, category Category, consider func(candidate *TemplInfo) bool, desc TemplateDescriptor, best *bestMatch) {
|
|
key = strings.ToLower(key)
|
|
|
|
v := s.treeMain.Get(key)
|
|
if v == nil {
|
|
return
|
|
}
|
|
|
|
for k, vv := range v {
|
|
if vv.category != category {
|
|
continue
|
|
}
|
|
|
|
if consider != nil && !consider(vv) {
|
|
continue
|
|
}
|
|
|
|
weight := s.dh.compareDescriptors(category, vv.subCategory == SubCategoryEmbedded, desc, k.d)
|
|
if best.isBetter(weight, vv) {
|
|
best.updateValues(weight, key, k.d, vv)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *TemplateStore) findBestMatchWalkPath(q TemplateQuery, k1 string, slashCountK1 int, best *bestMatch) {
|
|
s.treeMain.WalkPath(k1, func(k2 string, v map[nodeKey]*TemplInfo) (bool, error) {
|
|
slashCountK2 := strings.Count(k2, "/")
|
|
distance := slashCountK1 - slashCountK2
|
|
|
|
for k, vv := range v {
|
|
if vv.category != q.Category {
|
|
continue
|
|
}
|
|
|
|
if !q.Consider(vv) {
|
|
continue
|
|
}
|
|
|
|
weight := s.dh.compareDescriptors(q.Category, vv.subCategory == SubCategoryEmbedded, q.Desc, k.d)
|
|
|
|
weight.distance = distance
|
|
isBetter := best.isBetter(weight, vv)
|
|
|
|
if isBetter {
|
|
best.updateValues(weight, k2, k.d, vv)
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
})
|
|
}
|
|
|
|
func (t *TemplateStore) addDeferredTemplate(owner *TemplInfo, name string, n *parse.ListNode) error {
|
|
if _, found := t.templatesByPath.Get(name); found {
|
|
return nil
|
|
}
|
|
|
|
var templ tpl.Template
|
|
|
|
if owner.D.IsPlainText {
|
|
prototype := t.tns.parseText
|
|
tt, err := prototype.New(name).Parse("")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
|
|
}
|
|
tt.Tree.Root = n
|
|
templ = tt
|
|
} else {
|
|
prototype := t.tns.parseHTML
|
|
tt, err := prototype.New(name).Parse("")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
|
|
}
|
|
tt.Tree.Root = n
|
|
templ = tt
|
|
}
|
|
|
|
t.templatesByPath.Set(name, &TemplInfo{
|
|
Fi: owner.Fi,
|
|
PathInfo: owner.PathInfo,
|
|
D: owner.D,
|
|
Template: templ,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *TemplateStore) addFileContext(ti *TemplInfo, what string, inerr error) error {
|
|
if ti.Fi == nil {
|
|
return inerr
|
|
}
|
|
|
|
identifiers := s.extractIdentifiers(inerr.Error())
|
|
|
|
checkFilename := func(fi hugofs.FileMetaInfo, inErr error) (error, bool) {
|
|
lineMatcher := func(m herrors.LineMatcher) int {
|
|
if m.Position.LineNumber != m.LineNumber {
|
|
return -1
|
|
}
|
|
|
|
for _, id := range identifiers {
|
|
if strings.Contains(m.Line, id) {
|
|
// We found the line, but return a 0 to signal to
|
|
// use the column from the error message.
|
|
return 0
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
f, err := fi.Meta().Open()
|
|
if err != nil {
|
|
return inErr, false
|
|
}
|
|
defer f.Close()
|
|
|
|
fe := herrors.NewFileErrorFromName(inErr, fi.Meta().Filename)
|
|
fe.UpdateContent(f, lineMatcher)
|
|
|
|
return fe, fe.ErrorContext().Position.IsValid()
|
|
}
|
|
|
|
inerr = fmt.Errorf("%s: %w", what, inerr)
|
|
|
|
var (
|
|
currentErr error
|
|
ok bool
|
|
)
|
|
|
|
if currentErr, ok = checkFilename(ti.Fi, inerr); ok {
|
|
return currentErr
|
|
}
|
|
|
|
if ti.base != nil {
|
|
if currentErr, ok = checkFilename(ti.base.Fi, inerr); ok {
|
|
return currentErr
|
|
}
|
|
}
|
|
|
|
return currentErr
|
|
}
|
|
|
|
func (s *TemplateStore) extractIdentifiers(line string) []string {
|
|
m := identifiersRe.FindAllStringSubmatch(line, -1)
|
|
identifiers := make([]string, len(m))
|
|
for i := range m {
|
|
identifiers[i] = m[i][1]
|
|
}
|
|
return identifiers
|
|
}
|
|
|
|
func (s *TemplateStore) extractInlinePartials() error {
|
|
isPartialName := func(s string) bool {
|
|
return strings.HasPrefix(s, "partials/") || strings.HasPrefix(s, "_partials/")
|
|
}
|
|
|
|
p := s.tns
|
|
// We may find both inline and external partials in the current template namespaces,
|
|
// so only add the ones we have not seen before.
|
|
addIfNotSeen := func(isText bool, templs ...tpl.Template) error {
|
|
for _, templ := range templs {
|
|
if templ.Name() == "" || !isPartialName(templ.Name()) {
|
|
continue
|
|
}
|
|
name := templ.Name()
|
|
if !paths.HasExt(name) {
|
|
// Assume HTML. This in line with how the lookup works.
|
|
name = name + s.htmlFormat.MediaType.FirstSuffix.FullSuffix
|
|
}
|
|
if !strings.HasPrefix(name, "_") {
|
|
name = "_" + name
|
|
}
|
|
pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
|
|
ti, err := s.insertTemplate(pi, nil, false, s.treeMain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ti != nil {
|
|
ti.Template = templ
|
|
ti.noBaseOf = true
|
|
ti.subCategory = SubCategoryInline
|
|
ti.D.IsPlainText = isText
|
|
}
|
|
|
|
}
|
|
return nil
|
|
}
|
|
addIfNotSeen(false, p.templatesIn(p.parseHTML)...)
|
|
addIfNotSeen(true, p.templatesIn(p.parseText)...)
|
|
|
|
for _, t := range p.baseofHtmlClones {
|
|
if err := addIfNotSeen(false, p.templatesIn(t)...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, t := range p.baseofTextClones {
|
|
if err := addIfNotSeen(true, p.templatesIn(t)...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *TemplateStore) insertEmbedded() error {
|
|
return fs.WalkDir(embeddedTemplatesFs, ".", func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d == nil || d.IsDir() || strings.HasPrefix(d.Name(), ".") {
|
|
return nil
|
|
}
|
|
|
|
templb, err := embeddedTemplatesFs.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get the newlines on Windows in line with how we had it back when we used Go Generate
|
|
// to write the templates to Go files.
|
|
templ := string(bytes.ReplaceAll(templb, []byte("\r\n"), []byte("\n")))
|
|
name := strings.TrimPrefix(filepath.ToSlash(path), "embedded/templates/")
|
|
|
|
insertOne := func(name, content string) error {
|
|
pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
|
|
var (
|
|
ti *TemplInfo
|
|
err error
|
|
)
|
|
if pi.Section() == containerShortcodes {
|
|
ti, err = s.insertShortcode(pi, nil, false, s.treeShortcodes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
ti, err = s.insertTemplate(pi, nil, false, s.treeMain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if ti != nil {
|
|
// Currently none of the embedded templates need a baseof template.
|
|
ti.noBaseOf = true
|
|
ti.content = content
|
|
ti.subCategory = SubCategoryEmbedded
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := insertOne(name, templ); err != nil {
|
|
return err
|
|
}
|
|
|
|
if aliases, found := embeddedTemplatesAliases[name]; found {
|
|
for _, alias := range aliases {
|
|
if err := insertOne(alias, templ); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (s *TemplateStore) setTemplateByPath(p string, ti *TemplInfo) {
|
|
s.templatesByPath.Set(p, ti)
|
|
}
|
|
|
|
func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[string]map[TemplateDescriptor]*TemplInfo]) (*TemplInfo, error) {
|
|
k1, k2, _, d, err := s.toKeyCategoryAndDescriptor(pi)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m := tree.Get(k1)
|
|
if m == nil {
|
|
m = make(map[string]map[TemplateDescriptor]*TemplInfo)
|
|
tree.Insert(k1, m)
|
|
}
|
|
|
|
m1, found := m[k2]
|
|
if found {
|
|
if _, found := m1[d]; found {
|
|
if !replace {
|
|
return nil, nil
|
|
}
|
|
}
|
|
} else {
|
|
m1 = make(map[TemplateDescriptor]*TemplInfo)
|
|
m[k2] = m1
|
|
}
|
|
|
|
ti := &TemplInfo{
|
|
PathInfo: pi,
|
|
Fi: fi,
|
|
D: d,
|
|
category: CategoryShortcode,
|
|
noBaseOf: true,
|
|
}
|
|
|
|
m1[d] = ti
|
|
|
|
s.shortcodesByName.Set(k2, ti)
|
|
s.setTemplateByPath(pi.Path(), ti)
|
|
|
|
if fi != nil {
|
|
if pi2 := fi.Meta().PathInfo; pi2 != pi {
|
|
s.setTemplateByPath(pi2.Path(), ti)
|
|
}
|
|
}
|
|
|
|
return ti, nil
|
|
}
|
|
|
|
func (s *TemplateStore) insertTemplate(pi *paths.Path, fi hugofs.FileMetaInfo, replace bool, tree doctree.Tree[map[nodeKey]*TemplInfo]) (*TemplInfo, error) {
|
|
key, _, category, d, err := s.toKeyCategoryAndDescriptor(pi)
|
|
// See #13577. Warn for now.
|
|
if err != nil {
|
|
var loc string
|
|
if fi != nil {
|
|
loc = fmt.Sprintf("file %q", fi.Meta().Filename)
|
|
} else {
|
|
loc = fmt.Sprintf("path %q", pi.Path())
|
|
}
|
|
s.opts.Log.Warnf("skipping template %s: %s", loc, err)
|
|
return nil, nil
|
|
}
|
|
|
|
return s.insertTemplate2(pi, fi, key, category, d, replace, false, tree)
|
|
}
|
|
|
|
func (s *TemplateStore) insertTemplate2(
|
|
pi *paths.Path,
|
|
fi hugofs.FileMetaInfo,
|
|
key string,
|
|
category Category,
|
|
d TemplateDescriptor,
|
|
replace, isLegacyMapped bool,
|
|
tree doctree.Tree[map[nodeKey]*TemplInfo],
|
|
) (*TemplInfo, error) {
|
|
if category == 0 {
|
|
panic("category not set")
|
|
}
|
|
|
|
if category == CategoryPartial && d.OutputFormat == "" && d.MediaType == "" {
|
|
// See issue #13601.
|
|
d.OutputFormat = s.htmlFormat.Name
|
|
d.MediaType = s.htmlFormat.MediaType.Type
|
|
}
|
|
|
|
m := tree.Get(key)
|
|
nk := nodeKey{c: category, d: d}
|
|
|
|
if m == nil {
|
|
m = make(map[nodeKey]*TemplInfo)
|
|
tree.Insert(key, m)
|
|
}
|
|
|
|
if !replace {
|
|
if v, found := m[nk]; found {
|
|
if len(pi.IdentifiersUnknown()) >= len(v.PathInfo.IdentifiersUnknown()) {
|
|
// e.g. /pages/home.foo.html and /pages/home.html where foo may be a valid language name in another site.
|
|
return nil, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
ti := &TemplInfo{
|
|
PathInfo: pi,
|
|
Fi: fi,
|
|
D: d,
|
|
category: category,
|
|
noBaseOf: category > CategoryLayout,
|
|
isLegacyMapped: isLegacyMapped,
|
|
}
|
|
|
|
m[nk] = ti
|
|
|
|
if !isLegacyMapped {
|
|
s.setTemplateByPath(pi.Path(), ti)
|
|
if fi != nil {
|
|
if pi2 := fi.Meta().PathInfo; pi2 != pi {
|
|
s.setTemplateByPath(pi2.Path(), ti)
|
|
}
|
|
}
|
|
}
|
|
|
|
return ti, nil
|
|
}
|
|
|
|
func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) bool, replace bool) error {
|
|
if include == nil {
|
|
include = func(fi hugofs.FileMetaInfo) bool {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Set if we need to reset the base variants.
|
|
var (
|
|
resetBaseVariants bool
|
|
)
|
|
|
|
legacyOrdinalMappings := map[legacyTargetPathIdentifiers]legacyOrdinalMappingFi{}
|
|
|
|
walker := func(pth string, fi hugofs.FileMetaInfo) error {
|
|
if fi.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
if isDotFile(pth) || isBackupFile(pth) {
|
|
return nil
|
|
}
|
|
|
|
if !include(fi) {
|
|
return nil
|
|
}
|
|
|
|
piOrig := fi.Meta().PathInfo
|
|
|
|
// Convert any legacy value to new format.
|
|
fromLegacyPath := func(pi *paths.Path) *paths.Path {
|
|
p := pi.Path()
|
|
p = strings.TrimPrefix(p, "/_default")
|
|
if strings.HasPrefix(p, "/shortcodes") || strings.HasPrefix(p, "/partials") {
|
|
// Insert an underscore so it becomes /_shortcodes or /_partials.
|
|
p = "/_" + p[1:]
|
|
}
|
|
|
|
if strings.Contains(p, "-"+baseNameBaseof) {
|
|
// Before Hugo 0.146.0 we prepended one identifier (layout, type or kind) in front of the baseof keyword,
|
|
// and then separated with a hyphen before the baseof keyword.
|
|
// This identifier needs to be moved right after the baseof keyword and the hyphen removed, e.g.
|
|
// /docs/list-baseof.html => /docs/baseof.list.html.
|
|
dir, name := path.Split(p)
|
|
hyphenIdx := strings.Index(name, "-")
|
|
if hyphenIdx > 0 {
|
|
id := name[:hyphenIdx]
|
|
name = name[hyphenIdx+1+len(baseNameBaseof):]
|
|
if !strings.HasPrefix(name, ".") {
|
|
name = "." + name
|
|
}
|
|
p = path.Join(dir, baseNameBaseof+"."+id+name)
|
|
}
|
|
}
|
|
if p == pi.Path() {
|
|
return pi
|
|
}
|
|
return s.opts.PathParser.Parse(files.ComponentFolderLayouts, p)
|
|
}
|
|
|
|
pi := piOrig
|
|
var applyLegacyMapping bool
|
|
switch pi.Section() {
|
|
case containerPartials, containerShortcodes, containerMarkup:
|
|
// OK.
|
|
default:
|
|
pi = fromLegacyPath(pi)
|
|
applyLegacyMapping = strings.Count(pi.Path(), "/") <= 2
|
|
}
|
|
|
|
if applyLegacyMapping {
|
|
handleMapping := func(m1 legacyOrdinalMapping) {
|
|
key := legacyTargetPathIdentifiers{
|
|
targetPath: m1.mapping.targetPath,
|
|
targetCategory: m1.mapping.targetCategory,
|
|
kind: m1.mapping.targetDesc.Kind,
|
|
lang: pi.Lang(),
|
|
ext: pi.Ext(),
|
|
outputFormat: pi.OutputFormat(),
|
|
}
|
|
|
|
if m2, ok := legacyOrdinalMappings[key]; ok {
|
|
if m1.ordinal < m2.m.ordinal {
|
|
// Higher up == better match.
|
|
legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
|
|
}
|
|
} else {
|
|
legacyOrdinalMappings[key] = legacyOrdinalMappingFi{m1, fi}
|
|
}
|
|
}
|
|
|
|
if m1, ok := s.opts.legacyMappingTaxonomy[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
|
|
handleMapping(m1)
|
|
}
|
|
|
|
if m1, ok := s.opts.legacyMappingTerm[piOrig.PathBeforeLangAndOutputFormatAndExt()]; ok {
|
|
handleMapping(m1)
|
|
}
|
|
|
|
const (
|
|
sectionKindToken = "SECTIONKIND"
|
|
sectionToken = "THESECTION"
|
|
)
|
|
|
|
base := piOrig.PathBeforeLangAndOutputFormatAndExt()
|
|
identifiers := pi.IdentifiersUnknown()
|
|
if pi.Kind() != "" {
|
|
identifiers = append(identifiers, pi.Kind())
|
|
}
|
|
|
|
shouldIncludeSection := func(section string) bool {
|
|
switch section {
|
|
case containerShortcodes, containerPartials, containerMarkup:
|
|
return false
|
|
case "taxonomy", "":
|
|
return false
|
|
default:
|
|
for k, v := range s.opts.TaxonomySingularPlural {
|
|
if k == section || v == section {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
if shouldIncludeSection(pi.Section()) {
|
|
identifiers = append(identifiers, pi.Section())
|
|
}
|
|
|
|
identifiers = helpers.UniqueStrings(identifiers)
|
|
|
|
// Tokens on e.g. form /SECTIONKIND/THESECTION
|
|
insertSectionTokens := func(section string) []string {
|
|
kindOnly := isLayoutStandard(section)
|
|
var ss []string
|
|
s1 := base
|
|
if !kindOnly {
|
|
s1 = strings.ReplaceAll(s1, section, sectionToken)
|
|
}
|
|
s1 = strings.ReplaceAll(s1, kinds.KindSection, sectionKindToken)
|
|
if s1 != base {
|
|
ss = append(ss, s1)
|
|
}
|
|
s1 = strings.ReplaceAll(base, kinds.KindSection, sectionKindToken)
|
|
if !kindOnly {
|
|
s1 = strings.ReplaceAll(s1, section, sectionToken)
|
|
}
|
|
if s1 != base {
|
|
ss = append(ss, s1)
|
|
}
|
|
|
|
helpers.UniqueStringsReuse(ss)
|
|
|
|
return ss
|
|
}
|
|
|
|
for _, id := range identifiers {
|
|
if id == "" {
|
|
continue
|
|
}
|
|
|
|
p := insertSectionTokens(id)
|
|
for _, ss := range p {
|
|
if m1, ok := s.opts.legacyMappingSection[ss]; ok {
|
|
targetPath := m1.mapping.targetPath
|
|
|
|
if targetPath != "" {
|
|
targetPath = strings.ReplaceAll(targetPath, sectionToken, id)
|
|
targetPath = strings.ReplaceAll(targetPath, sectionKindToken, id)
|
|
targetPath = strings.ReplaceAll(targetPath, "//", "/")
|
|
}
|
|
m1.mapping.targetPath = targetPath
|
|
handleMapping(m1)
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if replace && pi.NameNoIdentifier() == baseNameBaseof {
|
|
// A baseof file has changed.
|
|
resetBaseVariants = true
|
|
}
|
|
|
|
var ti *TemplInfo
|
|
var err error
|
|
if pi.Type() == paths.TypeShortcode {
|
|
ti, err = s.insertShortcode(pi, fi, replace, s.treeShortcodes)
|
|
if err != nil || ti == nil {
|
|
return err
|
|
}
|
|
} else {
|
|
ti, err = s.insertTemplate(pi, fi, replace, s.treeMain)
|
|
if err != nil || ti == nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := s.tns.readTemplateInto(ti); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if err := helpers.Walk(s.opts.Fs, "", walker); err != nil {
|
|
if !herrors.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
for k, v := range legacyOrdinalMappings {
|
|
targetPath := k.targetPath
|
|
m := v.m.mapping
|
|
fi := v.fi
|
|
pi := fi.Meta().PathInfo
|
|
outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(k.outputFormat, k.ext)
|
|
category := m.targetCategory
|
|
desc := m.targetDesc
|
|
desc.Kind = k.kind
|
|
desc.Lang = k.lang
|
|
desc.OutputFormat = outputFormat.Name
|
|
desc.IsPlainText = outputFormat.IsPlainText
|
|
desc.MediaType = mediaType.Type
|
|
|
|
ti, err := s.insertTemplate2(pi, fi, targetPath, category, desc, true, true, s.treeMain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ti == nil {
|
|
continue
|
|
}
|
|
ti.isLegacyMapped = true
|
|
if err := s.tns.readTemplateInto(ti); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if resetBaseVariants {
|
|
s.tns.baseofHtmlClones = nil
|
|
s.tns.baseofTextClones = nil
|
|
s.treeMain.Walk(func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
|
|
for _, vv := range v {
|
|
if !vv.noBaseOf {
|
|
vv.state = processingStateInitial
|
|
}
|
|
}
|
|
return false, nil
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *TemplateStore) key(dir string) string {
|
|
dir = paths.AddLeadingSlash(dir)
|
|
if dir == "/" {
|
|
return ""
|
|
}
|
|
return paths.TrimTrailing(dir)
|
|
}
|
|
|
|
func (s *TemplateStore) parseTemplates() error {
|
|
if err := func() error {
|
|
// Read and parse all templates.
|
|
for _, v := range s.treeMain.All() {
|
|
for _, vv := range v {
|
|
if vv.state == processingStateTransformed {
|
|
continue
|
|
}
|
|
if err := s.parseTemplate(vv); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lookup and apply base templates where needed.
|
|
for key, v := range s.treeMain.All() {
|
|
for _, vv := range v {
|
|
if vv.state == processingStateTransformed {
|
|
continue
|
|
}
|
|
if !vv.noBaseOf {
|
|
d := vv.D
|
|
// Find all compatible base templates.
|
|
baseTemplates := s.FindAllBaseTemplateCandidates(key, d)
|
|
if len(baseTemplates) == 0 {
|
|
// The regular expression used to detect if a template needs a base template has some
|
|
// rare false positives. Assume we don't need one.
|
|
vv.noBaseOf = true
|
|
if err := s.parseTemplate(vv); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
vv.baseVariants = doctree.NewSimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]()
|
|
|
|
for _, base := range baseTemplates {
|
|
if err := s.tns.applyBaseTemplate(vv, base); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Prese shortcodes.
|
|
for _, v := range s.treeShortcodes.All() {
|
|
for _, vv := range v {
|
|
for _, vvv := range vv {
|
|
if vvv.state == processingStateTransformed {
|
|
continue
|
|
}
|
|
if err := s.parseTemplate(vvv); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// prepareTemplates prepares all templates for execution.
|
|
func (s *TemplateStore) prepareTemplates() error {
|
|
for t := range s.templates() {
|
|
if t.category == CategoryBaseof {
|
|
continue
|
|
}
|
|
if _, err := t.Prepare(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type PathTemplateDescriptor struct {
|
|
Path string
|
|
Desc TemplateDescriptor
|
|
}
|
|
|
|
// resolveOutputFormatAndOrMediaType resolves the output format and/or media type
|
|
// based on the given output format suffix and media type suffix.
|
|
// Either of the suffixes can be empty, and the function will try to find a match
|
|
// based on the other suffix. If both are empty, the function will return zero values.
|
|
func (s *TemplateStore) resolveOutputFormatAndOrMediaType(ofs, mns string) (output.Format, media.Type) {
|
|
var outputFormat output.Format
|
|
var mediaType media.Type
|
|
|
|
if ofs != "" {
|
|
if of, found := s.opts.OutputFormats.GetByName(ofs); found {
|
|
outputFormat = of
|
|
mediaType = of.MediaType
|
|
}
|
|
}
|
|
|
|
if mns != "" && mediaType.IsZero() {
|
|
if of, found := s.opts.OutputFormats.GetBySuffix(mns); found {
|
|
outputFormat = of
|
|
mediaType = of.MediaType
|
|
} else {
|
|
if mt, _, found := s.opts.MediaTypes.GetFirstBySuffix(mns); found {
|
|
mediaType = mt
|
|
if outputFormat.IsZero() {
|
|
// For e.g. index.xml we will in the default confg now have the application/rss+xml media type.
|
|
// Try a last time to find the output format using the SubType as the name.
|
|
// As to template resolution, this value is currently only used to
|
|
// decide if this is a text or HTML template.
|
|
outputFormat, _ = s.opts.OutputFormats.GetByName(mt.SubType)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return outputFormat, mediaType
|
|
}
|
|
|
|
func (s *TemplateStore) templates() iter.Seq[*TemplInfo] {
|
|
return func(yield func(*TemplInfo) bool) {
|
|
for _, v := range s.treeMain.All() {
|
|
for _, vv := range v {
|
|
if !vv.noBaseOf {
|
|
for vvv := range vv.BaseVariantsSeq() {
|
|
if !yield(vvv.Template) {
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
if !yield(vv) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for _, v := range s.treeShortcodes.All() {
|
|
for _, vv := range v {
|
|
for _, vvv := range vv {
|
|
if !yield(vvv) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *TemplateStore) toKeyCategoryAndDescriptor(p *paths.Path) (string, string, Category, TemplateDescriptor, error) {
|
|
k1 := p.Dir()
|
|
k2 := ""
|
|
|
|
outputFormat, mediaType := s.resolveOutputFormatAndOrMediaType(p.OutputFormat(), p.Ext())
|
|
nameNoIdentifier := p.NameNoIdentifier()
|
|
|
|
var layout string
|
|
unknownids := p.IdentifiersUnknown()
|
|
if p.Type() == paths.TypeShortcode {
|
|
if len(unknownids) > 1 {
|
|
// The name is the last identifier.
|
|
layout = unknownids[len(unknownids)-2]
|
|
}
|
|
} else if len(unknownids) > 0 {
|
|
// Pick the last, closest to the base name.
|
|
layout = unknownids[len(unknownids)-1]
|
|
}
|
|
|
|
d := TemplateDescriptor{
|
|
Lang: p.Lang(),
|
|
OutputFormat: p.OutputFormat(),
|
|
MediaType: mediaType.Type,
|
|
Kind: p.Kind(),
|
|
LayoutFromTemplate: layout,
|
|
IsPlainText: outputFormat.IsPlainText,
|
|
}
|
|
|
|
d.normalizeFromFile()
|
|
|
|
section := p.Section()
|
|
|
|
var category Category
|
|
switch p.Type() {
|
|
case paths.TypeShortcode:
|
|
category = CategoryShortcode
|
|
case paths.TypePartial:
|
|
category = CategoryPartial
|
|
case paths.TypeMarkup:
|
|
category = CategoryMarkup
|
|
}
|
|
|
|
if category == 0 {
|
|
if nameNoIdentifier == baseNameBaseof {
|
|
category = CategoryBaseof
|
|
} else {
|
|
switch section {
|
|
case "_hugo":
|
|
category = CategoryHugo
|
|
case "_server":
|
|
category = CategoryServer
|
|
default:
|
|
category = CategoryLayout
|
|
}
|
|
}
|
|
}
|
|
|
|
if category == CategoryPartial {
|
|
d.LayoutFromTemplate = ""
|
|
k1 = p.PathNoIdentifier()
|
|
}
|
|
|
|
if category == CategoryShortcode {
|
|
k1 = p.PathNoIdentifier()
|
|
parts := strings.Split(k1, "/"+containerShortcodes+"/")
|
|
k1 = parts[0]
|
|
if len(parts) > 1 {
|
|
k2 = parts[1]
|
|
}
|
|
k1 = s.key(k1)
|
|
}
|
|
|
|
// Legacy layout for home page.
|
|
if d.LayoutFromTemplate == "index" {
|
|
if d.Kind == "" {
|
|
d.Kind = kinds.KindHome
|
|
}
|
|
d.LayoutFromTemplate = ""
|
|
}
|
|
|
|
if d.LayoutFromTemplate == d.Kind {
|
|
d.LayoutFromTemplate = ""
|
|
}
|
|
|
|
k1 = strings.TrimPrefix(k1, "/_default")
|
|
if k1 == "/" {
|
|
k1 = ""
|
|
}
|
|
|
|
if category == CategoryMarkup {
|
|
// We store all template nodes for a given directory on the same level.
|
|
k1 = strings.TrimSuffix(k1, "/_markup")
|
|
parts := strings.Split(d.LayoutFromTemplate, "-")
|
|
if len(parts) < 2 {
|
|
return "", "", 0, TemplateDescriptor{}, fmt.Errorf("unrecognized render hook template")
|
|
}
|
|
// Either 2 or 3 parts, e.g. render-codeblock-go.
|
|
d.Variant1 = parts[1]
|
|
if len(parts) > 2 {
|
|
d.Variant2 = parts[2]
|
|
}
|
|
d.LayoutFromTemplate = "" // This allows using page layout as part of the key for lookups.
|
|
}
|
|
|
|
return k1, k2, category, d, nil
|
|
}
|
|
|
|
func (s *TemplateStore) transformTemplates() error {
|
|
lookup := func(name string, in *TemplInfo) *TemplInfo {
|
|
if in.D.IsPlainText {
|
|
templ := in.Template.(*texttemplate.Template).Lookup(name)
|
|
if templ != nil {
|
|
return &TemplInfo{
|
|
Template: templ,
|
|
}
|
|
}
|
|
} else {
|
|
templ := in.Template.(*htmltemplate.Template).Lookup(name)
|
|
if templ != nil {
|
|
return &TemplInfo{
|
|
Template: templ,
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
for vv := range s.templates() {
|
|
if vv.state == processingStateTransformed {
|
|
continue
|
|
}
|
|
vv.state = processingStateTransformed
|
|
if vv.category == CategoryBaseof {
|
|
continue
|
|
}
|
|
if !vv.noBaseOf {
|
|
// TODO(bep) I don't think this branch is ever called.
|
|
for vvv := range vv.BaseVariantsSeq() {
|
|
tctx, err := applyTemplateTransformers(vvv.Template, lookup)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for name, node := range tctx.deferNodes {
|
|
if err := s.addDeferredTemplate(vvv.Overlay, name, node); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
tctx, err := applyTemplateTransformers(vv, lookup)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for name, node := range tctx.deferNodes {
|
|
if err := s.addDeferredTemplate(vv, name, node); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *TemplateStore) init() error {
|
|
// Before Hugo 0.146 we had a very elaborate template lookup system, especially for
|
|
// terms and taxonomies. This is a way of preserving backwards compatibility
|
|
// by mapping old paths into the new tree.
|
|
s.opts.legacyMappingTaxonomy = make(map[string]legacyOrdinalMapping)
|
|
s.opts.legacyMappingTerm = make(map[string]legacyOrdinalMapping)
|
|
s.opts.legacyMappingSection = make(map[string]legacyOrdinalMapping)
|
|
|
|
// Placeholders.
|
|
const singular = "SINGULAR"
|
|
const plural = "PLURAL"
|
|
|
|
replaceTokens := func(s, singularv, pluralv string) string {
|
|
s = strings.Replace(s, singular, singularv, -1)
|
|
s = strings.Replace(s, plural, pluralv, -1)
|
|
return s
|
|
}
|
|
|
|
hasSingularOrPlural := func(s string) bool {
|
|
return strings.Contains(s, singular) || strings.Contains(s, plural)
|
|
}
|
|
|
|
expand := func(v layoutLegacyMapping) []layoutLegacyMapping {
|
|
var result []layoutLegacyMapping
|
|
|
|
if hasSingularOrPlural(v.sourcePath) || hasSingularOrPlural(v.target.targetPath) {
|
|
for s, p := range s.opts.TaxonomySingularPlural {
|
|
target := v.target
|
|
target.targetPath = replaceTokens(target.targetPath, s, p)
|
|
vv := replaceTokens(v.sourcePath, s, p)
|
|
result = append(result, layoutLegacyMapping{sourcePath: vv, target: target})
|
|
}
|
|
} else {
|
|
result = append(result, v)
|
|
}
|
|
return result
|
|
}
|
|
|
|
expandSections := func(v layoutLegacyMapping) []layoutLegacyMapping {
|
|
var result []layoutLegacyMapping
|
|
result = append(result, v)
|
|
baseofVariant := v
|
|
baseofVariant.sourcePath += "-" + baseNameBaseof
|
|
baseofVariant.target.targetCategory = CategoryBaseof
|
|
result = append(result, baseofVariant)
|
|
return result
|
|
}
|
|
|
|
var terms []layoutLegacyMapping
|
|
for _, v := range legacyTermMappings {
|
|
terms = append(terms, expand(v)...)
|
|
}
|
|
var taxonomies []layoutLegacyMapping
|
|
for _, v := range legacyTaxonomyMappings {
|
|
taxonomies = append(taxonomies, expand(v)...)
|
|
}
|
|
var sections []layoutLegacyMapping
|
|
for _, v := range legacySectionMappings {
|
|
sections = append(sections, expandSections(v)...)
|
|
}
|
|
|
|
for i, m := range terms {
|
|
s.opts.legacyMappingTerm[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
|
|
}
|
|
for i, m := range taxonomies {
|
|
s.opts.legacyMappingTaxonomy[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
|
|
}
|
|
for i, m := range sections {
|
|
s.opts.legacyMappingSection[m.sourcePath] = legacyOrdinalMapping{ordinal: i, mapping: m.target}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type TemplateStoreProvider interface {
|
|
GetTemplateStore() *TemplateStore
|
|
}
|
|
|
|
type TextTemplatHandler interface {
|
|
ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error
|
|
TextLookup(name string) *TemplInfo
|
|
TextParse(name, tpl string) (*TemplInfo, error)
|
|
}
|
|
|
|
type bestMatch struct {
|
|
templ *TemplInfo
|
|
desc TemplateDescriptor
|
|
w weight
|
|
key string
|
|
|
|
// settings.
|
|
defaultOutputformat string
|
|
}
|
|
|
|
func (best *bestMatch) reset() {
|
|
best.templ = nil
|
|
best.w = weight{}
|
|
best.desc = TemplateDescriptor{}
|
|
best.key = ""
|
|
}
|
|
|
|
func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool {
|
|
if best.templ == nil {
|
|
// Anything is better than nothing.
|
|
return true
|
|
}
|
|
if w.w1 <= 0 {
|
|
if best.w.w1 <= 0 {
|
|
return ti.PathInfo.Path() < best.templ.PathInfo.Path()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Note that for render hook templates, we need to make
|
|
// the embedded render hook template wih if they're a better match,
|
|
// e.g. render-codeblock-goat.html.
|
|
if best.templ.category != CategoryMarkup && best.w.w1 > 0 {
|
|
currentBestIsEmbedded := best.templ.subCategory == SubCategoryEmbedded
|
|
if currentBestIsEmbedded {
|
|
if ti.subCategory != SubCategoryEmbedded {
|
|
return true
|
|
}
|
|
} else {
|
|
if ti.subCategory == SubCategoryEmbedded {
|
|
// Prefer user provided template.
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
if w.distance < best.w.distance {
|
|
if w.w2 < best.w.w2 {
|
|
return false
|
|
}
|
|
if w.w3 < best.w.w3 {
|
|
return false
|
|
}
|
|
} else {
|
|
if w.w1 < best.w.w1 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
if w.isEqualWeights(best.w) {
|
|
// Tie breakers.
|
|
if w.distance < best.w.distance {
|
|
return true
|
|
}
|
|
|
|
if ti.D.LayoutFromTemplate != "" && best.desc.LayoutFromTemplate != "" {
|
|
return ti.D.LayoutFromTemplate != layoutAll
|
|
}
|
|
|
|
return w.distance < best.w.distance || ti.PathInfo.Path() < best.templ.PathInfo.Path()
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (best *bestMatch) updateValues(w weight, key string, k TemplateDescriptor, vv *TemplInfo) {
|
|
best.w = w
|
|
best.templ = vv
|
|
best.desc = k
|
|
best.key = key
|
|
}
|
|
|
|
type byPath []*TemplInfo
|
|
|
|
func (a byPath) Len() int { return len(a) }
|
|
func (a byPath) Less(i, j int) bool {
|
|
return a[i].PathInfo.Path() < a[j].PathInfo.Path()
|
|
}
|
|
|
|
func (a byPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
|
|
|
type keyTemplateInfo struct {
|
|
Key string
|
|
Info *TemplInfo
|
|
}
|
|
|
|
type nodeKey struct {
|
|
c Category
|
|
d TemplateDescriptor
|
|
}
|
|
|
|
type processingState int
|
|
|
|
// the parts of a template store that's set per site.
|
|
type storeSite struct {
|
|
opts SiteOptions
|
|
execHelper *templateExecHelper
|
|
executer texttemplate.Executer
|
|
}
|
|
|
|
type weight struct {
|
|
w1 int
|
|
w2 int
|
|
w3 int
|
|
distance int
|
|
}
|
|
|
|
func isLayoutStandard(s string) bool {
|
|
switch s {
|
|
case layoutAll, layoutList, layoutSingle:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (w weight) isEqualWeights(other weight) bool {
|
|
return w.w1 == other.w1 && w.w2 == other.w2 && w.w3 == other.w3
|
|
}
|
|
|
|
func configureSiteStorage(opts SiteOptions, watching bool) *storeSite {
|
|
funcsv := make(map[string]reflect.Value)
|
|
|
|
for k, v := range opts.TemplateFuncs {
|
|
vv := reflect.ValueOf(v)
|
|
funcsv[k] = vv
|
|
}
|
|
|
|
// Duplicate Go's internal funcs here for faster lookups.
|
|
for k, v := range htmltemplate.GoFuncs {
|
|
if _, exists := funcsv[k]; !exists {
|
|
vv, ok := v.(reflect.Value)
|
|
if !ok {
|
|
vv = reflect.ValueOf(v)
|
|
}
|
|
funcsv[k] = vv
|
|
}
|
|
}
|
|
|
|
for k, v := range texttemplate.GoFuncs {
|
|
if _, exists := funcsv[k]; !exists {
|
|
funcsv[k] = v
|
|
}
|
|
}
|
|
|
|
s := &storeSite{
|
|
opts: opts,
|
|
execHelper: &templateExecHelper{
|
|
watching: watching,
|
|
funcs: funcsv,
|
|
site: reflect.ValueOf(opts.Site),
|
|
siteParams: reflect.ValueOf(opts.Site.Params()),
|
|
},
|
|
}
|
|
|
|
s.executer = texttemplate.NewExecuter(s.execHelper)
|
|
|
|
return s
|
|
}
|
|
|
|
func isBackupFile(path string) bool {
|
|
return path[len(path)-1] == '~'
|
|
}
|
|
|
|
func isDotFile(path string) bool {
|
|
return filepath.Base(path)[0] == '.'
|
|
}
|