From bd50c9c7e738110a5626bd9fcfbc9e4f579a6423 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sat, 18 Oct 2025 17:42:43 +0200 Subject: [PATCH] Misc YAML adjustments Closes #14067 --- common/maps/params.go | 18 ++++++-- config/allconfig/allconfig.go | 4 +- config/allconfig/load.go | 15 +++++-- config/defaultConfigProvider.go | 18 +++++--- go.mod | 1 - go.sum | 4 -- hugolib/config_test.go | 79 +++++++++++++++++++++++++++++++++ hugolib/page__meta.go | 3 +- hugolib/site.go | 6 ++- navigation/menu.go | 2 +- related/inverted_index.go | 2 +- 11 files changed, 128 insertions(+), 24 deletions(-) diff --git a/common/maps/params.go b/common/maps/params.go index 819f796e4..aa1dce20c 100644 --- a/common/maps/params.go +++ b/common/maps/params.go @@ -14,7 +14,9 @@ package maps import ( + "errors" "fmt" + xmaps "maps" "strings" "github.com/spf13/cast" @@ -101,7 +103,6 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) { noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow) for k, v := range pp { - if k == MergeStrategyKey { continue } @@ -115,6 +116,9 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) { } } } else if !noUpdate { + if vvv, ok := v.(Params); ok { + v = xmaps.Clone(vvv) + } p[k] = v } @@ -266,9 +270,17 @@ func CleanConfigStringMapString(m map[string]string) map[string]string { // CleanConfigStringMap is the same as CleanConfigStringMapString but for // map[string]any. func CleanConfigStringMap(m map[string]any) map[string]any { + return doCleanConfigStringMap(m, 0) +} + +func doCleanConfigStringMap(m map[string]any, depth int) map[string]any { if len(m) == 0 { return m } + const maxDepth = 1000 + if depth > maxDepth { + panic(errors.New("max depth exceeded")) + } if _, found := m[MergeStrategyKey]; !found { return m } @@ -280,9 +292,9 @@ func CleanConfigStringMap(m map[string]any) map[string]any { } switch v2 := v.(type) { case map[string]any: - m2[k] = CleanConfigStringMap(v2) + m2[k] = doCleanConfigStringMap(v2, depth+1) case Params: - var p Params = CleanConfigStringMap(v2) + var p Params = doCleanConfigStringMap(v2, depth+1) m2[k] = p case map[string]string: m2[k] = CleanConfigStringMapString(v2) diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index 3366defd7..c21a18f94 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -56,7 +56,7 @@ import ( "github.com/gohugoio/hugo/resources/page/pagemeta" "github.com/spf13/afero" - xmaps "golang.org/x/exp/maps" + xmaps "maps" ) // InternalConfig is the internal configuration for Hugo, not read from any user provided config file. @@ -1054,6 +1054,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon mergedConfig := config.New() var differentRootKeys []string switch x := v.(type) { + case nil: case maps.Params: _, found := x["params"] if !found { @@ -1129,6 +1130,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon langConfigMap[k] = clone case maps.ParamsMergeStrategy: + default: panic(fmt.Sprintf("unknown type in languages config: %T", v)) diff --git a/config/allconfig/load.go b/config/allconfig/load.go index 5466f678b..318a1700a 100644 --- a/config/allconfig/load.go +++ b/config/allconfig/load.go @@ -40,7 +40,13 @@ import ( //lint:ignore ST1005 end user message. var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n") -func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { +func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("failed to load config: %v", r) + } + }() + if len(d.Environ) == 0 && !hugo.IsRunningAsTest() { d.Environ = os.Environ() } @@ -59,7 +65,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { return nil, fmt.Errorf("failed to load config: %w", err) } - configs, err := fromLoadConfigResult(d.Fs, d.Logger, res) + configs, err = fromLoadConfigResult(d.Fs, d.Logger, res) if err != nil { return nil, fmt.Errorf("failed to create config from result: %w", err) } @@ -93,7 +99,7 @@ func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) { loggers.SetGlobalLogger(d.Logger) - return configs, nil + return } // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.). @@ -538,11 +544,12 @@ func (l configLoader) loadConfig(configName string) (string, error) { return filename, nil } -func (l configLoader) deleteMergeStrategies() { +func (l configLoader) deleteMergeStrategies() (err error) { l.cfg.WalkParams(func(params ...maps.KeyParams) bool { params[len(params)-1].Params.DeleteMergeStrategy() return false }) + return } func (l configLoader) wrapFileError(err error, filename string) error { diff --git a/config/defaultConfigProvider.go b/config/defaultConfigProvider.go index 8c1d63851..11cadbbba 100644 --- a/config/defaultConfigProvider.go +++ b/config/defaultConfigProvider.go @@ -14,11 +14,13 @@ package config import ( + "errors" "fmt" + "slices" "strings" "sync" - xmaps "golang.org/x/exp/maps" + xmaps "maps" "github.com/spf13/cast" @@ -237,12 +239,16 @@ func (c *defaultConfigProvider) Merge(k string, v any) { func (c *defaultConfigProvider) Keys() []string { c.mu.RLock() defer c.mu.RUnlock() - return xmaps.Keys(c.root) + return slices.Collect(xmaps.Keys(c.root)) } func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams) bool) { - var walk func(params ...maps.KeyParams) - walk = func(params ...maps.KeyParams) { + maxDepth := 1000 + var walk func(depth int, params ...maps.KeyParams) + walk = func(depth int, params ...maps.KeyParams) { + if depth > maxDepth { + panic(errors.New("max depth exceeded")) + } if walkFn(params...) { return } @@ -253,11 +259,11 @@ func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams) paramsplus1 := make([]maps.KeyParams, i+1) copy(paramsplus1, params) paramsplus1[i] = maps.KeyParams{Key: k, Params: p2} - walk(paramsplus1...) + walk(depth+1, paramsplus1...) } } } - walk(maps.KeyParams{Key: "", Params: c.root}) + walk(0, maps.KeyParams{Key: "", Params: c.root}) } func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams) maps.ParamsMergeStrategy { diff --git a/go.mod b/go.mod index 440536bb0..aecdec0ee 100644 --- a/go.mod +++ b/go.mod @@ -75,7 +75,6 @@ require ( github.com/yuin/goldmark-emoji v1.0.6 go.uber.org/automaxprocs v1.5.3 gocloud.dev v0.43.0 - golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b golang.org/x/image v0.32.0 golang.org/x/mod v0.29.0 golang.org/x/net v0.46.0 diff --git a/go.sum b/go.sum index cfe459fd8..ee5bc631a 100644 --- a/go.sum +++ b/go.sum @@ -267,8 +267,6 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018141905-f04b60c79bd1 h1:95D/7I2Vv3N9icWWlayjoxp7rSzjcr6PaGXLw8DZzgY= -github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018141905-f04b60c79bd1/go.mod h1:m5hu1im5Qc7LDycVLvee6MPobJiRLBYHklypFJR0/aE= github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 h1:pxlAea9eRwuAnt/zKbGqlFO2ZszpIe24YpOVLf+N+4I= github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6/go.mod h1:m5hu1im5Qc7LDycVLvee6MPobJiRLBYHklypFJR0/aE= github.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxUW+NltUg= @@ -594,8 +592,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/hugolib/config_test.go b/hugolib/config_test.go index 635283836..99dbac002 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -1570,3 +1570,82 @@ title: "P1 us" ` Test(t, files) } + +func TestConfigYAMLAnchorsMerge(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.yaml -- +definitions: + params: ¶ms + p1: p1alias + +theme: "mytheme" +defaultContentLanguage: en +defaultContentLanguageInSubdir: true + +params: + <<: *params + +languages: + en: + weight: 1 + no: + weight: 2 + params: + <<: *params + p2: p2no + sv: + weight: 3 + params: *params + +-- layouts/all.html -- +Params: {{ site.Params }}| +-- themes/mytheme/hugo.yaml -- +definitions: + params: ¶ms + p1: p1aliastheme + p2: p2aliastheme + +params: *params + + +` + b := Test(t, files) + + b.AssertFileContent("public/en/index.html", "Params: map[p1:p1alias p2:p2aliastheme]|") + b.AssertFileContent("public/no/index.html", "Params: map[p1:p1alias p2:p2no]|") + b.AssertFileContent("public/sv/index.html", "Params: map[p1:p1alias p2:p2aliastheme]|") +} + +func TestConfigYAMLAnchorsCyclicReference(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.yaml -- +definitions: + params: ¶ms + p1: p1alias + +params: + p3: *params + +languages: + en: + weight: 1 + sv: + weight: 2 + params: *params + +-- layouts/all.html -- +Params: {{ site.Params }}| + +` + + for range 3 { + b := Test(t, files) + b.AssertFileContent("public/index.html", "Params: map[p3:map[p1:p1alias]]|") + b.AssertFileContent("public/sv/index.html", "Params: map[p1:p1alias p3:map[p1:p1alias]]|") + + } +} diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index f04e0bd56..00251186a 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -21,11 +21,12 @@ import ( "strings" "time" + xmaps "maps" + "github.com/bep/logg" "github.com/gobuffalo/flect" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/markup/converter" - xmaps "golang.org/x/exp/maps" "github.com/gohugoio/hugo/source" diff --git a/hugolib/site.go b/hugolib/site.go index 247e1cff9..3c89b1ee3 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -23,6 +23,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "sort" "strings" "sync" @@ -48,9 +49,10 @@ import ( "github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/resources" + xmaps "maps" + "github.com/gohugoio/hugo/tpl/tplimpl" "github.com/gohugoio/hugo/tpl/tplimplinit" - xmaps "golang.org/x/exp/maps" // Loads the template funcs namespaces. @@ -999,7 +1001,7 @@ func (w *WhatChanged) Changes() []identity.Identity { if w == nil || w.ids == nil { return nil } - return xmaps.Keys(w.ids) + return slices.Collect(xmaps.Keys(w.ids)) } func (w *WhatChanged) Drain() []identity.Identity { diff --git a/navigation/menu.go b/navigation/menu.go index a971f2e74..5d4c5b93e 100644 --- a/navigation/menu.go +++ b/navigation/menu.go @@ -16,6 +16,7 @@ package navigation import ( "html/template" + "slices" "sort" "github.com/gohugoio/hugo/common/maps" @@ -25,7 +26,6 @@ import ( "github.com/mitchellh/mapstructure" "github.com/spf13/cast" - "slices" ) var smc = newMenuCache() diff --git a/related/inverted_index.go b/related/inverted_index.go index b8f1ad3e2..ebed77baa 100644 --- a/related/inverted_index.go +++ b/related/inverted_index.go @@ -24,7 +24,7 @@ import ( "sync" "time" - xmaps "golang.org/x/exp/maps" + xmaps "maps" "github.com/gohugoio/hugo/common/collections" "github.com/gohugoio/hugo/common/maps" -- 2.39.5