diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go index 282db26cc..764a86a97 100644 --- a/common/hugo/hugo.go +++ b/common/hugo/hugo.go @@ -139,9 +139,13 @@ func (i HugoInfo) IsMultilingual() bool { 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{} diff --git a/hugolib/page__content.go b/hugolib/page__content.go index b42c2b419..5f7d6f930 100644 --- a/hugolib/page__content.go +++ b/hugolib/page__content.go @@ -667,7 +667,13 @@ func (c *cachedContentScope) mustContentToC(ctx context.Context) contentTableOfC 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) { cp := c.pco diff --git a/hugolib/page__per_output.go b/hugolib/page__per_output.go index 3add01408..71bc51828 100644 --- a/hugolib/page__per_output.go +++ b/hugolib/page__per_output.go @@ -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. res, err := executeToString(ctx, pco.po.p.s.GetTemplateStore(), templ, pco.po.p) 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 } @@ -323,7 +323,7 @@ func (pco *pageContentOutput) initRenderHooks() error { return false } - if ignoreInternal && candidate.SubCategory == tplimpl.SubCategoryEmbedded { + if ignoreInternal && candidate.SubCategory() == tplimpl.SubCategoryEmbedded { // Don't consider the internal hook templates. return false } diff --git a/hugolib/site_render.go b/hugolib/site_render.go index ed22d4258..6dbb19827 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -168,7 +168,7 @@ func pageRenderer( s.Log.Trace( 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) }, ) diff --git a/resources/resource_factories/create/create.go b/resources/resource_factories/create/create.go index 1d78a2923..2aecb5a93 100644 --- a/resources/resource_factories/create/create.go +++ b/resources/resource_factories/create/create.go @@ -55,12 +55,16 @@ type Client struct { remoteResourceLogger logg.LevelLogger } -type contextKey string +type contextKey uint8 + +const ( + contextKeyResourceID contextKey = iota +) // New creates a new Client with the given specification. func New(rs *resources.Spec) *Client { fileCache := rs.FileCaches.GetResourceCache() - resourceIDDispatcher := hcontext.NewContextDispatcher[string](contextKey("resourceID")) + resourceIDDispatcher := hcontext.NewContextDispatcher[string](contextKeyResourceID) httpCacheConfig := rs.Cfg.GetConfigSection("httpCacheCompiled").(hhttpcache.ConfigCompiled) var remoteResourceChecker *tasks.RunEvery if rs.Cfg.Watching() && !httpCacheConfig.IsPollingDisabled() { diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go index 27f13253a..b9ef4b244 100644 --- a/tpl/partials/partials.go +++ b/tpl/partials/partials.go @@ -157,8 +157,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any) if len(dataList) > 0 { data = dataList[0] } - name, desc := ns.deps.TemplateStore.TemplateDescriptorFromPath(name) - v := ns.deps.TemplateStore.LookupPartial(name, desc) + v := ns.deps.TemplateStore.LookupPartial(name) if v == nil { 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{ - name: templ.Template.Name(), + name: templ.Name(), result: result, } } diff --git a/tpl/template.go b/tpl/template.go index 865fea52e..f69ae2210 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -16,6 +16,7 @@ package tpl import ( "context" + "slices" "strings" "sync" "unicode" @@ -41,7 +42,17 @@ type RenderingContext struct { 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. var Context = struct { @@ -50,11 +61,13 @@ var Context = struct { DependencyScope hcontext.ContextDispatcher[int] Page hcontext.ContextDispatcher[page] IsInGoldmark hcontext.ContextDispatcher[bool] + CurrentTemplate hcontext.ContextDispatcher[*CurrentTemplateInfo] }{ - DependencyManagerScopedProvider: hcontext.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKey("DependencyManagerScopedProvider")), - DependencyScope: hcontext.NewContextDispatcher[int](contextKey("DependencyScope")), - Page: hcontext.NewContextDispatcher[page](contextKey("Page")), - IsInGoldmark: hcontext.NewContextDispatcher[bool](contextKey("IsInGoldmark")), + DependencyManagerScopedProvider: hcontext.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKeyDependencyManagerScopedProvider), + DependencyScope: hcontext.NewContextDispatcher[int](contextKeyDependencyScope), + Page: hcontext.NewContextDispatcher[page](contextKeyPage), + IsInGoldmark: hcontext.NewContextDispatcher[bool](contextKeyIsInGoldmark), + CurrentTemplate: hcontext.NewContextDispatcher[*CurrentTemplateInfo](cntextKeyCurrentTemplateInfo), } func init() { @@ -130,3 +143,46 @@ type DeferredExecution struct { Executed bool 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 +} diff --git a/tpl/templates/templates.go b/tpl/templates/templates.go index 98ae0a639..92165e824 100644 --- a/tpl/templates/templates.go +++ b/tpl/templates/templates.go @@ -102,3 +102,8 @@ func (ns *Namespace) DoDefer(ctx context.Context, id string, optsv any) string { return id } + +// Get information about the currently executing template. +func (ns *Namespace) Current(ctx context.Context) *tpl.CurrentTemplateInfo { + return tpl.Context.CurrentTemplate.Get(ctx) +} diff --git a/tpl/templates/templates_integration_test.go b/tpl/templates/templates_integration_test.go index aa5540862..a0dcf0348 100644 --- a/tpl/templates/templates_integration_test.go +++ b/tpl/templates/templates_integration_test.go @@ -14,6 +14,7 @@ package templates_test import ( + "path/filepath" "testing" "github.com/gohugoio/hugo/hugolib" @@ -127,3 +128,41 @@ Try printf: {{ (try (printf "hello %s" "world")).Value }} "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", + ) +} diff --git a/tpl/tplimpl/templates.go b/tpl/tplimpl/templates.go index 57012f6e3..1f911b9a5 100644 --- a/tpl/tplimpl/templates.go +++ b/tpl/tplimpl/templates.go @@ -25,9 +25,9 @@ func (t *templateNamespace) readTemplateInto(templ *TemplInfo) error { if err != nil { return err } - templ.Content = removeLeadingBOM(string(b)) - if !templ.NoBaseOf { - templ.NoBaseOf = !needsBaseTemplate(templ.Content) + templ.content = removeLeadingBOM(string(b)) + if !templ.noBaseOf { + templ.noBaseOf = !needsBaseTemplate(templ.content) } return nil }(); err != nil { @@ -43,7 +43,7 @@ var embeddedTemplatesAliases = map[string][]string{ } 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. return nil } @@ -62,18 +62,18 @@ func (t *templateNamespace) parseTemplate(ti *TemplInfo) error { if ti.D.IsPlainText { prototype := t.parseText - templ, err = prototype.New(name).Parse(ti.Content) + templ, err = prototype.New(name).Parse(ti.content) if err != nil { return err } } else { prototype := t.parseHTML - templ, err = prototype.New(name).Parse(ti.Content) + templ, err = prototype.New(name).Parse(ti.content) if err != nil { return err } - if ti.SubCategory == SubCategoryEmbedded { + if ti.subCategory == SubCategoryEmbedded { // In Hugo 0.146.0 we moved the internal templates around. // For the "_internal/twitter_cards.html" style templates, they // were moved to the _partials directory. @@ -111,17 +111,17 @@ func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTempla Base: base.Info, } - base.Info.Overlays = append(base.Info.Overlays, overlay) + base.Info.overlays = append(base.Info.overlays, overlay) var templ tpl.Template if overlay.D.IsPlainText { tt := texttemplate.Must(t.parseText.Clone()).New(overlay.PathInfo.PathNoLeadingSlash()) var err error - tt, err = tt.Parse(base.Info.Content) + tt, err = tt.Parse(base.Info.content) if err != nil { return err } - tt, err = tt.Parse(overlay.Content) + tt, err = tt.Parse(overlay.content) if err != nil { return err } @@ -130,11 +130,11 @@ func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTempla } else { tt := htmltemplate.Must(t.parseHTML.CloneShallow()).New(overlay.PathInfo.PathNoLeadingSlash()) var err error - tt, err = tt.Parse(base.Info.Content) + tt, err = tt.Parse(base.Info.content) if err != nil { return err } - tt, err = tt.Parse(overlay.Content) + tt, err = tt.Parse(overlay.content) if err != nil { return err } @@ -146,17 +146,17 @@ func (t *templateNamespace) applyBaseTemplate(overlay *TemplInfo, base keyTempla tb.Template = &TemplInfo{ Template: templ, - Base: base.Info, + base: base.Info, PathInfo: overlay.PathInfo, Fi: overlay.Fi, D: overlay.D, - NoBaseOf: true, + noBaseOf: true, } - variants := overlay.BaseVariants.Get(base.Key) + variants := overlay.baseVariants.Get(base.Key) if variants == nil { variants = make(map[TemplateDescriptor]*TemplWithBaseApplied) - overlay.BaseVariants.Insert(base.Key, variants) + overlay.baseVariants.Insert(base.Key, variants) } variants[base.Info.D] = tb return nil diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go index b0c30b98a..fe3180660 100644 --- a/tpl/tplimpl/templatestore.go +++ b/tpl/tplimpl/templatestore.go @@ -97,15 +97,16 @@ func NewStore(opts StoreOptions, siteOpts SiteOptions) (*TemplateStore, error) { 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](), + 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](), + templateDescriptorByPath: maps.NewCache[string, PathTemplateDescriptor](), // Note that the funcs passed below is just for name validation. tns: newTemplateNamespace(siteOpts.TemplateFuncs), @@ -191,9 +192,9 @@ type SubCategory int type TemplInfo struct { // The category of this template. - Category Category + category Category - SubCategory SubCategory + subCategory SubCategory // PathInfo info. PathInfo *paths.Path @@ -202,7 +203,7 @@ type TemplInfo struct { Fi hugofs.FileMetaInfo // The template content with any leading BOM removed. - Content string + content string // The parsed template. // 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. // 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. - BaseVariants *doctree.SimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied] + baseVariants *doctree.SimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied] // The template variants that are based on this template. - Overlays []*TemplInfo + overlays []*TemplInfo // The base template used, if any. - Base *TemplInfo + base *TemplInfo // The descriptior that this template represents. D TemplateDescriptor @@ -228,16 +229,20 @@ type TemplInfo struct { ParseInfo ParseInfo // The execution counter for this template. - ExecutionCounter atomic.Uint64 + 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) { + ti.baseVariants.Walk(func(key string, v map[TemplateDescriptor]*TemplWithBaseApplied) (bool, error) { for _, vv := range v { if !yield(vv) { return true, nil @@ -260,6 +265,11 @@ func (t *TemplInfo) GetIdentity() identity.Identity { } func (ti *TemplInfo) Name() string { + if ti.Template == nil { + if ti.PathInfo != nil { + return ti.PathInfo.PathNoLeadingSlash() + } + } return ti.Template.Name() } @@ -272,7 +282,7 @@ func (t *TemplInfo) IsProbablyDependency(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) { return true } @@ -288,11 +298,11 @@ func (ti *TemplInfo) String() string { } func (ti *TemplInfo) findBestMatchBaseof(s *TemplateStore, k1 string, slashCountK1 int, best *bestMatch) { - if ti.BaseVariants == nil { + if ti.baseVariants == nil { 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, "/") distance := slashCountK1 - slashCountK2 @@ -319,6 +329,18 @@ func (t *TemplInfo) isProbablyTheSameIDAs(other identity.Identity) bool { 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 @@ -378,9 +400,10 @@ type TemplateStore struct { siteOpts SiteOptions htmlFormat output.Format - treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo] - treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo] - templatesByPath *maps.Cache[string, *TemplInfo] + treeMain *doctree.SimpleTree[map[nodeKey]*TemplInfo] + treeShortcodes *doctree.SimpleTree[map[string]map[TemplateDescriptor]*TemplInfo] + templatesByPath *maps.Cache[string, *TemplInfo] + templateDescriptorByPath *maps.Cache[string, PathTemplateDescriptor] dh descriptorHandler @@ -414,7 +437,7 @@ func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, desc Te descBaseof := desc s.treeMain.Walk(func(k string, v map[nodeKey]*TemplInfo) (bool, error) { for _, vv := range v { - if vv.Category != CategoryBaseof { + if vv.category != CategoryBaseof { 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 { defer func() { - ti.ExecutionCounter.Add(1) - if ti.Base != nil { - ti.Base.ExecutionCounter.Add(1) + 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()) } @@ -498,7 +528,7 @@ func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo { return nil } m := best1.templ - if m.NoBaseOf { + if m.noBaseOf { return m } best1.reset() @@ -509,13 +539,15 @@ func (s *TemplateStore) LookupPagesLayout(q TemplateQuery) *TemplInfo { 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 != "" { panic("shortcode template descriptor must not have a layout") } best := s.getBest() 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 } @@ -564,10 +596,10 @@ func (s *TemplateStore) PrintDebug(prefix string, category Category, w io.Writer printOne := func(key string, vv *TemplInfo) { level := strings.Count(key, "/") - if category != vv.Category { + if category != vv.category { 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) fmt.Fprintf(w, "%s%s %s\n", strings.Repeat(" ", level), key, ts) } @@ -642,17 +674,17 @@ func (t *TemplateStore) UnusedTemplates() []*TemplInfo { var unused []*TemplInfo for vv := range t.templates() { - if vv.SubCategory != SubCategoryMain { + if vv.subCategory != SubCategoryMain { // Skip inline partials and internal templates. continue } - if vv.NoBaseOf { - if vv.ExecutionCounter.Load() == 0 { + if vv.noBaseOf { + if vv.executionCounter.Load() == 0 { unused = append(unused, vv) } } else { for vvv := range vv.BaseVariantsSeq() { - if vvv.Template.ExecutionCounter.Load() == 0 { + if vvv.Template.executionCounter.Load() == 0 { unused = append(unused, vvv.Template) } } @@ -681,7 +713,7 @@ func (s *TemplateStore) findBestMatchGet(key string, category Category, consider } for k, vv := range v { - if vv.Category != category { + if vv.category != category { continue } @@ -702,7 +734,7 @@ func (s *TemplateStore) findBestMatchWalkPath(q TemplateQuery, k1 string, slashC distance := slashCountK1 - slashCountK2 for k, vv := range v { - if vv.Category != q.Category { + if vv.category != q.Category { continue } @@ -803,8 +835,8 @@ func (s *TemplateStore) addFileContext(ti *TemplInfo, inerr error) error { return err } - if ti.Base != nil { - if err, ok := checkFilename(ti.Base.Fi, inerr); ok { + if ti.base != nil { + if err, ok := checkFilename(ti.base.Fi, inerr); ok { return err } } @@ -850,8 +882,8 @@ func (s *TemplateStore) extractInlinePartials() error { if ti != nil { ti.Template = templ - ti.NoBaseOf = true - ti.SubCategory = SubCategoryInline + ti.noBaseOf = true + ti.subCategory = SubCategoryInline ti.D.IsPlainText = isText } @@ -913,9 +945,9 @@ func (s *TemplateStore) insertEmbedded() error { if ti != nil { // Currently none of the embedded templates need a baseof template. - ti.NoBaseOf = true - ti.Content = content - ti.SubCategory = SubCategoryEmbedded + ti.noBaseOf = true + ti.content = content + ti.subCategory = SubCategoryEmbedded } return nil @@ -965,8 +997,8 @@ func (s *TemplateStore) insertShortcode(pi *paths.Path, fi hugofs.FileMetaInfo, PathInfo: pi, Fi: fi, D: d, - Category: CategoryShortcode, - NoBaseOf: true, + category: CategoryShortcode, + noBaseOf: true, } m1[d] = ti @@ -1022,8 +1054,8 @@ func (s *TemplateStore) insertTemplate2( PathInfo: pi, Fi: fi, D: d, - Category: category, - NoBaseOf: category > CategoryLayout, + category: category, + noBaseOf: category > CategoryLayout, isLegacyMapped: isLegacyMapped, } @@ -1231,7 +1263,7 @@ func (s *TemplateStore) insertTemplates(include func(fi hugofs.FileMetaInfo) boo s.tns.baseofTextClones = nil s.treeMain.Walk(func(key string, v map[nodeKey]*TemplInfo) (bool, error) { for _, vv := range v { - if !vv.NoBaseOf { + if !vv.noBaseOf { vv.state = processingStateInitial } } @@ -1270,20 +1302,20 @@ func (s *TemplateStore) parseTemplates() error { if vv.state == processingStateTransformed { continue } - if !vv.NoBaseOf { + 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 + vv.noBaseOf = true if err := s.tns.parseTemplate(vv); err != nil { return err } continue } - vv.BaseVariants = doctree.NewSimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]() + vv.baseVariants = doctree.NewSimpleTree[map[TemplateDescriptor]*TemplWithBaseApplied]() for _, base := range baseTemplates { if err := s.tns.applyBaseTemplate(vv, base); err != nil { @@ -1320,7 +1352,7 @@ func (s *TemplateStore) parseTemplates() error { // prepareTemplates prepares all templates for execution. func (s *TemplateStore) prepareTemplates() error { for t := range s.templates() { - if t.Category == CategoryBaseof { + if t.category == CategoryBaseof { continue } if _, err := t.Prepare(); err != nil { @@ -1330,38 +1362,51 @@ func (s *TemplateStore) prepareTemplates() error { 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. -func (s *TemplateStore) TemplateDescriptorFromPath(pth string) (string, TemplateDescriptor) { - var ( - mt media.Type - of output.Format - ) +func (s *TemplateStore) templateDescriptorFromPath(pth string) PathTemplateDescriptor { + // Check cache first. + d, _ := s.templateDescriptorByPath.GetOrCreate(pth, func() (PathTemplateDescriptor, error) { + var ( + mt media.Type + of output.Format + ) - // Common cases. - dotCount := strings.Count(pth, ".") - if dotCount <= 1 { - if dotCount == 0 { - // Asume HTML. - of, mt = s.resolveOutputFormatAndOrMediaType("html", "") + // Common cases. + dotCount := strings.Count(pth, ".") + if dotCount <= 1 { + if dotCount == 0 { + // Asume 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 { - pth = strings.TrimPrefix(pth, "/") - ext := path.Ext(pth) - pth = strings.TrimSuffix(pth, ext) - ext = ext[1:] - of, mt = s.resolveOutputFormatAndOrMediaType("", ext) + path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth) + pth = path.PathNoIdentifier() + of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext()) } - } else { - path := s.opts.PathParser.Parse(files.ComponentFolderLayouts, pth) - pth = path.PathNoIdentifier() - of, mt = s.resolveOutputFormatAndOrMediaType(path.OutputFormat(), path.Ext()) - } - return pth, TemplateDescriptor{ - OutputFormat: of.Name, - MediaType: mt.Type, - IsPlainText: of.IsPlainText, - } + return PathTemplateDescriptor{ + Path: pth, + Desc: TemplateDescriptor{ + OutputFormat: of.Name, + MediaType: mt.Type, + IsPlainText: of.IsPlainText, + }, + }, nil + }) + + return d } // 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) { for _, v := range s.treeMain.All() { for _, vv := range v { - if !vv.NoBaseOf { + if !vv.noBaseOf { for vvv := range vv.BaseVariantsSeq() { if !yield(vvv.Template) { return @@ -1562,10 +1607,10 @@ func (s *TemplateStore) transformTemplates() error { continue } vv.state = processingStateTransformed - if vv.Category == CategoryBaseof { + if vv.category == CategoryBaseof { continue } - if !vv.NoBaseOf { + if !vv.noBaseOf { for vvv := range vv.BaseVariantsSeq() { tctx, err := applyTemplateTransformers(vvv.Template, lookup) if err != nil { @@ -1709,13 +1754,13 @@ func (best *bestMatch) isBetter(w weight, ti *TemplInfo) bool { } if best.w.w1 > 0 { - currentBestIsEmbedded := best.templ.SubCategory == SubCategoryEmbedded + currentBestIsEmbedded := best.templ.subCategory == SubCategoryEmbedded if currentBestIsEmbedded { - if ti.SubCategory != SubCategoryEmbedded { + if ti.subCategory != SubCategoryEmbedded { return true } } else { - if ti.SubCategory == SubCategoryEmbedded { + if ti.subCategory == SubCategoryEmbedded { // Prefer user provided template. return false } diff --git a/tpl/tplimpl/templatestore_integration_test.go b/tpl/tplimpl/templatestore_integration_test.go index c109959ce..2f450e58d 100644 --- a/tpl/tplimpl/templatestore_integration_test.go +++ b/tpl/tplimpl/templatestore_integration_test.go @@ -1,6 +1,8 @@ package tplimpl_test import ( + "context" + "io" "testing" qt "github.com/frankban/quicktest" @@ -840,3 +842,42 @@ All. 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) + } +} diff --git a/tpl/tplimpl/templatetransform.go b/tpl/tplimpl/templatetransform.go index f9ce298de..cba4c6584 100644 --- a/tpl/tplimpl/templatetransform.go +++ b/tpl/tplimpl/templatetransform.go @@ -263,7 +263,7 @@ func (c *templateTransformContext) hasIdent(idents []string, ident string) bool // // {{ $_hugo_config:= `{ "version": 1 }` }} func (c *templateTransformContext) collectConfig(n *parse.PipeNode) { - if c.t.Category != CategoryShortcode { + if c.t.category != CategoryShortcode { return } if c.configChecked { @@ -304,7 +304,7 @@ func (c *templateTransformContext) collectConfig(n *parse.PipeNode) { // collectInner determines if the given CommandNode represents a // shortcode call to its .Inner. func (c *templateTransformContext) collectInner(n *parse.CommandNode) { - if c.t.Category != CategoryShortcode { + if c.t.category != CategoryShortcode { return } 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 { - if c.t.Category != CategoryPartial || c.returnNode != nil { + if c.t.category != CategoryPartial || c.returnNode != nil { return true }