Fix Params case handling in the index, sort and where func
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 21 Nov 2019 20:59:38 +0000 (21:59 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 22 Nov 2019 17:41:50 +0000 (18:41 +0100)
This means that you can now do:

```
{{ range where .Site.Pages "Params.MYPARAM" "foo" }}
```

33 files changed:
commands/import_jekyll.go
common/maps/maps.go
common/maps/maps_test.go
common/maps/params.go
common/maps/params_test.go
common/para/para.go
common/para/para_test.go
hugolib/case_insensitive_test.go
hugolib/page__meta.go
hugolib/page_test.go
hugolib/pages_map.go
hugolib/site.go
langs/config.go
langs/language.go
navigation/menu.go
navigation/pagemenus.go
resources/page/page_nop.go
resources/page/site.go
resources/page/testhelpers_test.go
resources/resource.go
resources/resource/params.go
resources/resource/resourcetypes.go
resources/resource_metadata.go
resources/transform.go
tpl/collections/collections_test.go
tpl/collections/index.go
tpl/collections/index_test.go
tpl/collections/sort.go
tpl/collections/sort_test.go
tpl/collections/where.go
tpl/collections/where_test.go
tpl/resources/resources.go
tpl/tplimpl/template_ast_transformers.go

index e5c39dc3418ec32670b2dd1bd9c33ba02fbf43b3..b1cc53378e6d3a42fe2a4996cb16eedda268f333 100644 (file)
@@ -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
index e0d4f964df8692334d3c415cae609c01fe32fce6..8b42ca7646dded3d36d6e0ce3c340d9993f42b36 100644 (file)
@@ -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
index 8b0aa5eb905cf5c90b2193fa1f3e3e5ccb291959..6e4947adb2b9d535e79abd73b9eade36bacfe78a 100644 (file)
@@ -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)
+                       }
+               })
        }
 }
 
index 2d62ad752ec5d19ba53a9763046a3525237a0671..ecb63d7a5e56c45b2a6294b376eda8131a477918 100644 (file)
@@ -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
+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
        }
 
-       switch m := result.(type) {
-       case map[string]interface{}:
-               v, key, owner := traverseParams(rest, m)
-               return v, key, owner, nil
-       default:
+       first := lookupFn(keySegments[0])
+       if first == nil {
                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
+       if len(keySegments) == 1 {
+               return first, keySegments[0], nil, nil
        }
 
-       if len(rest) == 0 {
-               // That was the last key.
-               return result, firstKey, m
+       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
        }
 
-       // That was not the last key.
-       return traverseParams(rest, cast.ToStringMap(result))
+       return nil, "", nil, nil
 }
index 6477de6f461ca5c71e7b12f239d4dcb7a45c9eb1..8016a8bd643df4792b78945539910a70beeb80ff 100644 (file)
@@ -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
index 319bdb78fd5c7b396dc1299cd896c327f92f15db..69bfc205b8b720bd4115a3c638f0ee1174edafb9 100644 (file)
@@ -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
 }
index 9f33a234c2a5094780d7ccd94f459f335c80572a..bda7f5d27a059ffce7de3c40bf6fd4d117bf1565 100644 (file)
@@ -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)
 
index 5c7dbe073a22f4a404dbfd5252c93f5c88b1f1e6..9c2662044244da2fe7e96a86138e6e7a92c4cca2 100644 (file)
@@ -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|",
        )
 }
 
index d137ac340052564da9721f64bf4fbbee6c61feeb..ca5c7007ebb824ef790864c94d9409cf066cb70e 100644 (file)
@@ -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{} {
index 6b9c4193d3baf98b33b4df4402fde7b886757038..abceec9c60e42cb2e8c12b420cd489044c82293c 100644 (file)
@@ -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")
 
 }
index aba1aa4bf604a822c38cc8c0954d7be15febc1ad..5af86c95dda811f47c15f12ca0ab9ffc37d81817 100644 (file)
@@ -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 {
index db0cd2ea5be765665676191e73fcec0f0fcb50a8..0b45c4803d4d587f2569a52b38cc78fbbc573e07 100644 (file)
@@ -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)
index 927f3558fa45f71bd6afd4083a3ffc7a678d545f..1842236506b310195f28f63cde081337929f9f72 100644 (file)
@@ -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 {
index f71b0255b3888df73a9f147086c9ab21faf29477..67cb3689a1d77b411e9da8fd1260bd573092bd5e 100644 (file)
@@ -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.
index 2cf9722e9ab5e40df58c287f61ff9c23252aea0a..ae2e0e4ff9fc77accc9bd6d753a798de6321e764 100644 (file)
@@ -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.
index 443c8cd61c2352f6f1ee41577401c8a9452967d6..352a9155708e332ece1cf39e5bcb26c96082b2b4 100644 (file)
@@ -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())
                        }
index ea1a44d8f242cf305b859de5cab9488b40fa6e94..09ac136fc2bdb2546f5b0bf758f3d74d1c349386 100644 (file)
@@ -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
 }
 
index 9153c855614c15e9129923b2df6b80d7e0516433..31058637b7633e45b66cd317909cbc56bda5e793 100644 (file)
@@ -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
 }
 
index 560166b0b6d6ba25597544bb1a2352e660e59d72..cc6a74f06de04db004cf608744952586cd02050a 100644 (file)
@@ -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
 }
 
index acf8e37c0d80452e9bd12329a93c56d4fb78341d..d206c17b514954fd9a93faf315ff09ace4232ade 100644 (file)
@@ -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
 }
 
index 4cb41715da1d3bfbe5a4e4c3ff479a0b77e66030..89da718ec8b0e6acd4f1fb7a83cd58cac8afd518 100644 (file)
@@ -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)
 
 }
index 7a055b25dd3dee3ad41e2a63bf30c48df5f39e03..b525d7d55b52f1b6948a2e2c94330c91b8b9b2fc 100644 (file)
@@ -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 {
index ce17df022fab6f5095a0067244938039503e22f1..7bf7479a3aa2fc446378063a4cf68181ab7e7d0e 100644 (file)
@@ -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)
index ee4912a103eea3316e1a38a26da3228bed69f093..0e44c6bbc8df048a5f90fd090afa997ad7069d12 100644 (file)
@@ -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()
 }
index cfbcd312bc324df7eeb7b89a64a951e0dc5d2d33..041a8e30c26796f70d9a30e57340557b6c4c8d3e 100644 (file)
@@ -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
 }
index d2989e22fc5f7366f1d1aa22d5edee13d0fa17f6..cd1d1577b0fa1a6beb6ecc63ae14cea9b39c7660 100644 (file)
@@ -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 {
index c4cded47cf60f1893e6620498cacda1712448658..0c380d8d5314aa787fde1604ca35eba0e4f5d91d 100644 (file)
@@ -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},
index 9639fe1d08a654522c2d1c2ce8d0649595478654..7ca764e9b98448f0bb6ae221c42c90cbcd9a8ebe 100644 (file)
@@ -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()))
        }
index 612a928cb806bf14b9f3a0705cec186fa2e9e864..2bf6e85fe214c4c05b8f68f3997d69368c8dbe88 100644 (file)
@@ -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"}},
index 42f0d370f8df5f067336d191b0dd9a6e2841a353..cada675f3d19306641d2136ca7967a716f00fd20 100644 (file)
@@ -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 {
index cdef7aefb5b25075d78275b7a7362c7310503082..d6a1dd14171bd60850349777696b21942ff58330 100644 (file)
@@ -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
index e676a34120b19fa2b472d4f88868c632674db6d4..20c4d1b3a8198e645b773c2a1b0863dcb58c8624 100644 (file)
@@ -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")
        }
index d257d7a311dc424891f2227c8479f90446c717c7..e25e70e350e545e8e8e6148c1915ff95e2a74fd3 100644 (file)
@@ -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