1
0
Fork 0
hugo/tpl/tplimpl/templatestore.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] == '.'
}