shithub: hugo

Download patch

ref: b2e3748a4e148a9624b9906bd8f34a238a54429c
parent: 6d2281c8ead70ac07122027c989807c0aa1a7722
author: John Feminella <[email protected]>
date: Sat Feb 18 21:50:08 EST 2017

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

--- a/docs/content/templates/list.md
+++ b/docs/content/templates/list.md
@@ -238,6 +238,7 @@
     {{ 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
@@ -246,6 +247,13 @@
 The below example sorts a list of posts by their rating.
 
     {{ range (.Data.Pages.ByParam "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 }}
 
--- a/docs/content/templates/variables.md
+++ b/docs/content/templates/variables.md
@@ -103,10 +103,55 @@
 **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 "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 "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.
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -314,11 +314,62 @@
 	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 {
--- a/hugolib/pageSort.go
+++ b/hugolib/pageSort.go
@@ -14,9 +14,8 @@
 package hugolib
 
 import (
-	"sort"
-
 	"github.com/spf13/cast"
+	"sort"
 )
 
 var spc = newPageCache()
--- a/hugolib/pageSort_test.go
+++ b/hugolib/pageSort_test.go
@@ -20,7 +20,6 @@
 	"testing"
 	"time"
 
-	"github.com/spf13/cast"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -121,11 +120,11 @@
 
 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 @@
 	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 @@
 	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
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -1336,7 +1336,7 @@
 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,8 +1348,35 @@
 	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) {