Reimplement and simplify Hugo's template system

See #13541 for details.

Fixes #13545
Fixes #13515
Closes #7964
Closes #13365
Closes #12988
Closes #4891
This commit is contained in:
Bjørn Erik Pedersen
2025-04-06 19:55:35 +02:00
parent 812ea0b325
commit 83cfdd78ca
138 changed files with 5342 additions and 4396 deletions

View File

@@ -19,23 +19,21 @@ import (
"errors"
"fmt"
"html/template"
"strings"
"sync"
"sync/atomic"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/highlight/chromalexers"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/gohugoio/hugo/markup/converter"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/page"
@@ -120,9 +118,9 @@ 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.Tmpl(), templ, pco.po.p)
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.Name(), err))
return "", pco.po.p.wrapError(fmt.Errorf("failed to execute template %s: %w", templ.Template.Name(), err))
}
return template.HTML(res), nil
}
@@ -274,103 +272,100 @@ func (pco *pageContentOutput) initRenderHooks() error {
return r
}
layoutDescriptor := pco.po.p.getLayoutDescriptor()
layoutDescriptor.RenderingHook = true
layoutDescriptor.LayoutOverride = false
layoutDescriptor.Layout = ""
// Inherit the descriptor from the page/current output format.
// This allows for fine-grained control of the template used for
// rendering of e.g. links.
base, layoutDescriptor := pco.po.p.getTemplateBasePathAndDescriptor()
switch tp {
case hooks.LinkRendererType:
layoutDescriptor.Kind = "render-link"
layoutDescriptor.Variant1 = "link"
case hooks.ImageRendererType:
layoutDescriptor.Kind = "render-image"
layoutDescriptor.Variant1 = "image"
case hooks.HeadingRendererType:
layoutDescriptor.Kind = "render-heading"
layoutDescriptor.Variant1 = "heading"
case hooks.PassthroughRendererType:
layoutDescriptor.Kind = "render-passthrough"
layoutDescriptor.Variant1 = "passthrough"
if id != nil {
layoutDescriptor.KindVariants = id.(string)
layoutDescriptor.Variant2 = id.(string)
}
case hooks.BlockquoteRendererType:
layoutDescriptor.Kind = "render-blockquote"
layoutDescriptor.Variant1 = "blockquote"
if id != nil {
layoutDescriptor.KindVariants = id.(string)
layoutDescriptor.Variant2 = id.(string)
}
case hooks.TableRendererType:
layoutDescriptor.Kind = "render-table"
layoutDescriptor.Variant1 = "table"
case hooks.CodeBlockRendererType:
layoutDescriptor.Kind = "render-codeblock"
layoutDescriptor.Variant1 = "codeblock"
if id != nil {
lang := id.(string)
lexer := chromalexers.Get(lang)
if lexer != nil {
layoutDescriptor.KindVariants = strings.Join(lexer.Config().Aliases, ",")
} else {
layoutDescriptor.KindVariants = lang
}
layoutDescriptor.Variant2 = id.(string)
}
}
getHookTemplate := func(f output.Format) (tpl.Template, bool) {
templ, found, err := pco.po.p.s.Tmpl().LookupLayout(layoutDescriptor, f)
if err != nil {
panic(err)
}
if found {
if isitp, ok := templ.(tpl.IsInternalTemplateProvider); ok && isitp.IsInternalTemplate() {
renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
switch templ.Name() {
case "_default/_markup/render-link.html":
if !renderHookConfig.Link.IsEnableDefault() {
return nil, false
}
case "_default/_markup/render-image.html":
if !renderHookConfig.Image.IsEnableDefault() {
return nil, false
}
}
}
}
return templ, found
renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
var ignoreInternal bool
switch layoutDescriptor.Variant1 {
case "link":
ignoreInternal = !renderHookConfig.Link.IsEnableDefault()
case "image":
ignoreInternal = !renderHookConfig.Image.IsEnableDefault()
}
templ, found1 := getHookTemplate(pco.po.f)
if !found1 || pco.po.p.reusePageOutputContent() {
defaultOutputFormat := pco.po.p.s.conf.C.DefaultOutputFormat
candidates := pco.po.p.s.renderFormats
// Some hooks may only be available in HTML, and if
// this site is configured to not have HTML output, we need to
// make sure we have a fallback. This should be very rare.
if pco.po.f.MediaType.FirstSuffix.Suffix != "html" {
if _, found := candidates.GetBySuffix("html"); !found {
candidates = append(candidates, output.HTMLFormat)
}
candidates := pco.po.p.s.renderFormats
var numCandidatesFound int
consider := func(candidate *tplimpl.TemplInfo) bool {
if layoutDescriptor.Variant1 != candidate.D.Variant1 {
return false
}
// Check if some of the other output formats would give a different template.
for _, f := range candidates {
if f.Name == pco.po.f.Name {
continue
}
templ2, found2 := getHookTemplate(f)
if found2 {
if !found1 && f.Name == defaultOutputFormat.Name {
templ = templ2
found1 = true
break
}
if templ != templ2 {
pco.po.p.pageOutputTemplateVariationsState.Add(1)
break
}
}
if layoutDescriptor.Variant2 != "" && candidate.D.Variant2 != "" && layoutDescriptor.Variant2 != candidate.D.Variant2 {
return false
}
if ignoreInternal && candidate.SubCategory == tplimpl.SubCategoryEmbedded {
// Don't consider the internal hook templates.
return false
}
if pco.po.p.pageOutputTemplateVariationsState.Load() > 1 {
return true
}
if candidate.D.OutputFormat == "" {
numCandidatesFound++
} else if _, found := candidates.GetByName(candidate.D.OutputFormat); found {
numCandidatesFound++
}
return true
}
getHookTemplate := func() (*tplimpl.TemplInfo, bool) {
q := tplimpl.TemplateQuery{
Path: base,
Category: tplimpl.CategoryMarkup,
Desc: layoutDescriptor,
Consider: consider,
}
v := pco.po.p.s.TemplateStore.LookupPagesLayout(q)
return v, v != nil
}
templ, found1 := getHookTemplate()
if found1 && templ == nil {
panic("found1 is true, but templ is nil")
}
if !found1 && layoutDescriptor.OutputFormat == pco.po.p.s.conf.DefaultOutputFormat {
numCandidatesFound++
}
if numCandidatesFound > 1 {
// More than one output format candidate found for this hook temoplate,
// so we cannot reuse the same rendered content.
pco.po.p.incrPageOutputTemplateVariation()
}
if !found1 {
@@ -384,7 +379,7 @@ func (pco *pageContentOutput) initRenderHooks() error {
}
r := hookRendererTemplate{
templateHandler: pco.po.p.s.Tmpl(),
templateHandler: pco.po.p.s.GetTemplateStore(),
templ: templ,
resolvePosition: resolvePosition,
}
@@ -488,7 +483,7 @@ func (t targetPathsHolder) targetPaths() page.TargetPaths {
return t.paths
}
func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Template, data any) (string, error) {
func executeToString(ctx context.Context, h *tplimpl.TemplateStore, templ *tplimpl.TemplInfo, data any) (string, error) {
b := bp.GetBuffer()
defer bp.PutBuffer(b)
if err := h.ExecuteWithContext(ctx, templ, b, data); err != nil {