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

@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/gohugoio/hugo/resources/page"
@@ -36,7 +37,6 @@ import (
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/output"
bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/tpl"
@@ -205,8 +205,7 @@ type shortcode struct {
indentation string // indentation from source.
info tpl.Info // One of the output formats (arbitrary)
templs []tpl.Template // All output formats
templ *tplimpl.TemplInfo
// If set, the rendered shortcode is sent as part of the surrounding content
// to Goldmark and similar.
@@ -230,16 +229,15 @@ func (s shortcode) insertPlaceholder() bool {
}
func (s shortcode) needsInner() bool {
return s.info != nil && s.info.ParseInfo().IsInner
return s.templ != nil && s.templ.ParseInfo.IsInner
}
func (s shortcode) configVersion() int {
if s.info == nil {
if s.templ == nil {
// Not set for inline shortcodes.
return 2
}
return s.info.ParseInfo().Config.Version
return s.templ.ParseInfo.Config.Version
}
func (s shortcode) innerString() string {
@@ -315,12 +313,12 @@ func prepareShortcode(
ctx context.Context,
level int,
s *Site,
tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
p *pageState,
po *pageOutput,
isRenderString bool,
) (shortcodeRenderer, error) {
p := po.p
toParseErr := func(err error) error {
source := p.m.content.mustSource()
return p.parseError(fmt.Errorf("failed to render shortcode %q: %w", sc.name, err), source, sc.pos)
@@ -333,7 +331,7 @@ func prepareShortcode(
// parsed and rendered by Goldmark.
ctx = tpl.Context.IsInGoldmark.Set(ctx, true)
}
r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p, isRenderString)
r, err := doRenderShortcode(ctx, level, s, sc, parent, po, isRenderString)
if err != nil {
return nil, false, toParseErr(err)
}
@@ -352,30 +350,29 @@ func doRenderShortcode(
ctx context.Context,
level int,
s *Site,
tplVariants tpl.TemplateVariants,
sc *shortcode,
parent *ShortcodeWithPage,
p *pageState,
po *pageOutput,
isRenderString bool,
) (shortcodeRenderer, error) {
var tmpl tpl.Template
var tmpl *tplimpl.TemplInfo
p := po.p
// Tracks whether this shortcode or any of its children has template variations
// in other languages or output formats. We are currently only interested in
// the output formats, so we may get some false positives -- we
// should improve on that.
// the output formats.
var hasVariants bool
if sc.isInline {
if !p.s.ExecHelper.Sec().EnableInlineShortcodes {
return zeroShortcode, nil
}
templName := path.Join("_inline_shortcode", p.Path(), sc.name)
templatePath := path.Join("_inline_shortcode", p.Path(), sc.name)
if sc.isClosing {
templStr := sc.innerString()
var err error
tmpl, err = s.TextTmpl().Parse(templName, templStr)
tmpl, err = s.TemplateStore.TextParse(templatePath, templStr)
if err != nil {
if isRenderString {
return zeroShortcode, p.wrapError(err)
@@ -389,21 +386,32 @@ func doRenderShortcode(
} else {
// Re-use of shortcode defined earlier in the same page.
var found bool
tmpl, found = s.TextTmpl().Lookup(templName)
if !found {
tmpl = s.TemplateStore.TextLookup(templatePath)
if tmpl == nil {
return zeroShortcode, fmt.Errorf("no earlier definition of shortcode %q found", sc.name)
}
}
tmpl = tpl.AddIdentity(tmpl)
} else {
var found, more bool
tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants)
if !found {
ofCount := map[string]int{}
include := func(match *tplimpl.TemplInfo) bool {
ofCount[match.D.OutputFormat]++
return true
}
base, layoutDescriptor := po.getTemplateBasePathAndDescriptor()
q := tplimpl.TemplateQuery{
Path: base,
Name: sc.name,
Category: tplimpl.CategoryShortcode,
Desc: layoutDescriptor,
Consider: include,
}
v := s.TemplateStore.LookupShortcode(q)
if v == nil {
s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path())
return zeroShortcode, nil
}
hasVariants = hasVariants || more
tmpl = v
hasVariants = hasVariants || len(ofCount) > 1
}
data := &ShortcodeWithPage{
@@ -427,7 +435,7 @@ func doRenderShortcode(
case string:
inner += innerData
case *shortcode:
s, err := prepareShortcode(ctx, level+1, s, tplVariants, innerData, data, p, isRenderString)
s, err := prepareShortcode(ctx, level+1, s, innerData, data, po, isRenderString)
if err != nil {
return zeroShortcode, err
}
@@ -484,7 +492,7 @@ func doRenderShortcode(
}
result, err := renderShortcodeWithPage(ctx, s.Tmpl(), tmpl, data)
result, err := renderShortcodeWithPage(ctx, s.GetTemplateStore(), tmpl, data)
if err != nil && sc.isInline {
fe := herrors.NewFileErrorFromName(err, p.File().Filename())
@@ -534,16 +542,11 @@ func (s *shortcodeHandler) hasName(name string) bool {
return ok
}
func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, p *pageState, f output.Format, isRenderString bool) (map[string]shortcodeRenderer, error) {
func (s *shortcodeHandler) prepareShortcodesForPage(ctx context.Context, po *pageOutput, isRenderString bool) (map[string]shortcodeRenderer, error) {
rendered := make(map[string]shortcodeRenderer)
tplVariants := tpl.TemplateVariants{
Language: p.Language().Lang,
OutputFormat: f,
}
for _, v := range s.shortcodes {
s, err := prepareShortcode(ctx, 0, s.s, tplVariants, v, nil, p, isRenderString)
s, err := prepareShortcode(ctx, 0, s.s, v, nil, po, isRenderString)
if err != nil {
return nil, err
}
@@ -636,7 +639,7 @@ Loop:
// we trust the template on this:
// if there's no inner, we're done
if !sc.isInline {
if !sc.info.ParseInfo().IsInner {
if !sc.templ.ParseInfo.IsInner {
return sc, nil
}
}
@@ -672,14 +675,19 @@ Loop:
sc.name = currItem.ValStr(source)
// Used to check if the template expects inner content.
templs := s.s.Tmpl().LookupVariants(sc.name)
if templs == nil {
// Used to check if the template expects inner content,
// so just pick one arbitrarily with the same name.
q := tplimpl.TemplateQuery{
Path: "",
Name: sc.name,
Category: tplimpl.CategoryShortcode,
Consider: nil,
}
templ := s.s.TemplateStore.LookupShortcode(q)
if templ == nil {
return nil, fmt.Errorf("%s: template for shortcode %q not found", errorPrefix, sc.name)
}
sc.info = templs[0].(tpl.Info)
sc.templs = templs
sc.templ = templ
case currItem.IsInlineShortcodeName():
sc.name = currItem.ValStr(source)
sc.isInline = true
@@ -778,7 +786,7 @@ func expandShortcodeTokens(
return source, nil
}
func renderShortcodeWithPage(ctx context.Context, h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
func renderShortcodeWithPage(ctx context.Context, h *tplimpl.TemplateStore, tmpl *tplimpl.TemplInfo, data *ShortcodeWithPage) (string, error) {
buffer := bp.GetBuffer()
defer bp.PutBuffer(buffer)