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") }}
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{}