tpl: Add templates.Current

This commit also

* Unexport all internal state in TemplateInfo.
* Make the dispatcher keys used for passing context.Context into uint8 from string to save memory allocations.

Co-authored-by: Joe Mooring <joe@mooring.com>

Updates #13571
This commit is contained in:
Bjørn Erik Pedersen 2025-04-08 18:41:06 +02:00
parent af0602c343
commit d4c6dd16b1
13 changed files with 322 additions and 123 deletions

View file

@ -139,9 +139,13 @@ func (i HugoInfo) IsMultilingual() bool {
return i.conf.IsMultilingual() return i.conf.IsMultilingual()
} }
type contextKey string type contextKey uint8
var markupScope = hcontext.NewContextDispatcher[string](contextKey("markupScope")) const (
contextKeyMarkupScope contextKey = iota
)
var markupScope = hcontext.NewContextDispatcher[string](contextKeyMarkupScope)
type Context struct{} type Context struct{}

View file

@ -667,7 +667,13 @@ func (c *cachedContentScope) mustContentToC(ctx context.Context) contentTableOfC
return ct return ct
} }
var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)]("contentCallback") type contextKey uint8
const (
contextKeyContentCallback contextKey = iota
)
var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)](contextKeyContentCallback)
func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfContents, error) { func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfContents, error) {
cp := c.pco cp := c.pco

View file

@ -120,7 +120,7 @@ func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (tem
// Make sure to send the *pageState and not the *pageContentOutput to the template. // Make sure to send the *pageState and not the *pageContentOutput to the template.
res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p) res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p)
if err != nil { if err != nil {
return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Template.Name(), err)) return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Name(), err))
} }
return template.HTML(res), nil return template.HTML(res), nil
} }
@ -323,7 +323,7 @@ func (pco *pageContentOutput) initRenderHooks() error {
return false return false
} }
if ignoreInternal && candidate.SubCategory == tplimpl.SubCategoryEmbedded { if ignoreInternal && candidate.SubCategory() == tplimpl.SubCategoryEmbedded {
// Don't consider the internal hook templates. // Don't consider the internal hook templates.
return false return false
} }

View file

@ -168,7 +168,7 @@ func pageRenderer(
s.Log.Trace( s.Log.Trace(
func() string { func() string {
return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Template.Name(), targetPath) return fmt.Sprintf("rendering outputFormat %q kind %q using layout %q to %q", p.pageOutput.f.Name, p.Kind(), templ.Name(), targetPath)
}, },
) )

View file

@ -55,12 +55,16 @@ type Client struct {
remoteResourceLogger logg.LevelLogger remoteResourceLogger logg.LevelLogger
} }
type contextKey string type contextKey uint8
const (
contextKeyResourceID contextKey = iota
)
// New creates a new Client with the given specification. // New creates a new Client with the given specification.
func New(rs *resources.Spec) *Client { func New(rs *resources.Spec) *Client {
fileCache := rs.FileCaches.GetResourceCache() fileCache := rs.FileCaches.GetResourceCache()
resourceIDDispatcher := hcontext.NewContextDispatcher[string](contextKey("resourceID")) resourceIDDispatcher := hcontext.NewContextDispatcher[string](contextKeyResourceID)
httpCacheConfig := rs.Cfg.GetConfigSection("httpCacheCompiled").(hhttpcache.ConfigCompiled) httpCacheConfig := rs.Cfg.GetConfigSection("httpCacheCompiled").(hhttpcache.ConfigCompiled)
var remoteResourceChecker *tasks.RunEvery var remoteResourceChecker *tasks.RunEvery
if rs.Cfg.Watching() && !httpCacheConfig.IsPollingDisabled() { if rs.Cfg.Watching() && !httpCacheConfig.IsPollingDisabled() {

View file

@ -157,8 +157,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
if len(dataList) > 0 { if len(dataList) > 0 {
data = dataList[0] data = dataList[0]
} }
name, desc := ns.deps.TemplateStore.TemplateDescriptorFromPath(name) v := ns.deps.TemplateStore.LookupPartial(name)
v := ns.deps.TemplateStore.LookupPartial(name, desc)
if v == nil { if v == nil {
return includeResult{err: fmt.Errorf("partial %q not found", name)} return includeResult{err: fmt.Errorf("partial %q not found", name)}
} }
@ -199,7 +198,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
} }
return includeResult{ return includeResult{
name: templ.Template.Name(), name: templ.Name(),
result: result, result: result,
} }
} }

View file

@ -16,6 +16,7 @@ package tpl
import ( import (
"context" "context"
"slices"
"strings" "strings"
"sync" "sync"
"unicode" "unicode"
@ -41,7 +42,17 @@ type RenderingContext struct {
SiteOutIdx int SiteOutIdx int
} }
type contextKey string type (
contextKey uint8
)
const (
contextKeyDependencyManagerScopedProvider contextKey = iota
contextKeyDependencyScope
contextKeyPage
contextKeyIsInGoldmark
cntextKeyCurrentTemplateInfo
)
// Context manages values passed in the context to templates. // Context manages values passed in the context to templates.
var Context = struct { var Context = struct {
@ -50,11 +61,13 @@ var Context = struct {
DependencyScope hcontext.ContextDispatcher[int] DependencyScope hcontext.ContextDispatcher[int]
Page hcontext.ContextDispatcher[page] Page hcontext.ContextDispatcher[page]
IsInGoldmark hcontext.ContextDispatcher[bool] IsInGoldmark hcontext.ContextDispatcher[bool]
CurrentTemplate hcontext.ContextDispatcher[*CurrentTemplateInfo]
}{ }{
DependencyManagerScopedProvider: hcontext.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKey("DependencyManagerScopedProvider")), DependencyManagerScopedProvider: hcontext.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKeyDependencyManagerScopedProvider),
DependencyScope: hcontext.NewContextDispatcher[int](contextKey("DependencyScope")), DependencyScope: hcontext.NewContextDispatcher[int](contextKeyDependencyScope),
Page: hcontext.NewContextDispatcher[page](contextKey("Page")), Page: hcontext.NewContextDispatcher[page](contextKeyPage),
IsInGoldmark: hcontext.NewContextDispatcher[bool](contextKey("IsInGoldmark")), IsInGoldmark: hcontext.NewContextDispatcher[bool](contextKeyIsInGoldmark),
CurrentTemplate: hcontext.NewContextDispatcher[*CurrentTemplateInfo](cntextKeyCurrentTemplateInfo),
} }
func init() { func init() {
@ -130,3 +143,46 @@ type DeferredExecution struct {
Executed bool Executed bool
Result string Result string
} }
type CurrentTemplateInfoOps interface {
CurrentTemplateInfoCommonOps
Base() CurrentTemplateInfoCommonOps
}
type CurrentTemplateInfoCommonOps interface {
// Template name.
Name() string
// Template source filename.
// Will be empty for internal templates.
Filename() string
}
// CurrentTemplateInfo as returned in templates.Current.
type CurrentTemplateInfo struct {
Parent *CurrentTemplateInfo
CurrentTemplateInfoOps
}
// CurrentTemplateInfos is a slice of CurrentTemplateInfo.
type CurrentTemplateInfos []*CurrentTemplateInfo
// Reverse creates a copy of the slice and reverses it.
func (c CurrentTemplateInfos) Reverse() CurrentTemplateInfos {
if len(c) == 0 {
return c
}
r := make(CurrentTemplateInfos, len(c))
copy(r, c)
slices.Reverse(r)
return r
}
// Ancestors returns the ancestors of the current template.
func (ti *CurrentTemplateInfo) Ancestors() CurrentTemplateInfos {
var ancestors []*CurrentTemplateInfo
for ti.Parent != nil {
ti = ti.Parent
ancestors = append(ancestors, ti)
}
return ancestors
}

View file

@ -102,3 +102,8 @@ func (ns *Namespace) DoDefer(ctx context.Context, id string, optsv any) string {
return id return id
} }
// Get information about the currently executing template.
func (ns *Namespace) Current(ctx context.Context) *tpl.CurrentTemplateInfo {
return tpl.Context.CurrentTemplate.Get(ctx)
}

View file

@ -14,6 +14,7 @@
package templates_test package templates_test
import ( import (
"path/filepath"
"testing" "testing"
"github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib"
@ -127,3 +128,41 @@ Try printf: {{ (try (printf "hello %s" "world")).Value }}
"Try printf: hello world", "Try printf: hello world",
) )
} }
func TestTemplatesCurrent(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/baseof.html --
baseof: {{ block "main" . }}{{ end }}
-- layouts/all.html --
{{ define "main" }}
all.current: {{ templates.Current.Name }}
all.current.filename: {{ templates.Current.Filename }}
all.base: {{ with templates.Current.Base }}{{ .Name }}{{ end }}|
all.parent: {{ with .Parent }}Name: {{ .Name }}{{ end }}|
{{ partial "p1.html" . }}
{{ end }}
-- layouts/_partials/p1.html --
p1.current: {{ with templates.Current }}Name: {{ .Name }}|{{ with .Parent }}Parent.Name: {{ .Name }}{{ end }}{{ end }}|
p1.current.Ancestors: {{ with templates.Current }}{{ range .Ancestors }}{{ .Name }}|{{ end }}{{ end }}
{{ partial "p2.html" . }}
-- layouts/_partials/p2.html --
p2.current: {{ with templates.Current }}Name: {{ .Name }}|{{ with .Parent }}Parent.Name: {{ .Name }}{{ end }}{{ end }}|
p2.current.Ancestors: {{ with templates.Current }}{{ range .Ancestors }}{{ .Name }}|{{ end }}{{ end }}
p3.current.Ancestors.Reverse: {{ with templates.Current }}{{ range .Ancestors.Reverse }}{{ .Name }}|{{ end }}{{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html",
"all.current: all.html",
filepath.FromSlash("all.current.filename: /layouts/all.html"),
"all.base: baseof.html",
"all.parent: |",
"p1.current: Name: _partials/p1.html|Parent.Name: all.html|",
"p1.current.Ancestors: all.html|",
"p2.current.Ancestors: _partials/p1.html|all.html",
)
}

View file

@ -25,9 +25,9 @@ func (t *templateNamespace) readTemplateInto(templ *TemplInfo) error {
if err != nil { if err != nil {
return err return err
} }
templ.Content = removeLeadingBOM(string(b)) templ.content = removeLeadingBOM(string(b))
if !templ.NoBaseOf { if !templ.noBaseOf {
templ.NoBaseOf = !needsBaseTemplate(templ.Content) templ.noBaseOf = !needsBaseTemplate(templ.content)
} }
return nil return nil
}(); err != nil { }(); err != nil {
@ -43,7 +43,7 @@ var embeddedTemplatesAliases = map[string][]string{
} }
func (t *templateNamespace) parseTemplate(ti *TemplInfo) error { func (t *templateNamespace) parseTemplate(ti *TemplInfo) error {
if !ti.NoBaseOf || ti.Category == CategoryBaseof { if !ti.noBaseOf || ti.category == CategoryBaseof {
// Delay parsing until we have the base template. // Delay parsing until we have the base template.
return nil return nil
} }
@ -62,18 +62,18 @@ func (t *templateNamespace) parseTemplate(ti *TemplInfo) error {
if ti.D.IsPlainText { if ti.D.IsPlainText {
prototype := t.parseText prototype := t.parseText
templ, err = prototype.New(name).Parse(ti.Content) templ, err = prototype.New(name).Parse(ti.content)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
prototype := t.parseHTML prototype := t.parseHTML
templ, err = prototype.New(name).Parse(ti.Content) templ, err = prototype.New(name).Parse(ti.content)
if err != nil { if err != nil {
return err return err
} }
if ti.SubCategory == SubCategoryEmbedded { if ti.subCategory == SubCategoryEmbedded {
// In Hugo 0.146.0 we moved the internal templates around. // In Hugo 0.146.0 we moved the internal templates around.
// For the "_internal/twitter_cards.html" style templates, they // For the "_internal/twitter_cards.html" style templates, they
// were moved to the _partials directory. // were moved to the _partials directory.
@ -111,17 +111,17 @@ func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTempla
Base: base.Info, Base: base.Info,
} }
base.Info.Overlays = append(base.Info.Overlays, overlay) base.Info.overlays = append(base.Info.overlays, overlay)
var templ tpl.Template var templ tpl.Template
if overlay.D.IsPlainText { if overlay.D.IsPlainText {
tt := texttemplate.Must(t.parseText.Clone()).New(overlay.PathInfo.PathNoLeadingSlash()) tt := texttemplate.Must(t.parseText.Clone()).New(overlay.PathInfo.PathNoLeadingSlash())
var err error var err error
tt, err = tt.Parse(base.Info.Content) tt, err = tt.Parse(base.Info.content)
if err != nil { if err != nil {
return err return err
} }
tt, err = tt.Parse(overlay.Content) tt, err = tt.Parse(overlay.content)
if err != nil { if err != nil {
return err return err
} }
@ -130,11 +130,11 @@ func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTempla
} else { } else {
tt := htmltemplate.Must(t.parseHTML.CloneShallow()).New(overlay.PathInfo.PathNoLeadingSlash()) tt := htmltemplate.Must(t.parseHTML.CloneShallow()).New(overlay.PathInfo.PathNoLeadingSlash())
var err error var err error
tt, err = tt.Parse(base.Info.Content) tt, err = tt.Parse(base.Info.content)
if err != nil { if err != nil {
return err return err
} }
tt, err = tt.Parse(overlay.Content) tt, err = tt.Parse(overlay.content)
if err != nil { if err != nil {
return err return err
} }
@ -146,17 +146,17 @@ func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTempla
tb.Template = &TemplInfo{ tb.Template = &TemplInfo{
Template: templ, Template: templ,
Base: base.Info, base: base.Info,
PathInfo: overlay.PathInfo, PathInfo: overlay.PathInfo,
Fi: overlay.Fi, Fi: overlay.Fi,
D: overlay.D, D: overlay.D,
NoBaseOf: true, noBaseOf: true,
} }
variants := overlay.BaseVariants.Get(base.Key) variants := overlay.baseVariants.Get(base.Key)
if variants == nil { if variants == nil {
variants = make(map[TemplateDescriptor]*TemplWithBaseApplied) variants = make(map[TemplateDescriptor]*TemplWithBaseApplied)
overlay.BaseVariants.Insert(base.Key, variants) overlay.baseVariants.Insert(base.Key, variants)
} }
variants[base.Info.D] = tb variants[base.Info.D] = tb
return nil return nil

View file

@ -97,15 +97,16 @@ func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) {
panic("HTML output format not found") panic("HTML output format not found")
} }
s := &TemplateStore{ s := &TemplateStore{
opts: opts, opts: opts,
siteOpts: siteOpts, siteOpts: siteOpts,
optsOrig: opts, optsOrig: opts,
siteOptsOrig: siteOpts, siteOptsOrig: siteOpts,
htmlFormat: html, htmlFormat: html,
storeSite: configureSiteStorage(siteOpts, opts.Watching), storeSite: configureSiteStorage(siteOpts, opts.Watching),
treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](), treeMain: doctree.NewSimpleTree[map[nodeKey]*TemplInfo](),
treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](), treeShortcodes: doctree.NewSimpleTree[map[string]map[TemplateDescriptor]*TemplInfo](),
templatesByPath: maps.NewCache[string, *TemplInfo](), templatesByPath: maps.NewCache[string, *TemplInfo](),
templateDescriptorByPath: maps.NewCache[string, PathTemplateDescriptor](),
// Note that the funcs passed below is just for name validation. // Note that the funcs passed below is just for name validation.
tns: newTemplateNamespace(siteOpts.TemplateFuncs), tns: newTemplateNamespace(siteOpts.TemplateFuncs),
@ -191,9 +192,9 @@ type SubCategory int
type TemplInfo struct { type TemplInfo struct {
// The category of this template. // The category of this template.
Category Category category Category
SubCategory SubCategory subCategory SubCategory
// PathInfo info. // PathInfo info.
PathInfo *paths.Path PathInfo *paths.Path
@ -202,7 +203,7 @@ type TemplInfo struct {
Fi hugofs.FileMetaInfo Fi hugofs.FileMetaInfo
// The template content with any leading BOM removed. // The template content with any leading BOM removed.
Content string content string
// The parsed template. // The parsed template.
// Note that any baseof template will be applied later. // Note that any baseof template will be applied later.
@ -210,16 +211,16 @@ type TemplInfo struct {
// If no baseof is needed, this will be set to true. // If no baseof is needed, this will be set to true.
// E.g. shortcode templates do not need a baseof. // E.g. shortcode templates do not need a baseof.
NoBaseOf bool noBaseOf bool
// If NoBaseOf is false, we will look for the final template in this tree. // If NoBaseOf is false, we will look for the final template in this tree.
BaseVariants *doctree.SimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied] baseVariants *doctree.SimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]
// The template variants that are based on this template. // The template variants that are based on this template.
Overlays []*TemplInfo overlays []*TemplInfo
// The base template used, if any. // The base template used, if any.
Base *TemplInfo base *TemplInfo
// The descriptior that this template represents. // The descriptior that this template represents.
D TemplateDescriptor D TemplateDescriptor
@ -228,16 +229,20 @@ type TemplInfo struct {
ParseInfo ParseInfo ParseInfo ParseInfo
// The execution counter for this template. // The execution counter for this template.
ExecutionCounter atomic.Uint64 executionCounter atomic.Uint64
// processing state. // processing state.
state processingState state processingState
isLegacyMapped bool isLegacyMapped bool
} }
func (ti *TemplInfo) SubCategory() SubCategory {
return ti.subCategory
}
func (ti *TemplInfo) BaseVariantsSeq() iter.Seq[*TemplWithBaseApplied] { func (ti *TemplInfo) BaseVariantsSeq() iter.Seq[*TemplWithBaseApplied] {
return func(yield func(*TemplWithBaseApplied) bool) { return func(yield func(*TemplWithBaseApplied) bool) {
ti.BaseVariants.Walk(func(key string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) { ti.baseVariants.Walk(func(key string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
for _, vv := range v { for _, vv := range v {
if !yield(vv) { if !yield(vv) {
return true, nil return true, nil
@ -260,6 +265,11 @@ func (t *TemplInfo) GetIdentity() identity.Identity {
} }
func (ti *TemplInfo) Name() string { func (ti *TemplInfo) Name() string {
if ti.Template == nil {
if ti.PathInfo != nil {
return ti.PathInfo.PathNoLeadingSlash()
}
}
return ti.Template.Name() return ti.Template.Name()
} }
@ -272,7 +282,7 @@ func (t *TemplInfo) IsProbablyDependency(other identity.Identity) bool {
} }
func (t *TemplInfo) IsProbablyDependent(other identity.Identity) bool { func (t *TemplInfo) IsProbablyDependent(other identity.Identity) bool {
for _, overlay := range t.Overlays { for _, overlay := range t.overlays {
if overlay.isProbablyTheSameIDAs(other) { if overlay.isProbablyTheSameIDAs(other) {
return true return true
} }
@ -288,11 +298,11 @@ func (ti *TemplInfo) String() string {
} }
func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, k1 string, slashCountK1 int, best *bestMatch) { func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, k1 string, slashCountK1 int, best *bestMatch) {
if ti.BaseVariants == nil { if ti.baseVariants == nil {
return return
} }
ti.BaseVariants.WalkPath(k1, func(k2 string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) { ti.baseVariants.WalkPath(k1, func(k2 string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) {
slashCountK2 := strings.Count(k2, "/") slashCountK2 := strings.Count(k2, "/")
distance := slashCountK1 - slashCountK2 distance := slashCountK1 - slashCountK2
@ -319,6 +329,18 @@ func (t *TemplInfo) isProbablyTheSameIDAs(other identity.Identity) bool {
return false 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 { type TemplWithBaseApplied struct {
// The template that's overlaid on top of the base template. // The template that's overlaid on top of the base template.
Overlay *TemplInfo Overlay *TemplInfo
@ -378,9 +400,10 @@ type TemplateStore struct {
siteOpts SiteOptions siteOpts SiteOptions
htmlFormat output.Format htmlFormat output.Format
treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo] treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo]
treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo] treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo]
templatesByPath *maps.Cache[string, *TemplInfo] templatesByPath *maps.Cache[string, *TemplInfo]
templateDescriptorByPath *maps.Cache[string, PathTemplateDescriptor]
dh descriptorHandler dh descriptorHandler
@ -414,7 +437,7 @@ func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc Te
descBaseof := desc descBaseof := desc
s.treeMain.Walk(func(k string, v map[nodeKey]*TemplInfo) (bool, error) { s.treeMain.Walk(func(k string, v map[nodeKey]*TemplInfo) (bool, error) {
for _, vv := range v { for _, vv := range v {
if vv.Category != CategoryBaseof { if vv.category != CategoryBaseof {
continue continue
} }
@ -430,14 +453,21 @@ func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc Te
func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error { func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error {
defer func() { defer func() {
ti.ExecutionCounter.Add(1) ti.executionCounter.Add(1)
if ti.Base != nil { if ti.base != nil {
ti.Base.ExecutionCounter.Add(1) ti.base.executionCounter.Add(1)
} }
}() }()
templ := ti.Template 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 { if t.opts.Metrics != nil {
defer t.opts.Metrics.MeasureSince(templ.Name(), time.Now()) defer t.opts.Metrics.MeasureSince(templ.Name(), time.Now())
} }
@ -498,7 +528,7 @@ func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo {
return nil return nil
} }
m := best1.templ m := best1.templ
if m.NoBaseOf { if m.noBaseOf {
return m return m
} }
best1.reset() best1.reset()
@ -509,13 +539,15 @@ func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo {
return best1.templ return best1.templ
} }
func (s *TemplateStore) LookupPartial(pth string, desc TemplateDescriptor) *TemplInfo { func (s *TemplateStore) LookupPartial(pth string) *TemplInfo {
d := s.templateDescriptorFromPath(pth)
desc := d.Desc
if desc.Layout != "" { if desc.Layout != "" {
panic("shortcode template descriptor must not have a layout") panic("shortcode template descriptor must not have a layout")
} }
best := s.getBest() best := s.getBest()
defer s.putBest(best) defer s.putBest(best)
s.findBestMatchGet(s.key(path.Join(containerPartials, pth)), CategoryPartial, nil, desc, best) s.findBestMatchGet(s.key(path.Join(containerPartials, d.Path)), CategoryPartial, nil, desc, best)
return best.templ return best.templ
} }
@ -564,10 +596,10 @@ func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer
printOne := func(key string, vv *TemplInfo) { printOne := func(key string, vv *TemplInfo) {
level := strings.Count(key, "/") level := strings.Count(key, "/")
if category != vv.Category { if category != vv.category {
return return
} }
s := strings.ReplaceAll(strings.TrimSpace(vv.Content), "\n", " ") s := strings.ReplaceAll(strings.TrimSpace(vv.content), "\n", " ")
ts := fmt.Sprintf("kind: %q layout: %q content: %.30s", vv.D.Kind, vv.D.Layout, s) ts := fmt.Sprintf("kind: %q layout: %q content: %.30s", vv.D.Kind, vv.D.Layout, s)
fmt.Fprintf(w, "%s%s %s\n", strings.Repeat(" ", level), key, ts) fmt.Fprintf(w, "%s%s %s\n", strings.Repeat(" ", level), key, ts)
} }
@ -642,17 +674,17 @@ func (t *TemplateStore) UnusedTemplates() []*TemplInfo {
var unused []*TemplInfo var unused []*TemplInfo
for vv := range t.templates() { for vv := range t.templates() {
if vv.SubCategory != SubCategoryMain { if vv.subCategory != SubCategoryMain {
// Skip inline partials and internal templates. // Skip inline partials and internal templates.
continue continue
} }
if vv.NoBaseOf { if vv.noBaseOf {
if vv.ExecutionCounter.Load() == 0 { if vv.executionCounter.Load() == 0 {
unused = append(unused, vv) unused = append(unused, vv)
} }
} else { } else {
for vvv := range vv.BaseVariantsSeq() { for vvv := range vv.BaseVariantsSeq() {
if vvv.Template.ExecutionCounter.Load() == 0 { if vvv.Template.executionCounter.Load() == 0 {
unused = append(unused, vvv.Template) unused = append(unused, vvv.Template)
} }
} }
@ -681,7 +713,7 @@ func (s *TemplateStore) findBestMatchGet(key string, category Category, consider
} }
for k, vv := range v { for k, vv := range v {
if vv.Category != category { if vv.category != category {
continue continue
} }
@ -702,7 +734,7 @@ func (s *TemplateStore) findBestMatchWalkPath(q TemplateQuery, k1 string, slashC
distance := slashCountK1 - slashCountK2 distance := slashCountK1 - slashCountK2
for k, vv := range v { for k, vv := range v {
if vv.Category != q.Category { if vv.category != q.Category {
continue continue
} }
@ -803,8 +835,8 @@ func (s *TemplateStore) addFileContext(ti *TemplInfo, inerr error) error {
return err return err
} }
if ti.Base != nil { if ti.base != nil {
if err, ok := checkFilename(ti.Base.Fi, inerr); ok { if err, ok := checkFilename(ti.base.Fi, inerr); ok {
return err return err
} }
} }
@ -850,8 +882,8 @@ func (s *TemplateStore) extractInlinePartials() error {
if ti != nil { if ti != nil {
ti.Template = templ ti.Template = templ
ti.NoBaseOf = true ti.noBaseOf = true
ti.SubCategory = SubCategoryInline ti.subCategory = SubCategoryInline
ti.D.IsPlainText = isText ti.D.IsPlainText = isText
} }
@ -913,9 +945,9 @@ func (s *TemplateStore) insertEmbedded() error {
if ti != nil { if ti != nil {
// Currently none of the embedded templates need a baseof template. // Currently none of the embedded templates need a baseof template.
ti.NoBaseOf = true ti.noBaseOf = true
ti.Content = content ti.content = content
ti.SubCategory = SubCategoryEmbedded ti.subCategory = SubCategoryEmbedded
} }
return nil return nil
@ -965,8 +997,8 @@ func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo,
PathInfo: pi, PathInfo: pi,
Fi: fi, Fi: fi,
D: d, D: d,
Category: CategoryShortcode, category: CategoryShortcode,
NoBaseOf: true, noBaseOf: true,
} }
m1[d] = ti m1[d] = ti
@ -1022,8 +1054,8 @@ func (s *TemplateStore) insertTemplate2(
PathInfo: pi, PathInfo: pi,
Fi: fi, Fi: fi,
D: d, D: d,
Category: category, category: category,
NoBaseOf: category > CategoryLayout, noBaseOf: category > CategoryLayout,
isLegacyMapped: isLegacyMapped, isLegacyMapped: isLegacyMapped,
} }
@ -1231,7 +1263,7 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo
s.tns.baseofTextClones = nil s.tns.baseofTextClones = nil
s.treeMain.Walk(func(key string, v map[nodeKey]*TemplInfo) (bool, error) { s.treeMain.Walk(func(key string, v map[nodeKey]*TemplInfo) (bool, error) {
for _, vv := range v { for _, vv := range v {
if !vv.NoBaseOf { if !vv.noBaseOf {
vv.state = processingStateInitial vv.state = processingStateInitial
} }
} }
@ -1270,20 +1302,20 @@ func (s *TemplateStore) parseTemplates() error {
if vv.state == processingStateTransformed { if vv.state == processingStateTransformed {
continue continue
} }
if !vv.NoBaseOf { if !vv.noBaseOf {
d := vv.D d := vv.D
// Find all compatible base templates. // Find all compatible base templates.
baseTemplates := s.FindAllBaseTemplateCandidates(key, d) baseTemplates := s.FindAllBaseTemplateCandidates(key, d)
if len(baseTemplates) == 0 { if len(baseTemplates) == 0 {
// The regular expression used to detect if a template needs a base template has some // The regular expression used to detect if a template needs a base template has some
// rare false positives. Assume we don't need one. // rare false positives. Assume we don't need one.
vv.NoBaseOf = true vv.noBaseOf = true
if err := s.tns.parseTemplate(vv); err != nil { if err := s.tns.parseTemplate(vv); err != nil {
return err return err
} }
continue continue
} }
vv.BaseVariants = doctree.NewSimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]() vv.baseVariants = doctree.NewSimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]()
for _, base := range baseTemplates { for _, base := range baseTemplates {
if err := s.tns.applyBaseTemplate(vv, base); err != nil { if err := s.tns.applyBaseTemplate(vv, base); err != nil {
@ -1320,7 +1352,7 @@ func (s *TemplateStore) parseTemplates() error {
// prepareTemplates prepares all templates for execution. // prepareTemplates prepares all templates for execution.
func (s *TemplateStore) prepareTemplates() error { func (s *TemplateStore) prepareTemplates() error {
for t := range s.templates() { for t := range s.templates() {
if t.Category == CategoryBaseof { if t.category == CategoryBaseof {
continue continue
} }
if _, err := t.Prepare(); err != nil { if _, err := t.Prepare(); err != nil {
@ -1330,38 +1362,51 @@ func (s *TemplateStore) prepareTemplates() error {
return nil return nil
} }
// TemplateDescriptorFromPath returns a template descriptor from the given path. type PathTemplateDescriptor struct {
Path string
Desc TemplateDescriptor
}
// templateDescriptorFromPath returns a template descriptor from the given path.
// This is currently used in partial lookups only. // This is currently used in partial lookups only.
func (s *TemplateStore) TemplateDescriptorFromPath(pth string) (string, TemplateDescriptor) { func (s *TemplateStore) templateDescriptorFromPath(pth string) PathTemplateDescriptor {
var ( // Check cache first.
mt media.Type d, _ := s.templateDescriptorByPath.GetOrCreate(pth, func() (PathTemplateDescriptor, error) {
of output.Format var (
) mt media.Type
of output.Format
)
// Common cases. // Common cases.
dotCount := strings.Count(pth, ".") dotCount := strings.Count(pth, ".")
if dotCount <= 1 { if dotCount <= 1 {
if dotCount == 0 { if dotCount == 0 {
// Asume HTML. // Asume HTML.
of, mt = s.resolveOutputFormatAndOrMediaType("html", "") of, mt = s.resolveOutputFormatAndOrMediaType("html", "")
} else {
pth = strings.TrimPrefix(pth, "/")
ext := path.Ext(pth)
pth = strings.TrimSuffix(pth, ext)
ext = ext[1:]
of, mt = s.resolveOutputFormatAndOrMediaType("", ext)
}
} else { } else {
pth = strings.TrimPrefix(pth, "/") path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth)
ext := path.Ext(pth) pth = path.PathNoIdentifier()
pth = strings.TrimSuffix(pth, ext) of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext())
ext = ext[1:]
of, mt = s.resolveOutputFormatAndOrMediaType("", ext)
} }
} else {
path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth)
pth = path.PathNoIdentifier()
of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext())
}
return pth, TemplateDescriptor{ return PathTemplateDescriptor{
OutputFormat: of.Name, Path: pth,
MediaType: mt.Type, Desc: TemplateDescriptor{
IsPlainText: of.IsPlainText, OutputFormat: of.Name,
} MediaType: mt.Type,
IsPlainText: of.IsPlainText,
},
}, nil
})
return d
} }
// resolveOutputFormatAndOrMediaType resolves the output format and/or media type // resolveOutputFormatAndOrMediaType resolves the output format and/or media type
@ -1404,7 +1449,7 @@ func (s *TemplateStore) templates() iter.Seq[*TemplInfo] {
return func(yield func(*TemplInfo) bool) { return func(yield func(*TemplInfo) bool) {
for _, v := range s.treeMain.All() { for _, v := range s.treeMain.All() {
for _, vv := range v { for _, vv := range v {
if !vv.NoBaseOf { if !vv.noBaseOf {
for vvv := range vv.BaseVariantsSeq() { for vvv := range vv.BaseVariantsSeq() {
if !yield(vvv.Template) { if !yield(vvv.Template) {
return return
@ -1562,10 +1607,10 @@ func (s *TemplateStore) transformTemplates() error {
continue continue
} }
vv.state = processingStateTransformed vv.state = processingStateTransformed
if vv.Category == CategoryBaseof { if vv.category == CategoryBaseof {
continue continue
} }
if !vv.NoBaseOf { if !vv.noBaseOf {
for vvv := range vv.BaseVariantsSeq() { for vvv := range vv.BaseVariantsSeq() {
tctx, err := applyTemplateTransformers(vvv.Template, lookup) tctx, err := applyTemplateTransformers(vvv.Template, lookup)
if err != nil { if err != nil {
@ -1709,13 +1754,13 @@ func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool {
} }
if best.w.w1 > 0 { if best.w.w1 > 0 {
currentBestIsEmbedded := best.templ.SubCategory == SubCategoryEmbedded currentBestIsEmbedded := best.templ.subCategory == SubCategoryEmbedded
if currentBestIsEmbedded { if currentBestIsEmbedded {
if ti.SubCategory != SubCategoryEmbedded { if ti.subCategory != SubCategoryEmbedded {
return true return true
} }
} else { } else {
if ti.SubCategory == SubCategoryEmbedded { if ti.subCategory == SubCategoryEmbedded {
// Prefer user provided template. // Prefer user provided template.
return false return false
} }

View file

@ -1,6 +1,8 @@
package tplimpl_test package tplimpl_test
import ( import (
"context"
"io"
"testing" "testing"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
@ -840,3 +842,42 @@ All.
b.AssertLogContains("Duplicate content path") b.AssertLogContains("Duplicate content path")
} }
func BenchmarkExecuteWithContext(b *testing.B) {
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "home"]
-- layouts/all.html --
{{ .Title }}|
{{ partial "p1.html" . }}
-- layouts/_partials/p1.html --
p1.
{{ partial "p2.html" . }}
{{ partial "p2.html" . }}
{{ partial "p3.html" . }}
{{ partial "p2.html" . }}
{{ partial "p2.html" . }}
{{ partial "p2.html" . }}
{{ partial "p3.html" . }}
-- layouts/_partials/p2.html --
{{ partial "p3.html" . }}
-- layouts/_partials/p3.html --
p3
-- content/p1.md --
`
bb := hugolib.Test(b, files)
store := bb.H.TemplateStore
ti := store.LookupByPath("/all.html")
bb.Assert(ti, qt.Not(qt.IsNil))
p := bb.H.Sites[0].RegularPages()[0]
bb.Assert(p, qt.Not(qt.IsNil))
b.ResetTimer()
for i := 0; i < b.N; i++ {
err := store.ExecuteWithContext(context.Background(), ti, io.Discard, p)
bb.Assert(err, qt.IsNil)
}
}

View file

@ -263,7 +263,7 @@ func (c *templateTransformContext) hasIdent(idents []string, ident string) bool
// //
// {{ $_hugo_config:= `{ "version": 1 }` }} // {{ $_hugo_config:= `{ "version": 1 }` }}
func (c *templateTransformContext) collectConfig(n *parse.PipeNode) { func (c *templateTransformContext) collectConfig(n *parse.PipeNode) {
if c.t.Category != CategoryShortcode { if c.t.category != CategoryShortcode {
return return
} }
if c.configChecked { if c.configChecked {
@ -304,7 +304,7 @@ func (c *templateTransformContext) collectConfig(n *parse.PipeNode) {
// collectInner determines if the given CommandNode represents a // collectInner determines if the given CommandNode represents a
// shortcode call to its .Inner. // shortcode call to its .Inner.
func (c *templateTransformContext) collectInner(n *parse.CommandNode) { func (c *templateTransformContext) collectInner(n *parse.CommandNode) {
if c.t.Category != CategoryShortcode { if c.t.category != CategoryShortcode {
return return
} }
if c.t.ParseInfo.IsInner || len(n.Args) == 0 { if c.t.ParseInfo.IsInner || len(n.Args) == 0 {
@ -328,7 +328,7 @@ func (c *templateTransformContext) collectInner(n *parse.CommandNode) {
} }
func (c *templateTransformContext) collectReturnNode(n *parse.CommandNode) bool { func (c *templateTransformContext) collectReturnNode(n *parse.CommandNode) bool {
if c.t.Category != CategoryPartial || c.returnNode != nil { if c.t.category != CategoryPartial || c.returnNode != nil {
return true return true
} }