From d392893cd73dc00c927f342778f6dca9628d328e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 9 Jun 2021 10:58:18 +0200 Subject: [PATCH] Misc config loading fixes The main motivation behind this is simplicity and correctnes, but the new small config library is also faster: ``` BenchmarkDefaultConfigProvider/Viper-16 252418 4546 ns/op 2720 B/op 30 allocs/op BenchmarkDefaultConfigProvider/Custom-16 450756 2651 ns/op 1008 B/op 6 allocs/op ``` Fixes #8633 Fixes #8618 Fixes #8630 Updates #8591 Closes #6680 Closes #5192 --- cache/filecache/filecache_config.go | 7 +- cache/filecache/filecache_config_test.go | 5 +- commands/commandeer.go | 2 - commands/commands_test.go | 9 +- commands/config.go | 5 +- commands/new_site.go | 4 +- commands/server_test.go | 4 +- common/maps/maps.go | 79 +- common/maps/maps_test.go | 2 +- common/maps/params.go | 148 ++++ common/maps/params_test.go | 87 ++ config/commonConfig_test.go | 4 +- config/compositeConfig.go | 113 +++ config/compositeConfig_test.go | 40 + config/configLoader.go | 23 +- config/configProvider.go | 5 + config/configProvider_test.go | 3 +- config/defaultConfigProvider.go | 372 +++++++++ config/defaultConfigProvider_test.go | 315 +++++++ config/docshelper.go | 45 + config/privacy/privacyConfig_test.go | 3 +- config/services/servicesConfig_test.go | 4 +- create/content_test.go | 5 +- deploy/deployConfig_test.go | 4 +- docs/config.toml | 2 +- .../en/getting-started/configuration.md | 20 + docs/data/docs.json | 59 ++ docs/layouts/shortcodes/code-toggle.html | 59 +- go.mod | 2 +- go.sum | 1 + helpers/content_test.go | 5 +- helpers/general_test.go | 4 +- helpers/path_test.go | 4 - helpers/testhelpers_test.go | 21 +- hugofs/fs_test.go | 9 +- hugofs/rootmapping_fs_test.go | 4 +- hugolib/config.go | 772 ++++++++---------- hugolib/config_test.go | 366 ++++----- hugolib/filesystems/basefs_test.go | 10 +- hugolib/hugo_modules_test.go | 30 +- hugolib/hugo_sites.go | 15 +- hugolib/hugo_sites_build_errors_test.go | 4 +- hugolib/image_test.go | 4 +- hugolib/js_test.go | 11 +- hugolib/minify_publisher_test.go | 4 +- hugolib/page__meta.go | 2 +- hugolib/page_test.go | 5 +- hugolib/pagebundler_test.go | 18 +- hugolib/pages_capture.go | 2 +- hugolib/paths/paths_test.go | 7 +- hugolib/resource_chain_babel_test.go | 6 +- hugolib/resource_chain_test.go | 18 +- hugolib/robotstxt_test.go | 4 +- hugolib/shortcode_test.go | 7 +- hugolib/site.go | 22 +- hugolib/site_output_test.go | 12 +- hugolib/site_test.go | 5 +- hugolib/template_test.go | 5 +- hugolib/testhelpers_test.go | 26 +- langs/config.go | 8 +- langs/i18n/i18n_test.go | 7 +- langs/language.go | 84 +- langs/language_test.go | 7 +- markup/asciidocext/convert_test.go | 18 +- markup/blackfriday/convert_test.go | 6 +- markup/highlight/config_test.go | 5 +- markup/markup_config/config.go | 3 +- markup/markup_config/config_test.go | 6 +- markup/markup_test.go | 8 +- markup/mmark/convert_test.go | 4 +- markup/org/convert_test.go | 5 +- media/mediaType.go | 4 +- minifiers/config.go | 4 +- minifiers/config_test.go | 6 +- minifiers/minifiers_test.go | 15 +- modules/collect.go | 2 +- modules/npm/package_builder.go | 8 +- output/outputFormat.go | 6 +- publisher/htmlElementsCollector_test.go | 5 +- related/inverted_index.go | 11 +- .../page/pagemeta/page_frontmatter_test.go | 13 +- resources/page/pagination_test.go | 4 +- resources/page/testhelpers_test.go | 4 +- resources/resource_metadata.go | 2 +- .../htesting/testhelpers.go | 4 +- resources/resource_transformers/js/options.go | 7 +- resources/testhelpers_test.go | 6 +- source/filesystem_test.go | 8 +- tpl/cast/docshelper.go | 8 +- tpl/collections/collections_test.go | 4 +- tpl/data/init_test.go | 4 +- tpl/data/resources_test.go | 8 +- tpl/encoding/encoding.go | 3 +- tpl/hugo/init_test.go | 5 +- tpl/images/images_test.go | 4 +- tpl/os/os_test.go | 9 +- tpl/path/path_test.go | 4 +- tpl/resources/resources.go | 2 +- tpl/site/init_test.go | 5 +- tpl/strings/init_test.go | 5 +- tpl/strings/strings_test.go | 4 +- tpl/tplimpl/template_funcs_test.go | 6 +- tpl/transform/remarshal_test.go | 10 +- tpl/transform/transform_test.go | 16 +- tpl/transform/unmarshal_test.go | 9 +- tpl/urls/init_test.go | 5 +- tpl/urls/urls_test.go | 5 +- 107 files changed, 2159 insertions(+), 1060 deletions(-) create mode 100644 config/compositeConfig.go create mode 100644 config/compositeConfig_test.go create mode 100644 config/defaultConfigProvider.go create mode 100644 config/defaultConfigProvider_test.go create mode 100644 config/docshelper.go diff --git a/cache/filecache/filecache_config.go b/cache/filecache/filecache_config.go index 0c6b569c1..801799e36 100644 --- a/cache/filecache/filecache_config.go +++ b/cache/filecache/filecache_config.go @@ -19,6 +19,8 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" @@ -123,6 +125,9 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) { _, isOsFs := fs.(*afero.OsFs) for k, v := range m { + if _, ok := v.(maps.Params); !ok { + continue + } cc := defaultCacheConfig dc := &mapstructure.DecoderConfig{ @@ -137,7 +142,7 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) { } if err := decoder.Decode(v); err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to decode filecache config") } if cc.Dir == "" { diff --git a/cache/filecache/filecache_config_test.go b/cache/filecache/filecache_config_test.go index cd1d2c82a..acc127e67 100644 --- a/cache/filecache/filecache_config_test.go +++ b/cache/filecache/filecache_config_test.go @@ -25,7 +25,6 @@ import ( "github.com/gohugoio/hugo/config" qt "github.com/frankban/quicktest" - "github.com/spf13/viper" ) func TestDecodeConfig(t *testing.T) { @@ -178,8 +177,8 @@ dir = "/" c.Assert(err, qt.Not(qt.IsNil)) } -func newTestConfig() *viper.Viper { - cfg := viper.New() +func newTestConfig() config.Provider { + cfg := config.New() cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject")) cfg.Set("contentDir", "content") cfg.Set("dataDir", "data") diff --git a/commands/commandeer.go b/commands/commandeer.go index 024651aad..06e27cf18 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -410,7 +410,5 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error { } config.Set("cacheDir", cacheDir) - cfg.Logger.Infoln("Using config file:", config.ConfigFileUsed()) - return nil } diff --git a/commands/commands_test.go b/commands/commands_test.go index a0eeb157b..99ffea48c 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -20,6 +20,8 @@ import ( "path/filepath" "testing" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/htesting" "github.com/spf13/afero" @@ -29,7 +31,6 @@ import ( "github.com/gohugoio/hugo/common/types" "github.com/spf13/cobra" - "github.com/spf13/viper" qt "github.com/frankban/quicktest" ) @@ -166,7 +167,7 @@ func TestFlags(t *testing.T) { name: "ignoreVendor as bool", args: []string{"server", "--ignoreVendor"}, check: func(c *qt.C, cmd *serverCmd) { - cfg := viper.New() + cfg := config.New() cmd.flagsToConfig(cfg) c.Assert(cfg.Get("ignoreVendor"), qt.Equals, true) }, @@ -176,7 +177,7 @@ func TestFlags(t *testing.T) { name: "ignoreVendorPaths", args: []string{"server", "--ignoreVendorPaths=github.com/**"}, check: func(c *qt.C, cmd *serverCmd) { - cfg := viper.New() + cfg := config.New() cmd.flagsToConfig(cfg) c.Assert(cfg.Get("ignoreVendorPaths"), qt.Equals, "github.com/**") }, @@ -216,7 +217,7 @@ func TestFlags(t *testing.T) { c.Assert(sc.serverPort, qt.Equals, 1366) c.Assert(sc.environment, qt.Equals, "testing") - cfg := viper.New() + cfg := config.New() sc.flagsToConfig(cfg) c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination") c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent") diff --git a/commands/config.go b/commands/config.go index 37bf45e3c..7ab429308 100644 --- a/commands/config.go +++ b/commands/config.go @@ -22,13 +22,14 @@ import ( "sort" "strings" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/parser" "github.com/gohugoio/hugo/parser/metadecoders" "github.com/gohugoio/hugo/modules" "github.com/spf13/cobra" - "github.com/spf13/viper" ) var _ cmder = (*configCmd)(nil) @@ -81,7 +82,7 @@ func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error { return err } - allSettings := cfg.Cfg.(*viper.Viper).AllSettings() + allSettings := cfg.Cfg.Get("").(maps.Params) // We need to clean up this, but we store objects in the config that // isn't really interesting to the end user, so filter these. diff --git a/commands/new_site.go b/commands/new_site.go index 6fac2c22c..71097b8ff 100644 --- a/commands/new_site.go +++ b/commands/new_site.go @@ -19,6 +19,7 @@ import ( "path/filepath" "strings" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/parser/metadecoders" _errors "github.com/pkg/errors" @@ -29,7 +30,6 @@ import ( "github.com/gohugoio/hugo/parser" "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" - "github.com/spf13/viper" ) var _ cmder = (*newSiteCmd)(nil) @@ -123,7 +123,7 @@ func (n *newSiteCmd) newSite(cmd *cobra.Command, args []string) error { forceNew, _ := cmd.Flags().GetBool("force") - return n.doNewSite(hugofs.NewDefault(viper.New()), createpath, forceNew) + return n.doNewSite(hugofs.NewDefault(config.New()), createpath, forceNew) } func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) { diff --git a/commands/server_test.go b/commands/server_test.go index cc2a6d92c..05d21a516 100644 --- a/commands/server_test.go +++ b/commands/server_test.go @@ -22,10 +22,10 @@ import ( "testing" "time" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" qt "github.com/frankban/quicktest" - "github.com/spf13/viper" ) func TestServer(t *testing.T) { @@ -101,7 +101,7 @@ func TestFixURL(t *testing.T) { t.Run(test.TestName, func(t *testing.T) { b := newCommandsBuilder() s := b.newServerCmd() - v := viper.New() + v := config.New() baseURL := test.CLIBaseURL v.Set("baseURL", test.CfgBaseURL) s.serverAppend = test.AppendPort diff --git a/common/maps/maps.go b/common/maps/maps.go index 41d9b6e15..5fb079009 100644 --- a/common/maps/maps.go +++ b/common/maps/maps.go @@ -18,53 +18,65 @@ import ( "strings" "github.com/gobwas/glob" - "github.com/spf13/cast" ) -// ToLower makes all the keys in the given map lower cased and will do so -// recursively. -// Notes: -// * This will modify the map given. -// * Any nested map[interface{}]interface{} will be converted to Params. -func ToLower(m Params) { - for k, v := range m { - var retyped bool - switch v.(type) { - case map[interface{}]interface{}: - var p Params = cast.ToStringMap(v) - v = p - ToLower(p) - retyped = true - case map[string]interface{}: - var p Params = v.(map[string]interface{}) - v = p - ToLower(p) - retyped = true - } - - lKey := strings.ToLower(k) - if retyped || k != lKey { - delete(m, k) - m[lKey] = v - } - } -} - +// ToStringMapE converts in to map[string]interface{}. func ToStringMapE(in interface{}) (map[string]interface{}, error) { - switch in.(type) { + switch vv := in.(type) { case Params: - return in.(Params), nil + return vv, nil + case map[string]string: + var m = map[string]interface{}{} + for k, v := range vv { + m[k] = v + } + return m, nil + default: return cast.ToStringMapE(in) } } +// ToParamsAndPrepare converts in to Params and prepares it for use. +// See PrepareParams. +func ToParamsAndPrepare(in interface{}) (Params, bool) { + m, err := ToStringMapE(in) + if err != nil { + return nil, false + } + PrepareParams(m) + return m, true +} + +// ToStringMap converts in to map[string]interface{}. func ToStringMap(in interface{}) map[string]interface{} { m, _ := ToStringMapE(in) return m } +// ToStringMapStringE converts in to map[string]string. +func ToStringMapStringE(in interface{}) (map[string]string, error) { + m, err := ToStringMapE(in) + if err != nil { + return nil, err + } + return cast.ToStringMapStringE(m) +} + +// ToStringMapString converts in to map[string]string. +func ToStringMapString(in interface{}) map[string]string { + m, _ := ToStringMapStringE(in) + return m +} + +// ToStringMapBool converts in to bool. +func ToStringMapBool(in interface{}) map[string]bool { + m, _ := ToStringMapE(in) + return cast.ToStringMapBool(m) +} + +// ToSliceStringMap converts in to []map[string]interface{}. func ToSliceStringMap(in interface{}) ([]map[string]interface{}, error) { switch v := in.(type) { case []map[string]interface{}: @@ -127,9 +139,8 @@ func (KeyRenamer) keyPath(k1, k2 string) string { k1, k2 = strings.ToLower(k1), strings.ToLower(k2) if k1 == "" { return k2 - } else { - return k1 + "/" + k2 } + return k1 + "/" + k2 } func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]interface{}) { diff --git a/common/maps/maps_test.go b/common/maps/maps_test.go index 7e527aac5..dbe97a15a 100644 --- a/common/maps/maps_test.go +++ b/common/maps/maps_test.go @@ -67,7 +67,7 @@ func TestToLower(t *testing.T) { for i, test := range tests { t.Run(fmt.Sprint(i), func(t *testing.T) { // ToLower modifies input. - ToLower(test.input) + PrepareParams(test.input) if !reflect.DeepEqual(test.expected, test.input) { t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input) } diff --git a/common/maps/params.go b/common/maps/params.go index 4c881093c..7e94d593b 100644 --- a/common/maps/params.go +++ b/common/maps/params.go @@ -14,6 +14,7 @@ package maps import ( + "fmt" "strings" "github.com/spf13/cast" @@ -29,6 +30,95 @@ func (p Params) Get(indices ...string) interface{} { return v } +// Set overwrites values in p with values in pp for common or new keys. +// This is done recursively. +func (p Params) Set(pp Params) { + for k, v := range pp { + vv, found := p[k] + if !found { + p[k] = v + } else { + switch vvv := vv.(type) { + case Params: + if pv, ok := v.(Params); ok { + vvv.Set(pv) + } else { + p[k] = v + } + default: + p[k] = v + } + } + } +} + +// Merge transfers values from pp to p for new keys. +// This is done recursively. +func (p Params) Merge(pp Params) { + p.merge("", pp) +} + +func (p Params) merge(ps ParamsMergeStrategy, pp Params) { + ns, found := p.GetMergeStrategy() + + var ms = ns + if !found && ps != "" { + ms = ps + } + + noUpdate := ms == ParamsMergeStrategyNone + noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow) + + for k, v := range pp { + + if k == mergeStrategyKey { + continue + } + vv, found := p[k] + + if found { + // Key matches, if both sides are Params, we try to merge. + if vvv, ok := vv.(Params); ok { + if pv, ok := v.(Params); ok { + vvv.merge(ms, pv) + } + + } + + } else if !noUpdate { + p[k] = v + + } + + } +} + +func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) { + if v, found := p[mergeStrategyKey]; found { + if s, ok := v.(ParamsMergeStrategy); ok { + return s, true + } + } + return ParamsMergeStrategyShallow, false +} + +func (p Params) DeleteMergeStrategy() bool { + if _, found := p[mergeStrategyKey]; found { + delete(p, mergeStrategyKey) + return true + } + return false +} + +func (p Params) SetDefaultMergeStrategy(s ParamsMergeStrategy) { + switch s { + case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow: + default: + panic(fmt.Sprintf("invalid merge strategy %q", s)) + } + p[mergeStrategyKey] = s +} + func getNested(m map[string]interface{}, indices []string) (interface{}, string, map[string]interface{}) { if len(indices) == 0 { return nil, "", nil @@ -108,3 +198,61 @@ func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interf return nil, "", nil, nil } + +// ParamsMergeStrategy tells what strategy to use in Params.Merge. +type ParamsMergeStrategy string + +const ( + // Do not merge. + ParamsMergeStrategyNone ParamsMergeStrategy = "none" + // Only add new keys. + ParamsMergeStrategyShallow ParamsMergeStrategy = "shallow" + // Add new keys, merge existing. + ParamsMergeStrategyDeep ParamsMergeStrategy = "deep" + + mergeStrategyKey = "_merge" +) + +func toMergeStrategy(v interface{}) ParamsMergeStrategy { + s := ParamsMergeStrategy(cast.ToString(v)) + switch s { + case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow: + return s + default: + return ParamsMergeStrategyDeep + } +} + +// PrepareParams +// * makes all the keys in the given map lower cased and will do so +// * This will modify the map given. +// * Any nested map[interface{}]interface{} will be converted to Params. +// * Any _merge value will be converted to proper type and value. +func PrepareParams(m Params) { + for k, v := range m { + var retyped bool + lKey := strings.ToLower(k) + if lKey == mergeStrategyKey { + v = toMergeStrategy(v) + retyped = true + } else { + switch v.(type) { + case map[interface{}]interface{}: + var p Params = cast.ToStringMap(v) + v = p + PrepareParams(p) + retyped = true + case map[string]interface{}: + var p Params = v.(map[string]interface{}) + v = p + PrepareParams(p) + retyped = true + } + } + + if retyped || k != lKey { + delete(m, k) + m[lKey] = v + } + } +} diff --git a/common/maps/params_test.go b/common/maps/params_test.go index df8cbf8d6..8859bb86b 100644 --- a/common/maps/params_test.go +++ b/common/maps/params_test.go @@ -69,3 +69,90 @@ func TestGetNestedParamFnNestedNewKey(t *testing.T) { c.Assert(nestedKey, qt.Equals, "new") c.Assert(owner, qt.DeepEquals, nested) } + +func TestParamsSetAndMerge(t *testing.T) { + c := qt.New(t) + + createParamsPair := func() (Params, Params) { + p1 := Params{"a": "av", "c": "cv", "nested": Params{"al2": "al2v", "cl2": "cl2v"}} + p2 := Params{"b": "bv", "a": "abv", "nested": Params{"bl2": "bl2v", "al2": "al2bv"}, mergeStrategyKey: ParamsMergeStrategyDeep} + return p1, p2 + } + + p1, p2 := createParamsPair() + + p1.Set(p2) + + c.Assert(p1, qt.DeepEquals, Params{ + "a": "abv", + "c": "cv", + "nested": Params{ + "al2": "al2bv", + "cl2": "cl2v", + "bl2": "bl2v", + }, + "b": "bv", + mergeStrategyKey: ParamsMergeStrategyDeep, + }) + + p1, p2 = createParamsPair() + + p1.Merge(p2) + + // Default is to do a shallow merge. + c.Assert(p1, qt.DeepEquals, Params{ + "c": "cv", + "nested": Params{ + "al2": "al2v", + "cl2": "cl2v", + }, + "b": "bv", + "a": "av", + }) + + p1, p2 = createParamsPair() + p1.SetDefaultMergeStrategy(ParamsMergeStrategyNone) + p1.Merge(p2) + p1.DeleteMergeStrategy() + + c.Assert(p1, qt.DeepEquals, Params{ + "a": "av", + "c": "cv", + "nested": Params{ + "al2": "al2v", + "cl2": "cl2v", + }, + }) + + p1, p2 = createParamsPair() + p1.SetDefaultMergeStrategy(ParamsMergeStrategyShallow) + p1.Merge(p2) + p1.DeleteMergeStrategy() + + c.Assert(p1, qt.DeepEquals, Params{ + "a": "av", + "c": "cv", + "nested": Params{ + "al2": "al2v", + "cl2": "cl2v", + }, + "b": "bv", + }) + + p1, p2 = createParamsPair() + p1.SetDefaultMergeStrategy(ParamsMergeStrategyDeep) + p1.Merge(p2) + p1.DeleteMergeStrategy() + + c.Assert(p1, qt.DeepEquals, Params{ + "nested": Params{ + "al2": "al2v", + "cl2": "cl2v", + "bl2": "bl2v", + }, + "b": "bv", + "a": "av", + "c": "cv", + }) + +} diff --git a/config/commonConfig_test.go b/config/commonConfig_test.go index d4273277a..55767913f 100644 --- a/config/commonConfig_test.go +++ b/config/commonConfig_test.go @@ -21,14 +21,12 @@ import ( "github.com/gohugoio/hugo/common/types" qt "github.com/frankban/quicktest" - - "github.com/spf13/viper" ) func TestBuild(t *testing.T) { c := qt.New(t) - v := viper.New() + v := New() v.Set("build", map[string]interface{}{ "useResourceCacheWhen": "always", }) diff --git a/config/compositeConfig.go b/config/compositeConfig.go new file mode 100644 index 000000000..c68419533 --- /dev/null +++ b/config/compositeConfig.go @@ -0,0 +1,113 @@ +// Copyright 2021 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 config + +import ( + "github.com/gohugoio/hugo/common/maps" +) + +// NewCompositeConfig creates a new composite Provider with a read-only base +// and a writeable layer. +func NewCompositeConfig(base, layer Provider) Provider { + return &compositeConfig{ + base: base, + layer: layer, + } +} + +// compositeConfig contains a read only config base with +// a possibly writeable config layer on top. +type compositeConfig struct { + base Provider + layer Provider +} + +func (c *compositeConfig) GetBool(key string) bool { + if c.layer.IsSet(key) { + return c.layer.GetBool(key) + } + return c.base.GetBool(key) +} + +func (c *compositeConfig) GetInt(key string) int { + if c.layer.IsSet(key) { + return c.layer.GetInt(key) + } + return c.base.GetInt(key) +} + +func (c *compositeConfig) Merge(key string, value interface{}) { + c.layer.Merge(key, value) +} + +func (c *compositeConfig) GetParams(key string) maps.Params { + if c.layer.IsSet(key) { + return c.layer.GetParams(key) + } + return c.base.GetParams(key) +} + +func (c *compositeConfig) GetStringMap(key string) map[string]interface{} { + if c.layer.IsSet(key) { + return c.layer.GetStringMap(key) + } + return c.base.GetStringMap(key) +} + +func (c *compositeConfig) GetStringMapString(key string) map[string]string { + if c.layer.IsSet(key) { + return c.layer.GetStringMapString(key) + } + return c.base.GetStringMapString(key) +} + +func (c *compositeConfig) GetStringSlice(key string) []string { + if c.layer.IsSet(key) { + return c.layer.GetStringSlice(key) + } + return c.base.GetStringSlice(key) +} + +func (c *compositeConfig) Get(key string) interface{} { + if c.layer.IsSet(key) { + return c.layer.Get(key) + } + return c.base.Get(key) +} + +func (c *compositeConfig) IsSet(key string) bool { + if c.layer.IsSet(key) { + return true + } + return c.base.IsSet(key) +} + +func (c *compositeConfig) GetString(key string) string { + if c.layer.IsSet(key) { + return c.layer.GetString(key) + } + return c.base.GetString(key) +} + +func (c *compositeConfig) Set(key string, value interface{}) { + c.layer.Set(key, value) +} + +func (c *compositeConfig) WalkParams(walkFn func(params ...KeyParams) bool) { + panic("not supported") +} + +func (c *compositeConfig) SetDefaultMergeStrategy() { + panic("not supported") +} diff --git a/config/compositeConfig_test.go b/config/compositeConfig_test.go new file mode 100644 index 000000000..60644102f --- /dev/null +++ b/config/compositeConfig_test.go @@ -0,0 +1,40 @@ +// Copyright 2021 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 config + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestCompositeConfig(t *testing.T) { + c := qt.New(t) + + c.Run("Set and get", func(c *qt.C) { + base, layer := New(), New() + cfg := NewCompositeConfig(base, layer) + + layer.Set("a1", "av") + base.Set("b1", "bv") + cfg.Set("c1", "cv") + + c.Assert(cfg.Get("a1"), qt.Equals, "av") + c.Assert(cfg.Get("b1"), qt.Equals, "bv") + c.Assert(cfg.Get("c1"), qt.Equals, "cv") + c.Assert(cfg.IsSet("c1"), qt.IsTrue) + c.Assert(layer.IsSet("c1"), qt.IsTrue) + c.Assert(base.IsSet("c1"), qt.IsFalse) + }) +} diff --git a/config/configLoader.go b/config/configLoader.go index 6d94f0b79..0998b1bef 100644 --- a/config/configLoader.go +++ b/config/configLoader.go @@ -20,7 +20,6 @@ import ( "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/parser/metadecoders" "github.com/spf13/afero" - "github.com/spf13/viper" ) var ( @@ -43,15 +42,11 @@ func IsValidConfigFilename(filename string) bool { // FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests. func FromConfigString(config, configType string) (Provider, error) { - v := newViper() m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config)) if err != nil { return nil, err } - - v.MergeConfigMap(m) - - return v, nil + return NewFrom(m), nil } // FromFile loads the configuration from the given filename. @@ -60,15 +55,7 @@ func FromFile(fs afero.Fs, filename string) (Provider, error) { if err != nil { return nil, err } - - v := newViper() - - err = v.MergeConfigMap(m) - if err != nil { - return nil, err - } - - return v, nil + return NewFrom(m), nil } // FromFileToMap is the same as FromFile, but it returns the config values @@ -116,9 +103,3 @@ func init() { func RenameKeys(m map[string]interface{}) { keyAliases.Rename(m) } - -func newViper() *viper.Viper { - v := viper.New() - - return v -} diff --git a/config/configProvider.go b/config/configProvider.go index 928bf948a..92206ca9e 100644 --- a/config/configProvider.go +++ b/config/configProvider.go @@ -14,6 +14,7 @@ package config import ( + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/types" ) @@ -22,11 +23,15 @@ type Provider interface { GetString(key string) string GetInt(key string) int GetBool(key string) bool + GetParams(key string) maps.Params GetStringMap(key string) map[string]interface{} GetStringMapString(key string) map[string]string GetStringSlice(key string) []string Get(key string) interface{} Set(key string, value interface{}) + Merge(key string, value interface{}) + SetDefaultMergeStrategy() + WalkParams(walkFn func(params ...KeyParams) bool) IsSet(key string) bool } diff --git a/config/configProvider_test.go b/config/configProvider_test.go index d9fff56b6..0afba1e58 100644 --- a/config/configProvider_test.go +++ b/config/configProvider_test.go @@ -17,12 +17,11 @@ import ( "testing" qt "github.com/frankban/quicktest" - "github.com/spf13/viper" ) func TestGetStringSlicePreserveString(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := New() s := "This is a string" sSlice := []string{"This", "is", "a", "slice"} diff --git a/config/defaultConfigProvider.go b/config/defaultConfigProvider.go new file mode 100644 index 000000000..d9c9db7f1 --- /dev/null +++ b/config/defaultConfigProvider.go @@ -0,0 +1,372 @@ +// Copyright 2021 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 config + +import ( + "fmt" + "sort" + "strings" + "sync" + + "github.com/spf13/cast" + + "github.com/gohugoio/hugo/common/maps" +) + +var ( + + // ConfigRootKeysSet contains all of the config map root keys. + // TODO(bep) use this for something (docs etc.) + ConfigRootKeysSet = map[string]bool{ + "build": true, + "caches": true, + "frontmatter": true, + "languages": true, + "imaging": true, + "markup": true, + "mediatypes": true, + "menus": true, + "minify": true, + "module": true, + "outputformats": true, + "params": true, + "permalinks": true, + "related": true, + "sitemap": true, + "taxonomies": true, + } + + // ConfigRootKeys is a sorted version of ConfigRootKeysSet. + ConfigRootKeys []string +) + +func init() { + for k := range ConfigRootKeysSet { + ConfigRootKeys = append(ConfigRootKeys, k) + } + sort.Strings(ConfigRootKeys) +} + +// New creates a Provider backed by an empty maps.Params. +func New() Provider { + return &defaultConfigProvider{ + root: make(maps.Params), + } +} + +// NewFrom creates a Provider backed by params. +func NewFrom(params maps.Params) Provider { + maps.PrepareParams(params) + return &defaultConfigProvider{ + root: params, + } +} + +// defaultConfigProvider is a Provider backed by a map where all keys are lower case. +// All methods are thread safe. +type defaultConfigProvider struct { + mu sync.RWMutex + root maps.Params + + keyCache sync.Map +} + +func (c *defaultConfigProvider) Get(k string) interface{} { + if k == "" { + return c.root + } + c.mu.RLock() + key, m := c.getNestedKeyAndMap(strings.ToLower(k), false) + if m == nil { + return nil + } + v := m[key] + c.mu.RUnlock() + return v +} + +func (c *defaultConfigProvider) GetBool(k string) bool { + v := c.Get(k) + return cast.ToBool(v) +} + +func (c *defaultConfigProvider) GetInt(k string) int { + v := c.Get(k) + return cast.ToInt(v) +} + +func (c *defaultConfigProvider) IsSet(k string) bool { + var found bool + c.mu.RLock() + key, m := c.getNestedKeyAndMap(strings.ToLower(k), false) + if m != nil { + _, found = m[key] + } + c.mu.RUnlock() + return found +} + +func (c *defaultConfigProvider) GetString(k string) string { + v := c.Get(k) + return cast.ToString(v) +} + +func (c *defaultConfigProvider) GetParams(k string) maps.Params { + v := c.Get(k) + if v == nil { + return nil + } + return v.(maps.Params) +} + +func (c *defaultConfigProvider) GetStringMap(k string) map[string]interface{} { + v := c.Get(k) + return maps.ToStringMap(v) +} + +func (c *defaultConfigProvider) GetStringMapString(k string) map[string]string { + v := c.Get(k) + return maps.ToStringMapString(v) +} + +func (c *defaultConfigProvider) GetStringSlice(k string) []string { + v := c.Get(k) + return cast.ToStringSlice(v) +} + +func (c *defaultConfigProvider) Set(k string, v interface{}) { + c.mu.Lock() + defer c.mu.Unlock() + + k = strings.ToLower(k) + + if k == "" { + if p, ok := maps.ToParamsAndPrepare(v); ok { + // Set the values directly in root. + c.root.Set(p) + } else { + c.root[k] = v + } + + return + } + + switch vv := v.(type) { + case map[string]interface{}: + var p maps.Params = vv + v = p + maps.PrepareParams(p) + } + + key, m := c.getNestedKeyAndMap(k, true) + + if existing, found := m[key]; found { + if p1, ok := existing.(maps.Params); ok { + if p2, ok := v.(maps.Params); ok { + p1.Set(p2) + return + } + } + } + + m[key] = v +} + +func (c *defaultConfigProvider) Merge(k string, v interface{}) { + c.mu.Lock() + defer c.mu.Unlock() + k = strings.ToLower(k) + + if k == "" { + rs, f := c.root.GetMergeStrategy() + if f && rs == maps.ParamsMergeStrategyNone { + // The user has set a "no merge" strategy on this, + // nothing more to do. + return + } + + if p, ok := maps.ToParamsAndPrepare(v); ok { + // As there may be keys in p not in root, we need to handle + // those as a special case. + for kk, vv := range p { + if pp, ok := vv.(maps.Params); ok { + if ppp, ok := c.root[kk]; ok { + ppp.(maps.Params).Merge(pp) + } else { + // We need to use the default merge strategy for + // this key. + np := make(maps.Params) + strategy := c.determineMergeStrategy(KeyParams{Key: "", Params: c.root}, KeyParams{Key: kk, Params: np}) + np.SetDefaultMergeStrategy(strategy) + np.Merge(pp) + if len(np) > 0 { + c.root[kk] = np + } + } + } + } + // Merge the rest. + c.root.Merge(p) + } else { + panic(fmt.Sprintf("unsupported type %T received in Merge", v)) + } + + return + } + + switch vv := v.(type) { + case map[string]interface{}: + var p maps.Params = vv + v = p + maps.PrepareParams(p) + } + + key, m := c.getNestedKeyAndMap(k, true) + + if existing, found := m[key]; found { + if p1, ok := existing.(maps.Params); ok { + if p2, ok := v.(maps.Params); ok { + p1.Merge(p2) + } + } + } else { + m[key] = v + } +} + +func (c *defaultConfigProvider) WalkParams(walkFn func(params ...KeyParams) bool) { + var walk func(params ...KeyParams) + walk = func(params ...KeyParams) { + if walkFn(params...) { + return + } + p1 := params[len(params)-1] + i := len(params) + for k, v := range p1.Params { + if p2, ok := v.(maps.Params); ok { + paramsplus1 := make([]KeyParams, i+1) + copy(paramsplus1, params) + paramsplus1[i] = KeyParams{Key: k, Params: p2} + walk(paramsplus1...) + } + } + } + walk(KeyParams{Key: "", Params: c.root}) +} + +func (c *defaultConfigProvider) determineMergeStrategy(params ...KeyParams) maps.ParamsMergeStrategy { + if len(params) == 0 { + return maps.ParamsMergeStrategyNone + } + + var ( + strategy maps.ParamsMergeStrategy + prevIsRoot bool + curr = params[len(params)-1] + ) + + if len(params) > 1 { + prev := params[len(params)-2] + prevIsRoot = prev.Key == "" + + // Inherit from parent (but not from the root unless it's set by user). + s, found := prev.Params.GetMergeStrategy() + if !prevIsRoot && !found { + panic("invalid state, merge strategy not set on parent") + } + if found || !prevIsRoot { + strategy = s + } + } + + switch curr.Key { + case "": + // Don't set a merge strategy on the root unless set by user. + // This will be handled as a special case. + case "params": + strategy = maps.ParamsMergeStrategyDeep + case "outputformats", "mediatypes": + if prevIsRoot { + strategy = maps.ParamsMergeStrategyShallow + } + case "menus": + isMenuKey := prevIsRoot + if !isMenuKey { + // Can also be set below languages. + // root > languages > en > menus + if len(params) == 4 && params[1].Key == "languages" { + isMenuKey = true + } + } + if isMenuKey { + strategy = maps.ParamsMergeStrategyShallow + } + default: + if strategy == "" { + strategy = maps.ParamsMergeStrategyNone + } + } + + return strategy +} + +type KeyParams struct { + Key string + Params maps.Params +} + +func (c *defaultConfigProvider) SetDefaultMergeStrategy() { + c.WalkParams(func(params ...KeyParams) bool { + if len(params) == 0 { + return false + } + p := params[len(params)-1].Params + var found bool + if _, found = p.GetMergeStrategy(); found { + // Set by user. + return false + } + strategy := c.determineMergeStrategy(params...) + if strategy != "" { + p.SetDefaultMergeStrategy(strategy) + } + return false + }) + +} + +func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, maps.Params) { + var parts []string + v, ok := c.keyCache.Load(key) + if ok { + parts = v.([]string) + } else { + parts = strings.Split(key, ".") + c.keyCache.Store(key, parts) + } + current := c.root + for i := 0; i < len(parts)-1; i++ { + next, found := current[parts[i]] + if !found { + if create { + next = make(maps.Params) + current[parts[i]] = next + } else { + return "", nil + } + } + current = next.(maps.Params) + } + return parts[len(parts)-1], current +} diff --git a/config/defaultConfigProvider_test.go b/config/defaultConfigProvider_test.go new file mode 100644 index 000000000..834165d96 --- /dev/null +++ b/config/defaultConfigProvider_test.go @@ -0,0 +1,315 @@ +// Copyright 2021 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 config + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/spf13/viper" + + "github.com/gohugoio/hugo/common/para" + + "github.com/gohugoio/hugo/common/maps" + + qt "github.com/frankban/quicktest" +) + +func TestDefaultConfigProvider(t *testing.T) { + c := qt.New(t) + + c.Run("Set and get", func(c *qt.C) { + cfg := New() + var k string + var v interface{} + + k, v = "foo", "bar" + cfg.Set(k, v) + c.Assert(cfg.Get(k), qt.Equals, v) + c.Assert(cfg.Get(strings.ToUpper(k)), qt.Equals, v) + c.Assert(cfg.GetString(k), qt.Equals, v) + + k, v = "foo", 42 + cfg.Set(k, v) + c.Assert(cfg.Get(k), qt.Equals, v) + c.Assert(cfg.GetInt(k), qt.Equals, v) + + c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{ + "foo": 42, + }) + }) + + c.Run("Set and get map", func(c *qt.C) { + cfg := New() + + cfg.Set("foo", map[string]interface{}{ + "bar": "baz", + }) + + c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{ + "bar": "baz", + }) + + c.Assert(cfg.GetStringMap("foo"), qt.DeepEquals, map[string]interface{}{"bar": string("baz")}) + c.Assert(cfg.GetStringMapString("foo"), qt.DeepEquals, map[string]string{"bar": string("baz")}) + }) + + c.Run("Set and get nested", func(c *qt.C) { + cfg := New() + + cfg.Set("a", map[string]interface{}{ + "B": "bv", + }) + cfg.Set("a.c", "cv") + + c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{ + "b": "bv", + "c": "cv", + }) + c.Assert(cfg.Get("a.c"), qt.Equals, "cv") + + cfg.Set("b.a", "av") + c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{ + "a": "av", + }) + + cfg.Set("b", map[string]interface{}{ + "b": "bv", + }) + + c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{ + "a": "av", + "b": "bv", + }) + + cfg = New() + + cfg.Set("a", "av") + + cfg.Set("", map[string]interface{}{ + "a": "av2", + "b": "bv2", + }) + + c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{ + "a": "av2", + "b": "bv2", + }) + + cfg = New() + + cfg.Set("a", "av") + + cfg.Set("", map[string]interface{}{ + "b": "bv2", + }) + + c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{ + "a": "av", + "b": "bv2", + }) + + cfg = New() + + cfg.Set("", map[string]interface{}{ + "foo": map[string]interface{}{ + "a": "av", + }, + }) + + cfg.Set("", map[string]interface{}{ + "foo": map[string]interface{}{ + "b": "bv2", + }, + }) + + c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{ + "a": "av", + "b": "bv2", + }) + }) + + c.Run("Merge default strategy", func(c *qt.C) { + cfg := New() + + cfg.Set("a", map[string]interface{}{ + "B": "bv", + }) + + cfg.Merge("a", map[string]interface{}{ + "B": "bv2", + "c": "cv2", + }) + + c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{ + "b": "bv", + "c": "cv2", + }) + + cfg = New() + + cfg.Set("a", "av") + + cfg.Merge("", map[string]interface{}{ + "a": "av2", + "b": "bv2", + }) + + c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{ + "a": "av", + "b": "bv2", + }) + }) + + c.Run("Merge shallow", func(c *qt.C) { + cfg := New() + + cfg.Set("a", map[string]interface{}{ + "_merge": "shallow", + "B": "bv", + "c": map[string]interface{}{ + "b": "bv", + }, + }) + + cfg.Merge("a", map[string]interface{}{ + "c": map[string]interface{}{ + "d": "dv2", + }, + "e": "ev2", + }) + + c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{ + "e": "ev2", + "_merge": maps.ParamsMergeStrategyShallow, + "b": "bv", + "c": maps.Params{ + "b": "bv", + }, + }) + }) + + c.Run("IsSet", func(c *qt.C) { + cfg := New() + + cfg.Set("a", map[string]interface{}{ + "B": "bv", + }) + + c.Assert(cfg.IsSet("A"), qt.IsTrue) + c.Assert(cfg.IsSet("a.b"), qt.IsTrue) + c.Assert(cfg.IsSet("z"), qt.IsFalse) + }) + + c.Run("Para", func(c *qt.C) { + cfg := New() + p := para.New(4) + r, _ := p.Start(context.Background()) + + setAndGet := func(k string, v int) error { + vs := strconv.Itoa(v) + cfg.Set(k, v) + err := errors.New("get failed") + if cfg.Get(k) != v { + return err + } + if cfg.GetInt(k) != v { + return err + } + if cfg.GetString(k) != vs { + return err + } + if !cfg.IsSet(k) { + return err + } + return nil + } + + for i := 0; i < 20; i++ { + i := i + r.Run(func() error { + const v = 42 + k := fmt.Sprintf("k%d", i) + if err := setAndGet(k, v); err != nil { + return err + } + + m := maps.Params{ + "new": 42, + } + + cfg.Merge("", m) + + return nil + }) + } + + c.Assert(r.Wait(), qt.IsNil) + }) +} + +func BenchmarkDefaultConfigProvider(b *testing.B) { + type cfger interface { + Get(key string) interface{} + Set(key string, value interface{}) + IsSet(key string) bool + } + + newMap := func() map[string]interface{} { + return map[string]interface{}{ + "a": map[string]interface{}{ + "b": map[string]interface{}{ + "c": 32, + "d": 43, + }, + }, + "b": 62, + } + } + + runMethods := func(b *testing.B, cfg cfger) { + m := newMap() + cfg.Set("mymap", m) + cfg.Set("num", 32) + if !(cfg.IsSet("mymap") && cfg.IsSet("mymap.a") && cfg.IsSet("mymap.a.b") && cfg.IsSet("mymap.a.b.c")) { + b.Fatal("IsSet failed") + } + + if cfg.Get("num") != 32 { + b.Fatal("Get failed") + } + + if cfg.Get("mymap.a.b.c") != 32 { + b.Fatal("Get failed") + } + } + + b.Run("Viper", func(b *testing.B) { + v := viper.New() + for i := 0; i < b.N; i++ { + runMethods(b, v) + } + }) + + b.Run("Custom", func(b *testing.B) { + cfg := New() + for i := 0; i < b.N; i++ { + runMethods(b, cfg) + } + }) +} diff --git a/config/docshelper.go b/config/docshelper.go new file mode 100644 index 000000000..336a0dc16 --- /dev/null +++ b/config/docshelper.go @@ -0,0 +1,45 @@ +// Copyright 2021 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 config + +import ( + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/docshelper" +) + +// This is is just some helpers used to create some JSON used in the Hugo docs. +func init() { + docsProvider := func() docshelper.DocProvider { + + cfg := New() + for _, configRoot := range ConfigRootKeys { + cfg.Set(configRoot, make(maps.Params)) + } + lang := maps.Params{ + "en": maps.Params{ + "menus": maps.Params{}, + "params": maps.Params{}, + }, + } + cfg.Set("languages", lang) + cfg.SetDefaultMergeStrategy() + + configHelpers := map[string]interface{}{ + "mergeStrategy": cfg.Get(""), + } + return docshelper.DocProvider{"config": configHelpers} + } + + docshelper.AddDocProviderFunc(docsProvider) +} diff --git a/config/privacy/privacyConfig_test.go b/config/privacy/privacyConfig_test.go index 0fb599c0a..c17ce713d 100644 --- a/config/privacy/privacyConfig_test.go +++ b/config/privacy/privacyConfig_test.go @@ -18,7 +18,6 @@ import ( qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/config" - "github.com/spf13/viper" ) func TestDecodeConfigFromTOML(t *testing.T) { @@ -94,7 +93,7 @@ PrivacyENhanced = true func TestDecodeConfigDefault(t *testing.T) { c := qt.New(t) - pc, err := DecodeConfig(viper.New()) + pc, err := DecodeConfig(config.New()) c.Assert(err, qt.IsNil) c.Assert(pc, qt.Not(qt.IsNil)) c.Assert(pc.YouTube.PrivacyEnhanced, qt.Equals, false) diff --git a/config/services/servicesConfig_test.go b/config/services/servicesConfig_test.go index 6e979b999..d7a52ba4f 100644 --- a/config/services/servicesConfig_test.go +++ b/config/services/servicesConfig_test.go @@ -18,7 +18,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/config" - "github.com/spf13/viper" + ) func TestDecodeConfigFromTOML(t *testing.T) { @@ -55,7 +55,7 @@ disableInlineCSS = true func TestUseSettingsFromRootIfSet(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() cfg.Set("disqusShortname", "root_short") cfg.Set("googleAnalytics", "ga_root") diff --git a/create/content_test.go b/create/content_test.go index 37dbf98bd..38ff7de8d 100644 --- a/create/content_test.go +++ b/create/content_test.go @@ -20,6 +20,8 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugolib" @@ -30,7 +32,6 @@ import ( "github.com/gohugoio/hugo/create" "github.com/gohugoio/hugo/helpers" "github.com/spf13/afero" - "github.com/spf13/viper" ) func TestNewContent(t *testing.T) { @@ -245,7 +246,7 @@ func readFileFromFs(t *testing.T, fs afero.Fs, filename string) string { return string(b) } -func newTestCfg(c *qt.C, mm afero.Fs) (*viper.Viper, *hugofs.Fs) { +func newTestCfg(c *qt.C, mm afero.Fs) (config.Provider, *hugofs.Fs) { cfg := ` theme = "mytheme" diff --git a/deploy/deployConfig_test.go b/deploy/deployConfig_test.go index 413b1211b..e30f31c32 100644 --- a/deploy/deployConfig_test.go +++ b/deploy/deployConfig_test.go @@ -21,7 +21,7 @@ import ( qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/config" - "github.com/spf13/viper" + ) func TestDecodeConfigFromTOML(t *testing.T) { @@ -164,7 +164,7 @@ Pattern = "[" # invalid regular expression func TestDecodeConfigDefault(t *testing.T) { c := qt.New(t) - dcfg, err := decodeConfig(viper.New()) + dcfg, err := decodeConfig(config.New()) c.Assert(err, qt.IsNil) c.Assert(len(dcfg.Targets), qt.Equals, 0) c.Assert(len(dcfg.Matchers), qt.Equals, 0) diff --git a/docs/config.toml b/docs/config.toml index efa1b0573..d21f6f172 100644 --- a/docs/config.toml +++ b/docs/config.toml @@ -7,7 +7,7 @@ footnotereturnlinkcontents = "↩" languageCode = "en-us" title = "Hugo" - ignoreErrors = ["error-remote-getjson"] + ignoreErrors = ["error-remote-getjson", "err-missing-instagram-accesstoken"] googleAnalytics = "UA-7131036-4" diff --git a/docs/content/en/getting-started/configuration.md b/docs/content/en/getting-started/configuration.md index 48a4af0c9..fd8c65d04 100644 --- a/docs/content/en/getting-started/configuration.md +++ b/docs/content/en/getting-started/configuration.md @@ -80,6 +80,26 @@ Considering the structure above, when running `hugo --environment staging`, Hugo {{% note %}} Default environments are __development__ with `hugo server` and __production__ with `hugo`. {{%/ note %}} + +## Merge Configuration from Themes + +{{< new-in "0.84.0" >}} The configuration merge described below was improved in Hugo 0.84.0 and made fully configurable. The big change/improvement was that we now, by default, do deep merging of `params` maps from themes. + +The configuration value for `_merge` can be one of: + +none +: No merge. + +shallow +: Only add values for new keys. + +shallow +: Add values for new keys, merge existing. + +Note that you don't need to be so verbose as in the default setup below; a `_merge` value higher up will be inherited if not set. + +{{< code-toggle config="mergeStrategy" skipHeader=true />}} + ## All Configuration Settings The following is the full list of Hugo-defined variables with their default diff --git a/docs/data/docs.json b/docs/data/docs.json index 2b17ad8af..b927f8d0a 100644 --- a/docs/data/docs.json +++ b/docs/data/docs.json @@ -1587,6 +1587,65 @@ "preserveTOC": false } }, + "mergeStrategy": { + "build": { + "_merge": "none" + }, + "caches": { + "_merge": "none" + }, + "frontmatter": { + "_merge": "none" + }, + "imaging": { + "_merge": "none" + }, + "languages": { + "_merge": "none", + "en": { + "_merge": "none", + "menus": { + "_merge": "shallow" + }, + "params": { + "_merge": "deep" + } + } + }, + "markup": { + "_merge": "none" + }, + "mediatypes": { + "_merge": "shallow" + }, + "menus": { + "_merge": "shallow" + }, + "minify": { + "_merge": "none" + }, + "module": { + "_merge": "none" + }, + "outputformats": { + "_merge": "shallow" + }, + "params": { + "_merge": "deep" + }, + "permalinks": { + "_merge": "none" + }, + "related": { + "_merge": "none" + }, + "sitemap": { + "_merge": "none" + }, + "taxonomies": { + "_merge": "none" + } + }, "minify": { "minifyOutput": false, "disableHTML": false, diff --git a/docs/layouts/shortcodes/code-toggle.html b/docs/layouts/shortcodes/code-toggle.html index da4b00719..0b92d520d 100644 --- a/docs/layouts/shortcodes/code-toggle.html +++ b/docs/layouts/shortcodes/code-toggle.html @@ -1,34 +1,41 @@ {{ $file := .Get "file" }} {{ $code := "" }} {{ with .Get "config" }} -{{ $file = $file | default "config" }} -{{ $sections := (split . ".") }} -{{ $configSection := index $.Site.Data.docs.config $sections }} -{{ $code = dict $sections $configSection }} + {{ $file = $file | default "config" }} + {{ $sections := (split . ".") }} + {{ $configSection := index $.Site.Data.docs.config $sections }} + {{ $code = dict $sections $configSection }} + {{ if $.Get "skipHeader"}} + {{ $code = $configSection }} + {{ end }} {{ else }} -{{ $code = $.Inner }} + {{ $code = $.Inner }} {{ end }} {{ $langs := (slice "yaml" "toml" "json") }}
-
- {{- with $file -}} -
{{ . }}.
- {{- end -}} - {{ range $langs }} -   - {{ end }} -
-
- {{ range $langs }} -
- {{ highlight ($code | transform.Remarshal . | safeHTML) . ""}} -
- {{ if ne ($.Get "copy") "false" }} - - {{/* Functionality located within filesaver.js The copy here is located in the css with .copy class so it can be replaced with JS on success */}} - {{end}} - {{ end }} -
- +
+ {{- with $file -}} +
+ {{ . }}. +
+ {{- end -}} + {{ range $langs }} + +   + {{ end }} +
+
+ {{ range $langs }} +
+ {{ highlight ($code | transform.Remarshal . | safeHTML) . ""}} +
+ {{ if ne ($.Get "copy") "false" }} + + {{/* Functionality located within filesaver.js The copy here is located in the css with .copy class so it can be replaced with JS on success */}} + {{end}} + {{ end }} +
+
diff --git a/go.mod b/go.mod index ca89a0ca6..f0c9f1fa7 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/spf13/fsync v0.9.0 github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.7.1 + github.com/spf13/viper v1.7.0 github.com/tdewolff/minify/v2 v2.9.16 github.com/yuin/goldmark v1.3.5 github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691 diff --git a/go.sum b/go.sum index ccdc49b69..d9cd59c34 100644 --- a/go.sum +++ b/go.sum @@ -642,6 +642,7 @@ github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= diff --git a/helpers/content_test.go b/helpers/content_test.go index 573c1c90d..515b788f1 100644 --- a/helpers/content_test.go +++ b/helpers/content_test.go @@ -19,12 +19,11 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/config" "github.com/spf13/afero" "github.com/gohugoio/hugo/common/loggers" - "github.com/spf13/viper" - qt "github.com/frankban/quicktest" ) @@ -103,7 +102,7 @@ func TestBytesToHTML(t *testing.T) { } func TestNewContentSpec(t *testing.T) { - cfg := viper.New() + cfg := config.New() c := qt.New(t) cfg.Set("summaryLength", 32) diff --git a/helpers/general_test.go b/helpers/general_test.go index 518188c17..bfabcbef4 100644 --- a/helpers/general_test.go +++ b/helpers/general_test.go @@ -19,7 +19,7 @@ import ( "strings" "testing" - "github.com/spf13/viper" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/common/loggers" @@ -29,7 +29,7 @@ import ( func TestResolveMarkup(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs()) c.Assert(err, qt.IsNil) diff --git a/helpers/path_test.go b/helpers/path_test.go index e1ac90b2f..c95951832 100644 --- a/helpers/path_test.go +++ b/helpers/path_test.go @@ -31,7 +31,6 @@ import ( "github.com/gohugoio/hugo/hugofs" "github.com/spf13/afero" - "github.com/spf13/viper" ) func TestMakePath(t *testing.T) { @@ -490,8 +489,6 @@ func TestExists(t *testing.T) { } func TestAbsPathify(t *testing.T) { - defer viper.Reset() - type test struct { inPath, workingDir, expected string } @@ -511,7 +508,6 @@ func TestAbsPathify(t *testing.T) { } for i, d := range data { - viper.Reset() // todo see comment in AbsPathify ps := newTestDefaultPathSpec("workingDir", d.workingDir) diff --git a/helpers/testhelpers_test.go b/helpers/testhelpers_test.go index 4a6ebd0b1..7d63e4d88 100644 --- a/helpers/testhelpers_test.go +++ b/helpers/testhelpers_test.go @@ -2,24 +2,24 @@ package helpers import ( "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/config" "github.com/spf13/afero" - "github.com/spf13/viper" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/modules" ) -func newTestPathSpec(fs *hugofs.Fs, v *viper.Viper) *PathSpec { +func newTestPathSpec(fs *hugofs.Fs, v config.Provider) *PathSpec { l := langs.NewDefaultLanguage(v) ps, _ := NewPathSpec(fs, l, nil) return ps } func newTestDefaultPathSpec(configKeyValues ...interface{}) *PathSpec { - v := viper.New() + v := config.New() fs := hugofs.NewMem(v) - cfg := newTestCfgFor(fs) + cfg := newTestCfg() for i := 0; i < len(configKeyValues); i += 2 { cfg.Set(configKeyValues[i].(string), configKeyValues[i+1]) @@ -27,15 +27,8 @@ func newTestDefaultPathSpec(configKeyValues ...interface{}) *PathSpec { return newTestPathSpec(fs, cfg) } -func newTestCfgFor(fs *hugofs.Fs) *viper.Viper { - v := newTestCfg() - v.SetFs(fs.Source) - - return v -} - -func newTestCfg() *viper.Viper { - v := viper.New() +func newTestCfg() config.Provider { + v := config.New() v.Set("contentDir", "content") v.Set("dataDir", "data") v.Set("i18nDir", "i18n") @@ -56,7 +49,7 @@ func newTestCfg() *viper.Viper { } func newTestContentSpec() *ContentSpec { - v := viper.New() + v := config.New() spec, err := NewContentSpec(v, loggers.NewErrorLogger(), afero.NewMemMapFs()) if err != nil { panic(err) diff --git a/hugofs/fs_test.go b/hugofs/fs_test.go index a343bbd1f..8d52267af 100644 --- a/hugofs/fs_test.go +++ b/hugofs/fs_test.go @@ -16,15 +16,16 @@ package hugofs import ( "testing" + "github.com/gohugoio/hugo/config" + qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/htesting/hqt" "github.com/spf13/afero" - "github.com/spf13/viper" ) func TestNewDefault(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() f := NewDefault(v) c.Assert(f.Source, qt.Not(qt.IsNil)) @@ -35,7 +36,7 @@ func TestNewDefault(t *testing.T) { func TestNewMem(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() f := NewMem(v) c.Assert(f.Source, qt.Not(qt.IsNil)) @@ -48,7 +49,7 @@ func TestNewMem(t *testing.T) { func TestWorkingDir(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() v.Set("workingDir", "/a/b/") diff --git a/hugofs/rootmapping_fs_test.go b/hugofs/rootmapping_fs_test.go index fc2ddeb62..db9ed25cd 100644 --- a/hugofs/rootmapping_fs_test.go +++ b/hugofs/rootmapping_fs_test.go @@ -20,7 +20,7 @@ import ( "sort" "testing" - "github.com/spf13/viper" + "github.com/gohugoio/hugo/config" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/htesting" @@ -29,7 +29,7 @@ import ( func TestLanguageRootMapping(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() v.Set("contentDir", "content") fs := NewBaseFileDecorator(afero.NewMemMapFs()) diff --git a/hugolib/config.go b/hugolib/config.go index eaa6710ae..f559d7fd3 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -43,34 +43,136 @@ import ( "github.com/gohugoio/hugo/config/services" "github.com/gohugoio/hugo/helpers" "github.com/spf13/afero" - "github.com/spf13/viper" ) -// SiteConfig represents the config in .Site.Config. -type SiteConfig struct { - // This contains all privacy related settings that can be used to - // make the YouTube template etc. GDPR compliant. - Privacy privacy.Config +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") - // Services contains config for services such as Google Analytics etc. - Services services.Config +// LoadConfig loads Hugo configuration into a new Viper and then adds +// a set of defaults. +func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (config.Provider, []string, error) { + + if d.Environment == "" { + d.Environment = hugo.EnvironmentProduction + } + + if len(d.Environ) == 0 { + d.Environ = os.Environ() + } + + var configFiles []string + + l := configLoader{ConfigSourceDescriptor: d, cfg: config.New()} + + if err := l.applyConfigDefaults(); err != nil { + return l.cfg, configFiles, err + } + + for _, name := range d.configFilenames() { + var filename string + filename, err := l.loadConfig(name) + if err == nil { + configFiles = append(configFiles, filename) + } else if err != ErrNoConfigFile { + return nil, nil, err + } + } + + if d.AbsConfigDir != "" { + dirnames, err := l.loadConfigFromConfigDir() + if err == nil { + configFiles = append(configFiles, dirnames...) + } else if err != ErrNoConfigFile { + return nil, nil, err + } + } + + // TODO(bep) improve this. This is currently needed to get the merge correctly. + if l.cfg.IsSet("languages") { + langs := l.cfg.GetParams("languages") + for _, lang := range langs { + langp := lang.(maps.Params) + if _, ok := langp["menus"]; !ok { + langp["menus"] = make(maps.Params) + } + if _, ok := langp["params"]; !ok { + langp["params"] = make(maps.Params) + } + } + + } + l.cfg.SetDefaultMergeStrategy() + + // We create languages based on the settings, so we need to make sure that + // all configuration is loaded/set before doing that. + for _, d := range doWithConfig { + if err := d(l.cfg); err != nil { + return l.cfg, configFiles, err + } + } + + // We made this a Glob pattern in Hugo 0.75, we don't need both. + if l.cfg.GetBool("ignoreVendor") { + helpers.Deprecated("--ignoreVendor", "--ignoreVendorPaths **", false) + l.cfg.Set("ignoreVendorPaths", "**") + } + + // Some settings are used before we're done collecting all settings, + // so apply OS environment both before and after. + if err := l.applyOsEnvOverrides(d.Environ); err != nil { + return l.cfg, configFiles, err + } + + modulesConfig, err := l.loadModulesConfig() + if err != nil { + return l.cfg, configFiles, err + } + + // Need to run these after the modules are loaded, but before + // they are finalized. + collectHook := func(m *modules.ModulesConfig) error { + // We don't need the merge strategy configuration anymore, + // remove it so it doesn't accidentaly show up in other settings. + l.cfg.WalkParams(func(params ...config.KeyParams) bool { + params[len(params)-1].Params.DeleteMergeStrategy() + return false + }) + + if err := l.loadLanguageSettings(nil); err != nil { + return err + } + + mods := m.ActiveModules + + // Apply default project mounts. + if err := modules.ApplyProjectConfigDefaults(l.cfg, mods[0]); err != nil { + return err + } + + return nil + } + + _, modulesConfigFiles, err := l.collectModules(modulesConfig, l.cfg, collectHook) + if err != nil { + return l.cfg, configFiles, err + } + + configFiles = append(configFiles, modulesConfigFiles...) + + if err := l.applyOsEnvOverrides(d.Environ); err != nil { + return l.cfg, configFiles, err + } + + if err = l.applyConfigAliases(); err != nil { + return l.cfg, configFiles, err + } + + return l.cfg, configFiles, err } -func loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) { - privacyConfig, err := privacy.DecodeConfig(cfg) - if err != nil { - return - } - - servicesConfig, err := services.DecodeConfig(cfg) - if err != nil { - return - } - - scfg.Privacy = privacyConfig - scfg.Services = servicesConfig - - return +// LoadConfigDefault is a convenience method to load the default "config.toml" config. +func LoadConfigDefault(fs afero.Fs) (config.Provider, error) { + v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"}) + return v, err } // ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.). @@ -98,13 +200,6 @@ type ConfigSourceDescriptor struct { Environ []string } -func (d ConfigSourceDescriptor) configFilenames() []string { - if d.Filename == "" { - return []string{"config"} - } - return strings.Split(d.Filename, ",") -} - func (d ConfigSourceDescriptor) configFileDir() string { if d.Path != "" { return d.Path @@ -112,178 +207,226 @@ func (d ConfigSourceDescriptor) configFileDir() string { return d.WorkingDir } -// LoadConfigDefault is a convenience method to load the default "config.toml" config. -func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) { - v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"}) - return v, err +func (d ConfigSourceDescriptor) configFilenames() []string { + if d.Filename == "" { + return []string{"config"} + } + return strings.Split(d.Filename, ",") } -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") +// SiteConfig represents the config in .Site.Config. +type SiteConfig struct { + // This contains all privacy related settings that can be used to + // make the YouTube template etc. GDPR compliant. + Privacy privacy.Config -// LoadConfig loads Hugo configuration into a new Viper and then adds -// a set of defaults. -func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (*viper.Viper, []string, error) { - if d.Environment == "" { - d.Environment = hugo.EnvironmentProduction - } - - if len(d.Environ) == 0 { - d.Environ = os.Environ() - } - - var configFiles []string - - v := viper.New() - l := configLoader{ConfigSourceDescriptor: d} - - for _, name := range d.configFilenames() { - var filename string - filename, err := l.loadConfig(name, v) - if err == nil { - configFiles = append(configFiles, filename) - } else if err != ErrNoConfigFile { - return nil, nil, err - } - } - - if d.AbsConfigDir != "" { - dirnames, err := l.loadConfigFromConfigDir(v) - if err == nil { - configFiles = append(configFiles, dirnames...) - } else if err != ErrNoConfigFile { - return nil, nil, err - } - } - - if err := loadDefaultSettingsFor(v); err != nil { - return v, configFiles, err - } - - // We create languages based on the settings, so we need to make sure that - // all configuration is loaded/set before doing that. - for _, d := range doWithConfig { - if err := d(v); err != nil { - return v, configFiles, err - } - } - - // This is invoked both after we load the main config and at the end - // to support OS env override of config options used in the module collector. - applyOsEnvOverrides := func() error { - if d.Environ == nil { - return nil - } - - const delim = "__env__delim" - - // Extract all that start with the HUGO prefix. - // The delimiter is the following rune, usually "_". - const hugoEnvPrefix = "HUGO" - var hugoEnv []types.KeyValueStr - for _, v := range d.Environ { - key, val := config.SplitEnvVar(v) - if strings.HasPrefix(key, hugoEnvPrefix) { - delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix) - if len(delimiterAndKey) < 2 { - continue - } - // Allow delimiters to be case sensitive. - // It turns out there isn't that many allowed special - // chars in environment variables when used in Bash and similar, - // so variables on the form HUGOxPARAMSxFOO=bar is one option. - key := strings.ReplaceAll(delimiterAndKey[1:], delimiterAndKey[:1], delim) - key = strings.ToLower(key) - hugoEnv = append(hugoEnv, types.KeyValueStr{ - Key: key, - Value: val, - }) - - } - } - - for _, env := range hugoEnv { - existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, v.Get) - if err != nil { - return err - } - - if existing != nil { - val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing) - if err != nil { - continue - } - - if owner != nil { - owner[nestedKey] = val - } else { - v.Set(env.Key, val) - } - } else if nestedKey != "" { - owner[nestedKey] = env.Value - } else { - v.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value) - } - } - - return nil - - } - - if err := applyOsEnvOverrides(); err != nil { - return v, configFiles, err - } - - // We made this a Glob pattern in Hugo 0.75, we don't need both. - if v.GetBool("ignoreVendor") { - helpers.Deprecated("--ignoreVendor", "--ignoreVendorPaths **", false) - v.Set("ignoreVendorPaths", "**") - } - - modulesConfig, err := l.loadModulesConfig(v) - if err != nil { - return v, configFiles, err - } - - // Need to run these after the modules are loaded, but before - // they are finalized. - collectHook := func(m *modules.ModulesConfig) error { - if err := loadLanguageSettings(v, nil); err != nil { - return err - } - - mods := m.ActiveModules - - // Apply default project mounts. - if err := modules.ApplyProjectConfigDefaults(v, mods[0]); err != nil { - return err - } - - return nil - } - - _, modulesConfigFiles, err := l.collectModules(modulesConfig, v, collectHook) - - if err == nil && len(modulesConfigFiles) > 0 { - configFiles = append(configFiles, modulesConfigFiles...) - } - - if err := applyOsEnvOverrides(); err != nil { - return v, configFiles, err - } - - return v, configFiles, err -} - -func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error { - _, err := langs.LoadLanguageSettings(cfg, oldLangs) - return err + // Services contains config for services such as Google Analytics etc. + Services services.Config } type configLoader struct { + cfg config.Provider ConfigSourceDescriptor } -func (l configLoader) loadConfig(configName string, v *viper.Viper) (string, error) { +// Handle some legacy values. +func (l configLoader) applyConfigAliases() error { + aliases := []types.KeyValueStr{{Key: "taxonomies", Value: "indexes"}} + + for _, alias := range aliases { + if l.cfg.IsSet(alias.Key) { + vv := l.cfg.Get(alias.Key) + l.cfg.Set(alias.Value, vv) + } + } + + return nil +} + +func (l configLoader) applyConfigDefaults() error { + defaultSettings := maps.Params{ + "cleanDestinationDir": false, + "watch": false, + "resourceDir": "resources", + "publishDir": "public", + "themesDir": "themes", + "buildDrafts": false, + "buildFuture": false, + "buildExpired": false, + "environment": hugo.EnvironmentProduction, + "uglyURLs": false, + "verbose": false, + "ignoreCache": false, + "canonifyURLs": false, + "relativeURLs": false, + "removePathAccents": false, + "titleCaseStyle": "AP", + "taxonomies": map[string]string{"tag": "tags", "category": "categories"}, + "permalinks": make(map[string]string), + "sitemap": config.Sitemap{Priority: -1, Filename: "sitemap.xml"}, + "disableLiveReload": false, + "pluralizeListTitles": true, + "forceSyncStatic": false, + "footnoteAnchorPrefix": "", + "footnoteReturnLinkContents": "", + "newContentEditor": "", + "paginate": 10, + "paginatePath": "page", + "summaryLength": 70, + "rssLimit": -1, + "sectionPagesMenu": "", + "disablePathToLower": false, + "hasCJKLanguage": false, + "enableEmoji": false, + "pygmentsCodeFencesGuessSyntax": false, + "defaultContentLanguage": "en", + "defaultContentLanguageInSubdir": false, + "enableMissingTranslationPlaceholders": false, + "enableGitInfo": false, + "ignoreFiles": make([]string, 0), + "disableAliases": false, + "debug": false, + "disableFastRender": false, + "timeout": "30s", + "enableInlineShortcodes": false, + } + + l.cfg.Merge("", defaultSettings) + + return nil +} + +func (l configLoader) applyOsEnvOverrides(environ []string) error { + if len(environ) == 0 { + return nil + } + + const delim = "__env__delim" + + // Extract all that start with the HUGO prefix. + // The delimiter is the following rune, usually "_". + const hugoEnvPrefix = "HUGO" + var hugoEnv []types.KeyValueStr + for _, v := range environ { + key, val := config.SplitEnvVar(v) + if strings.HasPrefix(key, hugoEnvPrefix) { + delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix) + if len(delimiterAndKey) < 2 { + continue + } + // Allow delimiters to be case sensitive. + // It turns out there isn't that many allowed special + // chars in environment variables when used in Bash and similar, + // so variables on the form HUGOxPARAMSxFOO=bar is one option. + key := strings.ReplaceAll(delimiterAndKey[1:], delimiterAndKey[:1], delim) + key = strings.ToLower(key) + hugoEnv = append(hugoEnv, types.KeyValueStr{ + Key: key, + Value: val, + }) + + } + } + + for _, env := range hugoEnv { + existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get) + if err != nil { + return err + } + + if existing != nil { + val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing) + if err != nil { + continue + } + + if owner != nil { + owner[nestedKey] = val + } else { + l.cfg.Set(env.Key, val) + } + } else if nestedKey != "" { + owner[nestedKey] = env.Value + } else { + // The container does not exist yet. + l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value) + } + } + + return nil +} + +func (l configLoader) collectModules(modConfig modules.Config, v1 config.Provider, hookBeforeFinalize func(m *modules.ModulesConfig) error) (modules.Modules, []string, error) { + workingDir := l.WorkingDir + if workingDir == "" { + workingDir = v1.GetString("workingDir") + } + + themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir")) + + var ignoreVendor glob.Glob + if s := v1.GetString("ignoreVendorPaths"); s != "" { + ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s)) + } + + filecacheConfigs, err := filecache.DecodeConfig(l.Fs, v1) + if err != nil { + return nil, nil, err + } + + v1.Set("filecacheConfigs", filecacheConfigs) + + var configFilenames []string + + hook := func(m *modules.ModulesConfig) error { + for _, tc := range m.ActiveModules { + if tc.ConfigFilename() != "" { + if tc.Watch() { + configFilenames = append(configFilenames, tc.ConfigFilename()) + } + + // Merge from theme config into v1 based on configured + // merge strategy. + v1.Merge("", tc.Cfg().Get("")) + + } + } + + if hookBeforeFinalize != nil { + return hookBeforeFinalize(m) + } + + return nil + } + + modulesClient := modules.NewClient(modules.ClientConfig{ + Fs: l.Fs, + Logger: l.Logger, + HookBeforeFinalize: hook, + WorkingDir: workingDir, + ThemesDir: themesDir, + CacheDir: filecacheConfigs.CacheDirModules(), + ModuleConfig: modConfig, + IgnoreVendor: ignoreVendor, + }) + + v1.Set("modulesClient", modulesClient) + + moduleConfig, err := modulesClient.Collect() + + // Avoid recreating these later. + v1.Set("allModules", moduleConfig.ActiveModules) + + if moduleConfig.GoModulesFilename != "" { + // We want to watch this for changes and trigger rebuild on version + // changes etc. + configFilenames = append(configFilenames, moduleConfig.GoModulesFilename) + } + + return moduleConfig.ActiveModules, configFilenames, err +} + +func (l configLoader) loadConfig(configName string) (string, error) { baseDir := l.configFileDir() var baseFilename string if filepath.IsAbs(configName) { @@ -318,24 +461,13 @@ func (l configLoader) loadConfig(configName string, v *viper.Viper) (string, err return "", l.wrapFileError(err, filename) } - if err = v.MergeConfigMap(m); err != nil { - return "", l.wrapFileError(err, filename) - } + // Set overwrites keys of the same name, recursively. + l.cfg.Set("", m) return filename, nil } -func (l configLoader) wrapFileError(err error, filename string) error { - err, _ = herrors.WithFileContextForFile( - err, - filename, - filename, - l.Fs, - herrors.SimpleLineMatcher) - return err -} - -func (l configLoader) loadConfigFromConfigDir(v *viper.Viper) ([]string, error) { +func (l configLoader) loadConfigFromConfigDir() ([]string, error) { sourceFs := l.Fs configDir := l.AbsConfigDir @@ -421,9 +553,8 @@ func (l configLoader) loadConfigFromConfigDir(v *viper.Viper) ([]string, error) // Migrate menu => menus etc. config.RenameKeys(root) - if err := v.MergeConfigMap(root); err != nil { - return l.wrapFileError(err, path) - } + // Set will overwrite keys with the same name, recursively. + l.cfg.Set("", root) return nil }) @@ -436,8 +567,13 @@ func (l configLoader) loadConfigFromConfigDir(v *viper.Viper) ([]string, error) return dirnames, nil } -func (l configLoader) loadModulesConfig(v1 *viper.Viper) (modules.Config, error) { - modConfig, err := modules.DecodeConfig(v1) +func (l configLoader) loadLanguageSettings(oldLangs langs.Languages) error { + _, err := langs.LoadLanguageSettings(l.cfg, oldLangs) + return err +} + +func (l configLoader) loadModulesConfig() (modules.Config, error) { + modConfig, err := modules.DecodeConfig(l.cfg) if err != nil { return modules.Config{}, err } @@ -445,211 +581,29 @@ func (l configLoader) loadModulesConfig(v1 *viper.Viper) (modules.Config, error) return modConfig, nil } -func (l configLoader) collectModules(modConfig modules.Config, v1 *viper.Viper, hookBeforeFinalize func(m *modules.ModulesConfig) error) (modules.Modules, []string, error) { - workingDir := l.WorkingDir - if workingDir == "" { - workingDir = v1.GetString("workingDir") - } - - themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir")) - - var ignoreVendor glob.Glob - if s := v1.GetString("ignoreVendorPaths"); s != "" { - ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s)) - } - - filecacheConfigs, err := filecache.DecodeConfig(l.Fs, v1) +func (configLoader) loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) { + privacyConfig, err := privacy.DecodeConfig(cfg) if err != nil { - return nil, nil, err - } - - v1.Set("filecacheConfigs", filecacheConfigs) - - var configFilenames []string - - hook := func(m *modules.ModulesConfig) error { - for _, tc := range m.ActiveModules { - if tc.ConfigFilename() != "" { - if tc.Watch() { - configFilenames = append(configFilenames, tc.ConfigFilename()) - } - if err := l.applyThemeConfig(v1, tc); err != nil { - return err - } - } - } - - if hookBeforeFinalize != nil { - return hookBeforeFinalize(m) - } - - return nil - } - - modulesClient := modules.NewClient(modules.ClientConfig{ - Fs: l.Fs, - Logger: l.Logger, - HookBeforeFinalize: hook, - WorkingDir: workingDir, - ThemesDir: themesDir, - CacheDir: filecacheConfigs.CacheDirModules(), - ModuleConfig: modConfig, - IgnoreVendor: ignoreVendor, - }) - - v1.Set("modulesClient", modulesClient) - - moduleConfig, err := modulesClient.Collect() - - // Avoid recreating these later. - v1.Set("allModules", moduleConfig.ActiveModules) - - if moduleConfig.GoModulesFilename != "" { - // We want to watch this for changes and trigger rebuild on version - // changes etc. - configFilenames = append(configFilenames, moduleConfig.GoModulesFilename) - } - - return moduleConfig.ActiveModules, configFilenames, err -} - -func (l configLoader) applyThemeConfig(v1 *viper.Viper, theme modules.Module) error { - const ( - paramsKey = "params" - languagesKey = "languages" - menuKey = "menus" - ) - - v2 := theme.Cfg() - - for _, key := range []string{paramsKey, "outputformats", "mediatypes"} { - l.mergeStringMapKeepLeft("", key, v1, v2) - } - - // Only add params and new menu entries, we do not add language definitions. - if v1.IsSet(languagesKey) && v2.IsSet(languagesKey) { - v1Langs := v1.GetStringMap(languagesKey) - for k := range v1Langs { - langParamsKey := languagesKey + "." + k + "." + paramsKey - l.mergeStringMapKeepLeft(paramsKey, langParamsKey, v1, v2) - } - v2Langs := v2.GetStringMap(languagesKey) - for k := range v2Langs { - if k == "" { - continue - } - - langMenuKey := languagesKey + "." + k + "." + menuKey - if v2.IsSet(langMenuKey) { - // Only add if not in the main config. - v2menus := v2.GetStringMap(langMenuKey) - for k, v := range v2menus { - menuEntry := menuKey + "." + k - menuLangEntry := langMenuKey + "." + k - if !v1.IsSet(menuEntry) && !v1.IsSet(menuLangEntry) { - v1.Set(menuLangEntry, v) - } - } - } - } - } - - // Add menu definitions from theme not found in project - if v2.IsSet(menuKey) { - v2menus := v2.GetStringMap(menuKey) - for k, v := range v2menus { - menuEntry := menuKey + "." + k - if !v1.IsSet(menuEntry) { - v1.SetDefault(menuEntry, v) - } - } - } - - return nil -} - -func (configLoader) mergeStringMapKeepLeft(rootKey, key string, v1, v2 config.Provider) { - if !v2.IsSet(key) { return } - if !v1.IsSet(key) && !(rootKey != "" && rootKey != key && v1.IsSet(rootKey)) { - v1.Set(key, v2.Get(key)) + servicesConfig, err := services.DecodeConfig(cfg) + if err != nil { return } - m1 := v1.GetStringMap(key) - m2 := v2.GetStringMap(key) + scfg.Privacy = privacyConfig + scfg.Services = servicesConfig - for k, v := range m2 { - if _, found := m1[k]; !found { - if rootKey != "" && v1.IsSet(rootKey+"."+k) { - continue - } - m1[k] = v - } - } + return } -func loadDefaultSettingsFor(v *viper.Viper) error { - v.RegisterAlias("indexes", "taxonomies") - - /* - - TODO(bep) from 0.56 these are configured as module mounts. - v.SetDefault("contentDir", "content") - v.SetDefault("layoutDir", "layouts") - v.SetDefault("assetDir", "assets") - v.SetDefault("staticDir", "static") - v.SetDefault("dataDir", "data") - v.SetDefault("i18nDir", "i18n") - v.SetDefault("archetypeDir", "archetypes") - */ - - v.SetDefault("cleanDestinationDir", false) - v.SetDefault("watch", false) - v.SetDefault("resourceDir", "resources") - v.SetDefault("publishDir", "public") - v.SetDefault("themesDir", "themes") - v.SetDefault("buildDrafts", false) - v.SetDefault("buildFuture", false) - v.SetDefault("buildExpired", false) - v.SetDefault("environment", hugo.EnvironmentProduction) - v.SetDefault("uglyURLs", false) - v.SetDefault("verbose", false) - v.SetDefault("ignoreCache", false) - v.SetDefault("canonifyURLs", false) - v.SetDefault("relativeURLs", false) - v.SetDefault("removePathAccents", false) - v.SetDefault("titleCaseStyle", "AP") - v.SetDefault("taxonomies", map[string]string{"tag": "tags", "category": "categories"}) - v.SetDefault("permalinks", make(map[string]string)) - v.SetDefault("sitemap", config.Sitemap{Priority: -1, Filename: "sitemap.xml"}) - v.SetDefault("disableLiveReload", false) - v.SetDefault("pluralizeListTitles", true) - v.SetDefault("forceSyncStatic", false) - v.SetDefault("footnoteAnchorPrefix", "") - v.SetDefault("footnoteReturnLinkContents", "") - v.SetDefault("newContentEditor", "") - v.SetDefault("paginate", 10) - v.SetDefault("paginatePath", "page") - v.SetDefault("summaryLength", 70) - v.SetDefault("rssLimit", -1) - v.SetDefault("sectionPagesMenu", "") - v.SetDefault("disablePathToLower", false) - v.SetDefault("hasCJKLanguage", false) - v.SetDefault("enableEmoji", false) - v.SetDefault("pygmentsCodeFencesGuessSyntax", false) - v.SetDefault("defaultContentLanguage", "en") - v.SetDefault("defaultContentLanguageInSubdir", false) - v.SetDefault("enableMissingTranslationPlaceholders", false) - v.SetDefault("enableGitInfo", false) - v.SetDefault("ignoreFiles", make([]string, 0)) - v.SetDefault("disableAliases", false) - v.SetDefault("debug", false) - v.SetDefault("disableFastRender", false) - v.SetDefault("timeout", "30s") - v.SetDefault("enableInlineShortcodes", false) - - return nil +func (l configLoader) wrapFileError(err error, filename string) error { + err, _ = herrors.WithFileContextForFile( + err, + filename, + filename, + l.Fs, + herrors.SimpleLineMatcher) + return err } diff --git a/hugolib/config_test.go b/hugolib/config_test.go index fb81c927e..77ac9b92f 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -17,11 +17,15 @@ import ( "bytes" "fmt" "path/filepath" + "strings" "testing" + "github.com/gohugoio/hugo/media" + "github.com/google/go-cmp/cmp" + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/maps" "github.com/spf13/afero" - "github.com/spf13/viper" ) func TestLoadConfig(t *testing.T) { @@ -77,12 +81,7 @@ func TestLoadConfigFromTheme(t *testing.T) { c := qt.New(t) - mainConfigBasic := ` -theme = "test-theme" -baseURL = "https://example.com/" - -` - mainConfig := ` + mainConfigTemplate := ` theme = "test-theme" baseURL = "https://example.com/" @@ -90,9 +89,12 @@ baseURL = "https://example.com/" date = ["date","publishDate"] [params] +MERGE_PARAMS p1 = "p1 main" -p2 = "p2 main" -top = "top" +[params.b] +b1 = "b1 main" +[params.b.c] +bc1 = "bc1 main" [mediaTypes] [mediaTypes."text/m1"] @@ -130,7 +132,14 @@ expiryDate = ["date"] [params] p1 = "p1 theme" p2 = "p2 theme" -p3 = "p3 theme" +[params.b] +b1 = "b1 theme" +b2 = "b2 theme" +[params.b.c] +bc1 = "bc1 theme" +bc2 = "bc2 theme" +[params.b.c.d] +bcd1 = "bcd1 theme" [mediaTypes] [mediaTypes."text/m1"] @@ -176,190 +185,137 @@ name = "menu-theme" ` - b := newTestSitesBuilder(t) - b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig) - b.CreateSites().Build(BuildCfg{}) - - got := b.Cfg.(*viper.Viper).AllSettings() - - b.AssertObject(` -map[string]interface {}{ - "p1": "p1 main", - "p2": "p2 main", - "p3": "p3 theme", - "top": "top", -}`, got["params"]) - - b.AssertObject(` -map[string]interface {}{ - "date": []interface {}{ - "date", - "publishDate", - }, -}`, got["frontmatter"]) - - b.AssertObject(` -map[string]interface {}{ - "text/m1": map[string]interface {}{ - "suffixes": []interface {}{ - "m1main", - }, - }, - "text/m2": map[string]interface {}{ - "suffixes": []interface {}{ - "m2theme", - }, - }, -}`, got["mediatypes"]) - - b.AssertObject(` -map[string]interface {}{ - "o1": map[string]interface {}{ - "basename": "o1main", - "mediatype": Type{ - MainType: "text", - SubType: "m1", - Delimiter: ".", - FirstSuffix: SuffixInfo{ - Suffix: "m1main", - FullSuffix: ".m1main", - }, - }, - }, - "o2": map[string]interface {}{ - "basename": "o2theme", - "mediatype": Type{ - MainType: "text", - SubType: "m2", - Delimiter: ".", - FirstSuffix: SuffixInfo{ - Suffix: "m2theme", - FullSuffix: ".m2theme", - }, - }, - }, -}`, got["outputformats"]) - - b.AssertObject(`map[string]interface {}{ - "en": map[string]interface {}{ - "languagename": "English", - "menus": map[string]interface {}{ - "theme": []map[string]interface {}{ - map[string]interface {}{ - "name": "menu-lang-en-theme", - }, - }, - }, - "params": map[string]interface {}{ - "pl1": "p1-en-main", - "pl2": "p2-en-theme", - }, - }, - "nb": map[string]interface {}{ - "languagename": "Norsk", - "menus": map[string]interface {}{ - "theme": []map[string]interface {}{ - map[string]interface {}{ - "name": "menu-lang-nb-theme", - }, - }, - }, - "params": map[string]interface {}{ - "pl1": "p1-nb-main", - "pl2": "p2-nb-theme", - }, - }, -} -`, got["languages"]) - - b.AssertObject(` -map[string]interface {}{ - "main": []map[string]interface {}{ - map[string]interface {}{ - "name": "menu-main-main", - }, - }, - "thememenu": []map[string]interface {}{ - map[string]interface {}{ - "name": "menu-theme", - }, - }, - "top": []map[string]interface {}{ - map[string]interface {}{ - "name": "menu-top-main", - }, - }, -} -`, got["menus"]) - - c.Assert(got["baseurl"], qt.Equals, "https://example.com/") - - if true { - return + buildForStrategy := func(t testing.TB, s string) *sitesBuilder { + mainConfig := strings.ReplaceAll(mainConfigTemplate, "MERGE_PARAMS", s) + b := newTestSitesBuilder(t) + b.WithConfigFile("toml", mainConfig).WithThemeConfigFile("toml", themeConfig) + return b.CreateSites().Build(BuildCfg{}) } - // Test variants with only values from theme - b = newTestSitesBuilder(t) - b.WithConfigFile("toml", mainConfigBasic).WithThemeConfigFile("toml", themeConfig) - b.CreateSites().Build(BuildCfg{}) - got = b.Cfg.(*viper.Viper).AllSettings() + c.Run("Merge default", func(c *qt.C) { + b := buildForStrategy(c, "") - b.AssertObject(`map[string]interface {}{ - "p1": "p1 theme", - "p2": "p2 theme", - "p3": "p3 theme", - "test-theme": map[string]interface {}{ - "p1": "p1 theme", - "p2": "p2 theme", - "p3": "p3 theme", - }, -}`, got["params"]) + got := b.Cfg.Get("").(maps.Params) - c.Assert(got["languages"], qt.IsNil) - b.AssertObject(` -map[string]interface {}{ - "text/m1": map[string]interface {}{ - "suffix": "m1theme", - }, - "text/m2": map[string]interface {}{ - "suffix": "m2theme", - }, -}`, got["mediatypes"]) + b.Assert(got["params"], qt.DeepEquals, maps.Params{ + "b": maps.Params{ + "b1": "b1 main", + "c": maps.Params{ + "bc1": "bc1 main", + "bc2": "bc2 theme", + "d": maps.Params{"bcd1": string("bcd1 theme")}, + }, + "b2": "b2 theme", + }, + "p2": "p2 theme", + "p1": "p1 main", + }) + + b.Assert(got["mediatypes"], qt.DeepEquals, maps.Params{ + "text/m2": maps.Params{ + "suffixes": []interface{}{ + "m2theme", + }, + }, + "text/m1": maps.Params{ + "suffixes": []interface{}{ + "m1main", + }, + }, + }) + + var eq = qt.CmpEquals( + cmp.Comparer(func(m1, m2 media.Type) bool { + if m1.SubType != m2.SubType { + return false + } + return m1.FirstSuffix == m2.FirstSuffix + }), + ) + + mediaTypes := b.H.Sites[0].mediaTypesConfig + m1, _ := mediaTypes.GetByType("text/m1") + m2, _ := mediaTypes.GetByType("text/m2") + + b.Assert(got["outputformats"], eq, maps.Params{ + "o1": maps.Params{ + "mediatype": m1, + "basename": "o1main", + }, + "o2": maps.Params{ + "basename": "o2theme", + "mediatype": m2, + }, + }) + + b.Assert(got["languages"], qt.DeepEquals, maps.Params{ + "en": maps.Params{ + "languagename": "English", + "params": maps.Params{ + "pl2": "p2-en-theme", + "pl1": "p1-en-main", + }, + "menus": maps.Params{ + "main": []map[string]interface{}{ + { + "name": "menu-lang-en-main", + }, + }, + "theme": []map[string]interface{}{ + { + "name": "menu-lang-en-theme", + }, + }, + }, + }, + "nb": maps.Params{ + "languagename": "Norsk", + "params": maps.Params{ + "top": "top-nb-theme", + "pl1": "p1-nb-main", + "pl2": "p2-nb-theme", + }, + "menus": maps.Params{ + "main": []map[string]interface{}{ + { + "name": "menu-lang-nb-main", + }, + }, + "theme": []map[string]interface{}{ + { + "name": "menu-lang-nb-theme", + }, + }, + "top": []map[string]interface{}{ + { + "name": "menu-lang-nb-top", + }, + }, + }, + }, + }) + + c.Assert(got["baseurl"], qt.Equals, "https://example.com/") + }) + + c.Run("Merge shallow", func(c *qt.C) { + b := buildForStrategy(c, fmt.Sprintf("_merge=%q", "shallow")) + + got := b.Cfg.Get("").(maps.Params) + + // Shallow merge, only add new keys to params. + b.Assert(got["params"], qt.DeepEquals, maps.Params{ + "p1": "p1 main", + "b": maps.Params{ + "b1": "b1 main", + "c": maps.Params{ + "bc1": "bc1 main", + }, + }, + "p2": "p2 theme", + }) + }) - b.AssertObject(` -map[string]interface {}{ - "o1": map[string]interface {}{ - "basename": "o1theme", - "mediatype": Type{ - MainType: "text", - SubType: "m1", - Suffix: "m1theme", - Delimiter: ".", - }, - }, - "o2": map[string]interface {}{ - "basename": "o2theme", - "mediatype": Type{ - MainType: "text", - SubType: "m2", - Suffix: "m2theme", - Delimiter: ".", - }, - }, -}`, got["outputformats"]) - b.AssertObject(` -map[string]interface {}{ - "main": []interface {}{ - map[string]interface {}{ - "name": "menu-main-theme", - }, - }, - "thememenu": []interface {}{ - map[string]interface {}{ - "name": "menu-theme", - }, - }, -}`, got["menu"]) } func TestPrivacyConfig(t *testing.T) { @@ -490,7 +446,12 @@ intSlice = [5,7,9] floatSlice = [3.14, 5.19] stringSlice = ["a", "b"] +[outputFormats] +[outputFormats.ofbase] +mediaType = "text/plain" + [params] +paramWithNoEnvOverride="nooverride" [params.api_config] api_key="default_key" another_key="default another_key" @@ -504,9 +465,16 @@ quality = 75 b.WithSourceFile("themes/mytheme/config.toml", ` +[outputFormats] +[outputFormats.oftheme] +mediaType = "text/plain" +[outputFormats.ofbase] +mediaType = "application/xml" + [params] [params.mytheme_section] theme_param="themevalue" +theme_param_nooverride="nooverride" [params.mytheme_section2] theme_param="themevalue2" @@ -530,14 +498,16 @@ theme_param="themevalue2" "HUGOxPARAMSxMYTHEME_SECTION2xTHEME_PARAM", "themevalue2_changed", "HUGO_PARAMS_EMPTY", ``, "HUGO_PARAMS_HTML", ``, - // + // Issue #8618 "HUGO_SERVICES_GOOGLEANALYTICS_ID", `gaid`, + "HUGO_PARAMS_A_B_C", "abc", ) b.Build(BuildCfg{}) cfg := b.H.Cfg - scfg := b.H.Sites[0].siteConfigConfig.Services + s := b.H.Sites[0] + scfg := s.siteConfigConfig.Services c.Assert(cfg.Get("environment"), qt.Equals, "test") c.Assert(cfg.GetBool("enablegitinfo"), qt.Equals, false) @@ -551,9 +521,23 @@ theme_param="themevalue2" c.Assert(cfg.Get("params.api_config.api_key"), qt.Equals, "new_key") c.Assert(cfg.Get("params.api_config.another_key"), qt.Equals, "default another_key") c.Assert(cfg.Get("params.mytheme_section.theme_param"), qt.Equals, "themevalue_changed") + c.Assert(cfg.Get("params.mytheme_section.theme_param_nooverride"), qt.Equals, "nooverride") c.Assert(cfg.Get("params.mytheme_section2.theme_param"), qt.Equals, "themevalue2_changed") c.Assert(cfg.Get("params.empty"), qt.Equals, ``) c.Assert(cfg.Get("params.html"), qt.Equals, ``) + params := cfg.Get("params").(maps.Params) + c.Assert(params["paramwithnoenvoverride"], qt.Equals, "nooverride") + c.Assert(cfg.Get("params.paramwithnoenvoverride"), qt.Equals, "nooverride") c.Assert(scfg.GoogleAnalytics.ID, qt.Equals, "gaid") + c.Assert(cfg.Get("params.a.b"), qt.DeepEquals, maps.Params{ + "c": "abc", + }) + + ofBase, _ := s.outputFormatsConfig.GetByName("ofbase") + ofTheme, _ := s.outputFormatsConfig.GetByName("oftheme") + + c.Assert(ofBase.MediaType, qt.Equals, media.TextType) + c.Assert(ofTheme.MediaType, qt.Equals, media.TextType) + } diff --git a/hugolib/filesystems/basefs_test.go b/hugolib/filesystems/basefs_test.go index 139d0c20e..a119d4c17 100644 --- a/hugolib/filesystems/basefs_test.go +++ b/hugolib/filesystems/basefs_test.go @@ -33,7 +33,7 @@ import ( "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugolib/paths" "github.com/gohugoio/hugo/modules" - "github.com/spf13/viper" + ) func initConfig(fs afero.Fs, cfg config.Provider) error { @@ -76,7 +76,7 @@ func initConfig(fs afero.Fs, cfg config.Provider) error { func TestNewBaseFs(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() fs := hugofs.NewMem(v) @@ -181,8 +181,8 @@ theme = ["atheme"] } } -func createConfig() *viper.Viper { - v := viper.New() +func createConfig() config.Provider { + v := config.New() v.Set("contentDir", "mycontent") v.Set("i18nDir", "myi18n") v.Set("staticDir", "mystatic") @@ -453,7 +453,7 @@ func countFilesAndGetFilenames(fs afero.Fs, dirname string) (int, []string, erro return counter, filenames, nil } -func setConfigAndWriteSomeFilesTo(fs afero.Fs, v *viper.Viper, key, val string, num int) { +func setConfigAndWriteSomeFilesTo(fs afero.Fs, v config.Provider, key, val string, num int) { workingDir := v.GetString("workingDir") v.Set(key, val) fs.Mkdir(val, 0755) diff --git a/hugolib/hugo_modules_test.go b/hugolib/hugo_modules_test.go index 5ebf8bd0b..96355f08b 100644 --- a/hugolib/hugo_modules_test.go +++ b/hugolib/hugo_modules_test.go @@ -22,6 +22,7 @@ import ( "testing" "time" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/modules/npm" "github.com/gohugoio/hugo/common/loggers" @@ -37,7 +38,6 @@ import ( qt "github.com/frankban/quicktest" "github.com/gohugoio/testmodBuilder/mods" - "github.com/spf13/viper" ) func TestHugoModulesVariants(t *testing.T) { @@ -45,7 +45,7 @@ func TestHugoModulesVariants(t *testing.T) { t.Skip("skip (relative) long running modules test when running locally") } - config := ` + tomlConfig := ` baseURL="https://example.org" workingDir = %q @@ -56,7 +56,7 @@ path="github.com/gohugoio/hugoTestModule2" ` createConfig := func(workingDir, moduleOpts string) string { - return fmt.Sprintf(config, workingDir, moduleOpts) + return fmt.Sprintf(tomlConfig, workingDir, moduleOpts) } newTestBuilder := func(t testing.TB, moduleOpts string) (*sitesBuilder, func()) { @@ -65,7 +65,7 @@ path="github.com/gohugoio/hugoTestModule2" b.Assert(err, qt.IsNil) workingDir := filepath.Join(tempDir, "myhugosite") b.Assert(os.MkdirAll(workingDir, 0777), qt.IsNil) - b.Fs = hugofs.NewDefault(viper.New()) + b.Fs = hugofs.NewDefault(config.New()) b.WithWorkingDir(workingDir).WithConfigFile("toml", createConfig(workingDir, moduleOpts)) b.WithTemplates( "index.html", ` @@ -333,7 +333,7 @@ func TestHugoModulesMatrix(t *testing.T) { for _, m := range testmods[:2] { c := qt.New(t) - v := viper.New() + v := config.New() workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-modules-test") c.Assert(err, qt.IsNil) @@ -671,7 +671,7 @@ func TestModulesSymlinks(t *testing.T) { c := qt.New(t) // We need to use the OS fs for this. - cfg := viper.New() + cfg := config.New() fs := hugofs.NewFrom(hugofs.Os, cfg) workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-mod-sym") @@ -839,13 +839,13 @@ workingDir = %q ` - config := fmt.Sprintf(configTemplate, workingDir) + tomlConfig := fmt.Sprintf(configTemplate, workingDir) b := newTestSitesBuilder(t).Running() - b.Fs = hugofs.NewDefault(viper.New()) + b.Fs = hugofs.NewDefault(config.New()) - b.WithWorkingDir(workingDir).WithConfigFile("toml", config) + b.WithWorkingDir(workingDir).WithConfigFile("toml", tomlConfig) b.WithTemplatesAdded("index.html", ` {{ .Title }} {{ .Content }} @@ -960,16 +960,16 @@ workingDir = %q %s ` - config := fmt.Sprintf(configTemplate, workingDir, mounts) - config = strings.Replace(config, "WORKING_DIR", workingDir, -1) + tomlConfig := fmt.Sprintf(configTemplate, workingDir, mounts) + tomlConfig = strings.Replace(tomlConfig, "WORKING_DIR", workingDir, -1) b := newTestSitesBuilder(c).Running() - b.Fs = hugofs.NewDefault(viper.New()) + b.Fs = hugofs.NewDefault(config.New()) os.MkdirAll(filepath.Join(workingDir, "content", "blog"), 0777) - b.WithWorkingDir(workingDir).WithConfigFile("toml", config) + b.WithWorkingDir(workingDir).WithConfigFile("toml", tomlConfig) return test{ b: b, @@ -1064,7 +1064,7 @@ func TestSiteWithGoModButNoModules(t *testing.T) { workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-no-mod") c.Assert(err, qt.IsNil) - cfg := viper.New() + cfg := config.New() cfg.Set("workingDir", workDir) fs := hugofs.NewFrom(hugofs.Os, cfg) @@ -1090,7 +1090,7 @@ func TestModuleAbsMount(t *testing.T) { absContentDir, clean2, err := htesting.CreateTempDir(hugofs.Os, "hugo-content") c.Assert(err, qt.IsNil) - cfg := viper.New() + cfg := config.New() cfg.Set("workingDir", workDir) fs := hugofs.NewFrom(hugofs.Os, cfg) diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 0607bde1c..d380cf737 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -374,7 +374,8 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) { s.h = h } - if err := applyDeps(cfg, sites...); err != nil { + var l configLoader + if err := l.applyDeps(cfg, sites...); err != nil { return nil, errors.Wrap(err, "add site dependencies") } @@ -407,7 +408,7 @@ func (h *HugoSites) loadGitInfo() error { return nil } -func applyDeps(cfg deps.DepsCfg, sites ...*Site) error { +func (l configLoader) applyDeps(cfg deps.DepsCfg, sites ...*Site) error { if cfg.TemplateProvider == nil { cfg.TemplateProvider = tplimpl.DefaultTemplateProvider } @@ -446,7 +447,7 @@ func applyDeps(cfg deps.DepsCfg, sites ...*Site) error { d.Site = s.Info - siteConfig, err := loadSiteConfig(s.language) + siteConfig, err := l.loadSiteConfig(s.language) if err != nil { return errors.Wrap(err, "load site config") } @@ -607,11 +608,12 @@ func (h *HugoSites) withSite(fn func(s *Site) error) error { func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error { oldLangs, _ := h.Cfg.Get("languagesSorted").(langs.Languages) - if err := loadLanguageSettings(h.Cfg, oldLangs); err != nil { + l := configLoader{cfg: h.Cfg} + if err := l.loadLanguageSettings(oldLangs); err != nil { return err } - depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: cfg} + depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: l.cfg} sites, err := createSitesFromConfig(depsCfg) if err != nil { @@ -629,7 +631,8 @@ func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error { s.h = h } - if err := applyDeps(depsCfg, sites...); err != nil { + var cl configLoader + if err := cl.applyDeps(depsCfg, sites...); err != nil { return err } diff --git a/hugolib/hugo_sites_build_errors_test.go b/hugolib/hugo_sites_build_errors_test.go index efe013c39..8b23e7ac7 100644 --- a/hugolib/hugo_sites_build_errors_test.go +++ b/hugolib/hugo_sites_build_errors_test.go @@ -5,9 +5,7 @@ import ( "path/filepath" "strings" "testing" - "time" - "github.com/fortytw2/leaktest" "github.com/gohugoio/hugo/htesting" qt "github.com/frankban/quicktest" @@ -318,7 +316,7 @@ Some content. // https://github.com/gohugoio/hugo/issues/5375 func TestSiteBuildTimeout(t *testing.T) { if !htesting.IsCI() { - defer leaktest.CheckTimeout(t, 10*time.Second)() + //defer leaktest.CheckTimeout(t, 10*time.Second)() } b := newTestSitesBuilder(t) diff --git a/hugolib/image_test.go b/hugolib/image_test.go index 0dacf2a33..4726f5b49 100644 --- a/hugolib/image_test.go +++ b/hugolib/image_test.go @@ -21,11 +21,11 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/htesting" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/hugofs" - "github.com/spf13/viper" ) // We have many tests for the different resize operations etc. in the resource package, @@ -38,7 +38,7 @@ func TestImageOps(t *testing.T) { defer clean() newBuilder := func(timeout interface{}) *sitesBuilder { - v := viper.New() + v := config.New() v.Set("workingDir", workDir) v.Set("baseURL", "https://example.org") v.Set("timeout", timeout) diff --git a/hugolib/js_test.go b/hugolib/js_test.go index cd0883fd5..75dc0e7de 100644 --- a/hugolib/js_test.go +++ b/hugolib/js_test.go @@ -21,11 +21,10 @@ import ( "testing" "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/htesting" - "github.com/spf13/viper" - qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/hugofs" @@ -88,7 +87,7 @@ document.body.textContent = greeter(user);` c.Assert(err, qt.IsNil) defer clean() - v := viper.New() + v := config.New() v.Set("workingDir", workDir) v.Set("disableKinds", []string{"taxonomy", "term", "page"}) b := newTestSitesBuilder(t).WithLogger(loggers.NewWarningLogger()) @@ -162,7 +161,7 @@ func TestJSBuild(t *testing.T) { c.Assert(err, qt.IsNil) defer clean() - config := fmt.Sprintf(` + tomlConfig := fmt.Sprintf(` baseURL = "https://example.org" workingDir = %q @@ -177,8 +176,8 @@ path="github.com/gohugoio/hugoTestProjectJSModImports" `, workDir) b := newTestSitesBuilder(t) - b.Fs = hugofs.NewDefault(viper.New()) - b.WithWorkingDir(workDir).WithConfigFile("toml", config).WithLogger(loggers.NewInfoLogger()) + b.Fs = hugofs.NewDefault(config.New()) + b.WithWorkingDir(workDir).WithConfigFile("toml", tomlConfig).WithLogger(loggers.NewInfoLogger()) b.WithSourceFile("go.mod", `module github.com/gohugoio/tests/testHugoModules go 1.15 diff --git a/hugolib/minify_publisher_test.go b/hugolib/minify_publisher_test.go index 66e674ade..ef460efa2 100644 --- a/hugolib/minify_publisher_test.go +++ b/hugolib/minify_publisher_test.go @@ -16,13 +16,13 @@ package hugolib import ( "testing" - "github.com/spf13/viper" + "github.com/gohugoio/hugo/config" ) func TestMinifyPublisher(t *testing.T) { t.Parallel() - v := viper.New() + v := config.New() v.Set("minify", true) v.Set("baseURL", "https://example.org/") diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index 3df997452..759fadd2d 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -336,7 +336,7 @@ func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, fron if frontmatter != nil { // Needed for case insensitive fetching of params values - maps.ToLower(frontmatter) + maps.PrepareParams(frontmatter) if p.bucket != nil { // Check for any cascade define on itself. if cv, found := frontmatter["cascade"]; found { diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 9d23aaa5c..5bc3db22f 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -37,7 +37,6 @@ import ( "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" "github.com/spf13/afero" - "github.com/spf13/viper" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" @@ -786,7 +785,7 @@ func TestPageWithLastmodFromGitInfo(t *testing.T) { c := qt.New(t) // We need to use the OS fs for this. - cfg := viper.New() + cfg := config.New() fs := hugofs.NewFrom(hugofs.Os, cfg) fs.Destination = &afero.MemMapFs{} @@ -1066,7 +1065,7 @@ func TestChompBOM(t *testing.T) { func TestPageWithEmoji(t *testing.T) { for _, enableEmoji := range []bool{true, false} { - v := viper.New() + v := config.New() v.Set("enableEmoji", enableEmoji) b := newTestSitesBuilder(t).WithViper(v) diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go index 7d775871a..b63d663e3 100644 --- a/hugolib/pagebundler_test.go +++ b/hugolib/pagebundler_test.go @@ -23,6 +23,8 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/helpers" @@ -35,7 +37,6 @@ import ( "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/deps" - "github.com/spf13/viper" qt "github.com/frankban/quicktest" ) @@ -352,12 +353,11 @@ func TestMultilingualDisableDefaultLanguage(t *testing.T) { c := qt.New(t) _, cfg := newTestBundleSourcesMultilingual(t) - cfg.Set("disableLanguages", []string{"en"}) - - err := loadDefaultSettingsFor(cfg) + l := configLoader{cfg: cfg} + err := l.applyConfigDefaults() c.Assert(err, qt.IsNil) - err = loadLanguageSettings(cfg, nil) + err = l.loadLanguageSettings(nil) c.Assert(err, qt.Not(qt.IsNil)) c.Assert(err.Error(), qt.Contains, "cannot disable default language") } @@ -397,7 +397,7 @@ func TestPageBundlerSiteWitSymbolicLinksInContent(t *testing.T) { c := qt.New(t) // We need to use the OS fs for this. - cfg := viper.New() + cfg := config.New() fs := hugofs.NewFrom(hugofs.Os, cfg) workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugosym") @@ -696,7 +696,7 @@ Single content. b.AssertFileContent("public/section-not-bundle/single/index.html", "Section Single", "|

Single content.

") } -func newTestBundleSources(t testing.TB) (*hugofs.Fs, *viper.Viper) { +func newTestBundleSources(t testing.TB) (*hugofs.Fs, config.Provider) { cfg, fs := newTestCfgBasic() c := qt.New(t) @@ -863,7 +863,7 @@ Content for 은행. return fs, cfg } -func newTestBundleSourcesMultilingual(t *testing.T) (*hugofs.Fs, *viper.Viper) { +func newTestBundleSourcesMultilingual(t *testing.T) (*hugofs.Fs, config.Provider) { cfg, fs := newTestCfgBasic() workDir := "/work" @@ -1319,7 +1319,7 @@ func TestPageBundlerHome(t *testing.T) { workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-bundler-home") c.Assert(err, qt.IsNil) - cfg := viper.New() + cfg := config.New() cfg.Set("workingDir", workDir) fs := hugofs.NewFrom(hugofs.Os, cfg) diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go index c7a42acdc..45221af71 100644 --- a/hugolib/pages_capture.go +++ b/hugolib/pages_capture.go @@ -130,7 +130,7 @@ func (c *pagesCollector) isCascadingEdit(dir contentDirKey) (bool, string) { section = s - maps.ToLower(pf.FrontMatter) + maps.PrepareParams(pf.FrontMatter) cascade1, ok := pf.FrontMatter["cascade"] hasCascade := n.p.bucket.cascade != nil && len(n.p.bucket.cascade) > 0 if !ok { diff --git a/hugolib/paths/paths_test.go b/hugolib/paths/paths_test.go index 59dbf0e00..d3ead4d17 100644 --- a/hugolib/paths/paths_test.go +++ b/hugolib/paths/paths_test.go @@ -16,17 +16,16 @@ package paths import ( "testing" - "github.com/gohugoio/hugo/langs" - qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/hugofs" - "github.com/spf13/viper" + "github.com/gohugoio/hugo/langs" ) func TestNewPaths(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() fs := hugofs.NewMem(v) v.Set("languages", map[string]interface{}{ diff --git a/hugolib/resource_chain_babel_test.go b/hugolib/resource_chain_babel_test.go index 61b2ef6d5..9e5c9c4a5 100644 --- a/hugolib/resource_chain_babel_test.go +++ b/hugolib/resource_chain_babel_test.go @@ -19,14 +19,14 @@ import ( "path/filepath" "testing" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/common/hexec" jww "github.com/spf13/jwalterweatherman" "github.com/gohugoio/hugo/htesting" - "github.com/spf13/viper" - qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/hugofs" @@ -91,7 +91,7 @@ class Car2 { var logBuf bytes.Buffer logger := loggers.NewBasicLoggerForWriter(jww.LevelInfo, &logBuf) - v := viper.New() + v := config.New() v.Set("workingDir", workDir) v.Set("disableKinds", []string{"taxonomy", "term", "page"}) b := newTestSitesBuilder(t).WithLogger(logger) diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go index 9ea1d8529..a367237ab 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -20,6 +20,8 @@ import ( "math/rand" "os" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass" "path/filepath" @@ -35,8 +37,6 @@ import ( "github.com/gohugoio/hugo/htesting" - "github.com/spf13/viper" - qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/hugofs" @@ -65,7 +65,7 @@ func TestSCSSWithIncludePaths(t *testing.T) { c.Assert(err, qt.IsNil) defer clean() - v := viper.New() + v := config.New() v.Set("workingDir", workDir) b := newTestSitesBuilder(c).WithLogger(loggers.NewErrorLogger()) // Need to use OS fs for this. @@ -130,7 +130,7 @@ func TestSCSSWithRegularCSSImport(t *testing.T) { c.Assert(err, qt.IsNil) defer clean() - v := viper.New() + v := config.New() v.Set("workingDir", workDir) b := newTestSitesBuilder(c).WithLogger(loggers.NewErrorLogger()) // Need to use OS fs for this. @@ -230,7 +230,7 @@ func TestSCSSWithThemeOverrides(t *testing.T) { theme := "mytheme" themesDir := filepath.Join(workDir, "themes") themeDirs := filepath.Join(themesDir, theme) - v := viper.New() + v := config.New() v.Set("workingDir", workDir) v.Set("theme", theme) b := newTestSitesBuilder(c).WithLogger(loggers.NewErrorLogger()) @@ -345,7 +345,7 @@ func TestSCSSWithIncludePathsSass(t *testing.T) { c.Assert(err, qt.IsNil) defer clean1() - v := viper.New() + v := config.New() v.Set("workingDir", workDir) v.Set("theme", "mytheme") b := newTestSitesBuilder(t).WithLogger(loggers.NewErrorLogger()) @@ -974,7 +974,7 @@ h1 { var logBuf bytes.Buffer - newTestBuilder := func(v *viper.Viper) *sitesBuilder { + newTestBuilder := func(v config.Provider) *sitesBuilder { v.Set("workingDir", workDir) v.Set("disableKinds", []string{"taxonomy", "term", "page"}) logger := loggers.NewBasicLoggerForWriter(jww.LevelInfo, &logBuf) @@ -997,7 +997,7 @@ Styles Content: Len: {{ len $styles.Content }}| return b } - b := newTestBuilder(viper.New()) + b := newTestBuilder(config.New()) cssDir := filepath.Join(workDir, "assets", "css", "components") b.Assert(os.MkdirAll(cssDir, 0777), qt.IsNil) @@ -1049,7 +1049,7 @@ Styles Content: Len: 770878| build := func(s string, shouldFail bool) error { b.Assert(os.RemoveAll(filepath.Join(workDir, "public")), qt.IsNil) - v := viper.New() + v := config.New() v.Set("build", map[string]interface{}{ "useResourceCacheWhen": s, }) diff --git a/hugolib/robotstxt_test.go b/hugolib/robotstxt_test.go index 6bc39e97c..2035c235f 100644 --- a/hugolib/robotstxt_test.go +++ b/hugolib/robotstxt_test.go @@ -16,7 +16,7 @@ package hugolib import ( "testing" - "github.com/spf13/viper" + "github.com/gohugoio/hugo/config" ) const robotTxtTemplate = `User-agent: Googlebot @@ -28,7 +28,7 @@ const robotTxtTemplate = `User-agent: Googlebot func TestRobotsTXTOutput(t *testing.T) { t.Parallel() - cfg := viper.New() + cfg := config.New() cfg.Set("baseURL", "http://auth/bub/") cfg.Set("enableRobotsTXT", true) diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index 51187a003..7eb0d01de 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -20,11 +20,10 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/markup/asciidocext" "github.com/gohugoio/hugo/markup/rst" - "github.com/spf13/viper" - "github.com/gohugoio/hugo/parser/pageparser" "github.com/gohugoio/hugo/resources/page" @@ -1214,7 +1213,7 @@ title: "Hugo Rocks!" func TestShortcodeEmoji(t *testing.T) { t.Parallel() - v := viper.New() + v := config.New() v.Set("enableEmoji", true) builder := newTestSitesBuilder(t).WithViper(v) @@ -1279,7 +1278,7 @@ func TestShortcodeRef(t *testing.T) { t.Run(fmt.Sprintf("plainIDAnchors=%t", plainIDAnchors), func(t *testing.T) { t.Parallel() - v := viper.New() + v := config.New() v.Set("baseURL", "https://example.org") v.Set("blackfriday", map[string]interface{}{ "plainIDAnchors": plainIDAnchors, diff --git a/hugolib/site.go b/hugolib/site.go index 12714892d..9921dcc97 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -77,7 +77,6 @@ import ( "github.com/spf13/afero" "github.com/spf13/cast" - "github.com/spf13/viper" ) // Site contains all the information relevant for constructing a static @@ -501,9 +500,9 @@ But this also means that your site configuration may not do what you expect. If var relatedContentConfig related.Config if cfg.Language.IsSet("related") { - relatedContentConfig, err = related.DecodeConfig(cfg.Language.Get("related")) + relatedContentConfig, err = related.DecodeConfig(cfg.Language.GetParams("related")) if err != nil { - return nil, err + return nil, errors.Wrap(err, "failed to decode related config") } } else { relatedContentConfig = related.DefaultConfig @@ -574,7 +573,8 @@ func NewSite(cfg deps.DepsCfg) (*Site, error) { return nil, err } - if err = applyDeps(cfg, s); err != nil { + var l configLoader + if err = l.applyDeps(cfg, s); err != nil { return nil, err } @@ -586,11 +586,11 @@ func NewSite(cfg deps.DepsCfg) (*Site, error) { // Note: This is mainly used in single site tests. // TODO(bep) test refactor -- remove func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) { - v := viper.New() - if err := loadDefaultSettingsFor(v); err != nil { + l := configLoader{cfg: config.New()} + if err := l.applyConfigDefaults(); err != nil { return nil, err } - return newSiteForLang(langs.NewDefaultLanguage(v), withTemplate...) + return newSiteForLang(langs.NewDefaultLanguage(l.cfg), withTemplate...) } // NewEnglishSite creates a new site in English language. @@ -598,11 +598,11 @@ func NewSiteDefaultLang(withTemplate ...func(templ tpl.TemplateManager) error) ( // Note: This is mainly used in single site tests. // TODO(bep) test refactor -- remove func NewEnglishSite(withTemplate ...func(templ tpl.TemplateManager) error) (*Site, error) { - v := viper.New() - if err := loadDefaultSettingsFor(v); err != nil { + l := configLoader{cfg: config.New()} + if err := l.applyConfigDefaults(); err != nil { return nil, err } - return newSiteForLang(langs.NewLanguage("en", v), withTemplate...) + return newSiteForLang(langs.NewLanguage("en", l.cfg), withTemplate...) } // newSiteForLang creates a new site in the given language. @@ -1314,7 +1314,7 @@ func (s *Site) initializeSiteInfo() error { return vvv } default: - m := cast.ToStringMapBool(v) + m := maps.ToStringMapBool(v) uglyURLs = func(p page.Page) bool { return m[p.Section()] } diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go index 1961dd06f..f3455f369 100644 --- a/hugolib/site_output_test.go +++ b/hugolib/site_output_test.go @@ -19,13 +19,13 @@ import ( "testing" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/resources/page" "github.com/spf13/afero" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/output" - "github.com/spf13/viper" ) func TestSiteWithPageOutputs(t *testing.T) { @@ -333,7 +333,7 @@ func TestCreateSiteOutputFormats(t *testing.T) { page.KindSection: []string{"JSON"}, } - cfg := viper.New() + cfg := config.New() cfg.Set("outputs", outputsConfig) outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false) @@ -358,7 +358,7 @@ func TestCreateSiteOutputFormats(t *testing.T) { // Issue #4528 t.Run("Mixed case", func(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() outputsConfig := map[string]interface{}{ // Note that we in Hugo 0.53.0 renamed this Kind to "taxonomy", @@ -380,7 +380,7 @@ func TestCreateSiteOutputFormatsInvalidConfig(t *testing.T) { page.KindHome: []string{"FOO", "JSON"}, } - cfg := viper.New() + cfg := config.New() cfg.Set("outputs", outputsConfig) _, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false) @@ -394,7 +394,7 @@ func TestCreateSiteOutputFormatsEmptyConfig(t *testing.T) { page.KindHome: []string{}, } - cfg := viper.New() + cfg := config.New() cfg.Set("outputs", outputsConfig) outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false) @@ -409,7 +409,7 @@ func TestCreateSiteOutputFormatsCustomFormats(t *testing.T) { page.KindHome: []string{}, } - cfg := viper.New() + cfg := config.New() cfg.Set("outputs", outputsConfig) var ( diff --git a/hugolib/site_test.go b/hugolib/site_test.go index 365679a32..e25991164 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -23,10 +23,9 @@ import ( "testing" "github.com/gobuffalo/flect" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/publisher" - "github.com/spf13/viper" - qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/resources/page" @@ -363,7 +362,7 @@ func TestMainSections(t *testing.T) { c := qt.New(t) for _, paramSet := range []bool{false, true} { c.Run(fmt.Sprintf("param-%t", paramSet), func(c *qt.C) { - v := viper.New() + v := config.New() if paramSet { v.Set("params", map[string]interface{}{ "mainSections": []string{"a1", "a2"}, diff --git a/hugolib/template_test.go b/hugolib/template_test.go index f487cec67..abb6d32f9 100644 --- a/hugolib/template_test.go +++ b/hugolib/template_test.go @@ -19,20 +19,19 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/identity" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/tpl" - - "github.com/spf13/viper" ) func TestTemplateLookupOrder(t *testing.T) { var ( fs *hugofs.Fs - cfg *viper.Viper + cfg config.Provider th testHelper ) diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index 09988f972..451022e5c 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -30,6 +30,7 @@ import ( "github.com/fsnotify/fsnotify" "github.com/gohugoio/hugo/common/herrors" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/resources/page" @@ -39,7 +40,6 @@ import ( "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/tpl" - "github.com/spf13/viper" "github.com/gohugoio/hugo/resources/resource" @@ -83,7 +83,7 @@ type sitesBuilder struct { // Default toml configFormat string configFileSet bool - viperSet bool + configSet bool // Default is empty. // TODO(bep) revisit this and consider always setting it to something. @@ -111,7 +111,7 @@ type filenameContent struct { } func newTestSitesBuilder(t testing.TB) *sitesBuilder { - v := viper.New() + v := config.New() fs := hugofs.NewMem(v) litterOptions := litter.Options{ @@ -140,7 +140,7 @@ func newTestSitesBuilderFromDepsCfg(t testing.TB, d deps.DepsCfg) *sitesBuilder b.WithWorkingDir(workingDir) - return b.WithViper(d.Cfg.(*viper.Viper)) + return b.WithViper(d.Cfg.(config.Provider)) } func (s *sitesBuilder) Running() *sitesBuilder { @@ -186,26 +186,26 @@ func (s *sitesBuilder) WithConfigTemplate(data interface{}, format, configTempla return s.WithConfigFile(format, b.String()) } -func (s *sitesBuilder) WithViper(v *viper.Viper) *sitesBuilder { +func (s *sitesBuilder) WithViper(v config.Provider) *sitesBuilder { s.T.Helper() if s.configFileSet { s.T.Fatal("WithViper: use Viper or config.toml, not both") } defer func() { - s.viperSet = true + s.configSet = true }() // Write to a config file to make sure the tests follow the same code path. var buff bytes.Buffer - m := v.AllSettings() + m := v.Get("").(maps.Params) s.Assert(parser.InterfaceToConfig(m, metadecoders.TOML, &buff), qt.IsNil) return s.WithConfigFile("toml", buff.String()) } func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder { s.T.Helper() - if s.viperSet { - s.T.Fatal("WithConfigFile: use Viper or config.toml, not both") + if s.configSet { + s.T.Fatal("WithConfigFile: use config.Config or config.toml, not both") } s.configFileSet = true filename := s.absFilename("config." + format) @@ -845,14 +845,14 @@ func (th testHelper) replaceDefaultContentLanguageValue(value string) string { return value } -func loadTestConfig(fs afero.Fs, withConfig ...func(cfg config.Provider) error) (*viper.Viper, error) { +func loadTestConfig(fs afero.Fs, withConfig ...func(cfg config.Provider) error) (config.Provider, error) { v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs}, withConfig...) return v, err } -func newTestCfgBasic() (*viper.Viper, *hugofs.Fs) { +func newTestCfgBasic() (config.Provider, *hugofs.Fs) { mm := afero.NewMemMapFs() - v := viper.New() + v := config.New() v.Set("defaultContentLanguageInSubdir", true) fs := hugofs.NewFrom(hugofs.NewBaseFileDecorator(mm), v) @@ -860,7 +860,7 @@ func newTestCfgBasic() (*viper.Viper, *hugofs.Fs) { return v, fs } -func newTestCfg(withConfig ...func(cfg config.Provider) error) (*viper.Viper, *hugofs.Fs) { +func newTestCfg(withConfig ...func(cfg config.Provider) error) (config.Provider, *hugofs.Fs) { mm := afero.NewMemMapFs() v, err := loadTestConfig(mm, func(cfg config.Provider) error { diff --git a/langs/config.go b/langs/config.go index 3b1da89ec..fe4ed9d14 100644 --- a/langs/config.go +++ b/langs/config.go @@ -43,13 +43,13 @@ func LoadLanguageSettings(cfg config.Provider, oldLangs Languages) (c LanguagesC var languages map[string]interface{} - languagesFromConfig := cfg.GetStringMap("languages") + languagesFromConfig := cfg.GetParams("languages") disableLanguages := cfg.GetStringSlice("disableLanguages") if len(disableLanguages) == 0 { languages = languagesFromConfig } else { - languages = make(map[string]interface{}) + languages = make(maps.Params) for k, v := range languagesFromConfig { for _, disabled := range disableLanguages { if disabled == defaultLang { @@ -57,7 +57,7 @@ func LoadLanguageSettings(cfg config.Provider, oldLangs Languages) (c LanguagesC } if strings.EqualFold(k, disabled) { - v.(map[string]interface{})["disabled"] = true + v.(maps.Params)["disabled"] = true break } } @@ -193,7 +193,7 @@ func toSortedLanguages(cfg config.Provider, l map[string]interface{}) (Languages case "params": m := maps.ToStringMap(v) // Needed for case insensitive fetching of params values - maps.ToLower(m) + maps.PrepareParams(m) for k, vv := range m { language.SetParam(k, vv) } diff --git a/langs/i18n/i18n_test.go b/langs/i18n/i18n_test.go index 18c122010..be20ca3c8 100644 --- a/langs/i18n/i18n_test.go +++ b/langs/i18n/i18n_test.go @@ -28,7 +28,6 @@ import ( "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/resources/page" "github.com/spf13/afero" - "github.com/spf13/viper" "github.com/gohugoio/hugo/deps" @@ -500,9 +499,9 @@ func newDepsConfig(tp *TranslationProvider, cfg config.Provider, fs *hugofs.Fs) } } -func getConfig() *viper.Viper { - v := viper.New() - v.SetDefault("defaultContentLanguage", "en") +func getConfig() config.Provider { + v := config.New() + v.Set("defaultContentLanguage", "en") v.Set("contentDir", "content") v.Set("dataDir", "data") v.Set("i18nDir", "i18n") diff --git a/langs/language.go b/langs/language.go index bdfde573c..a86d82ba3 100644 --- a/langs/language.go +++ b/langs/language.go @@ -20,7 +20,6 @@ import ( "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/config" - "github.com/spf13/cast" ) // These are the settings that should only be looked up in the global Viper @@ -55,18 +54,20 @@ type Language struct { // absolute directory reference. It is what we get. ContentDir string + // Global config. Cfg config.Provider + // Language specific config. + LocalCfg config.Provider + + // Composite config. + config.Provider + // These are params declared in the [params] section of the language merged with the // site's params, the most specific (language) wins on duplicate keys. params map[string]interface{} paramsMu sync.Mutex paramsSet bool - - // These are config values, i.e. the settings declared outside of the [params] section of the language. - // This is the map Hugo looks in when looking for configuration values (baseURL etc.). - // Values in this map can also be fetched from the params map above. - settings map[string]interface{} } func (l *Language) String() string { @@ -81,9 +82,12 @@ func NewLanguage(lang string, cfg config.Provider) *Language { for k, v := range cfg.GetStringMap("params") { params[k] = v } - maps.ToLower(params) + maps.PrepareParams(params) - l := &Language{Lang: lang, ContentDir: cfg.GetString("contentDir"), Cfg: cfg, params: params, settings: make(map[string]interface{})} + localCfg := config.New() + compositeConfig := config.NewCompositeConfig(cfg, localCfg) + + l := &Language{Lang: lang, ContentDir: cfg.GetString("contentDir"), Cfg: cfg, LocalCfg: localCfg, Provider: compositeConfig, params: params} return l } @@ -133,7 +137,7 @@ func (l *Language) Params() maps.Params { l.paramsMu.Lock() defer l.paramsMu.Unlock() if !l.paramsSet { - maps.ToLower(l.params) + maps.PrepareParams(l.params) l.paramsSet = true } return l.params @@ -183,42 +187,6 @@ func (l *Language) SetParam(k string, v interface{}) { l.params[k] = v } -// GetBool returns the value associated with the key as a boolean. -func (l *Language) GetBool(key string) bool { return cast.ToBool(l.Get(key)) } - -// GetString returns the value associated with the key as a string. -func (l *Language) GetString(key string) string { return cast.ToString(l.Get(key)) } - -// GetInt returns the value associated with the key as an int. -func (l *Language) GetInt(key string) int { return cast.ToInt(l.Get(key)) } - -// GetStringMap returns the value associated with the key as a map of interfaces. -func (l *Language) GetStringMap(key string) map[string]interface{} { - return maps.ToStringMap(l.Get(key)) -} - -// GetStringMapString returns the value associated with the key as a map of strings. -func (l *Language) GetStringMapString(key string) map[string]string { - return cast.ToStringMapString(l.Get(key)) -} - -// GetStringSlice returns the value associated with the key as a slice of strings. -func (l *Language) GetStringSlice(key string) []string { - return cast.ToStringSlice(l.Get(key)) -} - -// Get returns a value associated with the key relying on specified language. -// Get is case-insensitive for a key. -// -// Get returns an interface. For a specific value use one of the Get____ methods. -func (l *Language) Get(key string) interface{} { - local := l.GetLocal(key) - if local != nil { - return local - } - return l.Cfg.Get(key) -} - // GetLocal gets a configuration value set on language level. It will // not fall back to any global value. // It will return nil if a value with the given key cannot be found. @@ -228,31 +196,29 @@ func (l *Language) GetLocal(key string) interface{} { } key = strings.ToLower(key) if !globalOnlySettings[key] { - if v, ok := l.settings[key]; ok { - return v - } + return l.LocalCfg.Get(key) } return nil } -// Set sets the value for the key in the language's params. -func (l *Language) Set(key string, value interface{}) { - if l == nil { - panic("language not set") +func (l *Language) Set(k string, v interface{}) { + k = strings.ToLower(k) + if globalOnlySettings[k] { + return } - key = strings.ToLower(key) - l.settings[key] = value + l.Provider.Set(k, v) +} + +// Merge is currently not supported for Language. +func (l *Language) Merge(key string, value interface{}) { + panic("Not supported") } // IsSet checks whether the key is set in the language or the related config store. func (l *Language) IsSet(key string) bool { - key = strings.ToLower(key) - key = strings.ToLower(key) if !globalOnlySettings[key] { - if _, ok := l.settings[key]; ok { - return true - } + return l.Provider.IsSet(key) } return l.Cfg.IsSet(key) } diff --git a/langs/language_test.go b/langs/language_test.go index 97abe77cc..8557d781a 100644 --- a/langs/language_test.go +++ b/langs/language_test.go @@ -16,13 +16,14 @@ package langs import ( "testing" + "github.com/gohugoio/hugo/config" + qt "github.com/frankban/quicktest" - "github.com/spf13/viper" ) func TestGetGlobalOnlySetting(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() v.Set("defaultContentLanguageInSubdir", true) v.Set("contentDir", "content") v.Set("paginatePath", "page") @@ -37,7 +38,7 @@ func TestGetGlobalOnlySetting(t *testing.T) { func TestLanguageParams(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() v.Set("p1", "p1cfg") v.Set("contentDir", "content") diff --git a/markup/asciidocext/convert_test.go b/markup/asciidocext/convert_test.go index 4c183f7bb..14110bb04 100644 --- a/markup/asciidocext/convert_test.go +++ b/markup/asciidocext/convert_test.go @@ -22,17 +22,17 @@ import ( "testing" "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/markup/tableofcontents" - "github.com/spf13/viper" qt "github.com/frankban/quicktest" ) func TestAsciidoctorDefaultArgs(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() mconf := markup_config.Default p, err := Provider.New( @@ -57,7 +57,7 @@ func TestAsciidoctorDefaultArgs(t *testing.T) { func TestAsciidoctorNonDefaultArgs(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() mconf := markup_config.Default mconf.AsciidocExt.Backend = "manpage" mconf.AsciidocExt.NoHeaderOrFooter = false @@ -88,7 +88,7 @@ func TestAsciidoctorNonDefaultArgs(t *testing.T) { func TestAsciidoctorDisallowedArgs(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() mconf := markup_config.Default mconf.AsciidocExt.Backend = "disallowed-backend" mconf.AsciidocExt.Extensions = []string{"./disallowed-extension"} @@ -117,7 +117,7 @@ func TestAsciidoctorDisallowedArgs(t *testing.T) { func TestAsciidoctorArbitraryExtension(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() mconf := markup_config.Default mconf.AsciidocExt.Extensions = []string{"arbitrary-extension"} p, err := Provider.New( @@ -142,7 +142,7 @@ func TestAsciidoctorArbitraryExtension(t *testing.T) { func TestAsciidoctorDisallowedExtension(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() for _, disallowedExtension := range []string{ `foo-bar//`, `foo-bar\\ `, @@ -177,7 +177,7 @@ func TestAsciidoctorDisallowedExtension(t *testing.T) { func TestAsciidoctorWorkingFolderCurrent(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() mconf := markup_config.Default mconf.AsciidocExt.WorkingFolderCurrent = true mconf.AsciidocExt.Trace = false @@ -208,7 +208,7 @@ func TestAsciidoctorWorkingFolderCurrent(t *testing.T) { func TestAsciidoctorWorkingFolderCurrentAndExtensions(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() mconf := markup_config.Default mconf.AsciidocExt.NoHeaderOrFooter = true mconf.AsciidocExt.Extensions = []string{"asciidoctor-html5s", "asciidoctor-diagram"} @@ -247,7 +247,7 @@ func TestAsciidoctorWorkingFolderCurrentAndExtensions(t *testing.T) { func TestAsciidoctorAttributes(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() mconf := markup_config.Default mconf.AsciidocExt.Attributes = map[string]string{"my-base-url": "https://gohugo.io/", "my-attribute-name": "my value"} mconf.AsciidocExt.Trace = false diff --git a/markup/blackfriday/convert_test.go b/markup/blackfriday/convert_test.go index 414905a76..5b1c1a71f 100644 --- a/markup/blackfriday/convert_test.go +++ b/markup/blackfriday/convert_test.go @@ -16,7 +16,7 @@ package blackfriday import ( "testing" - "github.com/spf13/viper" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/markup/converter" @@ -140,7 +140,7 @@ func TestGetAllFlags(t *testing.T) { func TestConvert(t *testing.T) { c := qt.New(t) p, err := Provider.New(converter.ProviderConfig{ - Cfg: viper.New(), + Cfg: config.New(), }) c.Assert(err, qt.IsNil) conv, err := p.New(converter.DocumentContext{}) @@ -153,7 +153,7 @@ func TestConvert(t *testing.T) { func TestGetHTMLRendererAnchors(t *testing.T) { c := qt.New(t) p, err := Provider.New(converter.ProviderConfig{ - Cfg: viper.New(), + Cfg: config.New(), }) c.Assert(err, qt.IsNil) conv, err := p.New(converter.DocumentContext{ diff --git a/markup/highlight/config_test.go b/markup/highlight/config_test.go index 3c3747622..ab92ecf36 100644 --- a/markup/highlight/config_test.go +++ b/markup/highlight/config_test.go @@ -17,16 +17,15 @@ package highlight import ( "testing" - "github.com/spf13/viper" - qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/config" ) func TestConfig(t *testing.T) { c := qt.New(t) c.Run("applyLegacyConfig", func(c *qt.C) { - v := viper.New() + v := config.New() v.Set("pygmentsStyle", "hugo") v.Set("pygmentsUseClasses", false) v.Set("pygmentsCodeFences", false) diff --git a/markup/markup_config/config.go b/markup/markup_config/config.go index a3562cd24..d4a101709 100644 --- a/markup/markup_config/config.go +++ b/markup/markup_config/config.go @@ -24,7 +24,6 @@ import ( "github.com/gohugoio/hugo/markup/tableofcontents" "github.com/gohugoio/hugo/parser" "github.com/mitchellh/mapstructure" - "github.com/spf13/cast" ) type Config struct { @@ -73,7 +72,7 @@ func normalizeConfig(m map[string]interface{}) { if err != nil { return } - vm := cast.ToStringMap(v) + vm := maps.ToStringMap(v) // Changed from a bool in 0.81.0 if vv, found := vm["attribute"]; found { if vvb, ok := vv.(bool); ok { diff --git a/markup/markup_config/config_test.go b/markup/markup_config/config_test.go index 4a1f1232b..08d7b5995 100644 --- a/markup/markup_config/config_test.go +++ b/markup/markup_config/config_test.go @@ -16,7 +16,7 @@ package markup_config import ( "testing" - "github.com/spf13/viper" + "github.com/gohugoio/hugo/config" qt "github.com/frankban/quicktest" ) @@ -26,7 +26,7 @@ func TestConfig(t *testing.T) { c.Run("Decode", func(c *qt.C) { c.Parallel() - v := viper.New() + v := config.New() v.Set("markup", map[string]interface{}{ "goldmark": map[string]interface{}{ @@ -55,7 +55,7 @@ func TestConfig(t *testing.T) { c.Run("legacy", func(c *qt.C) { c.Parallel() - v := viper.New() + v := config.New() v.Set("blackfriday", map[string]interface{}{ "angledQuotes": true, diff --git a/markup/markup_test.go b/markup/markup_test.go index 6e7fe2059..71d39075d 100644 --- a/markup/markup_test.go +++ b/markup/markup_test.go @@ -16,17 +16,15 @@ package markup import ( "testing" - "github.com/spf13/viper" - - "github.com/gohugoio/hugo/markup/converter" - qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/markup/converter" ) func TestConverterRegistry(t *testing.T) { c := qt.New(t) - r, err := NewConverterProvider(converter.ProviderConfig{Cfg: viper.New()}) + r, err := NewConverterProvider(converter.ProviderConfig{Cfg: config.New()}) c.Assert(err, qt.IsNil) c.Assert("goldmark", qt.Equals, r.GetMarkupConfig().DefaultMarkdownHandler) diff --git a/markup/mmark/convert_test.go b/markup/mmark/convert_test.go index 01d3e8cbb..414e023ab 100644 --- a/markup/mmark/convert_test.go +++ b/markup/mmark/convert_test.go @@ -16,7 +16,7 @@ package mmark import ( "testing" - "github.com/spf13/viper" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/common/loggers" @@ -62,7 +62,7 @@ func TestGetMmarkExtensions(t *testing.T) { func TestConvert(t *testing.T) { c := qt.New(t) - p, err := Provider.New(converter.ProviderConfig{Cfg: viper.New(), Logger: loggers.NewErrorLogger()}) + p, err := Provider.New(converter.ProviderConfig{Cfg: config.New(), Logger: loggers.NewErrorLogger()}) c.Assert(err, qt.IsNil) conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) diff --git a/markup/org/convert_test.go b/markup/org/convert_test.go index 96c388457..e3676fc34 100644 --- a/markup/org/convert_test.go +++ b/markup/org/convert_test.go @@ -16,8 +16,9 @@ package org import ( "testing" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/common/loggers" - "github.com/spf13/viper" "github.com/gohugoio/hugo/markup/converter" @@ -28,7 +29,7 @@ func TestConvert(t *testing.T) { c := qt.New(t) p, err := Provider.New(converter.ProviderConfig{ Logger: loggers.NewErrorLogger(), - Cfg: viper.New(), + Cfg: config.New(), }) c.Assert(err, qt.IsNil) conv, err := p.New(converter.DocumentContext{}) diff --git a/media/mediaType.go b/media/mediaType.go index 21a91524d..817ea1ba8 100644 --- a/media/mediaType.go +++ b/media/mediaType.go @@ -385,8 +385,8 @@ func DecodeTypes(mms ...map[string]interface{}) (Types, error) { return m, err } - vm := v.(map[string]interface{}) - maps.ToLower(vm) + vm := maps.ToStringMap(v) + maps.PrepareParams(vm) _, delimiterSet := vm["delimiter"] _, suffixSet := vm["suffix"] diff --git a/minifiers/config.go b/minifiers/config.go index fc707ce37..675e5d2b8 100644 --- a/minifiers/config.go +++ b/minifiers/config.go @@ -99,10 +99,10 @@ func decodeConfig(cfg config.Provider) (conf minifyConfig, err error) { // Handle upstream renames. if td, found := m["tdewolff"]; found { - tdm := cast.ToStringMap(td) + tdm := maps.ToStringMap(td) for _, key := range []string{"css", "svg"} { if v, found := tdm[key]; found { - vm := cast.ToStringMap(v) + vm := maps.ToStringMap(v) if vv, found := vm["decimal"]; found { vvi := cast.ToInt(vv) if vvi > 0 { diff --git a/minifiers/config_test.go b/minifiers/config_test.go index 851863842..fca56b7f1 100644 --- a/minifiers/config_test.go +++ b/minifiers/config_test.go @@ -16,14 +16,14 @@ package minifiers import ( "testing" - "github.com/spf13/viper" + "github.com/gohugoio/hugo/config" qt "github.com/frankban/quicktest" ) func TestConfig(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() v.Set("minify", map[string]interface{}{ "disablexml": true, @@ -53,7 +53,7 @@ func TestConfig(t *testing.T) { func TestConfigLegacy(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() // This was a bool < Hugo v0.58. v.Set("minify", true) diff --git a/minifiers/minifiers_test.go b/minifiers/minifiers_test.go index 97fe4e465..37e017420 100644 --- a/minifiers/minifiers_test.go +++ b/minifiers/minifiers_test.go @@ -19,16 +19,15 @@ import ( "strings" "testing" - "github.com/gohugoio/hugo/media" - qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/output" - "github.com/spf13/viper" ) func TestNew(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() m, _ := New(media.DefaultTypes, output.DefaultFormats, v) var rawJS string @@ -76,7 +75,7 @@ func TestNew(t *testing.T) { func TestConfigureMinify(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() v.Set("minify", map[string]interface{}{ "disablexml": true, "tdewolff": map[string]interface{}{ @@ -110,7 +109,7 @@ func TestConfigureMinify(t *testing.T) { func TestJSONRoundTrip(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() m, _ := New(media.DefaultTypes, output.DefaultFormats, v) for _, test := range []string{`{ @@ -148,7 +147,7 @@ func TestJSONRoundTrip(t *testing.T) { func TestBugs(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() m, _ := New(media.DefaultTypes, output.DefaultFormats, v) for _, test := range []struct { @@ -171,7 +170,7 @@ func TestBugs(t *testing.T) { // Renamed to Precision in v2.7.0. Check that we support both. func TestDecodeConfigDecimalIsNowPrecision(t *testing.T) { c := qt.New(t) - v := viper.New() + v := config.New() v.Set("minify", map[string]interface{}{ "disablexml": true, "tdewolff": map[string]interface{}{ diff --git a/modules/collect.go b/modules/collect.go index db79f434e..163eda74a 100644 --- a/modules/collect.go +++ b/modules/collect.go @@ -424,7 +424,7 @@ func (c *collector) applyThemeConfig(tc *moduleAdapter) error { if err != nil { c.logger.Warnf("Failed to read module config for %q in %q: %s", tc.Path(), themeTOML, err) } else { - maps.ToLower(themeCfg) + maps.PrepareParams(themeCfg) } } diff --git a/modules/npm/package_builder.go b/modules/npm/package_builder.go index fe5d19832..a073b3d8f 100644 --- a/modules/npm/package_builder.go +++ b/modules/npm/package_builder.go @@ -29,7 +29,7 @@ import ( "github.com/gohugoio/hugo/hugofs" "github.com/spf13/afero" - "github.com/spf13/cast" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/helpers" ) @@ -122,7 +122,7 @@ func Pack(fs afero.Fs, fis []hugofs.FileMetaInfo) error { var commentsm map[string]interface{} comments, found := b.originalPackageJSON["comments"] if found { - commentsm = cast.ToStringMap(comments) + commentsm = maps.ToStringMap(comments) } else { commentsm = make(map[string]interface{}) } @@ -205,7 +205,7 @@ func (b *packageBuilder) addm(source string, m map[string]interface{}) { // These packages will be added by order of import (project, module1, module2...), // so that should at least give the project control over the situation. if devDeps, found := m[devDependenciesKey]; found { - mm := cast.ToStringMapString(devDeps) + mm := maps.ToStringMapString(devDeps) for k, v := range mm { if _, added := b.devDependencies[k]; !added { b.devDependencies[k] = v @@ -215,7 +215,7 @@ func (b *packageBuilder) addm(source string, m map[string]interface{}) { } if deps, found := m[dependenciesKey]; found { - mm := cast.ToStringMapString(deps) + mm := maps.ToStringMapString(deps) for k, v := range mm { if _, added := b.dependencies[k]; !added { b.dependencies[k] = v diff --git a/output/outputFormat.go b/output/outputFormat.go index 9a081121a..a52f43c8b 100644 --- a/output/outputFormat.go +++ b/output/outputFormat.go @@ -368,7 +368,11 @@ func decode(mediaTypes media.Types, input interface{}, output *Format) error { return err } - return decoder.Decode(input) + if err = decoder.Decode(input); err != nil { + return errors.Wrap(err, "failed to decode output format configuration") + } + + return nil } diff --git a/publisher/htmlElementsCollector_test.go b/publisher/htmlElementsCollector_test.go index 2400b1612..9ede80095 100644 --- a/publisher/htmlElementsCollector_test.go +++ b/publisher/htmlElementsCollector_test.go @@ -22,12 +22,13 @@ import ( "testing" "time" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/minifiers" "github.com/gohugoio/hugo/output" qt "github.com/frankban/quicktest" - "github.com/spf13/viper" ) func TestClassCollector(t *testing.T) { @@ -138,7 +139,7 @@ func TestClassCollector(t *testing.T) { if skipMinifyTest[test.name] { c.Skip("skip minify test") } - v := viper.New() + v := config.New() m, _ := minifiers.New(media.DefaultTypes, output.DefaultFormats, v) m.Minify(media.HTMLType, w, strings.NewReader(test.html)) diff --git a/related/inverted_index.go b/related/inverted_index.go index 2e0ea295d..329a8c998 100644 --- a/related/inverted_index.go +++ b/related/inverted_index.go @@ -22,6 +22,8 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/common/types" "github.com/mitchellh/mapstructure" ) @@ -404,16 +406,11 @@ func norm(num, min, max int) int { } // DecodeConfig decodes a slice of map into Config. -func DecodeConfig(in interface{}) (Config, error) { - if in == nil { +func DecodeConfig(m maps.Params) (Config, error) { + if m == nil { return Config{}, errors.New("no related config provided") } - m, ok := in.(map[string]interface{}) - if !ok { - return Config{}, fmt.Errorf("expected map[string]interface {} got %T", in) - } - if len(m) == 0 { return Config{}, errors.New("empty related config provided") } diff --git a/resources/page/pagemeta/page_frontmatter_test.go b/resources/page/pagemeta/page_frontmatter_test.go index 7c4c77a91..6e3833b0f 100644 --- a/resources/page/pagemeta/page_frontmatter_test.go +++ b/resources/page/pagemeta/page_frontmatter_test.go @@ -18,8 +18,9 @@ import ( "testing" "time" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/resources/resource" - "github.com/spf13/viper" qt "github.com/frankban/quicktest" ) @@ -72,7 +73,7 @@ func newTestFd() *FrontMatterDescriptor { func TestFrontMatterNewConfig(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() cfg.Set("frontmatter", map[string]interface{}{ "date": []string{"publishDate", "LastMod"}, @@ -89,7 +90,7 @@ func TestFrontMatterNewConfig(t *testing.T) { c.Assert(fc.publishDate, qt.DeepEquals, []string{"date"}) // Default - cfg = viper.New() + cfg = config.New() fc, err = newFrontmatterConfig(cfg) c.Assert(err, qt.IsNil) c.Assert(fc.date, qt.DeepEquals, []string{"date", "publishdate", "pubdate", "published", "lastmod", "modified"}) @@ -117,7 +118,7 @@ func TestFrontMatterDatesHandlers(t *testing.T) { for _, handlerID := range []string{":filename", ":fileModTime", ":git"} { - cfg := viper.New() + cfg := config.New() cfg.Set("frontmatter", map[string]interface{}{ "date": []string{handlerID, "date"}, @@ -157,7 +158,7 @@ func TestFrontMatterDatesCustomConfig(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() cfg.Set("frontmatter", map[string]interface{}{ "date": []string{"mydate"}, "lastmod": []string{"publishdate"}, @@ -204,7 +205,7 @@ func TestFrontMatterDatesDefaultKeyword(t *testing.T) { c := qt.New(t) - cfg := viper.New() + cfg := config.New() cfg.Set("frontmatter", map[string]interface{}{ "date": []string{"mydate", ":default"}, diff --git a/resources/page/pagination_test.go b/resources/page/pagination_test.go index 8d4f857d7..07ad6233b 100644 --- a/resources/page/pagination_test.go +++ b/resources/page/pagination_test.go @@ -18,7 +18,7 @@ import ( "html/template" "testing" - "github.com/spf13/viper" + "github.com/gohugoio/hugo/config" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/output" @@ -196,7 +196,7 @@ func doTestPagerNoPages(t *testing.T, paginator *Paginator) { func TestPaginationURLFactory(t *testing.T) { t.Parallel() c := qt.New(t) - cfg := viper.New() + cfg := config.New() cfg.Set("paginatePath", "zoo") for _, uglyURLs := range []bool{false, true} { diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index 187930461..1a0a6586a 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -29,7 +29,7 @@ import ( "github.com/bep/gitmap" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/resources/resource" - "github.com/spf13/viper" + "github.com/gohugoio/hugo/navigation" @@ -69,7 +69,7 @@ func newTestPageWithFile(filename string) *testPage { } func newTestPathSpec() *helpers.PathSpec { - return newTestPathSpecFor(viper.New()) + return newTestPathSpecFor(config.New()) } func newTestPathSpecFor(cfg config.Provider) *helpers.PathSpec { diff --git a/resources/resource_metadata.go b/resources/resource_metadata.go index a0c232c67..0dd0945e3 100644 --- a/resources/resource_metadata.go +++ b/resources/resource_metadata.go @@ -130,7 +130,7 @@ func AssignMetadata(metadata []map[string]interface{}, resources ...resource.Res if found { m := maps.ToStringMap(params) // Needed for case insensitive fetching of params values - maps.ToLower(m) + maps.PrepareParams(m) ma.updateParams(m) } } diff --git a/resources/resource_transformers/htesting/testhelpers.go b/resources/resource_transformers/htesting/testhelpers.go index 8eacf7da4..21333eccb 100644 --- a/resources/resource_transformers/htesting/testhelpers.go +++ b/resources/resource_transformers/htesting/testhelpers.go @@ -17,17 +17,17 @@ import ( "path/filepath" "github.com/gohugoio/hugo/cache/filecache" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/resources" "github.com/spf13/afero" - "github.com/spf13/viper" ) func NewTestResourceSpec() (*resources.Spec, error) { - cfg := viper.New() + cfg := config.New() cfg.Set("baseURL", "https://example.org") cfg.Set("publishDir", "public") diff --git a/resources/resource_transformers/js/options.go b/resources/resource_transformers/js/options.go index ceed22f34..84f571f17 100644 --- a/resources/resource_transformers/js/options.go +++ b/resources/resource_transformers/js/options.go @@ -20,9 +20,9 @@ import ( "path/filepath" "strings" - "github.com/spf13/afero" - + "github.com/gohugoio/hugo/common/maps" "github.com/pkg/errors" + "github.com/spf13/afero" "github.com/evanw/esbuild/pkg/api" @@ -30,7 +30,6 @@ import ( "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/media" "github.com/mitchellh/mapstructure" - "github.com/spf13/cast" ) const ( @@ -348,7 +347,7 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) { var defines map[string]string if opts.Defines != nil { - defines = cast.ToStringMapString(opts.Defines) + defines = maps.ToStringMapString(opts.Defines) } // By default we only need to specify outDir and no outFile diff --git a/resources/testhelpers_test.go b/resources/testhelpers_test.go index 32e4213ea..12dc8efe8 100644 --- a/resources/testhelpers_test.go +++ b/resources/testhelpers_test.go @@ -10,6 +10,7 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/modules" @@ -22,7 +23,6 @@ import ( "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" "github.com/spf13/afero" - "github.com/spf13/viper" ) type specDescriptor struct { @@ -31,8 +31,8 @@ type specDescriptor struct { fs afero.Fs } -func createTestCfg() *viper.Viper { - cfg := viper.New() +func createTestCfg() config.Provider { + cfg := config.New() cfg.Set("resourceDir", "resources") cfg.Set("contentDir", "content") cfg.Set("dataDir", "data") diff --git a/source/filesystem_test.go b/source/filesystem_test.go index 5dc187978..e6dc9ce16 100644 --- a/source/filesystem_test.go +++ b/source/filesystem_test.go @@ -19,6 +19,8 @@ import ( "runtime" "testing" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/langs" @@ -28,8 +30,6 @@ import ( qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" - - "github.com/spf13/viper" ) func TestEmptySourceFilesystem(t *testing.T) { @@ -76,8 +76,8 @@ func TestUnicodeNorm(t *testing.T) { } } -func newTestConfig() *viper.Viper { - v := viper.New() +func newTestConfig() config.Provider { + v := config.New() v.Set("contentDir", "content") v.Set("dataDir", "data") v.Set("i18nDir", "i18n") diff --git a/tpl/cast/docshelper.go b/tpl/cast/docshelper.go index 035db1740..a3cc26de5 100644 --- a/tpl/cast/docshelper.go +++ b/tpl/cast/docshelper.go @@ -15,18 +15,18 @@ package cast import ( "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/docshelper" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/tpl/internal" - "github.com/spf13/viper" ) // This file provides documentation support and is randomly put into this package. func init() { docsProvider := func() docshelper.DocProvider { d := &deps.Deps{ - Cfg: viper.New(), + Cfg: config.New(), Log: loggers.NewErrorLogger(), BuildStartListeners: &deps.Listeners{}, Site: page.NewDummyHugoSite(newTestConfig()), @@ -46,8 +46,8 @@ func init() { docshelper.AddDocProviderFunc(docsProvider) } -func newTestConfig() *viper.Viper { - v := viper.New() +func newTestConfig() config.Provider { + v := config.New() v.Set("contentDir", "content") return v } diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go index 1e0569751..3faf46930 100644 --- a/tpl/collections/collections_test.go +++ b/tpl/collections/collections_test.go @@ -32,7 +32,7 @@ import ( "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/langs" "github.com/spf13/afero" - "github.com/spf13/viper" + ) type tstNoStringer struct{} @@ -986,7 +986,7 @@ func newDeps(cfg config.Provider) *deps.Deps { } func newTestNs() *Namespace { - v := viper.New() + v := config.New() v.Set("contentDir", "content") return New(newDeps(v)) } diff --git a/tpl/data/init_test.go b/tpl/data/init_test.go index fedce8e5c..9174d42a6 100644 --- a/tpl/data/init_test.go +++ b/tpl/data/init_test.go @@ -17,10 +17,10 @@ import ( "testing" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/htesting/hqt" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/tpl/internal" - "github.com/spf13/viper" ) func TestInit(t *testing.T) { @@ -28,7 +28,7 @@ func TestInit(t *testing.T) { var found bool var ns *internal.TemplateFuncsNamespace - v := viper.New() + v := config.New() v.Set("contentDir", "content") langs.LoadLanguageSettings(v, nil) diff --git a/tpl/data/resources_test.go b/tpl/data/resources_test.go index 18061c052..1bf6d769f 100644 --- a/tpl/data/resources_test.go +++ b/tpl/data/resources_test.go @@ -34,12 +34,12 @@ import ( "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/langs" "github.com/spf13/afero" - "github.com/spf13/viper" + ) func TestScpGetLocal(t *testing.T) { t.Parallel() - v := viper.New() + v := config.New() fs := hugofs.NewMem(v) ps := helpers.FilePathSeparator @@ -144,7 +144,7 @@ func TestScpGetRemoteParallel(t *testing.T) { c.Assert(err, qt.IsNil) for _, ignoreCache := range []bool{false} { - cfg := viper.New() + cfg := config.New() cfg.Set("ignoreCache", ignoreCache) cfg.Set("contentDir", "content") @@ -223,7 +223,7 @@ func newDeps(cfg config.Provider) *deps.Deps { } func newTestNs() *Namespace { - v := viper.New() + v := config.New() v.Set("contentDir", "content") return New(newDeps(v)) } diff --git a/tpl/encoding/encoding.go b/tpl/encoding/encoding.go index 09e2b94bc..ce25151a0 100644 --- a/tpl/encoding/encoding.go +++ b/tpl/encoding/encoding.go @@ -20,6 +20,7 @@ import ( "errors" "html/template" + "github.com/gohugoio/hugo/common/maps" "github.com/spf13/cast" ) @@ -71,7 +72,7 @@ func (ns *Namespace) Jsonify(args ...interface{}) (template.HTML, error) { case 2: var opts map[string]string - opts, err = cast.ToStringMapStringE(args[0]) + opts, err = maps.ToStringMapStringE(args[0]) if err != nil { break } diff --git a/tpl/hugo/init_test.go b/tpl/hugo/init_test.go index c94a883fd..9b1b14be4 100644 --- a/tpl/hugo/init_test.go +++ b/tpl/hugo/init_test.go @@ -16,20 +16,21 @@ package hugo import ( "testing" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/htesting/hqt" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/tpl/internal" - "github.com/spf13/viper" ) func TestInit(t *testing.T) { c := qt.New(t) var found bool var ns *internal.TemplateFuncsNamespace - v := viper.New() + v := config.New() v.Set("contentDir", "content") s := page.NewDummyHugoSite(v) diff --git a/tpl/images/images_test.go b/tpl/images/images_test.go index b1b1e1cfd..2a2b79478 100644 --- a/tpl/images/images_test.go +++ b/tpl/images/images_test.go @@ -22,11 +22,11 @@ import ( "testing" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" "github.com/spf13/afero" "github.com/spf13/cast" - "github.com/spf13/viper" ) type tstNoStringer struct{} @@ -82,7 +82,7 @@ func TestNSConfig(t *testing.T) { t.Parallel() c := qt.New(t) - v := viper.New() + v := config.New() v.Set("workingDir", "/a/b") ns := New(&deps.Deps{Fs: hugofs.NewMem(v)}) diff --git a/tpl/os/os_test.go b/tpl/os/os_test.go index 3adb6f8c2..bbc0d018c 100644 --- a/tpl/os/os_test.go +++ b/tpl/os/os_test.go @@ -17,11 +17,12 @@ import ( "path/filepath" "testing" + "github.com/gohugoio/hugo/config" + qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" "github.com/spf13/afero" - "github.com/spf13/viper" ) func TestReadFile(t *testing.T) { @@ -30,7 +31,7 @@ func TestReadFile(t *testing.T) { workingDir := "/home/hugo" - v := viper.New() + v := config.New() v.Set("workingDir", workingDir) // f := newTestFuncsterWithViper(v) @@ -68,7 +69,7 @@ func TestFileExists(t *testing.T) { workingDir := "/home/hugo" - v := viper.New() + v := config.New() v.Set("workingDir", workingDir) ns := New(&deps.Deps{Fs: hugofs.NewMem(v)}) @@ -103,7 +104,7 @@ func TestStat(t *testing.T) { c := qt.New(t) workingDir := "/home/hugo" - v := viper.New() + v := config.New() v.Set("workingDir", workingDir) ns := New(&deps.Deps{Fs: hugofs.NewMem(v)}) diff --git a/tpl/path/path_test.go b/tpl/path/path_test.go index ce453b9a1..dc0761f2f 100644 --- a/tpl/path/path_test.go +++ b/tpl/path/path_test.go @@ -18,11 +18,11 @@ import ( "testing" qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" - "github.com/spf13/viper" ) -var ns = New(&deps.Deps{Cfg: viper.New()}) +var ns = New(&deps.Deps{Cfg: config.New()}) type tstNoStringer struct{} diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go index da2b4ca3b..850def00e 100644 --- a/tpl/resources/resources.go +++ b/tpl/resources/resources.go @@ -282,7 +282,7 @@ func (ns *Namespace) ToCSS(args ...interface{}) (resource.Resource, error) { } if m != nil { - maps.ToLower(m) + maps.PrepareParams(m) if t, found := m["transpiler"]; found { switch t { case transpilerDart, transpilerLibSass: diff --git a/tpl/site/init_test.go b/tpl/site/init_test.go index f4a7935ad..f4c2ecd5c 100644 --- a/tpl/site/init_test.go +++ b/tpl/site/init_test.go @@ -16,12 +16,13 @@ package site import ( "testing" + "github.com/gohugoio/hugo/config" + qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/htesting/hqt" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/tpl/internal" - "github.com/spf13/viper" ) func TestInit(t *testing.T) { @@ -29,7 +30,7 @@ func TestInit(t *testing.T) { var found bool var ns *internal.TemplateFuncsNamespace - v := viper.New() + v := config.New() v.Set("contentDir", "content") s := page.NewDummyHugoSite(v) diff --git a/tpl/strings/init_test.go b/tpl/strings/init_test.go index b356896cf..dd15418c8 100644 --- a/tpl/strings/init_test.go +++ b/tpl/strings/init_test.go @@ -16,12 +16,13 @@ package strings import ( "testing" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/htesting/hqt" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/tpl/internal" - "github.com/spf13/viper" ) func TestInit(t *testing.T) { @@ -30,7 +31,7 @@ func TestInit(t *testing.T) { var ns *internal.TemplateFuncsNamespace for _, nsf := range internal.TemplateFuncsNamespaceRegistry { - ns = nsf(&deps.Deps{Cfg: viper.New()}) + ns = nsf(&deps.Deps{Cfg: config.New()}) if ns.Name == name { found = true break diff --git a/tpl/strings/strings_test.go b/tpl/strings/strings_test.go index 6e14a408c..18c033793 100644 --- a/tpl/strings/strings_test.go +++ b/tpl/strings/strings_test.go @@ -17,14 +17,14 @@ import ( "html/template" "testing" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" qt "github.com/frankban/quicktest" "github.com/spf13/cast" - "github.com/spf13/viper" ) -var ns = New(&deps.Deps{Cfg: viper.New()}) +var ns = New(&deps.Deps{Cfg: config.New()}) type tstNoStringer struct{} diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go index 67e957924..6ddf13b76 100644 --- a/tpl/tplimpl/template_funcs_test.go +++ b/tpl/tplimpl/template_funcs_test.go @@ -37,13 +37,13 @@ import ( "github.com/gohugoio/hugo/tpl/internal" "github.com/gohugoio/hugo/tpl/partials" "github.com/spf13/afero" - "github.com/spf13/viper" + ) var logger = loggers.NewErrorLogger() func newTestConfig() config.Provider { - v := viper.New() + v := config.New() v.Set("contentDir", "content") v.Set("dataDir", "data") v.Set("i18nDir", "i18n") @@ -206,7 +206,7 @@ func BenchmarkPartialCached(b *testing.B) { func doBenchmarkPartial(b *testing.B, f func(ns *partials.Namespace) error) { c := qt.New(b) - config := newDepsConfig(viper.New()) + config := newDepsConfig(config.New()) config.WithTemplate = func(templ tpl.TemplateManager) error { err := templ.AddTemplate("partials/bench1", `{{ shuffle (seq 1 10) }}`) if err != nil { diff --git a/tpl/transform/remarshal_test.go b/tpl/transform/remarshal_test.go index eb5f4253c..9f3e05e61 100644 --- a/tpl/transform/remarshal_test.go +++ b/tpl/transform/remarshal_test.go @@ -16,16 +16,16 @@ package transform import ( "testing" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/htesting" qt "github.com/frankban/quicktest" - "github.com/spf13/viper" ) func TestRemarshal(t *testing.T) { t.Parallel() - v := viper.New() + v := config.New() v.Set("contentDir", "content") ns := New(newDeps(v)) c := qt.New(t) @@ -112,7 +112,7 @@ title: Test Metadata func TestRemarshalComments(t *testing.T) { t.Parallel() - v := viper.New() + v := config.New() v.Set("contentDir", "content") ns := New(newDeps(v)) @@ -158,7 +158,7 @@ func TestTestRemarshalError(t *testing.T) { t.Parallel() c := qt.New(t) - v := viper.New() + v := config.New() v.Set("contentDir", "content") ns := New(newDeps(v)) @@ -172,7 +172,7 @@ func TestTestRemarshalError(t *testing.T) { func TestTestRemarshalMapInput(t *testing.T) { t.Parallel() c := qt.New(t) - v := viper.New() + v := config.New() v.Set("contentDir", "content") ns := New(newDeps(v)) diff --git a/tpl/transform/transform_test.go b/tpl/transform/transform_test.go index b98d82d27..1dbf97f98 100644 --- a/tpl/transform/transform_test.go +++ b/tpl/transform/transform_test.go @@ -26,7 +26,7 @@ import ( "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/langs" - "github.com/spf13/viper" + ) type tstNoStringer struct{} @@ -35,7 +35,7 @@ func TestEmojify(t *testing.T) { t.Parallel() c := qt.New(t) - v := viper.New() + v := config.New() ns := New(newDeps(v)) for _, test := range []struct { @@ -64,7 +64,7 @@ func TestHighlight(t *testing.T) { t.Parallel() c := qt.New(t) - v := viper.New() + v := config.New() v.Set("contentDir", "content") ns := New(newDeps(v)) @@ -96,7 +96,7 @@ func TestHTMLEscape(t *testing.T) { t.Parallel() c := qt.New(t) - v := viper.New() + v := config.New() v.Set("contentDir", "content") ns := New(newDeps(v)) @@ -126,7 +126,7 @@ func TestHTMLUnescape(t *testing.T) { t.Parallel() c := qt.New(t) - v := viper.New() + v := config.New() v.Set("contentDir", "content") ns := New(newDeps(v)) @@ -156,7 +156,7 @@ func TestMarkdownify(t *testing.T) { t.Parallel() c := qt.New(t) - v := viper.New() + v := config.New() v.Set("contentDir", "content") ns := New(newDeps(v)) @@ -185,7 +185,7 @@ func TestMarkdownify(t *testing.T) { func TestMarkdownifyBlocksOfText(t *testing.T) { t.Parallel() c := qt.New(t) - v := viper.New() + v := config.New() v.Set("contentDir", "content") ns := New(newDeps(v)) @@ -211,7 +211,7 @@ func TestPlainify(t *testing.T) { t.Parallel() c := qt.New(t) - v := viper.New() + v := config.New() ns := New(newDeps(v)) for _, test := range []struct { diff --git a/tpl/transform/unmarshal_test.go b/tpl/transform/unmarshal_test.go index ec81c316a..85e3610d1 100644 --- a/tpl/transform/unmarshal_test.go +++ b/tpl/transform/unmarshal_test.go @@ -19,13 +19,14 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/media" qt "github.com/frankban/quicktest" - "github.com/spf13/viper" ) const ( @@ -79,7 +80,7 @@ func (t testContentResource) Key() string { } func TestUnmarshal(t *testing.T) { - v := viper.New() + v := config.New() ns := New(newDeps(v)) c := qt.New(t) @@ -173,7 +174,7 @@ a;b;c`, mime: media.CSVType}, map[string]interface{}{"DElimiter": ";", "Comment" } func BenchmarkUnmarshalString(b *testing.B) { - v := viper.New() + v := config.New() ns := New(newDeps(v)) const numJsons = 100 @@ -196,7 +197,7 @@ func BenchmarkUnmarshalString(b *testing.B) { } func BenchmarkUnmarshalResource(b *testing.B) { - v := viper.New() + v := config.New() ns := New(newDeps(v)) const numJsons = 100 diff --git a/tpl/urls/init_test.go b/tpl/urls/init_test.go index f88aaf398..27b21144a 100644 --- a/tpl/urls/init_test.go +++ b/tpl/urls/init_test.go @@ -16,11 +16,12 @@ package urls import ( "testing" + "github.com/gohugoio/hugo/config" + qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/htesting/hqt" "github.com/gohugoio/hugo/tpl/internal" - "github.com/spf13/viper" ) func TestInit(t *testing.T) { @@ -29,7 +30,7 @@ func TestInit(t *testing.T) { var ns *internal.TemplateFuncsNamespace for _, nsf := range internal.TemplateFuncsNamespaceRegistry { - ns = nsf(&deps.Deps{Cfg: viper.New()}) + ns = nsf(&deps.Deps{Cfg: config.New()}) if ns.Name == name { found = true break diff --git a/tpl/urls/urls_test.go b/tpl/urls/urls_test.go index 9c005d2df..23068c243 100644 --- a/tpl/urls/urls_test.go +++ b/tpl/urls/urls_test.go @@ -17,14 +17,15 @@ import ( "net/url" "testing" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/htesting/hqt" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" - "github.com/spf13/viper" ) -var ns = New(&deps.Deps{Cfg: viper.New()}) +var ns = New(&deps.Deps{Cfg: config.New()}) type tstNoStringer struct{}