shithub: hugo

Download patch

ref: a2670bf460e10ed5de69f90abbe7c4e2b32068cf
parent: 1a36ce9b0903e02a5068aed5f807ed9d21f48ece
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Mon Nov 11 09:37:37 EST 2019

tpl/collections: Allow dict to create nested structures

Fixes #6497

--- a/docs/content/en/functions/dict.md
+++ b/docs/content/en/functions/dict.md
@@ -21,6 +21,12 @@
 
 `dict` is especially useful for passing more than one value to a partial template.
 
+Note that the `key` can be either a `string` or a `string slice`. The latter is useful to create a deply nested structure, e.g.:
+
+```go-text-template
+{{ $m := dict (slice "a" "b" "c") "value" }}
+```
+
 
 ## Example: Using `dict` to pass multiple values to a `partial`
 
--- a/tpl/collections/collections.go
+++ b/tpl/collections/collections.go
@@ -145,22 +145,41 @@
 // Dictionary creates a map[string]interface{} from the given parameters by
 // walking the parameters and treating them as key-value pairs.  The number
 // of parameters must be even.
+// The keys can be string slices, which will create the needed nested structure.
 func (ns *Namespace) Dictionary(values ...interface{}) (map[string]interface{}, error) {
 	if len(values)%2 != 0 {
 		return nil, errors.New("invalid dictionary call")
 	}
 
-	dict := make(map[string]interface{}, len(values)/2)
+	root := make(map[string]interface{})
 
 	for i := 0; i < len(values); i += 2 {
-		key, ok := values[i].(string)
-		if !ok {
-			return nil, errors.New("dictionary keys must be strings")
+		dict := root
+		var key string
+		switch v := values[i].(type) {
+		case string:
+			key = v
+		case []string:
+			for i := 0; i < len(v)-1; i++ {
+				key = v[i]
+				var m map[string]interface{}
+				v, found := dict[key]
+				if found {
+					m = v.(map[string]interface{})
+				} else {
+					m = make(map[string]interface{})
+					dict[key] = m
+				}
+				dict = m
+			}
+			key = v[len(v)-1]
+		default:
+			return nil, errors.New("invalid dictionary key")
 		}
 		dict[key] = values[i+1]
 	}
 
-	return dict, nil
+	return root, nil
 }
 
 // EchoParam returns a given value if it is set; otherwise, it returns an
--- a/tpl/collections/collections_test.go
+++ b/tpl/collections/collections_test.go
@@ -182,7 +182,6 @@
 }
 
 func TestDictionary(t *testing.T) {
-	t.Parallel()
 	c := qt.New(t)
 
 	ns := New(&deps.Deps{})
@@ -192,22 +191,30 @@
 		expect interface{}
 	}{
 		{[]interface{}{"a", "b"}, map[string]interface{}{"a": "b"}},
+		{[]interface{}{[]string{"a", "b"}, "c"}, map[string]interface{}{"a": map[string]interface{}{"b": "c"}}},
+		{[]interface{}{[]string{"a", "b"}, "c", []string{"a", "b2"}, "c2", "b", "c"},
+			map[string]interface{}{"a": map[string]interface{}{"b": "c", "b2": "c2"}, "b": "c"}},
 		{[]interface{}{"a", 12, "b", []int{4}}, map[string]interface{}{"a": 12, "b": []int{4}}},
 		// errors
 		{[]interface{}{5, "b"}, false},
 		{[]interface{}{"a", "b", "c"}, false},
 	} {
-		errMsg := qt.Commentf("[%d] %v", i, test.values)
+		i := i
+		test := test
+		c.Run(fmt.Sprint(i), func(c *qt.C) {
+			c.Parallel()
+			errMsg := qt.Commentf("[%d] %v", i, test.values)
 
-		result, err := ns.Dictionary(test.values...)
+			result, err := ns.Dictionary(test.values...)
 
-		if b, ok := test.expect.(bool); ok && !b {
-			c.Assert(err, qt.Not(qt.IsNil), errMsg)
-			continue
-		}
+			if b, ok := test.expect.(bool); ok && !b {
+				c.Assert(err, qt.Not(qt.IsNil), errMsg)
+				return
+			}
 
-		c.Assert(err, qt.IsNil, errMsg)
-		c.Assert(result, qt.DeepEquals, test.expect, errMsg)
+			c.Assert(err, qt.IsNil, errMsg)
+			c.Assert(result, qt.DeepEquals, test.expect, qt.Commentf(fmt.Sprint(result)))
+		})
 	}
 }