hugo

Unnamed repository; edit this file 'description' to name the repository.

git clone git://git.shimmy1996.com/hugo.git
commit b2e3748a4e148a9624b9906bd8f34a238a54429c
parent 6d2281c8ead70ac07122027c989807c0aa1a7722
Author: John Feminella <jxf+github@jxf.me>
Date:   Sun, 19 Feb 2017 02:50:08 -0500

hugolib: Enhance `.Param` to permit arbitrarily nested parameter references

The Param method currently assumes that its argument is a single,
distinct, top-level key to look up in the Params map. This enhances the
Param method; it will now also attempt to see if the key can be
interpreted as a nested chain of keys to look up in Params.

Fixes #2598
Diffstat:
Mdocs/content/templates/list.md | 8++++++++
Mdocs/content/templates/variables.md | 49+++++++++++++++++++++++++++++++++++++++++++++++--
Mhugolib/page.go | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mhugolib/pageSort.go | 3+--
Mhugolib/pageSort_test.go | 11++++++-----
Mhugolib/page_test.go | 31+++++++++++++++++++++++++++++--
6 files changed, 142 insertions(+), 11 deletions(-)
diff --git a/docs/content/templates/list.md b/docs/content/templates/list.md
@@ -238,6 +238,7 @@ your list templates:
     {{ end }}
 
 ### Order by Parameter
+
 Order based on the specified frontmatter parameter. Pages without that
 parameter will use the site's `.Site.Params` default. If the parameter is not
 found at all in some entries, those entries will appear together at the end
@@ -249,6 +250,13 @@ The below example sorts a list of posts by their rating.
       <!-- ... -->
     {{ end }}
 
+If the frontmatter field of interest is nested beneath another field, you can
+also get it:
+
+    {{ range (.Date.Pages.ByParam "author.last_name") }}
+      <!-- ... -->
+    {{ end }}
+
 ### Reverse Order
 Can be applied to any of the above. Using Date for an example.
 
diff --git a/docs/content/templates/variables.md b/docs/content/templates/variables.md
@@ -103,10 +103,55 @@ which would render
 **See also:** [Archetypes]({{% ref "content/archetypes.md" %}}) for consistency of `Params` across pieces of content.
 
 ### Param method
-In Hugo you can declare params both for the site and the individual page.  A common use case is to have a general value for the site and a more specific value for some of the pages (i.e. an image).
+
+In Hugo you can declare params both for the site and the individual page. A
+common use case is to have a general value for the site and a more specific
+value for some of the pages (i.e. a header image):
+
+```
+{{ $.Param "header_image" }}
+```
+
+The `.Param` method provides a way to resolve a single value whether it's
+in a page parameter or a site parameter.
+
+When frontmatter contains nested fields, like:
+
+```
+---
+author:
+  given_name: John
+  family_name: Feminella
+  display_name: John Feminella
+---
+```
+
+then `.Param` can access them by concatenating the field names together with a
+dot:
+
 ```
-$.Param "image"
+{{ $.Param "author.display_name" }}
 ```
+
+If your frontmatter contains a top-level key that is ambiguous with a nested
+key, as in the following case,
+
+```
+---
+favorites.flavor: vanilla
+favorites:
+  flavor: chocolate
+---
+```
+
+then the top-level key will be preferred. In the previous example, this
+
+```
+{{ $.Param "favorites.flavor" }}
+```
+
+will print `vanilla`, not `chocolate`.
+
 ### Taxonomy Terms Page Variables
 
 [Taxonomy Terms](/templates/terms/) pages are of the type `Page` and have the following additional variables. These are available in `layouts/_defaults/terms.html` for example.
diff --git a/hugolib/page.go b/hugolib/page.go
@@ -314,13 +314,64 @@ func (p *Page) Param(key interface{}) (interface{}, error) {
 	if err != nil {
 		return nil, err
 	}
+
 	keyStr = strings.ToLower(keyStr)
+	result, _ := p.traverseDirect(keyStr)
+	if result != nil {
+		return result, nil
+	}
+
+	keySegments := strings.Split(keyStr, ".")
+	if len(keySegments) == 1 {
+		return nil, nil
+	}
+
+	return p.traverseNested(keySegments)
+}
+
+func (p *Page) traverseDirect(key string) (interface{}, error) {
+	keyStr := strings.ToLower(key)
 	if val, ok := p.Params[keyStr]; ok {
 		return val, nil
 	}
+
 	return p.Site.Params[keyStr], nil
 }
 
+func (p *Page) traverseNested(keySegments []string) (interface{}, error) {
+	result := traverse(keySegments, p.Params)
+	if result != nil {
+		return result, nil
+	}
+
+	result = traverse(keySegments, p.Site.Params)
+	if result != nil {
+		return result, nil
+	}
+
+	// Didn't find anything, but also no problems.
+	return nil, nil
+}
+
+func traverse(keys []string, m map[string]interface{}) interface{} {
+	// Shift first element off.
+	firstKey, rest := keys[0], keys[1:]
+	result := m[firstKey]
+
+	// No point in continuing here.
+	if result == nil {
+		return result
+	}
+
+	if len(rest) == 0 {
+		// That was the last key.
+		return result
+	} else {
+		// That was not the last key.
+		return traverse(rest, cast.ToStringMap(result))
+	}
+}
+
 func (p *Page) Author() Author {
 	authors := p.Authors()
 
diff --git a/hugolib/pageSort.go b/hugolib/pageSort.go
@@ -14,9 +14,8 @@
 package hugolib
 
 import (
-	"sort"
-
 	"github.com/spf13/cast"
+	"sort"
 )
 
 var spc = newPageCache()
diff --git a/hugolib/pageSort_test.go b/hugolib/pageSort_test.go
@@ -20,7 +20,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/spf13/cast"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -121,11 +120,11 @@ func TestPageSortReverse(t *testing.T) {
 
 func TestPageSortByParam(t *testing.T) {
 	t.Parallel()
-	var k interface{} = "arbitrary"
+	var k interface{} = "arbitrarily.nested"
 	s := newTestSite(t)
 
 	unsorted := createSortTestPages(s, 10)
-	delete(unsorted[9].Params, cast.ToString(k))
+	delete(unsorted[9].Params, "arbitrarily")
 
 	firstSetValue, _ := unsorted[0].Param(k)
 	secondSetValue, _ := unsorted[1].Param(k)
@@ -137,7 +136,7 @@ func TestPageSortByParam(t *testing.T) {
 	assert.Equal(t, "xyz92", lastSetValue)
 	assert.Equal(t, nil, unsetValue)
 
-	sorted := unsorted.ByParam("arbitrary")
+	sorted := unsorted.ByParam("arbitrarily.nested")
 	firstSetSortedValue, _ := sorted[0].Param(k)
 	secondSetSortedValue, _ := sorted[1].Param(k)
 	lastSetSortedValue, _ := sorted[8].Param(k)
@@ -182,7 +181,9 @@ func createSortTestPages(s *Site, num int) Pages {
 	for i := 0; i < num; i++ {
 		p := s.newPage(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i)))
 		p.Params = map[string]interface{}{
-			"arbitrary": "xyz" + fmt.Sprintf("%v", 100-i),
+			"arbitrarily": map[string]interface{}{
+				"nested": ("xyz" + fmt.Sprintf("%v", 100-i)),
+			},
 		}
 
 		w := 5
diff --git a/hugolib/page_test.go b/hugolib/page_test.go
@@ -1336,7 +1336,7 @@ some content
 func TestPageParams(t *testing.T) {
 	t.Parallel()
 	s := newTestSite(t)
-	want := map[string]interface{}{
+	wantedMap := map[string]interface{}{
 		"tags": []string{"hugo", "web"},
 		// Issue #2752
 		"social": []interface{}{
@@ -1348,10 +1348,37 @@ func TestPageParams(t *testing.T) {
 	for i, c := range pagesParamsTemplate {
 		p, err := s.NewPageFrom(strings.NewReader(c), "content/post/params.md")
 		require.NoError(t, err, "err during parse", "#%d", i)
-		assert.Equal(t, want, p.Params, "#%d", i)
+		for key, _ := range wantedMap {
+			assert.Equal(t, wantedMap[key], p.Params[key], "#%d", key)
+		}
 	}
 }
 
+func TestTraverse(t *testing.T) {
+	exampleParams := `---
+rating: "5 stars"
+tags:
+  - hugo
+  - web
+social:
+  twitter: "@jxxf"
+  facebook: "https://example.com"
+---`
+	t.Parallel()
+	s := newTestSite(t)
+	p, _ := s.NewPageFrom(strings.NewReader(exampleParams), "content/post/params.md")
+	fmt.Println("%v", p.Params)
+
+	topLevelKeyValue, _ := p.Param("rating")
+	assert.Equal(t, "5 stars", topLevelKeyValue)
+
+	nestedStringKeyValue, _ := p.Param("social.twitter")
+	assert.Equal(t, "@jxxf", nestedStringKeyValue)
+
+	nonexistentKeyValue, _ := p.Param("doesn't.exist")
+	assert.Nil(t, nonexistentKeyValue)
+}
+
 func TestPageSimpleMethods(t *testing.T) {
 	t.Parallel()
 	s := newTestSite(t)