mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-25 07:48:52 +00:00
Vendored
+2
-2
@@ -69,7 +69,7 @@ func New(opts Options) *Cache {
|
||||
|
||||
infol := opts.Log.InfoCommand("dynacache")
|
||||
|
||||
evictedIdentities := collections.NewStack[KeyIdentity]()
|
||||
evictedIdentities := collections.NewStackThreadSafe[KeyIdentity]()
|
||||
|
||||
onEvict := func(k, v any) {
|
||||
if !opts.Watching {
|
||||
@@ -129,7 +129,7 @@ type Cache struct {
|
||||
partitions map[string]PartitionManager
|
||||
|
||||
onEvict func(k, v any)
|
||||
evictedIdentities *collections.Stack[KeyIdentity]
|
||||
evictedIdentities *collections.StackThreadSafe[KeyIdentity]
|
||||
|
||||
opts Options
|
||||
infol logg.LevelLogger
|
||||
|
||||
+55
-11
@@ -21,24 +21,24 @@ import (
|
||||
"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 {
|
||||
@@ -49,7 +49,7 @@ func (s *Stack[T]) Pop() (T, bool) {
|
||||
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 {
|
||||
@@ -58,18 +58,18 @@ func (s *Stack[T]) Peek() (T, bool) {
|
||||
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
|
||||
@@ -77,7 +77,7 @@ func (s *Stack[T]) Drain() []T {
|
||||
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
|
||||
@@ -89,3 +89,47 @@ func (s *Stack[T]) DrainMatching(predicate func(T) bool) []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
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ func TestNewStack(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
s := NewStackThreadSafe[int]()
|
||||
|
||||
c.Assert(s, qt.IsNotNil)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func TestStackBasic(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
s := NewStackThreadSafe[int]()
|
||||
|
||||
c.Assert(s.Len(), qt.Equals, 0)
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestStackDrain(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[string]()
|
||||
s := NewStackThreadSafe[string]()
|
||||
s.Push("a")
|
||||
s.Push("b")
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestStackDrainMatching(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
s := NewStack[int]()
|
||||
s := NewStackThreadSafe[int]()
|
||||
s.Push(1)
|
||||
s.Push(2)
|
||||
s.Push(3)
|
||||
|
||||
@@ -73,6 +73,18 @@ func (m *Map[K, T]) Set(key K, value T) {
|
||||
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()
|
||||
|
||||
+1
-2
@@ -29,7 +29,6 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -76,7 +75,7 @@ func (a aliasHandler) renderAlias(permalink string, p page.Page, matrix sitesmat
|
||||
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)
|
||||
|
||||
@@ -117,6 +117,10 @@ func (tree *SimpleTree[T]) All() iter.Seq2[string, T] {
|
||||
}
|
||||
}
|
||||
|
||||
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)}
|
||||
|
||||
@@ -224,7 +224,7 @@ type WalkContext[T any] struct {
|
||||
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])
|
||||
@@ -261,9 +261,9 @@ func (ctx *WalkContext[T]) HandleEvents() error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/bep/logg"
|
||||
"github.com/yuin/goldmark/util"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
@@ -419,7 +420,7 @@ func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...s
|
||||
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)
|
||||
|
||||
+1
-1
@@ -1610,7 +1610,7 @@ func (s *Site) renderAndWritePage(statCounter *uint64, targetPath string, p *pag
|
||||
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 {
|
||||
|
||||
@@ -145,6 +145,15 @@ func (ns *Namespace) lookup(name string) (*tplimpl.TemplInfo, error) {
|
||||
// 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]
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"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"
|
||||
@@ -53,6 +54,7 @@ const (
|
||||
contextKeyPage
|
||||
contextKeyIsInGoldmark
|
||||
cntextKeyCurrentTemplateInfo
|
||||
contextKeyPartialDecoratorIDStack
|
||||
)
|
||||
|
||||
// Context manages values passed in the context to templates.
|
||||
@@ -63,12 +65,14 @@ var Context = struct {
|
||||
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() {
|
||||
@@ -81,6 +85,12 @@ 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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/deps"
|
||||
"github.com/gohugoio/hugo/tpl/internal"
|
||||
"github.com/gohugoio/hugo/tpl/partials"
|
||||
)
|
||||
|
||||
const name = "templates"
|
||||
@@ -29,6 +30,19 @@ func init() {
|
||||
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,
|
||||
@@ -47,6 +61,23 @@ func init() {
|
||||
[][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{
|
||||
|
||||
@@ -20,9 +20,12 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -37,7 +40,8 @@ func New(deps *deps.Deps) *Namespace {
|
||||
|
||||
// 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.
|
||||
@@ -70,6 +74,46 @@ type DeferOpts struct {
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user