diff --git a/commands/import_jekyll.go b/commands/import_jekyll.go index e5c39dc34..b1cc53378 100644 --- a/commands/import_jekyll.go +++ b/commands/import_jekyll.go @@ -30,12 +30,12 @@ import ( "github.com/gohugoio/hugo/parser/metadecoders" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/parser" "github.com/spf13/afero" - "github.com/spf13/cast" "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" ) @@ -420,7 +420,7 @@ func convertJekyllPost(s *hugolib.Site, path, relPath, targetDir string, draft b } func convertJekyllMetaData(m interface{}, postName string, postDate time.Time, draft bool) (interface{}, error) { - metadata, err := cast.ToStringMapE(m) + metadata, err := maps.ToStringMapE(m) if err != nil { return nil, err } @@ -472,7 +472,7 @@ func convertJekyllMetaData(m interface{}, postName string, postDate time.Time, d } func convertJekyllContent(m interface{}, content string) string { - metadata, _ := cast.ToStringMapE(m) + metadata, _ := maps.ToStringMapE(m) lines := strings.Split(content, "\n") var resultLines []string diff --git a/common/maps/maps.go b/common/maps/maps.go index e0d4f964d..8b42ca764 100644 --- a/common/maps/maps.go +++ b/common/maps/maps.go @@ -25,26 +25,45 @@ import ( // recursively. // Notes: // * This will modify the map given. -// * Any nested map[interface{}]interface{} will be converted to map[string]interface{}. -func ToLower(m map[string]interface{}) { +// * 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{}: - v = cast.ToStringMap(v) - ToLower(v.(map[string]interface{})) + var p Params = cast.ToStringMap(v) + v = p + ToLower(p) + retyped = true case map[string]interface{}: - ToLower(v.(map[string]interface{})) + var p Params = v.(map[string]interface{}) + v = p + ToLower(p) + retyped = true } lKey := strings.ToLower(k) - if k != lKey { + if retyped || k != lKey { delete(m, k) m[lKey] = v } - } } +func ToStringMapE(in interface{}) (map[string]interface{}, error) { + switch in.(type) { + case Params: + return in.(Params), nil + default: + return cast.ToStringMapE(in) + } +} + +func ToStringMap(in interface{}) map[string]interface{} { + m, _ := ToStringMapE(in) + return m +} + type keyRename struct { pattern glob.Glob newKey string diff --git a/common/maps/maps_test.go b/common/maps/maps_test.go index 8b0aa5eb9..6e4947adb 100644 --- a/common/maps/maps_test.go +++ b/common/maps/maps_test.go @@ -14,6 +14,7 @@ package maps import ( + "fmt" "reflect" "testing" @@ -21,7 +22,6 @@ import ( ) func TestToLower(t *testing.T) { - tests := []struct { input map[string]interface{} expected map[string]interface{} @@ -30,7 +30,7 @@ func TestToLower(t *testing.T) { map[string]interface{}{ "abC": 32, }, - map[string]interface{}{ + Params{ "abc": 32, }, }, @@ -48,16 +48,16 @@ func TestToLower(t *testing.T) { "J": 25, }, }, - map[string]interface{}{ + Params{ "abc": 32, - "def": map[string]interface{}{ + "def": Params{ "23": "A value", - "24": map[string]interface{}{ + "24": Params{ "abcde": "A value", "efghi": "Another value", }, }, - "ghi": map[string]interface{}{ + "ghi": Params{ "j": 25, }, }, @@ -65,11 +65,13 @@ func TestToLower(t *testing.T) { } for i, test := range tests { - // ToLower modifies input. - ToLower(test.input) - if !reflect.DeepEqual(test.expected, test.input) { - t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input) - } + t.Run(fmt.Sprint(i), func(t *testing.T) { + // ToLower modifies input. + ToLower(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 2d62ad752..ecb63d7a5 100644 --- a/common/maps/params.go +++ b/common/maps/params.go @@ -19,76 +19,89 @@ import ( "github.com/spf13/cast" ) +// Params is a map where all keys are lower case. +type Params map[string]interface{} + +// Get does a lower case and nested search in this map. +// It will return nil if none found. +func (p Params) Get(indices ...string) interface{} { + v, _, _ := getNested(p, indices) + return v +} + +func getNested(m map[string]interface{}, indices []string) (interface{}, string, map[string]interface{}) { + if len(indices) == 0 { + return nil, "", nil + } + + first := indices[0] + v, found := m[strings.ToLower(cast.ToString(first))] + if !found { + return nil, "", nil + } + + if len(indices) == 1 { + return v, first, m + } + + switch m2 := v.(type) { + case Params: + return getNested(m2, indices[1:]) + case map[string]interface{}: + return getNested(m2, indices[1:]) + default: + return nil, "", nil + } +} + // GetNestedParam gets the first match of the keyStr in the candidates given. // It will first try the exact match and then try to find it as a nested map value, // using the given separator, e.g. "mymap.name". // It assumes that all the maps given have lower cased keys. -func GetNestedParam(keyStr, separator string, candidates ...map[string]interface{}) (interface{}, error) { +func GetNestedParam(keyStr, separator string, candidates ...Params) (interface{}, error) { keyStr = strings.ToLower(keyStr) - lookupFn := func(key string) interface{} { - for _, m := range candidates { - if v, ok := m[key]; ok { - return v - } + // Try exact match first + for _, m := range candidates { + if v, ok := m[keyStr]; ok { + return v, nil } - - return nil - } - - v, _, _, err := GetNestedParamFn(keyStr, separator, lookupFn) - return v, err -} - -func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interface{}) (interface{}, string, map[string]interface{}, error) { - result, _ := traverseDirectParams(keyStr, lookupFn) - if result != nil { - return result, keyStr, nil, nil } keySegments := strings.Split(keyStr, separator) - if len(keySegments) == 1 { - return nil, keyStr, nil, nil + for _, m := range candidates { + if v := m.Get(keySegments...); v != nil { + return v, nil + } } - return traverseNestedParams(keySegments, lookupFn) + return nil, nil + } -func traverseDirectParams(keyStr string, lookupFn func(key string) interface{}) (interface{}, error) { - return lookupFn(keyStr), nil -} - -func traverseNestedParams(keySegments []string, lookupFn func(key string) interface{}) (interface{}, string, map[string]interface{}, error) { - firstKey, rest := keySegments[0], keySegments[1:] - result := lookupFn(firstKey) - if result == nil || len(rest) == 0 { - return result, firstKey, nil, nil - } - - switch m := result.(type) { - case map[string]interface{}: - v, key, owner := traverseParams(rest, m) - return v, key, owner, nil - default: +func GetNestedParamFn(keyStr, separator string, lookupFn func(key string) interface{}) (interface{}, string, map[string]interface{}, error) { + keySegments := strings.Split(strings.ToLower(keyStr), separator) + if len(keySegments) == 0 { return nil, "", nil, nil } -} -func traverseParams(keys []string, m map[string]interface{}) (interface{}, string, map[string]interface{}) { - // Shift first element off. - firstKey, rest := keys[0], keys[1:] - result := m[firstKey] - - // No point in continuing here. - if result == nil { - return result, "", nil + first := lookupFn(keySegments[0]) + if first == nil { + return nil, "", nil, nil } - if len(rest) == 0 { - // That was the last key. - return result, firstKey, m + if len(keySegments) == 1 { + return first, keySegments[0], nil, nil } - // That was not the last key. - return traverseParams(rest, cast.ToStringMap(result)) + switch m := first.(type) { + case map[string]interface{}: + v, key, owner := getNested(m, keySegments[1:]) + return v, key, owner, nil + case Params: + v, key, owner := getNested(m, keySegments[1:]) + return v, key, owner, nil + } + + return nil, "", nil, nil } diff --git a/common/maps/params_test.go b/common/maps/params_test.go index 6477de6f4..8016a8bd6 100644 --- a/common/maps/params_test.go +++ b/common/maps/params_test.go @@ -35,7 +35,7 @@ func TestGetNestedParam(t *testing.T) { c := qt.New(t) - must := func(keyStr, separator string, candidates ...map[string]interface{}) interface{} { + must := func(keyStr, separator string, candidates ...Params) interface{} { v, err := GetNestedParam(keyStr, separator, candidates...) c.Assert(err, qt.IsNil) return v diff --git a/common/para/para.go b/common/para/para.go index 319bdb78f..69bfc205b 100644 --- a/common/para/para.go +++ b/common/para/para.go @@ -37,8 +37,8 @@ type Runner interface { type errGroupRunner struct { *errgroup.Group - w *Workers - ctx context.Context + w *Workers + ctx context.Context } func (g *errGroupRunner) Run(fn func() error) { @@ -68,6 +68,6 @@ func (w *Workers) Start(ctx context.Context) (Runner, context.Context) { return &errGroupRunner{ Group: g, ctx: ctx, - w: w, + w: w, }, ctx } diff --git a/common/para/para_test.go b/common/para/para_test.go index 9f33a234c..bda7f5d27 100644 --- a/common/para/para_test.go +++ b/common/para/para_test.go @@ -15,6 +15,7 @@ package para import ( "context" + "runtime" "sort" "sync" "sync/atomic" @@ -25,6 +26,9 @@ import ( ) func TestPara(t *testing.T) { + if runtime.NumCPU() < 4 { + t.Skipf("skip para test, CPU count is %d", runtime.NumCPU()) + } c := qt.New(t) diff --git a/hugolib/case_insensitive_test.go b/hugolib/case_insensitive_test.go index 5c7dbe073..9c2662044 100644 --- a/hugolib/case_insensitive_test.go +++ b/hugolib/case_insensitive_test.go @@ -61,7 +61,7 @@ angledQuotes = false hrefTargetBlank = false [Languages.en.Colors] BLUE = "blues" -yellow = "golden" +Yellow = "golden" ` caseMixingPage1En = ` --- @@ -137,18 +137,6 @@ func TestCaseInsensitiveConfigurationVariations(t *testing.T) { c := qt.New(t) - // See issues 2615, 1129, 2590 and maybe some others - // Also see 2598 - // - // Viper is now, at least for the Hugo part, case insensitive - // So we need tests for all of it, with needed adjustments on the Hugo side. - // Not sure what that will be. Let us see. - - // So all the below with case variations: - // config: regular fields, blackfriday config, param with nested map - // language: new and overridden values, in regular fields and nested paramsmap - // page frontmatter: regular fields, blackfriday config, param with nested map - mm := afero.NewMemMapFs() caseMixingTestsWriteCommonSources(t, mm) @@ -168,17 +156,27 @@ Block Page Colors: {{ .Params.COLOR }}|{{ .Params.Colors.Blue }} {{ define "main"}} Page Colors: {{ .Params.CoLOR }}|{{ .Params.Colors.Blue }} Site Colors: {{ .Site.Params.COlOR }}|{{ .Site.Params.COLORS.YELLOW }} +{{ template "index-color" (dict "name" "Page" "params" .Params) }} +{{ template "index-color" (dict "name" "Site" "params" .Site.Params) }} + {{ .Content }} {{ partial "partial.html" . }} {{ end }} +{{ define "index-color" }} +{{ $yellow := index .params "COLoRS" "yELLOW" }} +{{ $colors := index .params "COLoRS" }} +{{ $yellow2 := index $colors "yEllow" }} +index1|{{ .name }}: {{ $yellow }}| +index2|{{ .name }}: {{ $yellow2 }}| +{{ end }} `) writeSource(t, fs, filepath.Join("layouts", "_default", "single.html"), ` Page Title: {{ .Title }} Site Title: {{ .Site.Title }} Site Lang Mood: {{ .Site.Language.Params.MOoD }} -Page Colors: {{ .Params.COLOR }}|{{ .Params.Colors.Blue }} -Site Colors: {{ .Site.Params.COLOR }}|{{ .Site.Params.COLORS.YELLOW }} +Page Colors: {{ .Params.COLOR }}|{{ .Params.Colors.Blue }}|{{ index .Params "ColOR" }} +Site Colors: {{ .Site.Params.COLOR }}|{{ .Site.Params.COLORS.YELLOW }}|{{ index .Site.Params "ColOR" }} {{ $page2 := .Site.GetPage "/sect2/page2" }} {{ if $page2 }} Page2: {{ $page2.Params.ColoR }} @@ -200,8 +198,8 @@ Page2: {{ $page2.Params.ColoR }} } th.assertFileContent(filepath.Join("public", "nn", "sect1", "page1", "index.html"), - "Page Colors: red|heavenly", - "Site Colors: green|yellow", + "Page Colors: red|heavenly|red", + "Site Colors: green|yellow|green", "Site Lang Mood: Happy", "Shortcode Page: red|heavenly", "Shortcode Site: green|yellow", @@ -230,6 +228,10 @@ Page2: {{ $page2.Params.ColoR }} "Block Page Colors: black|sky", "Partial Page: black|sky", "Partial Site: green|yellow", + "index1|Page: flower|", + "index1|Site: yellow|", + "index2|Page: flower|", + "index2|Site: yellow|", ) } diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index d137ac340..ca5c7007e 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -228,7 +228,7 @@ func (p *pageMeta) Param(key interface{}) (interface{}, error) { return resource.Param(p, p.s.Info.Params(), key) } -func (p *pageMeta) Params() map[string]interface{} { +func (p *pageMeta) Params() maps.Params { return p.params } @@ -312,7 +312,7 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte return errors.New("missing frontmatter data") } - pm.params = make(map[string]interface{}) + pm.params = make(maps.Params) if frontmatter != nil { // Needed for case insensitive fetching of params values @@ -320,7 +320,7 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte if p.IsNode() { // Check for any cascade define on itself. if cv, found := frontmatter["cascade"]; found { - cvm := cast.ToStringMap(cv) + cvm := maps.ToStringMap(cv) if bucket.cascade == nil { bucket.cascade = cvm } else { @@ -479,7 +479,7 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte } pm.params[loki] = pm.aliases case "sitemap": - p.m.sitemap = config.DecodeSitemap(p.s.siteCfg.sitemap, cast.ToStringMap(v)) + p.m.sitemap = config.DecodeSitemap(p.s.siteCfg.sitemap, maps.ToStringMap(v)) pm.params[loki] = p.m.sitemap sitemapSet = true case "iscjklanguage": @@ -495,7 +495,7 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte switch vv := v.(type) { case []map[interface{}]interface{}: for _, vvv := range vv { - resources = append(resources, cast.ToStringMap(vvv)) + resources = append(resources, maps.ToStringMap(vvv)) } case []map[string]interface{}: resources = append(resources, vv...) @@ -503,7 +503,7 @@ func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatte for _, vvv := range vv { switch vvvv := vvv.(type) { case map[interface{}]interface{}: - resources = append(resources, cast.ToStringMap(vvvv)) + resources = append(resources, maps.ToStringMap(vvvv)) case map[string]interface{}: resources = append(resources, vvvv) } @@ -642,7 +642,7 @@ func (p *pageMeta) applyDefaultValues() error { var renderingConfigOverrides map[string]interface{} bfParam := getParamToLower(p, "blackfriday") if bfParam != nil { - renderingConfigOverrides = cast.ToStringMap(bfParam) + renderingConfigOverrides = maps.ToStringMap(bfParam) } cp := p.s.ContentSpec.Converters.Get(p.markup) @@ -705,14 +705,9 @@ func getParam(m resource.ResourceParamsProvider, key string, stringToLower bool) return helpers.SliceToLower(val) } return v - case map[string]interface{}: // JSON and TOML - return v - case map[interface{}]interface{}: // YAML + default: return v } - - //p.s.Log.ERROR.Printf("GetParam(\"%s\"): Unknown type %s\n", key, reflect.TypeOf(v)) - return nil } func getParamToLower(m resource.ResourceParamsProvider, key string) interface{} { diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 6b9c4193d..abceec9c6 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -1573,7 +1573,8 @@ baseURL = "https://example.org" {{ $withStringParam := .Site.GetPage "withstringparam" }} Author page: {{ $withParam.Param "author.name" }} -Author page string: {{ $withStringParam.Param "author.name" }}| +Author name page string: {{ $withStringParam.Param "author.name" }}| +Author page string: {{ $withStringParam.Param "author" }}| Author site config: {{ $noParam.Param "author.name" }} `, @@ -1603,8 +1604,10 @@ author = "Jo Nesbø" `) b.Build(BuildCfg{}) - b.AssertFileContent("public/index.html", "Author page: Ernest Miller Hemingway") - b.AssertFileContent("public/index.html", "Author page string: |") - b.AssertFileContent("public/index.html", "Author site config: Kurt Vonnegut") + b.AssertFileContent("public/index.html", + "Author page: Ernest Miller Hemingway", + "Author name page string: Kurt Vonnegut|", + "Author page string: Jo Nesbø|", + "Author site config: Kurt Vonnegut") } diff --git a/hugolib/pages_map.go b/hugolib/pages_map.go index aba1aa4bf..5af86c95d 100644 --- a/hugolib/pages_map.go +++ b/hugolib/pages_map.go @@ -20,6 +20,8 @@ import ( "strings" "sync" + "github.com/gohugoio/hugo/common/maps" + radix "github.com/armon/go-radix" "github.com/spf13/cast" @@ -359,7 +361,7 @@ func (m *pagesMap) cleanKey(key string) string { func (m *pagesMap) mergeCascades(b1, b2 *pagesMapBucket) { if b1.cascade == nil { - b1.cascade = make(map[string]interface{}) + b1.cascade = make(maps.Params) } if b2 != nil && b2.cascade != nil { for k, v := range b2.cascade { diff --git a/hugolib/site.go b/hugolib/site.go index db0cd2ea5..0b45c4803 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -28,6 +28,8 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/resources/resource" + "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/hugofs/files" @@ -581,7 +583,7 @@ func (s *SiteInfo) Taxonomies() interface{} { return s.s.Taxonomies } -func (s *SiteInfo) Params() map[string]interface{} { +func (s *SiteInfo) Params() maps.Params { return s.s.Language().Params() } @@ -654,14 +656,9 @@ type SiteSocial map[string]string // Param is a convenience method to do lookups in SiteInfo's Params map. // -// This method is also implemented on Page and Node. +// This method is also implemented on Page. func (s *SiteInfo) Param(key interface{}) (interface{}, error) { - keyStr, err := cast.ToStringE(key) - if err != nil { - return nil, err - } - keyStr = strings.ToLower(keyStr) - return s.Params()[keyStr], nil + return resource.Param(s, nil, key) } func (s *SiteInfo) IsMultiLingual() bool { @@ -1272,7 +1269,7 @@ func (s *Site) getMenusFromConfig() navigation.Menus { s.Log.DEBUG.Printf("found menu: %q, in site config\n", name) menuEntry := navigation.MenuEntry{Menu: name} - ime, err := cast.ToStringMapE(entry) + ime, err := maps.ToStringMapE(entry) if err != nil { s.Log.ERROR.Printf("unable to process menus in site config\n") s.Log.ERROR.Println(err) diff --git a/langs/config.go b/langs/config.go index 927f3558f..184223650 100644 --- a/langs/config.go +++ b/langs/config.go @@ -171,7 +171,7 @@ func toSortedLanguages(cfg config.Provider, l map[string]interface{}) (Languages i := 0 for lang, langConf := range l { - langsMap, err := cast.ToStringMapE(langConf) + langsMap, err := maps.ToStringMapE(langConf) if err != nil { return nil, fmt.Errorf("Language config is not a map: %T", langConf) @@ -192,7 +192,7 @@ func toSortedLanguages(cfg config.Provider, l map[string]interface{}) (Languages case "disabled": language.Disabled = cast.ToBool(v) case "params": - m := cast.ToStringMap(v) + m := maps.ToStringMap(v) // Needed for case insensitive fetching of params values maps.ToLower(m) for k, vv := range m { diff --git a/langs/language.go b/langs/language.go index f71b0255b..67cb3689a 100644 --- a/langs/language.go +++ b/langs/language.go @@ -177,7 +177,7 @@ 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 cast.ToStringMap(l.Get(key)) + return maps.ToStringMap(l.Get(key)) } // GetStringMapString returns the value associated with the key as a map of strings. diff --git a/navigation/menu.go b/navigation/menu.go index 2cf9722e9..ae2e0e4ff 100644 --- a/navigation/menu.go +++ b/navigation/menu.go @@ -14,6 +14,7 @@ package navigation import ( + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/compare" @@ -59,7 +60,7 @@ type Page interface { Section() string Weight() int IsPage() bool - Params() map[string]interface{} + Params() maps.Params } // Menu is a collection of menu entries. diff --git a/navigation/pagemenus.go b/navigation/pagemenus.go index 443c8cd61..352a91557 100644 --- a/navigation/pagemenus.go +++ b/navigation/pagemenus.go @@ -14,6 +14,8 @@ package navigation import ( + "github.com/gohugoio/hugo/common/maps" + "github.com/pkg/errors" "github.com/spf13/cast" ) @@ -73,7 +75,7 @@ func PageMenusFromPage(p Page) (PageMenus, error) { } // Could be a structured menu entry - menus, err := cast.ToStringMapE(ms) + menus, err := maps.ToStringMapE(ms) if err != nil { return pm, errors.Wrapf(err, "unable to process menus for %q", p.LinkTitle()) } @@ -81,7 +83,7 @@ func PageMenusFromPage(p Page) (PageMenus, error) { for name, menu := range menus { menuEntry := MenuEntry{Page: p, Name: p.LinkTitle(), Weight: p.Weight(), Menu: name} if menu != nil { - ime, err := cast.ToStringMapE(menu) + ime, err := maps.ToStringMapE(menu) if err != nil { return pm, errors.Wrapf(err, "unable to process menus for %q", p.LinkTitle()) } diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go index ea1a44d8f..09ac136fc 100644 --- a/resources/page/page_nop.go +++ b/resources/page/page_nop.go @@ -300,7 +300,7 @@ func (p *nopPage) Param(key interface{}) (interface{}, error) { return nil, nil } -func (p *nopPage) Params() map[string]interface{} { +func (p *nopPage) Params() maps.Params { return nil } diff --git a/resources/page/site.go b/resources/page/site.go index 9153c8556..31058637b 100644 --- a/resources/page/site.go +++ b/resources/page/site.go @@ -17,6 +17,8 @@ import ( "html/template" "time" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/common/hugo" @@ -39,7 +41,7 @@ type Site interface { Taxonomies() interface{} LastChange() time.Time Menus() navigation.Menus - Params() map[string]interface{} + Params() maps.Params Data() map[string]interface{} } @@ -107,7 +109,7 @@ func (t testSite) BaseURL() template.URL { return "" } -func (t testSite) Params() map[string]interface{} { +func (t testSite) Params() maps.Params { return nil } diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index 560166b0b..cc6a74f06 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -370,7 +370,7 @@ func (p *testPage) Param(key interface{}) (interface{}, error) { return resource.Param(p, nil, key) } -func (p *testPage) Params() map[string]interface{} { +func (p *testPage) Params() maps.Params { return p.params } diff --git a/resources/resource.go b/resources/resource.go index acf8e37c0..d206c17b5 100644 --- a/resources/resource.go +++ b/resources/resource.go @@ -30,9 +30,9 @@ import ( "github.com/pkg/errors" "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" - "github.com/spf13/afero" "github.com/gohugoio/hugo/helpers" @@ -228,7 +228,7 @@ func (l *genericResource) Name() string { return l.name } -func (l *genericResource) Params() map[string]interface{} { +func (l *genericResource) Params() maps.Params { return l.params } diff --git a/resources/resource/params.go b/resources/resource/params.go index 4cb41715d..89da718ec 100644 --- a/resources/resource/params.go +++ b/resources/resource/params.go @@ -19,12 +19,16 @@ import ( "github.com/spf13/cast" ) -func Param(r ResourceParamsProvider, fallback map[string]interface{}, key interface{}) (interface{}, error) { +func Param(r ResourceParamsProvider, fallback maps.Params, key interface{}) (interface{}, error) { keyStr, err := cast.ToStringE(key) if err != nil { return nil, err } + if fallback == nil { + return maps.GetNestedParam(keyStr, ".", r.Params()) + } + return maps.GetNestedParam(keyStr, ".", r.Params(), fallback) } diff --git a/resources/resource/resourcetypes.go b/resources/resource/resourcetypes.go index 7a055b25d..b525d7d55 100644 --- a/resources/resource/resourcetypes.go +++ b/resources/resource/resourcetypes.go @@ -14,6 +14,7 @@ package resource import ( + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/resources/images/exif" @@ -85,7 +86,7 @@ type ResourceMetaProvider interface { type ResourceParamsProvider interface { // Params set in front matter for this resource. - Params() map[string]interface{} + Params() maps.Params } type ResourceDataProvider interface { diff --git a/resources/resource_metadata.go b/resources/resource_metadata.go index ce17df022..7bf7479a3 100644 --- a/resources/resource_metadata.go +++ b/resources/resource_metadata.go @@ -129,7 +129,7 @@ func AssignMetadata(metadata []map[string]interface{}, resources ...resource.Res params, found := meta["params"] if found { - m := cast.ToStringMap(params) + m := maps.ToStringMap(params) // Needed for case insensitive fetching of params values maps.ToLower(m) ma.updateParams(m) diff --git a/resources/transform.go b/resources/transform.go index ee4912a10..0e44c6bbc 100644 --- a/resources/transform.go +++ b/resources/transform.go @@ -26,11 +26,11 @@ import ( bp "github.com/gohugoio/hugo/bufferpool" - "github.com/gohugoio/hugo/resources/internal" - "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/resources/internal" "github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/media" @@ -200,7 +200,7 @@ func (r *resourceAdapter) Name() string { return r.target.Name() } -func (r *resourceAdapter) Params() map[string]interface{} { +func (r *resourceAdapter) Params() maps.Params { r.init(false, false) return r.target.Params() } diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go index cfbcd312b..041a8e30c 100644 --- a/tpl/collections/collections_test.go +++ b/tpl/collections/collections_test.go @@ -22,6 +22,8 @@ import ( "testing" "time" + "github.com/gohugoio/hugo/common/maps" + qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" @@ -891,6 +893,15 @@ type TstX struct { unexported string } +type TstParams struct { + params maps.Params +} + +func (x TstParams) Params() maps.Params { + return x.params + +} + type TstXIHolder struct { XI TstXI } diff --git a/tpl/collections/index.go b/tpl/collections/index.go index d2989e22f..cd1d1577b 100644 --- a/tpl/collections/index.go +++ b/tpl/collections/index.go @@ -17,6 +17,10 @@ import ( "errors" "fmt" "reflect" + + "github.com/spf13/cast" + + "github.com/gohugoio/hugo/common/maps" ) // Index returns the result of indexing its first argument by the following @@ -34,6 +38,11 @@ func (ns *Namespace) Index(item interface{}, args ...interface{}) (interface{}, return nil, errors.New("index of untyped nil") } + lowerm, ok := item.(maps.Params) + if ok { + return lowerm.Get(cast.ToStringSlice(args)...), nil + } + var indices []interface{} if len(args) == 1 { @@ -79,6 +88,7 @@ func (ns *Namespace) Index(item interface{}, args ...interface{}) (interface{}, if err != nil { return nil, err } + if x := v.MapIndex(index); x.IsValid() { v = x } else { diff --git a/tpl/collections/index_test.go b/tpl/collections/index_test.go index c4cded47c..0c380d8d5 100644 --- a/tpl/collections/index_test.go +++ b/tpl/collections/index_test.go @@ -17,6 +17,8 @@ import ( "fmt" "testing" + "github.com/gohugoio/hugo/common/maps" + qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/deps" ) @@ -42,7 +44,8 @@ func TestIndex(t *testing.T) { {[]map[string]map[string]string{{"a": {"b": "c"}}}, []interface{}{0, "a", "b"}, "c", false}, {map[string]map[string]interface{}{"a": {"b": []string{"c", "d"}}}, []interface{}{"a", "b", 1}, "d", false}, {map[string]map[string]string{"a": {"b": "c"}}, []interface{}{[]string{"a", "b"}}, "c", false}, - + {maps.Params{"a": "av"}, []interface{}{"A"}, "av", false}, + {maps.Params{"a": map[string]interface{}{"b": "bv"}}, []interface{}{"A", "B"}, "bv", false}, // errors {nil, nil, nil, true}, {[]int{0, 1}, []interface{}{"1"}, nil, true}, diff --git a/tpl/collections/sort.go b/tpl/collections/sort.go index 9639fe1d0..7ca764e9b 100644 --- a/tpl/collections/sort.go +++ b/tpl/collections/sort.go @@ -19,6 +19,7 @@ import ( "sort" "strings" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/tpl/compare" "github.com/spf13/cast" ) @@ -75,11 +76,19 @@ func (ns *Namespace) Sort(seq interface{}, args ...interface{}) (interface{}, er } else { v := p.Pairs[i].Value var err error - for _, elemName := range path { + for i, elemName := range path { v, err = evaluateSubElem(v, elemName) if err != nil { return nil, err } + if !v.IsValid() { + continue + } + // Special handling of lower cased maps. + if params, ok := v.Interface().(maps.Params); ok { + v = reflect.ValueOf(params.Get(path[i+1:]...)) + break + } } p.Pairs[i].Key = v } @@ -89,6 +98,7 @@ func (ns *Namespace) Sort(seq interface{}, args ...interface{}) (interface{}, er keys := seqv.MapKeys() for i := 0; i < seqv.Len(); i++ { p.Pairs[i].Value = seqv.MapIndex(keys[i]) + if sortByField == "" { p.Pairs[i].Key = keys[i] } else if sortByField == "value" { @@ -96,11 +106,19 @@ func (ns *Namespace) Sort(seq interface{}, args ...interface{}) (interface{}, er } else { v := p.Pairs[i].Value var err error - for _, elemName := range path { + for i, elemName := range path { v, err = evaluateSubElem(v, elemName) if err != nil { return nil, err } + if !v.IsValid() { + continue + } + // Special handling of lower cased maps. + if params, ok := v.Interface().(maps.Params); ok { + v = reflect.ValueOf(params.Get(path[i+1:]...)) + break + } } p.Pairs[i].Key = v } @@ -135,6 +153,7 @@ func (p pairList) Less(i, j int) bool { // can only call Interface() on valid reflect Values return sortComp.Lt(iv.Interface(), jv.Interface()) } + // if j is invalid, test i against i's zero value return sortComp.Lt(iv.Interface(), reflect.Zero(iv.Type())) } diff --git a/tpl/collections/sort_test.go b/tpl/collections/sort_test.go index 612a928cb..2bf6e85fe 100644 --- a/tpl/collections/sort_test.go +++ b/tpl/collections/sort_test.go @@ -18,6 +18,8 @@ import ( "reflect" "testing" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/deps" ) @@ -100,6 +102,20 @@ func TestSort(t *testing.T) { "asc", []*TstX{{A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, {A: "g", B: "h"}, {A: "i", B: "j"}}, }, + // Lower case Params, slice + { + []TstParams{{params: maps.Params{"color": "indigo"}}, {params: maps.Params{"color": "blue"}}, {params: maps.Params{"color": "green"}}}, + ".Params.COLOR", + "asc", + []TstParams{{params: maps.Params{"color": "blue"}}, {params: maps.Params{"color": "green"}}, {params: maps.Params{"color": "indigo"}}}, + }, + // Lower case Params, map + { + map[string]TstParams{"1": {params: maps.Params{"color": "indigo"}}, "2": {params: maps.Params{"color": "blue"}}, "3": {params: maps.Params{"color": "green"}}}, + ".Params.CoLoR", + "asc", + []TstParams{{params: maps.Params{"color": "blue"}}, {params: maps.Params{"color": "green"}}, {params: maps.Params{"color": "indigo"}}}, + }, // test map sorting by struct's method { map[string]TstX{"1": {A: "i", B: "j"}, "2": {A: "e", B: "f"}, "3": {A: "c", B: "d"}, "4": {A: "g", B: "h"}, "5": {A: "a", B: "b"}}, diff --git a/tpl/collections/where.go b/tpl/collections/where.go index 42f0d370f..cada675f3 100644 --- a/tpl/collections/where.go +++ b/tpl/collections/where.go @@ -18,6 +18,8 @@ import ( "fmt" "reflect" "strings" + + "github.com/gohugoio/hugo/common/maps" ) // Where returns a filtered subset of a given data type. @@ -277,6 +279,7 @@ func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) if !obj.IsValid() { return zero, errors.New("can't evaluate an invalid value") } + typ := obj.Type() obj, isNil := indirect(obj) @@ -295,6 +298,7 @@ func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) if objPtr.Kind() != reflect.Interface && objPtr.CanAddr() { objPtr = objPtr.Addr() } + mt, ok := objPtr.Type().MethodByName(elemName) if ok { switch { @@ -368,16 +372,22 @@ func parseWhereArgs(args ...interface{}) (mv reflect.Value, op string, err error // Array or Slice. func (ns *Namespace) checkWhereArray(seqv, kv, mv reflect.Value, path []string, op string) (interface{}, error) { rv := reflect.MakeSlice(seqv.Type(), 0, 0) + for i := 0; i < seqv.Len(); i++ { var vvv reflect.Value rvv := seqv.Index(i) + if kv.Kind() == reflect.String { - vvv = rvv - for _, elemName := range path { - var err error - vvv, err = evaluateSubElem(vvv, elemName) - if err != nil { - continue + if params, ok := rvv.Interface().(maps.Params); ok { + vvv = reflect.ValueOf(params.Get(path...)) + } else { + vvv = rvv + for _, elemName := range path { + var err error + vvv, err = evaluateSubElem(vvv, elemName) + if err != nil { + continue + } } } } else { diff --git a/tpl/collections/where_test.go b/tpl/collections/where_test.go index cdef7aefb..d6a1dd141 100644 --- a/tpl/collections/where_test.go +++ b/tpl/collections/where_test.go @@ -16,9 +16,12 @@ package collections import ( "fmt" "reflect" + "strings" "testing" "time" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/deps" ) @@ -162,6 +165,37 @@ func TestWhere(t *testing.T) { {1: "a", 2: "m"}, }, }, + { + seq: []maps.Params{ + {"a": "a1", "b": "b1"}, {"a": "a2", "b": "b2"}, + }, + key: "B", match: "b2", + expect: []maps.Params{ + maps.Params{"a": "a2", "b": "b2"}, + }, + }, + { + seq: []maps.Params{ + maps.Params{ + "a": map[string]interface{}{ + "b": "b1", + }, + }, + maps.Params{ + "a": map[string]interface{}{ + "b": "b2", + }, + }, + }, + key: "A.B", match: "b2", + expect: []maps.Params{ + maps.Params{ + "a": map[string]interface{}{ + "b": "b2", + }, + }, + }, + }, { seq: []*TstX{ {A: "a", B: "b"}, {A: "c", B: "d"}, {A: "e", B: "f"}, @@ -557,11 +591,24 @@ func TestWhere(t *testing.T) { "zap": []interface{}{map[interface{}]interface{}{"a": 5, "b": 6}}, }, }, + { + seq: map[string]interface{}{ + "foo": []interface{}{maps.Params{"a": 1, "b": 2}}, + "bar": []interface{}{maps.Params{"a": 3, "b": 4}}, + "zap": []interface{}{maps.Params{"a": 5, "b": 6}}, + }, + key: "B", op: ">", match: 3, + expect: map[string]interface{}{ + "bar": []interface{}{maps.Params{"a": 3, "b": 4}}, + "zap": []interface{}{maps.Params{"a": 5, "b": 6}}, + }, + }, } { testVariants := createTestVariants(test) for j, test := range testVariants { - name := fmt.Sprintf("[%d/%d] %T %s %s", i, j, test.seq, test.op, test.key) + name := fmt.Sprintf("%d/%d %T %s %s", i, j, test.seq, test.op, test.key) + name = strings.ReplaceAll(name, "[]", "slice-of-") t.Run(name, func(t *testing.T) { var results interface{} var err error diff --git a/tpl/resources/resources.go b/tpl/resources/resources.go index e676a3412..20c4d1b3a 100644 --- a/tpl/resources/resources.go +++ b/tpl/resources/resources.go @@ -19,11 +19,11 @@ import ( "fmt" "path/filepath" - _errors "github.com/pkg/errors" - + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources/resource" + _errors "github.com/pkg/errors" "github.com/gohugoio/hugo/resources/resource_factories/bundler" "github.com/gohugoio/hugo/resources/resource_factories/create" @@ -301,7 +301,7 @@ func (ns *Namespace) resolveArgs(args []interface{}) (resources.ResourceTransfor return nil, nil, fmt.Errorf("type %T not supported in Resource transformations", args[0]) } - m, err := cast.ToStringMapE(args[0]) + m, err := maps.ToStringMapE(args[0]) if err != nil { return nil, nil, _errors.Wrap(err, "invalid options type") } diff --git a/tpl/tplimpl/template_ast_transformers.go b/tpl/tplimpl/template_ast_transformers.go index d257d7a31..e25e70e35 100644 --- a/tpl/tplimpl/template_ast_transformers.go +++ b/tpl/tplimpl/template_ast_transformers.go @@ -19,11 +19,10 @@ import ( texttemplate "text/template" "text/template/parse" - "github.com/pkg/errors" - + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/tpl" "github.com/mitchellh/mapstructure" - "github.com/spf13/cast" + "github.com/pkg/errors" ) // decl keeps track of the variable mappings, i.e. $mysite => .Site etc. @@ -315,7 +314,7 @@ func (c *templateContext) collectConfig(n *parse.PipeNode) { if s, ok := cmd.Args[0].(*parse.StringNode); ok { errMsg := "failed to decode $_hugo_config in template" - m, err := cast.ToStringMapE(s.Text) + m, err := maps.ToStringMapE(s.Text) if err != nil { c.err = errors.Wrap(err, errMsg) return