infol := opts.Log.InfoCommand("dynacache")
- evictedIdentities := collections.NewStack[KeyIdentity]()
+ evictedIdentities := collections.NewStackThreadSafe[KeyIdentity]()
onEvict := func(k, v any) {
if !opts.Watching {
partitions map[string]PartitionManager
onEvict func(k, v any)
- evictedIdentities *collections.Stack[KeyIdentity]
+ evictedIdentities *collections.StackThreadSafe[KeyIdentity]
opts Options
infol logg.LevelLogger
"github.com/gohugoio/hugo/common/hiter"
)
-// Stack is a simple LIFO stack that is safe for concurrent use.
-type Stack[T any] struct {
+// StackThreadSafe is a simple LIFO stack that is safe for concurrent use.
+type StackThreadSafe[T any] struct {
items []T
zero T
mu sync.RWMutex
}
-func NewStack[T any]() *Stack[T] {
- return &Stack[T]{}
+func NewStackThreadSafe[T any]() *StackThreadSafe[T] {
+ return &StackThreadSafe[T]{}
}
-func (s *Stack[T]) Push(item T) {
+func (s *StackThreadSafe[T]) Push(item T) {
s.mu.Lock()
defer s.mu.Unlock()
s.items = append(s.items, item)
}
-func (s *Stack[T]) Pop() (T, bool) {
+func (s *StackThreadSafe[T]) Pop() (T, bool) {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.items) == 0 {
return item, true
}
-func (s *Stack[T]) Peek() (T, bool) {
+func (s *StackThreadSafe[T]) Peek() (T, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
if len(s.items) == 0 {
return s.items[len(s.items)-1], true
}
-func (s *Stack[T]) Len() int {
+func (s *StackThreadSafe[T]) Len() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.items)
}
// All returns all items in the stack, from bottom to top.
-func (s *Stack[T]) All() iter.Seq2[int, T] {
+func (s *StackThreadSafe[T]) All() iter.Seq2[int, T] {
return hiter.Lock2(slices.All(s.items), s.mu.RLock, s.mu.RUnlock)
}
-func (s *Stack[T]) Drain() []T {
+func (s *StackThreadSafe[T]) Drain() []T {
s.mu.Lock()
defer s.mu.Unlock()
items := s.items
return items
}
-func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
+func (s *StackThreadSafe[T]) DrainMatching(predicate func(T) bool) []T {
s.mu.Lock()
defer s.mu.Unlock()
var items []T
}
return items
}
+
+// Stack is a simple LIFO stack that is not safe for concurrent use.
+type Stack[T any] struct {
+ items []T
+ zero T
+}
+
+func NewStack[T any]() *Stack[T] {
+ return &Stack[T]{}
+}
+
+func (s *Stack[T]) Push(item T) {
+ s.items = append(s.items, item)
+}
+
+func (s *Stack[T]) Pop() (T, bool) {
+ if len(s.items) == 0 {
+ return s.zero, false
+ }
+ item := s.items[len(s.items)-1]
+ s.items = s.items[:len(s.items)-1]
+ return item, true
+}
+
+func (s *Stack[T]) Peek() (T, bool) {
+ if len(s.items) == 0 {
+ return s.zero, false
+ }
+ return s.items[len(s.items)-1], true
+}
+
+func (s *Stack[T]) Len() int {
+ return len(s.items)
+}
+
+func (s *Stack[T]) All() iter.Seq2[int, T] {
+ return slices.All(s.items)
+}
+
+func (s *Stack[T]) Drain() []T {
+ items := s.items
+ s.items = nil
+ return items
+}
t.Parallel()
c := qt.New(t)
- s := NewStack[int]()
+ s := NewStackThreadSafe[int]()
c.Assert(s, qt.IsNotNil)
}
t.Parallel()
c := qt.New(t)
- s := NewStack[int]()
+ s := NewStackThreadSafe[int]()
c.Assert(s.Len(), qt.Equals, 0)
t.Parallel()
c := qt.New(t)
- s := NewStack[string]()
+ s := NewStackThreadSafe[string]()
s.Push("a")
s.Push("b")
t.Parallel()
c := qt.New(t)
- s := NewStack[int]()
+ s := NewStackThreadSafe[int]()
s.Push(1)
s.Push(2)
s.Push(3)
m.mu.Unlock()
}
+// Delete deletes the given key from the map.
+// It returns true if the key was found and deleted, false otherwise.
+func (m *Map[K, T]) Delete(key K) bool {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ if _, found := m.m[key]; found {
+ delete(m.m, key)
+ return true
+ }
+ return false
+}
+
// WithWriteLock executes the given function with a write lock on the map.
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T) error) error {
m.mu.Lock()
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/resources/page"
- "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/tpl/tplimpl"
)
p,
}
- ctx := tpl.Context.Page.Set(context.Background(), p)
+ ctx := a.ts.PrepareTopLevelRenderCtx(context.Background(), p)
buffer := new(bytes.Buffer)
err := a.ts.ExecuteWithContext(ctx, t, buffer, data)
}
}
+func (tree *SimpleTree[T]) Len() int {
+ return tree.tree.Len()
+}
+
// NewSimpleThreadSafeTree creates a new SimpleTree.
func NewSimpleThreadSafeTree[T any]() *SimpleThreadSafeTree[T] {
return &SimpleThreadSafeTree[T]{tree: radix.New[T](), mu: new(sync.RWMutex)}
events []*Event[T]
hooksPostInit sync.Once
- hooksPost *collections.Stack[func() error]
+ hooksPost *collections.StackThreadSafe[func() error]
}
type eventHandlers[T any] map[string][]func(*Event[T])
return nil
}
-func (ctx *WalkContext[T]) HooksPost() *collections.Stack[func() error] {
+func (ctx *WalkContext[T]) HooksPost() *collections.StackThreadSafe[func() error] {
ctx.hooksPostInit.Do(func() {
- ctx.hooksPost = collections.NewStack[func() error]()
+ ctx.hooksPost = collections.NewStackThreadSafe[func() error]()
})
return ctx.hooksPost
}
"testing"
"github.com/bep/logg"
+ "github.com/yuin/goldmark/util"
qt "github.com/frankban/quicktest"
"github.com/fsnotify/fsnotify"
content := strings.TrimSpace(s.FileContent(filename))
for _, m := range matches {
- cm := qt.Commentf("File: %s Expect: %s Got: %s", filename, m, content)
+ cm := qt.Commentf("File: %s Expect:\n%s Got:\n%s\nWith Space Visuals:\n%s", filename, m, content, util.VisualizeSpaces([]byte(content)))
lines := strings.SplitSeq(m, "\n")
for match := range lines {
match = strings.TrimSpace(match)
of := p.outputFormat()
p.incrRenderState()
- ctx := tpl.Context.Page.Set(context.Background(), p)
+ ctx := s.TemplateStore.PrepareTopLevelRenderCtx(context.Background(), p)
ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p)
if err := s.renderForTemplate(ctx, p.Kind(), of.Name, d, renderBuffer, templ); err != nil {
// include is a helper function that lookups and executes the named partial.
// Returns the final template name and the rendered output.
func (ns *Namespace) doInclude(ctx context.Context, key string, templ *tplimpl.TemplInfo, dataList ...any) includeResult {
+ if templ.ParseInfo.HasPartialInner {
+ stack := tpl.Context.PartialDecoratorIDStack.Get(ctx)
+ if stack != nil {
+ if id, ok := stack.Peek(); ok {
+ // Signal that inner exists.
+ id.Bool = true
+ }
+ }
+ }
var data any
if len(dataList) > 0 {
data = dataList[0]
"github.com/bep/helpers/contexthelpers"
bp "github.com/gohugoio/hugo/bufferpool"
+ "github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs"
contextKeyPage
contextKeyIsInGoldmark
cntextKeyCurrentTemplateInfo
+ contextKeyPartialDecoratorIDStack
)
// Context manages values passed in the context to templates.
Page contexthelpers.ContextDispatcher[page]
IsInGoldmark contexthelpers.ContextDispatcher[bool]
CurrentTemplate contexthelpers.ContextDispatcher[*CurrentTemplateInfo]
+ PartialDecoratorIDStack contexthelpers.ContextDispatcher[*collections.Stack[*StringBool]]
}{
DependencyManagerScopedProvider: contexthelpers.NewContextDispatcher[identity.DependencyManagerScopedProvider](contextKeyDependencyManagerScopedProvider),
DependencyScope: contexthelpers.NewContextDispatcher[int](contextKeyDependencyScope),
Page: contexthelpers.NewContextDispatcher[page](contextKeyPage),
IsInGoldmark: contexthelpers.NewContextDispatcher[bool](contextKeyIsInGoldmark),
CurrentTemplate: contexthelpers.NewContextDispatcher[*CurrentTemplateInfo](cntextKeyCurrentTemplateInfo),
+ PartialDecoratorIDStack: contexthelpers.NewContextDispatcher[*collections.Stack[*StringBool]](contextKeyPartialDecoratorIDStack),
}
func init() {
}
}
+// StringBool is a helper struct to hold a string and a bool value.
+type StringBool struct {
+ Str string
+ Bool bool
+}
+
type page interface {
IsNode() bool
}
--- /dev/null
+// 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.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package templates_test
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/hugolib"
+)
+
+func TestDecoratorInnerNeverCalled(t *testing.T) {
+ t.Parallel()
+
+ filesTemplate := `
+-- hugo.toml --
+disableKinds = ["section", "taxonomy", "term", "sitemap", "RSS"]
+-- content/p1.md --
+---
+title: "Page 1"
+---
+-- content/p2.md --
+---
+title: "Page 2"
+---
+-- layouts/_partials/cards.html --
+Start:{{ range . }}{{ PLACEHOLDER . }}{{ end }}End$
+-- layouts/home.html --
+1:${{ with partial "cards.html" (site.RegularPages) }}{{ printf "Got %T" . }}|{{ end }}$
+2:${{ with partial "cards.html" (site.RegularPages | first 0) }}{{ printf "Got %T" . }}|{{ end }}$
+`
+
+ for _, placeholder := range []string{"inner", "templates.Inner"} {
+ files := strings.ReplaceAll(filesTemplate, "PLACEHOLDER", placeholder)
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "1:$Start:Got *hugolib.pageState|Got *hugolib.pageState|End$$",
+ "2:$Start:End$$",
+ )
+
+ }
+}
+
+func TestDecoratorInlinePartialInnerNeverCalled(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["section", "taxonomy", "term", "sitemap", "RSS"]
+-- content/p1.md --
+---
+title: "Page 1"
+---
+-- content/p2.md --
+---
+title: "Page 2"
+---
+-- layouts/home.html --
+${{ with partial "cards.html" (site.RegularPages) }}{{ printf "Got %T" . }}|{{ end }}$
+{{ define "_partials/cards.html" }}Start:{{ range . }}{{ inner . }}{{ end }}End${{ end }}
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "$Start:Got *hugolib.pageState|Got *hugolib.pageState|End$$",
+ )
+}
+
+func TestDecoratorInlinePartial(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["section", "taxonomy", "term", "sitemap", "rss"]
+-- layouts/home.html --
+Home.
+{{ with partial "decorate.html" "Important!" }}Notice: {{ . }}{{ end }}
+{{ define "_partials/decorate.html" }}<b>{{ inner . }}</b>{{ end }}
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "<b>Notice: Important!</b>")
+}
+
+func TestDecoratorNestedSimple(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["section", "taxonomy", "term", "sitemap", "rss"]
+-- layouts/home.html --
+Home.
+{{ with partial "a.html" "warning" }}{{ with partial "b.html" . }}{{ with partial "c.html" . }}{{ . }}{{ end }}{{ end }}{{ end }}
+-- layouts/_partials/a.html --
+<a>{{ inner . }}</a>
+-- layouts/_partials/b.html --
+<b>{{ inner . }}</b>
+-- layouts/_partials/c.html --
+<c>{{ inner . }}</c>
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html", "<a><b><c>warning</c></b></a>")
+}
+
+func TestDecoratorNested2(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ["section", "taxonomy", "term", "sitemap", "RSS"]
+title = "Test title"
+-- content/p1.md --
+---
+title: "Page 1"
+---
+-- content/p2.md --
+---
+title: "Page 2"
+---
+-- layouts/page.html --
+{{ .Title }}
+-- layouts/home.html --
+{{ $pages := site.RegularPages }}
+{{ with partial "ul.html" $pages }}<a href="{{ .RelPermalink }}">{{ with partial "bold.html" . }}<span>{{ .LinkTitle }}</span>{{ end }}</a>{{ end }}
+-- layouts/_partials/ul.html --
+<ul>
+{{- range . }}
+ <li>{{ inner . }}</li>
+{{- end }}
+</ul>
+-- layouts/_partials/bold.html --
+<b>{{ inner $ }}</b>
+`
+
+ b, err := hugolib.TestE(t, files)
+
+ b.Assert(err, qt.IsNil)
+ b.AssertFileContent("public/index.html", `
+<ul>
+ <li><a href="/p1/"><b><span>Page 1</span></b></a></li>
+ <li><a href="/p2/"><b><span>Page 2</span></b></a></li>
+</ul>
+`)
+}
+
+func TestDecoratorMultiple(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+title = "Test title"
+-- layouts/home.html --
+{{ with partial "d1.html" . }}X2{{ . }}X4{{ end }}
+-- layouts/_partials/d1.html --
+X1{{ inner "X3" }}X5
+{{ with partial "d2.html" . }}X7{{ . }}X9{{ end }}
+{{ with partial "noinner.html" "N3" }}N1{{ . }}N5{{ end }}
+X14{{ inner "X15" }}X16
+-- layouts/_partials/d2.html --
+X6{{ inner "X8" }}X10
+{{ with partial "d3.html" . }}A1{{ . }}A2{{ end }}
+X11{{ inner "X12" }}X13
+-- layouts/_partials/d3.html --
+A3{{ inner "A4" }}A5
+A6{{ inner "A7" }}A8
+-- layouts/_partials/noinner.html --
+N2{{ . }}N4
+`
+
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "X1X2X3X4X5",
+ "X6X7X8X9X10",
+ "X11X7X12X9X13",
+ "X14X2X15X4X16",
+ "A3A1A4A2A5",
+ "N1N2N3N4N5", // partial with with, but no inner.
+ )
+}
+
+func TestDecoratorEditInner(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+baseURL = "http://example.org/"
+disableLiveReload = true
+-- layouts/_partials/a.html --
+<b>{{ inner . }}</b>
+-- layouts/home.html --
+{{ with partial "a.html" "Hello" }}{{ . }} World0{{ end }}$
+`
+ b := hugolib.TestRunning(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "<b>Hello World0</b>$",
+ )
+
+ for i := range 4 {
+ b.EditFileReplaceAll("layouts/home.html", fmt.Sprintf("World%d", i), fmt.Sprintf("World%d", i+1)).Build()
+
+ b.AssertFileContent("public/index.html")
+ }
+}
+
+func TestDecoratorEditPartial(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+baseURL = "http://example.org/"
+disableLiveReload = true
+-- layouts/_partials/a.html --
+<b>{{ inner (printf "%s World0" .) }}</b>
+-- layouts/home.html --
+{{ with partial "a.html" "Hello" }}{{ . }}{{ end }}$
+`
+ b := hugolib.TestRunning(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "<b>Hello World0</b>$",
+ )
+
+ for i := range 4 {
+ b.EditFileReplaceAll("layouts/_partials/a.html", fmt.Sprintf("World%d", i), fmt.Sprintf("World%d", i+1)).Build()
+
+ b.AssertFileContent("public/index.html")
+ }
+}
+
+func TestDecoratorDuplicateInner(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_partials/a.html --
+<b>{{ inner . }}</b>
+-- layouts/home.html --
+1: {{ with partial "a.html" "Hello" }}{{ . }}{{ end }}$
+2: {{ with partial "a.html" "World" }}{{ . }}{{ end }}$
+
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "1: <b>Hello</b>$",
+ "2: <b>World</b>$",
+ )
+}
+
+func TestDecoratorInAllTemplateTypes(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_partials/b.html --
+<b>{{ inner . }}</b>
+-- layouts/_markup/render-link.html --
+{{ with partial "b.html" "hello" }}{{ . }} world{{ end }}
+-- layouts/_shortcodes/a.html --
+{{ with partial "b.html" (.Get 0) }}{{ . }} world{{ end }}
+-- layouts/_partials/a.html --
+{{ with partial "b.html" . }}{{ . }} world{{ end }}
+-- layouts/home.html --
+partial: {{ partial "a.html" "hello" }}$
+
+{{ .Content}}
+-- content/_index.md --
+---
+title: "Home"
+---
+shortcode: {{< a "hello" >}}$
+link: [example](/some-url)$
+`
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "partial: <b>hello world</b>$",
+ "shortcode: <b>hello world</b>$",
+ "link: <b>hello world</b>$</p>",
+ )
+}
+
+func TestDecoratorInAllPartialFuncNames(t *testing.T) {
+ t.Parallel()
+
+ filesTemplate := `
+-- hugo.toml --
+-- layouts/_partials/b.html --
+<b>{{ inner . }}</b>
+-- layouts/home.html --
+{{ with FUNC "b.html" "hello" }}{{ . }} world{{ end }}$
+`
+
+ for _, partialFunc := range []string{"partial", "partialCached", "partials.Include", "partials.IncludeCached"} {
+ files := strings.ReplaceAll(filesTemplate, "FUNC", partialFunc)
+ b := hugolib.Test(t, files)
+
+ b.AssertFileContent("public/index.html",
+ "<b>hello world</b>$",
+ )
+ }
+}
+
+func TestDecoratorReturn(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+-- layouts/_partials/add.html --
+{{ $sum := .sum }}
+{{ $sum = add $sum (inner 1) }}
+{{ $sum = add $sum (inner 2) }}
+{{ $sum = add $sum (inner 3) }}
+{{ return $sum }}
+-- layouts/home.html --
+{{ $v := dict "sum" 1 }}
+Sum: {{ with partial "add.html" $v }}
+{{ $sum := mul . 2 }}
+{{ return $sum }}
+{{ end }}$
+`
+ b := hugolib.Test(t, files)
+
+ // .sum = 1
+ // inner 1 => 2
+ // inner 2 => 4
+ // inner 3 => 6
+ // 1 + 2 + 4 + 6 = 13
+ b.AssertFileContent("public/index.html", "Sum: 13$")
+}
+
+func TestDecoratorFailOnInnerInWith(t *testing.T) {
+ t.Parallel()
+
+ filesTemplate := `
+-- hugo.toml --
+-- layouts/_partials/b.html --
+<b>{{ inner . }}</b>
+-- layouts/home.html --
+{{ with partial "b.html" "hello" }}
+This construct creates a loop: {{PLACEHOLDER . }}
+{{ end }}$
+`
+ for _, placeholder := range []string{"inner", "templates.Inner", " inner", "\ninner"} {
+ files := strings.ReplaceAll(filesTemplate, "PLACEHOLDER", placeholder)
+ b, err := hugolib.TestE(t, files)
+
+ b.Assert(err, qt.Not(qt.IsNil))
+ b.Assert(err.Error(), qt.Contains, "inner cannot be used inside a with block that wraps a partial decorator")
+ }
+}
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/tpl/internal"
+ "github.com/gohugoio/hugo/tpl/partials"
)
const name = "templates"
ns := &internal.TemplateFuncsNamespace{
Name: name,
Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
+ OnCreated: func(m map[string]any) {
+ LOOP:
+ for _, v := range m {
+ switch v := v.(type) {
+ case *partials.Namespace:
+ ctx.partialsNs = v
+ break LOOP
+ }
+ }
+ if ctx.partialsNs == nil {
+ panic("partialsNs namespace not found")
+ }
+ },
}
ns.AddMethodMapping(ctx.Current,
[][2]string{},
)
+ ns.AddMethodMapping(ctx.Inner,
+ []string{"inner"},
+ [][2]string{},
+ )
+
+ // For internal use only.
+ ns.AddMethodMapping(ctx._PushPartialDecorator,
+ []string{"_pushPartialDecorator"},
+ [][2]string{},
+ )
+
+ // For internal use only.
+ ns.AddMethodMapping(ctx._PopPartialDecorator,
+ []string{"_popPartialDecorator"},
+ [][2]string{},
+ )
+
ns.AddMethodMapping(ctx.Exists,
nil,
[][2]string{
"strconv"
"sync/atomic"
+ "github.com/gohugoio/hugo/tpl/tplimpl"
+
"github.com/gohugoio/hugo/common/hashing"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/tpl"
+ "github.com/gohugoio/hugo/tpl/partials"
"github.com/mitchellh/mapstructure"
)
// Namespace provides template functions for the "templates" namespace.
type Namespace struct {
- deps *deps.Deps
+ deps *deps.Deps
+ partialsNs *partials.Namespace
}
// Exists returns whether the template with the given name exists.
Data any
}
+// Inner executes the inner content of a partial decorator.
+// Note that there is only one inner block per partial decorator, but inner may be called multiple times with, typically, different data.
+func (ns *Namespace) Inner(ctx context.Context, data any) (any, error) {
+ stack := tpl.Context.PartialDecoratorIDStack.Get(ctx)
+ id, ok := stack.Peek()
+ if !ok {
+ panic("no partial decorator ID on stack")
+ }
+
+ // Signal that inner exists.
+ id.Bool = true
+
+ partialName := fmt.Sprintf("%s%s", tplimpl.PartialDecoratorPrefix, id.Str)
+
+ v, err := ns.partialsNs.Include(ctx, partialName, data)
+
+ return v, err
+}
+
+// For internal use only.
+func (ns *Namespace) _PushPartialDecorator(ctx context.Context, id string) (any, error) {
+ tpl.Context.PartialDecoratorIDStack.Get(ctx).Push(&tpl.StringBool{Str: id, Bool: false})
+ return "", nil
+}
+
+// For internal use only.
+func (ns *Namespace) _PopPartialDecorator(ctx context.Context, id string) bool {
+ stack := tpl.Context.PartialDecoratorIDStack.Get(ctx)
+ if stack == nil || stack.Len() == 0 {
+ panic("decorator stack is nil or empty")
+ }
+
+ // The stack is tied to the context, so no data race.
+ top, ok := stack.Pop()
+ if !ok || top.Str != id {
+ panic("partial decorator ID mismatch")
+ }
+ return top.Bool // return whether inner exists in the wrapped partial.
+}
+
// DoDefer defers the execution of a template block.
// For internal use only.
func (ns *Namespace) DoDefer(ctx context.Context, id string, optsv any) string {
// 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
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.
"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"
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)
}
D: d,
matrix: matrix,
category: category,
+ subCategory: subCategory,
noBaseOf: category > CategoryLayout,
isLegacyMapped: isLegacyMapped,
}
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.
if vv.category == CategoryBaseof {
continue
}
- tctx, err := applyTemplateTransformers(vv, lookup)
+ tctx, err := applyTemplateTransformers(vv, s, lookup)
if err != nil {
return err
}
import (
"errors"
"fmt"
+ "regexp"
"slices"
"strings"
templateNotFound map[string]bool
deferNodes map[string]*parse.ListNode
lookupFn func(name string, in *TemplInfo) *TemplInfo
+ store *TemplateStore
// The last error encountered.
err error
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),
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 {
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() {
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
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) {
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)
if x == nil {
return true, nil
}
- c.collectInner(x)
+ c.collectInnerInShortcode(x)
+ c.collectInnerInPartial(x)
keep := c.collectReturnNode(x)
for _, elem := range x.Args {
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)
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
}
}
}
-// 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
}
}
}
+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