From: Bjørn Erik Pedersen Date: Thu, 1 Jan 2026 12:56:55 +0000 (+0100) Subject: Fix partial decorator detection in partial with blocks with outer range break or... X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=09048aad7655385e9a287448eed65408d0b850ab;p=brevno-suite%2Fhugo Fix partial decorator detection in partial with blocks with outer range break or continue E.g.: ```handlebars {{- $items := slice "a" "b" "c" }} {{- range $items }} {{- with partial "b" . -}} {{break}} {{- else }} else: {{ . -}} {{- end }} {{- end }} ``` Fixes #14333 --- diff --git a/tpl/templates/decorator_integration_test.go b/tpl/templates/decorator_integration_test.go index a441148ea..253a09316 100644 --- a/tpl/templates/decorator_integration_test.go +++ b/tpl/templates/decorator_integration_test.go @@ -367,3 +367,89 @@ This construct creates a loop: {{PLACEHOLDER . }} b.Assert(err.Error(), qt.Contains, "inner cannot be used inside a with block that wraps a partial decorator") } } + +func TestPartialDecoratorInParens(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +-- layouts/home.html -- +Home. +{{ define "_partials/b.html" }} +{{ inner . }} +{{ end }} +{{ with (partial "b.html" "Important!") }}Notice: {{ . }}{{ end }} +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/index.html", "Notice: Important!") +} + +func TestPartialDecoratorBreakInWith(t *testing.T) { + t.Parallel() + + filesTemplate := ` +-- hugo.toml -- +-- layouts/home.html -- +Home. +{{ define "_partials/b.html" }} +{{ inner . }} +{{ end }} +{{ with (partial "b.html" "Important!") }} +{{ range seq 1 5 }} +{{ if eq . 3 }} +PLACEHOLDER +{{ end }} +Notice: {{ . }} +{{ end }} +{{ end }} +` + + for _, placeholder := range []string{"{{ break }}", "{{ continue }}"} { + files := strings.ReplaceAll(filesTemplate, "PLACEHOLDER", placeholder) + b := hugolib.Test(t, files) + b.AssertFileContent("public/index.html", "Notice: 1", "Notice: 2", "! Notice: 3") + if strings.Contains(placeholder, "continue") { + b.AssertFileContent("public/index.html", "Notice: 4", "Notice: 5") + } else { + b.AssertFileContent("public/index.html", "! Notice: 4", "! Notice: 5") + } + } +} + +func TestPartialWithBreakOutsideRange14333(t *testing.T) { + t.Parallel() + + filesTemplate := ` +-- hugo.toml -- +-- layouts/home.html -- +Home. {{ partial "a" }}:Done. +{{- define "_partials/a" }} + {{- $items := slice "a" "b" "c" }} + {{- range $items }} + {{- with partial "b" . -}} + PLACEHOLDER + {{- else }} + else: {{ . -}} + {{- end }} + {{- end }} +{{- end }} +{{- define "_partials/b" }} +{{ $b := true }} +{{ if ne . "b" }} +{{ $b = false }} +{{ end }} +{{ return $b }} +{{ end }} +` + for _, placeholder := range []string{"{{ break }}", "{{ continue }}", "{{- break }}"} { + files := strings.ReplaceAll(filesTemplate, "PLACEHOLDER", placeholder) + b := hugolib.Test(t, files) + if strings.Contains(placeholder, "continue") { + b.AssertFileContent("public/index.html", "else: c:Done.") + } else { + b.AssertFileContent("public/index.html", "else: a:Done.") + } + } +} diff --git a/tpl/tplimpl/templatetransform.go b/tpl/tplimpl/templatetransform.go index e4cec9813..6ace0f109 100644 --- a/tpl/tplimpl/templatetransform.go +++ b/tpl/tplimpl/templatetransform.go @@ -231,11 +231,20 @@ func (c *templateTransformContext) isWithPartial(args []parse.Node) bool { return false } - if id1, ok := args[0].(*parse.IdentifierNode); ok && (id1.Ident == "partial" || id1.Ident == "partialCached") { + first := args[0] + + if pn, ok := first.(*parse.PipeNode); ok { + if len(pn.Cmds) == 0 || pn.Cmds[0] == nil { + return false + } + return c.isWithPartial(pn.Cmds[0].Args) + } + + if id1, ok := first.(*parse.IdentifierNode); ok && (id1.Ident == "partial" || id1.Ident == "partialCached") { return true } - if chain, ok := args[0].(*parse.ChainNode); ok { + if chain, ok := first.(*parse.ChainNode); ok { if id2, ok := chain.Node.(*parse.IdentifierNode); !ok || (id2.Ident != "partials") { return false } @@ -266,12 +275,52 @@ const PartialDecoratorPrefix = "_internal/decorator_" var templatesInnerRe = regexp.MustCompile(`{{\s*(templates\.Inner\b|inner\b)`) +// hasBreakOrContinueOutsideRange returns true if the given list node contains a break or continue statement without being nested in a range. +func (c *templateTransformContext) hasBreakOrContinueOutsideRange(n *parse.ListNode) bool { + if n == nil { + return false + } + for _, node := range n.Nodes { + switch x := node.(type) { + case *parse.ListNode: + if c.hasBreakOrContinueOutsideRange(x) { + return true + } + case *parse.RangeNode: + // skip + case *parse.IfNode: + if c.hasBreakOrContinueOutsideRange(x.List) { + return true + } + if c.hasBreakOrContinueOutsideRange(x.ElseList) { + return true + } + case *parse.WithNode: + if c.hasBreakOrContinueOutsideRange(x.List) { + return true + } + if c.hasBreakOrContinueOutsideRange(x.ElseList) { + return true + } + case *parse.BreakNode, *parse.ContinueNode: + return true + + } + } + return false +} + 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 } + + // See #14333. That is a very odd construct, but we need to guard against it. + if c.hasBreakOrContinueOutsideRange(withNode.List) { + return + } innerHash := hashing.XxHashFromStringHexEncoded(c.t.Name() + withNodeInnerString) internalPartialName := fmt.Sprintf("_partials/%s%s", PartialDecoratorPrefix, innerHash) @@ -321,6 +370,9 @@ func (c *templateTransformContext) handleWithPartial(withNode *parse.WithNode) { 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) + if pn, ok := withNode.Pipe.Cmds[0].Args[0].(*parse.PipeNode); ok { + withNode.Pipe.Cmds[0].Args = pn.Cmds[0].Args + } withNode.Pipe.Cmds = append(orNode.Pipe.Cmds, withNode.Pipe.Cmds...) withNode.List = newInner