shithub: hugo

Download patch

ref: a3fe5e5e35f311f22b6b4fc38abfcf64cd2c7d6f
parent: cd07e6d57b158a76f812e8c4c9567dbc84f57939
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Thu Nov 21 16:59:38 EST 2019

Fix Params case handling in the index, sort and where func

This means that you can now do:

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

--- a/commands/import_jekyll.go
+++ b/commands/import_jekyll.go
@@ -30,12 +30,12 @@
 
 	"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 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 convertJekyllContent(m interface{}, content string) string {
-	metadata, _ := cast.ToStringMapE(m)
+	metadata, _ := maps.ToStringMapE(m)
 
 	lines := strings.Split(content, "\n")
 	var resultLines []string
--- a/common/maps/maps.go
+++ b/common/maps/maps.go
@@ -25,24 +25,43 @@
 // 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 {
--- 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 @@
 )
 
 func TestToLower(t *testing.T) {
-
 	tests := []struct {
 		input    map[string]interface{}
 		expected map[string]interface{}
@@ -30,7 +30,7 @@
 			map[string]interface{}{
 				"abC": 32,
 			},
-			map[string]interface{}{
+			Params{
 				"abc": 32,
 			},
 		},
@@ -48,16 +48,16 @@
 					"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 @@
 	}
 
 	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)
+			}
+		})
 	}
 }
 
--- a/common/maps/params.go
+++ b/common/maps/params.go
@@ -19,76 +19,89 @@
 	"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
 }
--- a/common/maps/params_test.go
+++ b/common/maps/params_test.go
@@ -35,7 +35,7 @@
 
 	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
--- a/common/para/para.go
+++ b/common/para/para.go
@@ -37,8 +37,8 @@
 
 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 @@
 	return &errGroupRunner{
 		Group: g,
 		ctx:   ctx,
-		w:  w,
+		w:     w,
 	}, ctx
 }
--- a/common/para/para_test.go
+++ b/common/para/para_test.go
@@ -15,6 +15,7 @@
 
 import (
 	"context"
+	"runtime"
 	"sort"
 	"sync"
 	"sync/atomic"
@@ -25,6 +26,9 @@
 )
 
 func TestPara(t *testing.T) {
+	if runtime.NumCPU() < 4 {
+		t.Skipf("skip para test, CPU count is %d", runtime.NumCPU())
+	}
 
 	c := qt.New(t)
 
--- a/hugolib/case_insensitive_test.go
+++ b/hugolib/case_insensitive_test.go
@@ -61,7 +61,7 @@
 hrefTargetBlank = false
 [Languages.en.Colors]
 BLUE = "blues"
-yellow = "golden"
+Yellow = "golden"
 `
 	caseMixingPage1En = `
 ---
@@ -137,18 +137,6 @@
 
 	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,9 +156,19 @@
 {{ 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"), `
@@ -177,8 +175,8 @@
 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 @@
 	}
 
 	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 @@
 		"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|",
 	)
 }
 
--- a/hugolib/page__meta.go
+++ b/hugolib/page__meta.go
@@ -228,7 +228,7 @@
 	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 @@
 		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 @@
 		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 @@
 			}
 			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 @@
 			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 @@
 				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 @@
 		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 @@
 			return helpers.SliceToLower(val)
 		}
 		return v
-	case map[string]interface{}: // JSON and TOML
+	default:
 		return v
-	case map[interface{}]interface{}: // YAML
-		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{} {
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -1573,7 +1573,8 @@
 {{ $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 @@
 `)
 	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")
 
 }
--- a/hugolib/pages_map.go
+++ b/hugolib/pages_map.go
@@ -20,6 +20,8 @@
 	"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) 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 {
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -28,6 +28,8 @@
 	"strings"
 	"time"
 
+	"github.com/gohugoio/hugo/resources/resource"
+
 	"github.com/gohugoio/hugo/markup/converter"
 
 	"github.com/gohugoio/hugo/hugofs/files"
@@ -581,7 +583,7 @@
 	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 @@
 
 // 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 @@
 					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)
--- a/langs/config.go
+++ b/langs/config.go
@@ -171,7 +171,7 @@
 	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 @@
 			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 {
--- a/langs/language.go
+++ b/langs/language.go
@@ -177,7 +177,7 @@
 
 // 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.
--- 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 @@
 	Section() string
 	Weight() int
 	IsPage() bool
-	Params() map[string]interface{}
+	Params() maps.Params
 }
 
 // Menu is a collection of menu entries.
--- 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 @@
 	}
 
 	// 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 @@
 	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())
 			}
--- a/resources/page/page_nop.go
+++ b/resources/page/page_nop.go
@@ -300,7 +300,7 @@
 	return nil, nil
 }
 
-func (p *nopPage) Params() map[string]interface{} {
+func (p *nopPage) Params() maps.Params {
 	return nil
 }
 
--- a/resources/page/site.go
+++ b/resources/page/site.go
@@ -17,6 +17,8 @@
 	"html/template"
 	"time"
 
+	"github.com/gohugoio/hugo/common/maps"
+
 	"github.com/gohugoio/hugo/config"
 
 	"github.com/gohugoio/hugo/common/hugo"
@@ -39,7 +41,7 @@
 	Taxonomies() interface{}
 	LastChange() time.Time
 	Menus() navigation.Menus
-	Params() map[string]interface{}
+	Params() maps.Params
 	Data() map[string]interface{}
 }
 
@@ -107,7 +109,7 @@
 	return ""
 }
 
-func (t testSite) Params() map[string]interface{} {
+func (t testSite) Params() maps.Params {
 	return nil
 }
 
--- a/resources/page/testhelpers_test.go
+++ b/resources/page/testhelpers_test.go
@@ -370,7 +370,7 @@
 	return resource.Param(p, nil, key)
 }
 
-func (p *testPage) Params() map[string]interface{} {
+func (p *testPage) Params() maps.Params {
 	return p.params
 }
 
--- a/resources/resource.go
+++ b/resources/resource.go
@@ -30,9 +30,9 @@
 	"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 @@
 	return l.name
 }
 
-func (l *genericResource) Params() map[string]interface{} {
+func (l *genericResource) Params() maps.Params {
 	return l.params
 }
 
--- a/resources/resource/params.go
+++ b/resources/resource/params.go
@@ -19,10 +19,14 @@
 	"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)
--- 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 ResourceParamsProvider interface {
 	// Params set in front matter for this resource.
-	Params() map[string]interface{}
+	Params() maps.Params
 }
 
 type ResourceDataProvider interface {
--- a/resources/resource_metadata.go
+++ b/resources/resource_metadata.go
@@ -129,7 +129,7 @@
 
 				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)
--- a/resources/transform.go
+++ b/resources/transform.go
@@ -26,11 +26,11 @@
 
 	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 @@
 	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()
 }
--- a/tpl/collections/collections_test.go
+++ b/tpl/collections/collections_test.go
@@ -22,6 +22,8 @@
 	"testing"
 	"time"
 
+	"github.com/gohugoio/hugo/common/maps"
+
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/common/loggers"
 	"github.com/gohugoio/hugo/config"
@@ -889,6 +891,15 @@
 type TstX struct {
 	A, B       string
 	unexported string
+}
+
+type TstParams struct {
+	params maps.Params
+}
+
+func (x TstParams) Params() maps.Params {
+	return x.params
+
 }
 
 type TstXIHolder struct {
--- a/tpl/collections/index.go
+++ b/tpl/collections/index.go
@@ -17,6 +17,10 @@
 	"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 @@
 		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 @@
 			if err != nil {
 				return nil, err
 			}
+
 			if x := v.MapIndex(index); x.IsValid() {
 				v = x
 			} else {
--- a/tpl/collections/index_test.go
+++ b/tpl/collections/index_test.go
@@ -17,6 +17,8 @@
 	"fmt"
 	"testing"
 
+	"github.com/gohugoio/hugo/common/maps"
+
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/deps"
 )
@@ -42,7 +44,8 @@
 		{[]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},
--- a/tpl/collections/sort.go
+++ b/tpl/collections/sort.go
@@ -19,6 +19,7 @@
 	"sort"
 	"strings"
 
+	"github.com/gohugoio/hugo/common/maps"
 	"github.com/gohugoio/hugo/tpl/compare"
 	"github.com/spf13/cast"
 )
@@ -75,11 +76,19 @@
 			} 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 @@
 		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 @@
 			} 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 @@
 			// 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()))
 	}
--- a/tpl/collections/sort_test.go
+++ b/tpl/collections/sort_test.go
@@ -18,6 +18,8 @@
 	"reflect"
 	"testing"
 
+	"github.com/gohugoio/hugo/common/maps"
+
 	"github.com/gohugoio/hugo/deps"
 )
 
@@ -99,6 +101,20 @@
 			"TstRp",
 			"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
 		{
--- a/tpl/collections/where.go
+++ b/tpl/collections/where.go
@@ -18,6 +18,8 @@
 	"fmt"
 	"reflect"
 	"strings"
+
+	"github.com/gohugoio/hugo/common/maps"
 )
 
 // Where returns a filtered subset of a given data type.
@@ -277,6 +279,7 @@
 	if !obj.IsValid() {
 		return zero, errors.New("can't evaluate an invalid value")
 	}
+
 	typ := obj.Type()
 	obj, isNil := indirect(obj)
 
@@ -295,6 +298,7 @@
 	if objPtr.Kind() != reflect.Interface && objPtr.CanAddr() {
 		objPtr = objPtr.Addr()
 	}
+
 	mt, ok := objPtr.Type().MethodByName(elemName)
 	if ok {
 		switch {
@@ -368,16 +372,22 @@
 // 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 {
--- a/tpl/collections/where_test.go
+++ b/tpl/collections/where_test.go
@@ -16,9 +16,12 @@
 import (
 	"fmt"
 	"reflect"
+	"strings"
 	"testing"
 	"time"
 
+	"github.com/gohugoio/hugo/common/maps"
+
 	"github.com/gohugoio/hugo/deps"
 )
 
@@ -163,6 +166,37 @@
 			},
 		},
 		{
+			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 @@
 				"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
--- a/tpl/resources/resources.go
+++ b/tpl/resources/resources.go
@@ -19,11 +19,11 @@
 	"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 @@
 		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")
 	}
--- a/tpl/tplimpl/template_ast_transformers.go
+++ b/tpl/tplimpl/template_ast_transformers.go
@@ -19,11 +19,10 @@
 	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 @@
 
 	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