From ee482180c250cb3c74a11a75f791eadceb6f7480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 23 Apr 2025 16:27:14 +0200 Subject: [PATCH] config: Fix _merge issue when key doesn't exist on the left side Fixes #13643 Fixes #13646 --- config/allconfig/allconfig.go | 100 ++++++++++++++---- .../allconfig/allconfig_integration_test.go | 60 +++++++++++ config/allconfig/alldecoders.go | 26 +++-- config/allconfig/load.go | 60 +---------- config/commonConfig.go | 2 +- config/services/servicesConfig.go | 3 + hugolib/config_test.go | 4 +- 7 files changed, 165 insertions(+), 90 deletions(-) diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index e73153a94..d3ee28490 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -800,30 +800,58 @@ func (c *Configs) IsZero() bool { func (c *Configs) Init() error { var languages langs.Languages - defaultContentLanguage := c.Base.DefaultContentLanguage - for k, v := range c.LanguageConfigMap { + + var langKeys []string + var hasEn bool + + const en = "en" + + for k := range c.LanguageConfigMap { + langKeys = append(langKeys, k) + if k == en { + hasEn = true + } + } + + // Sort the LanguageConfigSlice by language weight (if set) or lang. + sort.Slice(langKeys, func(i, j int) bool { + ki := langKeys[i] + kj := langKeys[j] + lki := c.LanguageConfigMap[ki] + lkj := c.LanguageConfigMap[kj] + li := lki.Languages[ki] + lj := lkj.Languages[kj] + if li.Weight != lj.Weight { + return li.Weight < lj.Weight + } + return ki < kj + }) + + // See issue #13646. + defaultConfigLanguageFallback := en + if !hasEn { + // Pick the first one. + defaultConfigLanguageFallback = langKeys[0] + } + + if c.Base.DefaultContentLanguage == "" { + c.Base.DefaultContentLanguage = defaultConfigLanguageFallback + } + + for _, k := range langKeys { + v := c.LanguageConfigMap[k] + if v.DefaultContentLanguage == "" { + v.DefaultContentLanguage = defaultConfigLanguageFallback + } + c.LanguageConfigSlice = append(c.LanguageConfigSlice, v) languageConf := v.Languages[k] - language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf) + language, err := langs.NewLanguage(k, c.Base.DefaultContentLanguage, v.TimeZone, languageConf) if err != nil { return err } languages = append(languages, language) } - // Sort the sites by language weight (if set) or lang. - sort.Slice(languages, func(i, j int) bool { - li := languages[i] - lj := languages[j] - if li.Weight != lj.Weight { - return li.Weight < lj.Weight - } - return li.Lang < lj.Lang - }) - - for _, l := range languages { - c.LanguageConfigSlice = append(c.LanguageConfigSlice, c.LanguageConfigMap[l.Lang]) - } - // Filter out disabled languages. var n int for _, l := range languages { @@ -836,12 +864,12 @@ func (c *Configs) Init() error { var languagesDefaultFirst langs.Languages for _, l := range languages { - if l.Lang == defaultContentLanguage { + if l.Lang == c.Base.DefaultContentLanguage { languagesDefaultFirst = append(languagesDefaultFirst, l) } } for _, l := range languages { - if l.Lang != defaultContentLanguage { + if l.Lang != c.Base.DefaultContentLanguage { languagesDefaultFirst = append(languagesDefaultFirst, l) } } @@ -927,17 +955,48 @@ func (c Configs) GetByLang(lang string) config.AllProvider { return nil } +func newDefaultConfig() *Config { + return &Config{ + Taxonomies: map[string]string{"tag": "tags", "category": "categories"}, + Sitemap: config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, + RootConfig: RootConfig{ + Environment: hugo.EnvironmentProduction, + TitleCaseStyle: "AP", + PluralizeListTitles: true, + CapitalizeListTitles: true, + StaticDir: []string{"static"}, + SummaryLength: 70, + Timeout: "60s", + + CommonDirs: config.CommonDirs{ + ArcheTypeDir: "archetypes", + ContentDir: "content", + ResourceDir: "resources", + PublishDir: "public", + ThemesDir: "themes", + AssetDir: "assets", + LayoutDir: "layouts", + I18nDir: "i18n", + DataDir: "data", + }, + }, + } +} + // fromLoadConfigResult creates a new Config from res. func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadConfigResult) (*Configs, error) { if !res.Cfg.IsSet("languages") { // We need at least one lang := res.Cfg.GetString("defaultContentLanguage") + if lang == "" { + lang = "en" + } res.Cfg.Set("languages", maps.Params{lang: maps.Params{}}) } bcfg := res.BaseConfig cfg := res.Cfg - all := &Config{} + all := newDefaultConfig() err := decodeConfigFromParams(fs, logger, bcfg, cfg, all, nil) if err != nil { @@ -947,6 +1006,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon langConfigMap := make(map[string]*Config) languagesConfig := cfg.GetStringMap("languages") + var isMultihost bool if err := all.CompileConfig(logger); err != nil { diff --git a/config/allconfig/allconfig_integration_test.go b/config/allconfig/allconfig_integration_test.go index cae04ba85..033947203 100644 --- a/config/allconfig/allconfig_integration_test.go +++ b/config/allconfig/allconfig_integration_test.go @@ -5,6 +5,7 @@ import ( "testing" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/media" @@ -234,3 +235,62 @@ baseURL = "https://example.com" b.Assert(c.IsContentFile("foo.md"), qt.Equals, true) b.Assert(len(s), qt.Equals, 6) } + +func TestMergeDeep(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +theme = ["theme1", "theme2"] +_merge = "deep" +-- themes/theme1/hugo.toml -- +[sitemap] +filename = 'mysitemap.xml' +[services] +[services.googleAnalytics] +id = 'foo bar' +[taxonomies] + foo = 'bars' +-- themes/theme2/config/_default/hugo.toml -- +[taxonomies] + bar = 'baz' +-- layouts/home.html -- +GA ID: {{ site.Config.Services.GoogleAnalytics.ID }}. + +` + + b := hugolib.Test(t, files) + + conf := b.H.Configs + base := conf.Base + + b.Assert(base.Environment, qt.Equals, hugo.EnvironmentProduction) + b.Assert(base.BaseURL, qt.Equals, "https://example.com") + b.Assert(base.Sitemap.Filename, qt.Equals, "mysitemap.xml") + b.Assert(base.Taxonomies, qt.DeepEquals, map[string]string{"bar": "baz", "foo": "bars"}) + + b.AssertFileContent("public/index.html", "GA ID: foo bar.") +} + +func TestDefaultConfigLanguageBlankWhenNoEnglishExists(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +[languages] +[languages.nn] +weight = 20 +[languages.sv] +weight = 10 +[languages.sv.taxonomies] + tag = "taggar" +-- layouts/all.html -- +All. +` + + b := hugolib.Test(t, files) + + b.Assert(b.H.Conf.DefaultContentLanguage(), qt.Equals, "sv") +} diff --git a/config/allconfig/alldecoders.go b/config/allconfig/alldecoders.go index 0bf8508d9..1cfa1afc4 100644 --- a/config/allconfig/alldecoders.go +++ b/config/allconfig/alldecoders.go @@ -249,14 +249,18 @@ var allDecoderSetups = map[string]decodeWeight{ key: "sitemap", decode: func(d decodeWeight, p decodeConfig) error { var err error - p.c.Sitemap, err = config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, p.p.GetStringMap(d.key)) + if p.p.IsSet(d.key) { + p.c.Sitemap, err = config.DecodeSitemap(p.c.Sitemap, p.p.GetStringMap(d.key)) + } return err }, }, "taxonomies": { key: "taxonomies", decode: func(d decodeWeight, p decodeConfig) error { - p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key)) + if p.p.IsSet(d.key) { + p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key)) + } return nil }, }, @@ -306,15 +310,17 @@ var allDecoderSetups = map[string]decodeWeight{ } // Validate defaultContentLanguage. - var found bool - for lang := range p.c.Languages { - if lang == p.c.DefaultContentLanguage { - found = true - break + if p.c.DefaultContentLanguage != "" { + var found bool + for lang := range p.c.Languages { + if lang == p.c.DefaultContentLanguage { + found = true + break + } + } + if !found { + return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage) } - } - if !found { - return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage) } return nil diff --git a/config/allconfig/load.go b/config/allconfig/load.go index 9289cf294..2d9185f6f 100644 --- a/config/allconfig/load.go +++ b/config/allconfig/load.go @@ -159,63 +159,9 @@ func (l configLoader) applyConfigAliases() error { func (l configLoader) applyDefaultConfig() error { defaultSettings := maps.Params{ - "baseURL": "", - "cleanDestinationDir": false, - "watch": false, - "contentDir": "content", - "resourceDir": "resources", - "publishDir": "public", - "publishDirOrig": "public", - "themesDir": "themes", - "assetDir": "assets", - "layoutDir": "layouts", - "i18nDir": "i18n", - "dataDir": "data", - "archetypeDir": "archetypes", - "configDir": "config", - "staticDir": "static", - "buildDrafts": false, - "buildFuture": false, - "buildExpired": false, - "params": maps.Params{}, - "environment": hugo.EnvironmentProduction, - "uglyURLs": false, - "verbose": false, - "ignoreCache": false, - "canonifyURLs": false, - "relativeURLs": false, - "removePathAccents": false, - "titleCaseStyle": "AP", - "taxonomies": maps.Params{"tag": "tags", "category": "categories"}, - "permalinks": maps.Params{}, - "sitemap": maps.Params{"priority": -1, "filename": "sitemap.xml"}, - "menus": maps.Params{}, - "disableLiveReload": false, - "pluralizeListTitles": true, - "capitalizeListTitles": true, - "forceSyncStatic": false, - "footnoteAnchorPrefix": "", - "footnoteReturnLinkContents": "", - "newContentEditor": "", - "paginate": 0, // Moved into the paginator struct in Hugo v0.128.0. - "paginatePath": "", // Moved into the paginator struct in Hugo v0.128.0. - "summaryLength": 70, - "rssLimit": -1, - "sectionPagesMenu": "", - "disablePathToLower": false, - "hasCJKLanguage": false, - "enableEmoji": false, - "defaultContentLanguage": "en", - "defaultContentLanguageInSubdir": false, - "enableMissingTranslationPlaceholders": false, - "enableGitInfo": false, - "ignoreFiles": make([]string, 0), - "disableAliases": false, - "debug": false, - "disableFastRender": false, - "timeout": "60s", - "timeZone": "", - "enableInlineShortcodes": false, + // These dirs are used early/before we build the config struct. + "themesDir": "themes", + "configDir": "config", } l.cfg.SetDefaults(defaultSettings) diff --git a/config/commonConfig.go b/config/commonConfig.go index 3dfd9b409..947078672 100644 --- a/config/commonConfig.go +++ b/config/commonConfig.go @@ -17,6 +17,7 @@ import ( "fmt" "net/http" "regexp" + "slices" "sort" "strings" @@ -28,7 +29,6 @@ import ( "github.com/gohugoio/hugo/common/herrors" "github.com/mitchellh/mapstructure" "github.com/spf13/cast" - "slices" ) type BaseConfig struct { diff --git a/config/services/servicesConfig.go b/config/services/servicesConfig.go index f302244d4..f9d5e1a6e 100644 --- a/config/services/servicesConfig.go +++ b/config/services/servicesConfig.go @@ -101,6 +101,9 @@ func DecodeConfig(cfg config.Provider) (c Config, err error) { if c.RSS.Limit == 0 { c.RSS.Limit = cfg.GetInt(rssLimitKey) + if c.RSS.Limit == 0 { + c.RSS.Limit = -1 + } } return diff --git a/hugolib/config_test.go b/hugolib/config_test.go index cbf821ee7..4275fb02a 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -475,7 +475,7 @@ name = "menu-theme" }) }) - // Issue #8724 + // Issue #8724 ##13643 for _, mergeStrategy := range []string{"none", "shallow"} { c.Run(fmt.Sprintf("Merge with sitemap config in theme, mergestrategy %s", mergeStrategy), func(c *qt.C) { smapConfigTempl := `[sitemap] @@ -495,7 +495,7 @@ name = "menu-theme" b.Assert(got.Sitemap, qt.DeepEquals, config.SitemapConfig{ChangeFreq: "", Disable: false, Priority: -1, Filename: "sitemap.xml"}) b.AssertFileContent("public/sitemap.xml", "schemas/sitemap") } else { - b.Assert(got.Sitemap, qt.DeepEquals, config.SitemapConfig{ChangeFreq: "monthly", Disable: false, Priority: -1, Filename: "sitemap.xml"}) + b.Assert(got.Sitemap, qt.DeepEquals, config.SitemapConfig{ChangeFreq: "monthly", Disable: false, Priority: 0.5, Filename: "sitemap.xml"}) b.AssertFileContent("public/sitemap.xml", "monthly") } })