Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f42c422a12 | |||
| 0b71db299a | |||
| 5c7fad23a3 | |||
| 700bb78198 | |||
| d3b5d47a43 | |||
| 86cd1838ff | |||
| 871da3370a | |||
| 2637aa1540 | |||
| bf1d20d735 | |||
| 1deec99bba | |||
| b7bb557c6a | |||
| 7c19c196c3 | |||
| 555443b64a | |||
| 1f2de189ed | |||
| c6ae33c61d | |||
| edeebf0d33 | |||
| ea9675f6e7 | |||
| b1f7e35a98 | |||
| a0d4e1fb26 | |||
| 96777d9b89 | |||
| 25126e5f87 |
@@ -65,8 +65,6 @@ See the [features] section of the documentation for a comprehensive summary of H
|
||||
|
||||
<p> </p>
|
||||
<p float="left">
|
||||
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
|
||||
|
||||
<a href="https://www.jetbrains.com/go/?utm_source=OSS&utm_medium=referral&utm_campaign=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/goland.svg" width="200" alt="The complete IDE crafted for professional Go developers."></a>
|
||||
|
||||
<a href="https://cloudcannon.com/hugo-cms/?utm_campaign=HugoSponsorship&utm_source=sponsor&utm_content=gohugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/cloudcannon-cms-logo.svg" width="200" alt="CloudCannon"></a>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -19,7 +19,7 @@ import "github.com/gohugoio/hugo/common/version"
|
||||
// This should be the only one.
|
||||
var CurrentVersion = version.Version{
|
||||
Major: 0,
|
||||
Minor: 153,
|
||||
PatchLevel: 3,
|
||||
Suffix: "",
|
||||
Minor: 155,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -915,10 +915,9 @@ func (c *Configs) Init(logger loggers.Logger) error {
|
||||
// avoid initializing the same config more than once.
|
||||
for i, l := range c.Languages {
|
||||
langConfig := c.LanguageConfigMap[l.Lang]
|
||||
sitesMatrix := sitesmatrix.NewIntSetsBuilder(c.ConfiguredDimensions).WithLanguageIndices(i).WithAllIfNotSet().Build()
|
||||
for _, s := range allDecoderSetups {
|
||||
if getInitializer := s.getInitializer; getInitializer != nil {
|
||||
if err := getInitializer(langConfig).InitConfig(logger, sitesMatrix, c.ConfiguredDimensions); err != nil {
|
||||
if err := getInitializer(langConfig).InitConfig(logger, nil, c.ConfiguredDimensions); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.9
|
||||
github.com/tdewolff/minify/v2 v2.24.8
|
||||
github.com/tdewolff/parse/v2 v2.8.5
|
||||
github.com/tetratelabs/wazero v1.10.1
|
||||
github.com/tetratelabs/wazero v1.11.0
|
||||
github.com/yuin/goldmark v1.7.13
|
||||
github.com/yuin/goldmark-emoji v1.0.6
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
|
||||
@@ -511,8 +511,8 @@ github.com/tdewolff/parse/v2 v2.8.5 h1:ZmBiA/8Do5Rpk7bDye0jbbDUpXXbCdc3iah4VeUvw
|
||||
github.com/tdewolff/parse/v2 v2.8.5/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8=
|
||||
github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
|
||||
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
|
||||
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
||||
|
||||
@@ -137,6 +137,7 @@ func ExtractAndGroupRootPaths(in []string) []string {
|
||||
return nil
|
||||
}
|
||||
const maxGroups = 5
|
||||
const maxRootGroups = 10
|
||||
sort.Strings(in)
|
||||
var groups []string
|
||||
tree := radix.New[[]string]()
|
||||
@@ -186,6 +187,13 @@ LOOP:
|
||||
|
||||
tree.Walk(collect)
|
||||
|
||||
// Limit the total number of root groups to keep output manageable
|
||||
if len(groups) > maxRootGroups {
|
||||
remaining := len(groups) - maxRootGroups
|
||||
groups = groups[:maxRootGroups]
|
||||
groups = append(groups, fmt.Sprintf("... and %d more", remaining))
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
|
||||
@@ -345,19 +345,34 @@ func TestAbsPathify(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExtractAndGroupRootPaths(t *testing.T) {
|
||||
in := []string{
|
||||
filepath.FromSlash("/a/b/c/d"),
|
||||
filepath.FromSlash("/a/b/c/e"),
|
||||
filepath.FromSlash("/a/b/e/f"),
|
||||
filepath.FromSlash("/a/b"),
|
||||
filepath.FromSlash("/a/b/c/b/g"),
|
||||
filepath.FromSlash("/c/d/e"),
|
||||
}
|
||||
|
||||
result := helpers.ExtractAndGroupRootPaths(in)
|
||||
|
||||
c := qt.New(t)
|
||||
c.Assert(result, qt.DeepEquals, []string{"/a/b/{c,e}", "/c/d/e"})
|
||||
|
||||
t.Run("Basic grouping", func(t *testing.T) {
|
||||
in := []string{
|
||||
filepath.FromSlash("/a/b/c/d"),
|
||||
filepath.FromSlash("/a/b/c/e"),
|
||||
filepath.FromSlash("/a/b/e/f"),
|
||||
filepath.FromSlash("/a/b"),
|
||||
filepath.FromSlash("/a/b/c/b/g"),
|
||||
filepath.FromSlash("/c/d/e"),
|
||||
}
|
||||
|
||||
result := helpers.ExtractAndGroupRootPaths(in)
|
||||
c.Assert(result, qt.DeepEquals, []string{"/a/b/{c,e}", "/c/d/e"})
|
||||
})
|
||||
|
||||
t.Run("Limits number of root groups", func(t *testing.T) {
|
||||
in := []string{}
|
||||
// Create 15 different root paths to exceed maxRootGroups (10)
|
||||
for i := 0; i < 15; i++ {
|
||||
in = append(in, filepath.FromSlash(fmt.Sprintf("/path%d/subdir", i)))
|
||||
}
|
||||
|
||||
result := helpers.ExtractAndGroupRootPaths(in)
|
||||
// Should have 10 paths + 1 "... and X more" message
|
||||
c.Assert(len(result), qt.Equals, 11)
|
||||
c.Assert(result[10], qt.Matches, `\.\.\. and \d+ more`)
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkExtractAndGroupRootPaths(b *testing.B) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -365,3 +365,54 @@ Resource: {{ .Name }}|p1: {{ .Params.p1 }}|
|
||||
|
||||
b.AssertFileContent("public/b1/index.html", "Title: b1|p1: v1|", "Resource: p2.md|p1: v1|")
|
||||
}
|
||||
|
||||
// Issue 14310
|
||||
// Issue 14321
|
||||
func TestCascadeIssue14310(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ['home', 'rss', 'sitemap', 'taxonomy', 'term']
|
||||
defaultContentLanguage = 'en'
|
||||
defaultContentLanguageInSubdir = true
|
||||
[languages.en]
|
||||
weight = 1
|
||||
[languages.de]
|
||||
weight = 2
|
||||
[[cascade]]
|
||||
[cascade.params]
|
||||
size = 'medium'
|
||||
[cascade.target]
|
||||
kind = 'page'
|
||||
-- layouts/all.html --
|
||||
|color: {{ .Params.color }}|size: {{ .Params.size }}|
|
||||
-- content/s1/_index.de.md --
|
||||
---
|
||||
title: s1 (de)
|
||||
cascade:
|
||||
params:
|
||||
color: red (de)
|
||||
---
|
||||
-- content/s1/_index.en.md --
|
||||
---
|
||||
title: s1 (en)
|
||||
cascade:
|
||||
params:
|
||||
color: red (en)
|
||||
---
|
||||
-- content/s1/p1.de.md --
|
||||
---
|
||||
title: p1 (de)
|
||||
---
|
||||
-- content/s1/p1.en.md --
|
||||
---
|
||||
title: p1 (en)
|
||||
---
|
||||
`
|
||||
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/en/s1/p1/index.html", "|color: red (en)|size: medium|") // fails: file contains "|color: red (en)|size: |"
|
||||
b.AssertFileContent("public/de/s1/p1/index.html", "|color: red (de)|size: medium|")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -544,7 +544,7 @@ Content Tag 1.
|
||||
|
||||
b.AssertFileContent("public/en/posts/p1/index.html",
|
||||
"Single: en|page|/en/posts/p1/|Post 1|<p>Content 1.</p>\n|Len Resources: 2|",
|
||||
"Resources: text|/en/posts/p1/f1.txt|text/plain|map[icon:enicon] - page||application/octet-stream|map[background:post.jpg draft:false iscjklanguage:false title:Post Sub 1] -",
|
||||
"Resources: text|/en/posts/p1/f1.txt|text/plain|map[icon:enicon] - page||application/octet-stream|map[draft:false iscjklanguage:false title:Post Sub 1] -",
|
||||
"Icon: enicon",
|
||||
"Icon fingerprinted: enicon|/en/posts/p1/f1.e5746577af5cbfc4f34c558051b7955a9a5a795a84f1c6ab0609cb3473a924cb.txt|",
|
||||
"NextInSection: |\nPrevInSection: /en/posts/p2/|Post 2|",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -266,7 +266,7 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
|
||||
warpc.Options{
|
||||
CompilationCacheDir: compilationCacheDir,
|
||||
PoolSize: 1,
|
||||
Memory: 256, // 256 MiB (4096 MiB Max)
|
||||
Memory: 384, // 384 MiB (4096 MiB Max)
|
||||
Infof: logger.InfoCommand("webp").Logf,
|
||||
Warnf: logger.WarnCommand("webp").Logf,
|
||||
},
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# Release env.
|
||||
# These will be replaced by script before release.
|
||||
HUGORELEASER_TAG=v0.153.2
|
||||
HUGORELEASER_COMMITISH=798533a2013eab97198b0a155a8f4afab7e79865
|
||||
HUGORELEASER_TAG=v0.154.0
|
||||
HUGORELEASER_COMMITISH=0b71db299a2bd89be876d7dc972ded03a222f560
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -73,9 +73,10 @@ typedef struct
|
||||
|
||||
typedef struct
|
||||
{
|
||||
float quality; // between 0 and 100. Set to 0 for lossless.
|
||||
char hint[64]; // drawing, icon, photo, picture, or text. Default is photo.
|
||||
int preset; // preset to use; resolved from hint.
|
||||
float quality; // between 1 and 100.
|
||||
char compression[32]; // "lossy" or "lossless"
|
||||
char hint[64]; // drawing, icon, photo, picture, or text. Default is photo.
|
||||
int preset; // preset to use; resolved from hint.
|
||||
|
||||
bool useSharpYuv; // use sharp YUV for better quality.
|
||||
|
||||
@@ -302,7 +303,7 @@ static uint8_t initEncoderConfig(WebPConfig *config, InputOptions opts)
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (opts.quality == 0)
|
||||
if (strcmp(opts.compression, "lossless") == 0)
|
||||
{
|
||||
// Activate the lossless compression mode with the desired efficiency level
|
||||
// between 0 (fastest, lowest compression) and 9 (slower, best compression).
|
||||
@@ -396,6 +397,12 @@ InputMessage parse_input_message(const char *line)
|
||||
if (options_object != NULL)
|
||||
{
|
||||
msg.data.options.quality = (int)json_object_get_number(options_object, "quality");
|
||||
const char *compression_str = json_object_get_string(options_object, "compression");
|
||||
if (compression_str != NULL)
|
||||
{
|
||||
strncpy(msg.data.options.compression, compression_str, sizeof(msg.data.options.compression) - 1);
|
||||
msg.data.options.compression[sizeof(msg.data.options.compression) - 1] = '\0';
|
||||
}
|
||||
const char *hint_str = json_object_get_string(options_object, "hint");
|
||||
if (hint_str != NULL)
|
||||
{
|
||||
|
||||
@@ -792,11 +792,9 @@ func (d *Dispatchers) Webp() (Dispatcher[WebpInput, WebpOutput], error) {
|
||||
return d.webp.start()
|
||||
}
|
||||
|
||||
func (d *Dispatchers) NewWepCodec(quality int, hint string) (*WebpCodec, error) {
|
||||
func (d *Dispatchers) NewWepCodec() (*WebpCodec, error) {
|
||||
return &WebpCodec{
|
||||
d: d.Webp,
|
||||
quality: quality,
|
||||
hint: hint,
|
||||
d: d.Webp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"io"
|
||||
"maps"
|
||||
|
||||
"github.com/gohugoio/hugo/common/himage"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
@@ -82,9 +83,7 @@ type WebpOutput struct {
|
||||
}
|
||||
|
||||
type WebpCodec struct {
|
||||
d func() (Dispatcher[WebpInput, WebpOutput], error)
|
||||
quality int
|
||||
hint string
|
||||
d func() (Dispatcher[WebpInput, WebpOutput], error)
|
||||
}
|
||||
|
||||
// Decode reads a WEBP image from r and returns it as an image.Image.
|
||||
@@ -201,7 +200,7 @@ func (d *WebpCodec) DecodeConfig(r io.Reader) (image.Config, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *WebpCodec) Encode(w io.Writer, img image.Image) error {
|
||||
func (d *WebpCodec) Encode(w io.Writer, img image.Image, opts map[string]any) error {
|
||||
b := img.Bounds()
|
||||
if b.Dx() >= 1<<16 || b.Dy() >= 1<<16 {
|
||||
return errors.New("webp: image is too large to encode")
|
||||
@@ -294,11 +293,9 @@ func (d *WebpCodec) Encode(w io.Writer, img image.Image) error {
|
||||
return fmt.Errorf("no image bytes extracted from %T", img)
|
||||
}
|
||||
|
||||
// Commands:
|
||||
// encodeNRGBA
|
||||
// encodeGray
|
||||
// decode
|
||||
// config
|
||||
opts = maps.Clone(opts)
|
||||
opts["useSharpYuv"] = true // Use sharp (and slow) RGB->YUV conversion.
|
||||
|
||||
message := Message[WebpInput]{
|
||||
Header: Header{
|
||||
Version: 1,
|
||||
@@ -310,11 +307,7 @@ func (d *WebpCodec) Encode(w io.Writer, img image.Image) error {
|
||||
Data: WebpInput{
|
||||
Source: bytes.NewReader(imageBytes),
|
||||
Destination: w,
|
||||
Options: map[string]any{
|
||||
"quality": d.quality, // a number between 0 and 100. Set to 0 for lossless.
|
||||
"hint": d.hint, // drawing, icon, photo, picture, or text
|
||||
"useSharpYuv": true, // Use sharp (and slow) RGB->YUV conversion.
|
||||
},
|
||||
Options: opts,
|
||||
Params: map[string]any{
|
||||
"width": bounds.Max.X,
|
||||
"height": bounds.Max.Y,
|
||||
|
||||
@@ -37,27 +37,27 @@ type Decoder interface {
|
||||
DecodeConfig(r io.Reader) (image.Config, error)
|
||||
}
|
||||
|
||||
// Encoder defines the encoding of an image format.
|
||||
type Encoder interface {
|
||||
Encode(w io.Writer, src image.Image) error
|
||||
}
|
||||
|
||||
type ToEncoder interface {
|
||||
EncodeTo(conf ImageConfig, w io.Writer, src image.Image) error
|
||||
}
|
||||
|
||||
// CodecStdlib defines both decoding and encoding of an image format as defined by the standard library.
|
||||
type CodecStdlib interface {
|
||||
// EncoderWithOptions defines the encoding of an image format with the given options.
|
||||
type Encoder interface {
|
||||
Encode(w io.Writer, src image.Image, options map[string]any) error
|
||||
}
|
||||
|
||||
// EncodeDecoder defines both decoding and encoding of an image format as defined by the standard library.
|
||||
type EncodeDecoder interface {
|
||||
Decoder
|
||||
Encoder
|
||||
}
|
||||
|
||||
// Codec is a generic image codec supporting multiple formats.
|
||||
type Codec struct {
|
||||
webp CodecStdlib
|
||||
webp EncodeDecoder
|
||||
}
|
||||
|
||||
func newCodec(webp CodecStdlib) *Codec {
|
||||
func newCodec(webp EncodeDecoder) *Codec {
|
||||
return &Codec{webp: webp}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,12 @@ func (d *Codec) EncodeTo(conf ImageConfig, w io.Writer, img image.Image) error {
|
||||
case BMP:
|
||||
return bmp.Encode(w, img)
|
||||
case WEBP:
|
||||
return d.webp.Encode(w, img)
|
||||
opts := map[string]any{
|
||||
"compression": conf.Compression,
|
||||
"quality": conf.Quality,
|
||||
"hint": conf.Hint,
|
||||
}
|
||||
return d.webp.Encode(w, img, opts)
|
||||
default:
|
||||
return errors.New("format not supported")
|
||||
}
|
||||
|
||||
@@ -58,7 +58,8 @@ var (
|
||||
".webp": WEBP,
|
||||
}
|
||||
|
||||
imageFormatsBySubType = map[string]Format{
|
||||
// These are the image types we can process.
|
||||
processableImageSubTypes = map[string]Format{
|
||||
media.Builtin.JPEGType.SubType: JPEG,
|
||||
media.Builtin.PNGType.SubType: PNG,
|
||||
media.Builtin.TIFFType.SubType: TIFF,
|
||||
@@ -85,6 +86,11 @@ var anchorPositions = map[string]gift.Anchor{
|
||||
smartCropIdentifier: SmartCropAnchor,
|
||||
}
|
||||
|
||||
var compressionMethods = map[string]bool{
|
||||
"lossy": true,
|
||||
"lossless": true,
|
||||
}
|
||||
|
||||
// These encoding hints are currently only relevant for Webp.
|
||||
var hints = map[string]bool{
|
||||
"picture": true,
|
||||
@@ -118,7 +124,7 @@ func ImageFormatFromExt(ext string) (Format, bool) {
|
||||
}
|
||||
|
||||
func ImageFormatFromMediaSubType(sub string) (Format, bool) {
|
||||
f, found := imageFormatsBySubType[sub]
|
||||
f, found := processableImageSubTypes[sub]
|
||||
return f, found
|
||||
}
|
||||
|
||||
@@ -127,6 +133,7 @@ const (
|
||||
defaultResampleFilter = "box"
|
||||
defaultBgColor = "#ffffff"
|
||||
defaultHint = "photo"
|
||||
defaultCompression = "lossy"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -135,6 +142,7 @@ var (
|
||||
"bgColor": defaultBgColor,
|
||||
"hint": defaultHint,
|
||||
"quality": defaultJPEGQuality,
|
||||
"compression": defaultCompression,
|
||||
}
|
||||
|
||||
defaultImageConfig *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]
|
||||
@@ -226,6 +234,8 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
|
||||
c.Filter = filter
|
||||
} else if _, ok := hints[part]; ok {
|
||||
c.Hint = part
|
||||
} else if _, ok := compressionMethods[part]; ok {
|
||||
c.Compression = part
|
||||
} else if part[0] == '#' {
|
||||
c.BgColor, err = hexStringToColorGo(part[1:])
|
||||
if err != nil {
|
||||
@@ -239,7 +249,6 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
|
||||
if c.Quality < 1 || c.Quality > 100 {
|
||||
return c, errors.New("quality ranges from 1 to 100 inclusive")
|
||||
}
|
||||
c.qualitySetForImage = true
|
||||
} else if part[0] == 'r' {
|
||||
c.Rotate, err = strconv.Atoi(part[1:])
|
||||
if err != nil {
|
||||
@@ -306,10 +315,15 @@ func DecodeImageConfig(options []string, defaults *config.ConfigNamespace[Imagin
|
||||
}
|
||||
|
||||
if c.Quality <= 0 && c.TargetFormat.RequiresDefaultQuality() {
|
||||
// We need a quality setting for all JPEGs and WEBPs.
|
||||
// We need a quality setting for all JPEGs and WEBPs,
|
||||
// unless the user explicitly set quality.
|
||||
c.Quality = defaults.Config.Imaging.Quality
|
||||
}
|
||||
|
||||
if c.Compression == "" {
|
||||
c.Compression = defaults.Config.Imaging.Compression
|
||||
}
|
||||
|
||||
if c.BgColor == nil && c.TargetFormat != sourceFormat {
|
||||
if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() {
|
||||
c.BgColor = defaults.Config.BgColor
|
||||
@@ -339,11 +353,11 @@ type ImageConfig struct {
|
||||
// If set, this will be used as the key in filenames etc.
|
||||
Key string
|
||||
|
||||
// Quality ranges from 1 to 100 inclusive, higher is better.
|
||||
// Quality ranges from 0 to 100 inclusive, higher is better.
|
||||
// This is only relevant for JPEG and WEBP images.
|
||||
// For WebP, 0 means lossless.
|
||||
// Default is 75.
|
||||
Quality int
|
||||
qualitySetForImage bool // Whether the above is set for this image.
|
||||
Quality int
|
||||
|
||||
// Rotate rotates an image by the given angle counter-clockwise.
|
||||
// The rotation will be performed first.
|
||||
@@ -360,6 +374,8 @@ type ImageConfig struct {
|
||||
// when target is set to webp.
|
||||
Hint string
|
||||
|
||||
Compression string
|
||||
|
||||
Width int
|
||||
Height int
|
||||
|
||||
@@ -412,6 +428,11 @@ type ImagingConfig struct {
|
||||
// Default image quality setting (1-100). Only used for JPEG and WebP images.
|
||||
Quality int
|
||||
|
||||
// Compression method to use.
|
||||
// One of "lossy" or "lossless".
|
||||
// Note that lossless is currently only supported for WebP.
|
||||
Compression string
|
||||
|
||||
// Resample filter to use in resize operations.
|
||||
ResampleFilter string
|
||||
|
||||
@@ -431,7 +452,7 @@ type ImagingConfig struct {
|
||||
}
|
||||
|
||||
func (cfg *ImagingConfig) init() error {
|
||||
if cfg.Quality < 0 || cfg.Quality > 100 {
|
||||
if cfg.Quality < 1 || cfg.Quality > 100 {
|
||||
return errors.New("image quality must be a number between 1 and 100")
|
||||
}
|
||||
|
||||
@@ -439,6 +460,7 @@ func (cfg *ImagingConfig) init() error {
|
||||
cfg.Anchor = strings.ToLower(cfg.Anchor)
|
||||
cfg.ResampleFilter = strings.ToLower(cfg.ResampleFilter)
|
||||
cfg.Hint = strings.ToLower(cfg.Hint)
|
||||
cfg.Compression = strings.ToLower(cfg.Compression)
|
||||
|
||||
if cfg.Anchor == "" {
|
||||
cfg.Anchor = smartCropIdentifier
|
||||
|
||||
@@ -133,7 +133,6 @@ func newImageConfig(action string, width, height, quality, rotate int, filter, a
|
||||
c.Width = width
|
||||
c.Height = height
|
||||
c.Quality = quality
|
||||
c.qualitySetForImage = quality != 75
|
||||
c.Rotate = rotate
|
||||
c.BgColor, _ = hexStringToColorGo(bgColor)
|
||||
c.Anchor = SmartCropAnchor
|
||||
|
||||
@@ -137,7 +137,7 @@ func NewImageProcessor(warnl logg.LevelLogger, wasmDispatchers *warpc.Dispatcher
|
||||
return nil, err
|
||||
}
|
||||
|
||||
webpCodec, err := wasmDispatchers.NewWepCodec(cfg.Config.Imaging.Quality, cfg.Config.Imaging.Hint)
|
||||
webpCodec, err := wasmDispatchers.NewWepCodec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -300,9 +300,10 @@ func GetDefaultImageConfig(defaults *config.ConfigNamespace[ImagingConfig, Imagi
|
||||
defaults = defaultImageConfig
|
||||
}
|
||||
return ImageConfig{
|
||||
Anchor: -1, // The real values start at 0.
|
||||
Hint: "photo",
|
||||
Quality: defaults.Config.Imaging.Quality,
|
||||
Anchor: -1, // The real values start at 0.
|
||||
Hint: "photo",
|
||||
Quality: defaults.Config.Imaging.Quality,
|
||||
Compression: defaults.Config.Imaging.Compression,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,17 @@ import (
|
||||
"github.com/gohugoio/hugo/resources/images/imagetesting"
|
||||
)
|
||||
|
||||
const goldenProcess = `
|
||||
{{ define "process"}}
|
||||
{{ $img := .img.Process .spec }}
|
||||
{{ $ext := path.Ext $img.RelPermalink }}
|
||||
{{ $name := printf "images/%s%s" (.spec | anchorize) $ext }}
|
||||
{{ with $img | resources.Copy $name }}
|
||||
{{ .Publish }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
// Note, if you're enabling writeGoldenFiles on a MacOS ARM 64 you need to run the test with GOARCH=amd64, e.g.
|
||||
func TestImagesGoldenFiltersMisc(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -324,15 +335,8 @@ Home.
|
||||
{{ template "process" (dict "spec" "resize 100x100 r180" "img" $gopher) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 jpg #b31280" "img" $gopher) }}
|
||||
|
||||
{{ define "process"}}
|
||||
{{ $img := .img.Process .spec }}
|
||||
{{ $ext := path.Ext $img.RelPermalink }}
|
||||
{{ $name := printf "images/%s%s" (.spec | anchorize) $ext }}
|
||||
{{ with $img | resources.Copy $name }}
|
||||
{{ .Publish }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
` + goldenProcess
|
||||
|
||||
opts := imagetesting.DefaultGoldenOpts
|
||||
opts.T = t
|
||||
@@ -378,7 +382,7 @@ Home.
|
||||
{{/* These are sorted. The end file name will be created from the spec + extension, so make sure these are unique. */}}
|
||||
{{ template "process" (dict "spec" "crop 300x300 gif" "img" $animWebp) }}
|
||||
{{ template "process" (dict "spec" "crop 300x300 smart" "img" $fuzzyCircle) }}
|
||||
{{ template "process" (dict "spec" "crop 300x300 smart #ff9999" "img" $fuzzyCircle) }}
|
||||
{{ template "process" (dict "spec" "crop 300x300 smart #ff9999" "img" $fuzzyCircle) }}
|
||||
{{ template "process" (dict "spec" "crop 300x300" "img" $animWebp) }}
|
||||
{{ template "process" (dict "spec" "crop 500x200 smart webp" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "crop 500x200 smart webp" "img" $sunset) }}
|
||||
@@ -388,6 +392,14 @@ Home.
|
||||
{{ template "process" (dict "spec" "png" "img" $highContrast) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300" "img" $giphy) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 webp" "img" $giphy) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 webp lossless" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 webp q1" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 webp q33" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 webp q75" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 webp q100" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 webp drawing" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 webp icon" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 webp q50 drawing" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "resize 400x" "img" $highContrast) }}
|
||||
|
||||
{{ define "process"}}
|
||||
@@ -478,7 +490,7 @@ Home.
|
||||
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "fit" "spec" "200x200" ) }}
|
||||
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "200x200" ) }}
|
||||
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 center" ) }}
|
||||
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 smart" ) }}
|
||||
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 smart" ) }}
|
||||
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 center r90" ) }}
|
||||
{{ template "invoke" (dict "copyFormat" "jpg" "base" $sunset "method" "crop" "spec" "350x400 center q20" ) }}
|
||||
{{ template "invoke" (dict "copyFormat" "png" "base" $gopher "method" "resize" "spec" "100x" ) }}
|
||||
@@ -513,3 +525,37 @@ Home.
|
||||
|
||||
imagetesting.RunGolden(opts)
|
||||
}
|
||||
|
||||
func TestImagesGoldenConfigLossyVsQuality(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if imagetesting.SkipGoldenTests {
|
||||
t.Skip("Skip golden test on this architecture")
|
||||
}
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[imaging]
|
||||
quality = 90 # will only apply to jpeg in this setup.
|
||||
compression = "lossless" # for webp
|
||||
-- assets/sunset.jpg --
|
||||
sourcefilename: ../testdata/sunset.jpg
|
||||
-- layouts/home.html --
|
||||
Home.
|
||||
{{ $sunset := resources.Get "sunset.jpg" }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 webp" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 webp lossy" "img" $sunset) }}
|
||||
{{ template "process" (dict "spec" "resize 300x300 jpeg" "img" $sunset) }}
|
||||
|
||||
` + goldenProcess
|
||||
|
||||
// Will be used as the base folder for generated images.
|
||||
name := "losslessvsquality"
|
||||
|
||||
opts := imagetesting.DefaultGoldenOpts
|
||||
opts.T = t
|
||||
opts.Name = name
|
||||
opts.Files = files
|
||||
|
||||
imagetesting.RunGolden(opts)
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 752 B |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
@@ -246,8 +246,8 @@ func (d cascadeConfigDecoder) decodePageMatcher(m any, v *PageMatcher) error {
|
||||
}
|
||||
|
||||
// DecodeCascadeConfigOptions
|
||||
func (v *PageMatcher) compileSitesMatrix(configuredDimensions *sitesmatrix.ConfiguredDimensions) error {
|
||||
if v.Sites.Matrix.IsZero() {
|
||||
func (v *PageMatcher) compileSitesMatrix(defaults sitesmatrix.VectorStore, configuredDimensions *sitesmatrix.ConfiguredDimensions) error {
|
||||
if v.Sites.Matrix.IsZero() && defaults == nil {
|
||||
// Nothing to do.
|
||||
v.SitesMatrixCompiled = nil
|
||||
return nil
|
||||
@@ -255,8 +255,12 @@ func (v *PageMatcher) compileSitesMatrix(configuredDimensions *sitesmatrix.Confi
|
||||
intSetsCfg := sitesmatrix.IntSetsConfig{
|
||||
Globs: v.Sites.Matrix,
|
||||
}
|
||||
b := sitesmatrix.NewIntSetsBuilder(configuredDimensions).WithConfig(intSetsCfg).WithAllIfNotSet()
|
||||
|
||||
b := sitesmatrix.NewIntSetsBuilder(configuredDimensions).WithConfig(intSetsCfg)
|
||||
if defaults != nil && v.Sites.Matrix.IsZero() {
|
||||
b = b.WithDimensionsFromOtherIfNotSet(defaults)
|
||||
}
|
||||
b = b.WithAllIfNotSet()
|
||||
v.SitesMatrixCompiled = b.Build()
|
||||
return nil
|
||||
}
|
||||
@@ -281,6 +285,17 @@ func (p *PageMatcherParamsConfig) init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PageMatcherParamsConfig) hasSitesMatrix() bool {
|
||||
if m, ok := p.Fields["sites"]; ok {
|
||||
mm := maps.ToStringMap(m)
|
||||
if sm, found := mm["matrix"]; found {
|
||||
mmm := maps.ToStringMap(sm)
|
||||
return len(mmm) > 0
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type PageMatcherParamsConfigs struct {
|
||||
c []*config.ConfigNamespace[[]PageMatcherParamsConfig, CascadeConfig]
|
||||
}
|
||||
@@ -347,16 +362,23 @@ func (c *PageMatcherParamsConfigs) SourceHash() uint64 {
|
||||
return h.Sum64()
|
||||
}
|
||||
|
||||
func (c *PageMatcherParamsConfigs) InitConfig(logger loggers.Logger, _ sitesmatrix.VectorStore, configuredDimensions *sitesmatrix.ConfiguredDimensions) error {
|
||||
func (c *PageMatcherParamsConfigs) InitConfig(logger loggers.Logger, defaultsIn sitesmatrix.VectorStore, configuredDimensions *sitesmatrix.ConfiguredDimensions) error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
for _, cc := range c.c {
|
||||
for i := range cc.Config.Cascades {
|
||||
checkCascadePattern(logger, cc.Config.Cascades[i].Target)
|
||||
if err := cc.Config.Cascades[i].Target.compileSitesMatrix(configuredDimensions); err != nil {
|
||||
ccc := cc.Config.Cascades[i]
|
||||
checkCascadePattern(logger, ccc.Target)
|
||||
defaults := defaultsIn
|
||||
hasSitesMatrix := ccc.hasSitesMatrix()
|
||||
if hasSitesMatrix {
|
||||
defaults = nil
|
||||
}
|
||||
if err := ccc.Target.compileSitesMatrix(defaults, configuredDimensions); err != nil {
|
||||
return fmt.Errorf("failed to compile cascade target %d: %w", i, err)
|
||||
}
|
||||
cc.Config.Cascades[i] = ccc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -336,11 +336,6 @@ func (l PermalinkExpander) pageToPermalinkSectionSlugs(p Page, attr string) (str
|
||||
|
||||
// pageToPermalinkContentBaseName returns the URL-safe form of the content base name.
|
||||
func (l PermalinkExpander) pageToPermalinkContentBaseName(p Page, _ string) (string, error) {
|
||||
// For section pages with _index.md files, return empty string to match the behavior of pageToPermalinkFilename.
|
||||
// Sections without files should use their directory name.
|
||||
if p.PathInfo().IsBranchBundle() && p.File() != nil {
|
||||
return "", nil
|
||||
}
|
||||
return l.urlize(p.PathInfo().Unnormalized().BaseNameNoIdentifier()), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -457,39 +457,3 @@ title: aBc
|
||||
b = hugolib.Test(t, files)
|
||||
b.AssertFileExists("public/aBc/index.html", true)
|
||||
}
|
||||
|
||||
// Issue 14104
|
||||
func TestIssue14104(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[permalinks.page]
|
||||
foo = "/:sections[1:]/:slugorcontentbasename/"
|
||||
[permalinks.section]
|
||||
foo = "/:sections[1:]/:slugorcontentbasename/"
|
||||
-- content/foo/_index.md --
|
||||
---
|
||||
title: Foo
|
||||
---
|
||||
-- content/foo/bar/_index.md --
|
||||
---
|
||||
title: Bar
|
||||
---
|
||||
-- content/foo/bar/somepage.md --
|
||||
---
|
||||
title: Some Page
|
||||
---
|
||||
-- layouts/list.html --
|
||||
List|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
-- layouts/single.html --
|
||||
Single|{{ .Kind }}|{{ .RelPermalink }}|
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
// Section page should be at /bar/index.html, not /bar/bar/index.html
|
||||
b.AssertFileContent("public/bar/index.html", "List|section|/bar/|")
|
||||
// Regular page should be at /bar/somepage/index.html
|
||||
b.AssertFileContent("public/bar/somepage/index.html", "Single|page|/bar/somepage/|")
|
||||
}
|
||||
|
||||
@@ -400,6 +400,20 @@ func (r *resourceAdapter) getImageOps() images.ImageResourceOps {
|
||||
return img
|
||||
}
|
||||
|
||||
// IsImage reports whether the given resource is an image that can be processed.
|
||||
func IsImage(v any) bool {
|
||||
r, ok := v.(resource.Resource)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
mt := r.MediaType()
|
||||
if mt.MainType != "image" {
|
||||
return false
|
||||
}
|
||||
_, isImage := images.ImageFormatFromMediaSubType(mt.SubType)
|
||||
return isImage
|
||||
}
|
||||
|
||||
func (r *resourceAdapter) publish() {
|
||||
if r.publishOnce == nil {
|
||||
return
|
||||
|
||||
@@ -64,6 +64,8 @@ func (ns *Namespace) Apply(ctx context.Context, c any, fname string, args ...any
|
||||
}
|
||||
}
|
||||
|
||||
var typeOfReflectValue = reflect.TypeOf(reflect.Value{})
|
||||
|
||||
func applyFnToThis(ctx context.Context, fn, this reflect.Value, args ...any) (reflect.Value, error) {
|
||||
num := fn.Type().NumIn()
|
||||
if num > 0 && hreflect.IsContextType(fn.Type().In(0)) {
|
||||
@@ -91,7 +93,11 @@ func applyFnToThis(ctx context.Context, fn, this reflect.Value, args ...any) (re
|
||||
}*/
|
||||
|
||||
for i := range num {
|
||||
// AssignableTo reports whether xt is assignable to type targ.
|
||||
// Go's built-in template funcs (e.g. len) use reflect.Value as argument type.
|
||||
if fn.Type().In(i) == typeOfReflectValue && n[i].Type() != typeOfReflectValue {
|
||||
n[i] = reflect.ValueOf(n[i])
|
||||
}
|
||||
|
||||
if xt, targ := n[i].Type(), fn.Type().In(i); !xt.AssignableTo(targ) {
|
||||
return reflect.ValueOf(nil), errors.New("called apply using " + xt.String() + " as type " + targ.String())
|
||||
}
|
||||
|
||||
@@ -39,6 +39,21 @@ baseURL = 'http://example.com/'
|
||||
`)
|
||||
}
|
||||
|
||||
func TestApplyBuiltInIssue13418(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = 'http://example.com/'
|
||||
-- layouts/home.html --
|
||||
len: {{ apply (slice "hello") "len" "." }}
|
||||
not: {{ apply (slice "hello") "not" "." }}
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "len: [5]", "not: [false]")
|
||||
}
|
||||
|
||||
// Issue 9865
|
||||
func TestSortStable(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -15,6 +15,9 @@ package reflect
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/hreflect"
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
)
|
||||
|
||||
// New returns a new instance of the reflect-namespaced template functions.
|
||||
@@ -34,3 +37,27 @@ func (ns *Namespace) IsMap(v any) bool {
|
||||
func (ns *Namespace) IsSlice(v any) bool {
|
||||
return hreflect.IsSlice(v)
|
||||
}
|
||||
|
||||
// IsPage reports whether v is a Hugo Page.
|
||||
func (ns *Namespace) IsPage(v any) bool {
|
||||
_, ok := v.(page.Page)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsResource reports whether v is a Hugo Resource.
|
||||
func (ns *Namespace) IsResource(v any) bool {
|
||||
_, ok := v.(resource.Resource)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsSite reports whether v is a Hugo Site.
|
||||
func (ns *Namespace) IsSite(v any) bool {
|
||||
_, ok := v.(page.Site)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsImageResource reports whether v is a Hugo Image Resource.
|
||||
// If this returns true, you may process it and get information about its width, height, etc.
|
||||
func (ns *Namespace) IsImageResource(v any) bool {
|
||||
return resources.IsImage(v)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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 reflect_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
func TestIs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
-- assets/a.png --
|
||||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||
-- assets/b.svg --
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
|
||||
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
|
||||
</svg>
|
||||
-- assets/c.txt --
|
||||
This is a text file.
|
||||
-- assets/d.avif --
|
||||
AAAAHGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZgAAAOptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABwaWN0AAAAAAAAAAAAAAAAAAAAAA5waXRtAAAAAAABAAAAImlsb2MAAAAAREAAAQABAAAAAAEOAAEAAAAAAAAAEgAAACNpaW5mAAAAAAABAAAAFWluZmUCAAAAAAEAAGF2MDEAAAAAamlwcnAAAABLaXBjbwAAABNjb2xybmNseAABAA0ABoAAAAAMYXYxQ4EgAgAAAAAUaXNwZQAAAAAAAAABAAAAAQAAABBwaXhpAAAAAAMICAgAAAAXaXBtYQAAAAAAAAABAAEEAYIDBAAAABptZGF0EgAKBzgABhAQ0GkyBRAAAAtA
|
||||
-- layouts/home.html --
|
||||
{{ $a := resources.Get "a.png" }}
|
||||
{{ $a10 := $a.Fit "10x10" }}
|
||||
{{ $b := resources.Get "b.svg" }}
|
||||
{{ $c := resources.Get "c.txt" }}
|
||||
{{ $d := resources.Get "d.avif" }}
|
||||
PNG.ResourceType: {{ $a.ResourceType }}
|
||||
SVG.ResourceType: {{ $b.ResourceType }}
|
||||
Text.ResourceType: {{ $c.ResourceType }}
|
||||
AVIF.ResourceType: {{ $d.ResourceType }}
|
||||
IsSite: false: {{ reflect.IsSite . }}|true: {{ reflect.IsSite .Site }}|true: {{ reflect.IsSite site }}
|
||||
IsPage: true: {{ reflect.IsPage . }}|false: {{ reflect.IsPage .Site }}|false: {{ reflect.IsPage site }}
|
||||
IsResource: true: {{ reflect.IsResource . }}|true: {{ reflect.IsResource $a }}|true: {{ reflect.IsResource $b }}|true: {{ reflect.IsResource $c }}
|
||||
IsImageResource: false: {{ reflect.IsImageResource . }}|true: {{ reflect.IsImageResource $a }}|true: {{ reflect.IsImageResource $a10 }}|false: {{ reflect.IsImageResource $b }}|false: {{ reflect.IsImageResource $c }}|false: {{ reflect.IsImageResource $d }}
|
||||
|
||||
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", `
|
||||
PNG.ResourceType: image
|
||||
SVG.ResourceType: image
|
||||
Text.ResourceType: text
|
||||
AVIF.ResourceType: image
|
||||
IsSite: false: false|true: true|true: true
|
||||
IsPage: true: true|false: false|false: false
|
||||
IsResource: true: true|true: true|true: true|true: true
|
||||
IsImageResource: false: false|true: true|true: true|false: false|false: false|false: false
|
||||
`)
|
||||
}
|
||||
@@ -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
|
||||
|
||||