]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Fix partial decorator detection in partial with blocks with outer range break or...
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 1 Jan 2026 12:56:55 +0000 (13:56 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 1 Jan 2026 17:05:10 +0000 (18:05 +0100)
E.g.:

```handlebars
{{- $items := slice "a" "b" "c" }}
  {{- range $items }}
    {{- with partial "b" . -}}
 {{break}}
{{- else }}
  else: {{ . -}}
{{- end }}
  {{- end }}
 ```

Fixes #14333

tpl/templates/decorator_integration_test.go
tpl/tplimpl/templatetransform.go

index a441148ea17c85be1e03645bdf62c26cbc04aaa4..253a09316ee5ae631d0cb0fdb6309eac1ebb544d 100644 (file)
@@ -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" }}
+<b>{{ inner . }}</b>
+{{ end }}
+{{ with (partial "b.html" "Important!") }}Notice: {{ . }}{{ end }}
+`
+
+       b := hugolib.Test(t, files)
+
+       b.AssertFileContent("public/index.html", "<b>Notice: Important!</b>")
+}
+
+func TestPartialDecoratorBreakInWith(t *testing.T) {
+       t.Parallel()
+
+       filesTemplate := `
+-- hugo.toml --
+-- layouts/home.html --
+Home.
+{{ define "_partials/b.html" }}
+<b>{{ inner . }}</b>
+{{ 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.")
+               }
+       }
+}
index e4cec981387312b6564bc22d754e8f1b2993487f..6ace0f109e93f0394c7e9a67c83da711f485869b 100644 (file)
@@ -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