Allow partials to work as decorators

Fixes #13193
This commit is contained in:
Bjørn Erik Pedersen
2025-12-23 14:08:19 +01:00
parent 555443b64a
commit 7c19c196c3
18 changed files with 774 additions and 47 deletions
+3
View File
@@ -21,6 +21,9 @@ type ParseInfo struct {
// Set for shortcode templates with any {{ .Inner }}
IsInner bool
// Set for partial templates with any {{ inner }} or {{ templates.Inner }}
HasPartialInner bool
// Set for partials with a return statement.
HasReturn bool
+16
View File
@@ -53,6 +53,22 @@ func (s *TemplateStore) parseTemplate(ti *TemplInfo, replace bool) error {
return err
}
func (t *templateNamespace) newBlankTemplate(ti *TemplInfo) tpl.Template {
if ti.D.IsPlainText {
tt, err := t.parseText.New(ti.Name()).Parse("")
if err != nil {
panic(err)
}
return tt
}
tt, err := t.parseHTML.New(ti.Name()).Parse("")
if err != nil {
panic(err)
}
return tt
}
func (t *templateNamespace) doParseTemplate(ti *TemplInfo, replace bool) error {
if !ti.noBaseOf || ti.category == CategoryBaseof {
// Delay parsing until we have the base template.
+30 -1
View File
@@ -35,6 +35,7 @@ import (
"sync/atomic"
"time"
"github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/loggers"
@@ -489,6 +490,15 @@ func (s *TemplateStore) FindAllBaseTemplateCandidates(overlayKey string, d1 Temp
return result
}
// PrepareTopLevelRenderCtx prepares a context for top-level rendering of a page.
func (t *TemplateStore) PrepareTopLevelRenderCtx(ctx context.Context, p page.Page) context.Context {
if p != nil {
ctx = tpl.Context.Page.Set(ctx, p)
}
ctx = tpl.Context.PartialDecoratorIDStack.Set(ctx, collections.NewStack[*tpl.StringBool]())
return ctx
}
func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, wr io.Writer, data any) error {
return t.ExecuteWithContextAndKey(ctx, "", ti, wr, data)
}
@@ -1262,6 +1272,7 @@ func (s *TemplateStore) insertTemplate2(
D: d,
matrix: matrix,
category: category,
subCategory: subCategory,
noBaseOf: category > CategoryLayout,
isLegacyMapped: isLegacyMapped,
}
@@ -1570,6 +1581,24 @@ func (s *TemplateStore) createTemplatesSnapshot() error {
return nil
}
func (s *TemplateStore) addTransformedTemplateInsert(name string, subCategory SubCategory) (*TemplInfo, error) {
pi := s.opts.PathParser.Parse(files.ComponentFolderLayouts, name)
ti, err := s.insertTemplate(pi, nil, subCategory, true, s.treeMain)
if err != nil {
return nil, err
}
return ti, nil
}
func (s *TemplateStore) addTransformedTemplateSetTree(this *TemplInfo, root *parse.ListNode) (*parse.Tree, error) {
templ := s.tns.newBlankTemplate(this)
tree := getParseTree(templ)
tree.Root = root
this.Template = templ
this.state = processingStateTransformed
return tree, nil
}
func (s *TemplateStore) parseTemplates(replace bool) error {
if err := func() error {
// Read and parse all templates.
@@ -1858,7 +1887,7 @@ func (s *TemplateStore) transformTemplates() error {
if vv.category == CategoryBaseof {
continue
}
tctx, err := applyTemplateTransformers(vv, lookup)
tctx, err := applyTemplateTransformers(vv, s, lookup)
if err != nil {
return err
}
+177 -21
View File
@@ -3,6 +3,7 @@ package tplimpl
import (
"errors"
"fmt"
"regexp"
"slices"
"strings"
@@ -22,6 +23,7 @@ type templateTransformContext struct {
templateNotFound map[string]bool
deferNodes map[string]*parse.ListNode
lookupFn func(name string, in *TemplInfo) *TemplInfo
store *TemplateStore
// The last error encountered.
err error
@@ -53,11 +55,13 @@ func (c templateTransformContext) getIfNotVisited(name string) *TemplInfo {
func newTemplateTransformContext(
t *TemplInfo,
store *TemplateStore,
lookupFn func(name string, in *TemplInfo) *TemplInfo,
) *templateTransformContext {
return &templateTransformContext{
t: t,
lookupFn: lookupFn,
store: store,
visited: make(map[string]bool),
templateNotFound: make(map[string]bool),
deferNodes: make(map[string]*parse.ListNode),
@@ -66,28 +70,25 @@ func newTemplateTransformContext(
func applyTemplateTransformers(
t *TemplInfo,
store *TemplateStore,
lookupFn func(name string, in *TemplInfo) *TemplInfo,
) (*templateTransformContext, error) {
if t == nil {
return nil, errors.New("expected template, but none provided")
}
c := newTemplateTransformContext(t, lookupFn)
c := newTemplateTransformContext(t, store, lookupFn)
c.t.ParseInfo = defaultParseInfo
tree := getParseTree(t.Template)
if tree == nil {
panic(fmt.Errorf("template %s not parsed", t))
}
_, err := c.applyTransformations(tree.Root)
if err == nil && c.returnNode != nil {
// This is a partial with a return statement.
c.t.ParseInfo.HasReturn = true
tree.Root = c.wrapInPartialReturnWrapper(tree.Root)
if err := c.applyTransformationsAndSetReturnWrapper(tree); err != nil {
return c, fmt.Errorf("failed to transform template %q: %w", t.Name(), err)
}
return c, err
return c, c.err
}
func getParseTree(templ tpl.Template) *parse.Tree {
@@ -105,11 +106,17 @@ const (
partialReturnWrapperTempl = `{{ $_hugo_dot := $ }}{{ $ := .Arg }}{{ range (slice .Arg) }}{{ $_hugo_dot.Set ("PLACEHOLDER") }}{{ end }}`
doDeferTempl = `{{ doDefer ("PLACEHOLDER1") ("PLACEHOLDER2") }}`
// _pushPartialDecorator is always falsy.
pushPartialDecoratorTempl = `{{ if or (_pushPartialDecorator ("PLACEHOLDER")) }}{{ end }}`
popPartialDecoratorTempl = `{{ if (_popPartialDecorator ("PLACEHOLDER1")) }}{{ . }}{{ else }}("PLACEHOLDER2"){{ end }}`
)
var (
partialReturnWrapper *parse.ListNode
doDefer *parse.ListNode
popPartialDecorator *parse.ListNode
pushPartialDecorator *parse.ListNode
)
func init() {
@@ -124,6 +131,18 @@ func init() {
panic(err)
}
doDefer = templ.Tree.Root
templ, err = texttemplate.New("").Funcs(texttemplate.FuncMap{"_popPartialDecorator": func(string) string { return "" }}).Parse(popPartialDecoratorTempl)
if err != nil {
panic(err)
}
popPartialDecorator = templ.Tree.Root
templ, err = texttemplate.New("").Funcs(texttemplate.FuncMap{"_pushPartialDecorator": func(string) string { return "" }}).Parse(pushPartialDecoratorTempl)
if err != nil {
panic(err)
}
pushPartialDecorator = templ.Tree.Root
}
// wrapInPartialReturnWrapper copies and modifies the parsed nodes of a
@@ -142,7 +161,20 @@ func (c *templateTransformContext) wrapInPartialReturnWrapper(n *parse.ListNode)
return wrapper
}
// applyTransformations do 2 things:
func (c *templateTransformContext) applyTransformationsAndSetReturnWrapper(tree *parse.Tree) error {
_, err := c.applyTransformations(tree.Root)
if err != nil {
return err
}
if c.returnNode != nil {
// This is a partial with a return statement.
c.t.ParseInfo.HasReturn = true
tree.Root = c.wrapInPartialReturnWrapper(tree.Root)
}
return nil
}
// applyTransformations does 2 things:
// 1) Parses partial return statement.
// 2) Tracks template (partial) dependencies and some other info.
func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, error) {
@@ -156,7 +188,7 @@ func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, err
case *parse.IfNode:
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
case *parse.WithNode:
c.handleDefer(x)
c.handleWith(x)
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
case *parse.RangeNode:
c.applyTransformationsToNodes(x.Pipe, x.List, x.ElseList)
@@ -178,7 +210,8 @@ func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, err
if x == nil {
return true, nil
}
c.collectInner(x)
c.collectInnerInShortcode(x)
c.collectInnerInPartial(x)
keep := c.collectReturnNode(x)
for _, elem := range x.Args {
@@ -193,14 +226,118 @@ func (c *templateTransformContext) applyTransformations(n parse.Node) (bool, err
return true, c.err
}
func (c *templateTransformContext) handleDefer(withNode *parse.WithNode) {
func (c *templateTransformContext) isWithPartial(args []parse.Node) bool {
if len(args) == 0 {
return false
}
if id1, ok := args[0].(*parse.IdentifierNode); ok && (id1.Ident == "partial" || id1.Ident == "partialCached") {
return true
}
if chain, ok := args[0].(*parse.ChainNode); ok {
if id2, ok := chain.Node.(*parse.IdentifierNode); !ok || (id2.Ident != "partials") {
return false
}
if len(chain.Field) != 1 {
return false
}
if chain.Field[0] != "Include" && chain.Field[0] != "IncludeCached" {
return false
}
return true
}
return false
}
func (c *templateTransformContext) isWithDefer(idArg parse.Node) bool {
id, ok := idArg.(*parse.ChainNode)
if !ok || len(id.Field) != 1 || id.Field[0] != "Defer" {
return false
}
if id2, ok := id.Node.(*parse.IdentifierNode); !ok || id2.Ident != "templates" {
return false
}
return true
}
// PartialDecoratorPrefix is the prefix used for internal partial decorator templates.
const PartialDecoratorPrefix = "_internal/decorator_"
var templatesInnerRe = regexp.MustCompile(`{{\s*(templates\.Inner\b|inner\b)`)
func (c *templateTransformContext) handleWithPartial(withNode *parse.WithNode) {
withNodeInnerString := withNode.List.String()
if templatesInnerRe.MatchString(withNodeInnerString) {
c.err = fmt.Errorf("inner cannot be used inside a with block that wraps a partial decorator")
return
}
innerHash := hashing.XxHashFromStringHexEncoded(c.t.Name() + withNodeInnerString)
internalPartialName := fmt.Sprintf("_partials/%s%s", PartialDecoratorPrefix, innerHash)
if c.lookupFn(internalPartialName, c.t) == nil {
innerCopy := withNode.List.CopyList()
ti, err := c.store.addTransformedTemplateInsert(internalPartialName, SubCategoryInline)
if err != nil {
c.err = fmt.Errorf("failed to create internal partial decorator template %q: %w", internalPartialName, err)
return
}
if ti == nil {
c.err = fmt.Errorf("failed to find internal partial decorator template %q after insertion", internalPartialName)
return
}
cc := newTemplateTransformContext(ti, c.store, c.lookupFn)
tree, err := c.store.addTransformedTemplateSetTree(ti, innerCopy)
if err != nil {
c.err = fmt.Errorf("failed to add internal partial decorator template %q: %w", internalPartialName, err)
return
}
if err := cc.applyTransformationsAndSetReturnWrapper(tree); err != nil {
c.err = fmt.Errorf("failed to transform internal partial decorator template %q: %w", internalPartialName, err)
return
}
}
newInner := popPartialDecorator.CopyList()
ifNode := newInner.Nodes[0].(*parse.IfNode)
placeholderPipe := ifNode.Pipe.Cmds[0].Args[0].(*parse.PipeNode)
// Set PLACEHOLDER1 to the unique ID for this partial decorator.
sn1 := placeholderPipe.Cmds[0].Args[1].(*parse.PipeNode).Cmds[0].Args[0].(*parse.StringNode)
sn1.Text = innerHash
sn1.Quoted = fmt.Sprintf("%q", sn1.Text)
ifNode.ElseList = withNode.List.CopyList()
newPipe := pushPartialDecorator.CopyList()
orNode := newPipe.Nodes[0].(*parse.IfNode)
setContext := orNode.Pipe.Cmds[0].Args[1]
// Replace PLACEHOLDER with the unique ID for this partial decorator.
sn2 := setContext.(*parse.PipeNode).Cmds[0].Args[1].(*parse.PipeNode).Cmds[0].Args[0].(*parse.StringNode)
sn2.Text = innerHash
sn2.Quoted = fmt.Sprintf("%q", sn2.Text)
withNode.Pipe.Cmds = append(orNode.Pipe.Cmds, withNode.Pipe.Cmds...)
withNode.List = newInner
}
func (c *templateTransformContext) handleWith(withNode *parse.WithNode) {
if len(withNode.Pipe.Cmds) != 1 {
return
}
cmd := withNode.Pipe.Cmds[0]
if len(cmd.Args) != 1 {
if c.isWithPartial(withNode.Pipe.Cmds[0].Args) {
c.handleWithPartial(withNode)
return
}
cmd := withNode.Pipe.Cmds[0]
idArg := cmd.Args[0]
p, ok := idArg.(*parse.PipeNode)
@@ -220,11 +357,7 @@ func (c *templateTransformContext) handleDefer(withNode *parse.WithNode) {
idArg = cmd.Args[0]
id, ok := idArg.(*parse.ChainNode)
if !ok || len(id.Field) != 1 || id.Field[0] != "Defer" {
return
}
if id2, ok := id.Node.(*parse.IdentifierNode); !ok || id2.Ident != "templates" {
if !c.isWithDefer(idArg) {
return
}
@@ -304,9 +437,9 @@ func (c *templateTransformContext) collectConfig(n *parse.PipeNode) {
}
}
// collectInner determines if the given CommandNode represents a
// collectInnerInShortcode determines if the given CommandNode represents a
// shortcode call to its .Inner.
func (c *templateTransformContext) collectInner(n *parse.CommandNode) {
func (c *templateTransformContext) collectInnerInShortcode(n *parse.CommandNode) {
if c.t.category != CategoryShortcode {
return
}
@@ -330,6 +463,29 @@ func (c *templateTransformContext) collectInner(n *parse.CommandNode) {
}
}
func (c *templateTransformContext) collectInnerInPartial(n *parse.CommandNode) {
if c.t.category != CategoryPartial {
return
}
if c.t.ParseInfo.HasPartialInner || len(n.Args) == 0 {
return
}
switch v := n.Args[0].(type) {
case *parse.IdentifierNode:
if v.Ident == "inner" {
c.t.ParseInfo.HasPartialInner = true
}
case *parse.ChainNode:
if v.Field[0] == "Inner" {
if id, ok := v.Node.(*parse.IdentifierNode); ok && id.Ident == "templates" {
c.t.ParseInfo.HasPartialInner = true
}
}
}
}
func (c *templateTransformContext) collectReturnNode(n *parse.CommandNode) bool {
if c.t.category != CategoryPartial || c.returnNode != nil {
return true