From b2e3748a4e148a9624b9906bd8f34a238a54429c Mon Sep 17 00:00:00 2001 From: John Feminella Date: Sun, 19 Feb 2017 02:50:08 -0500 Subject: [PATCH] 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 --- docs/content/templates/list.md | 8 +++++ docs/content/templates/variables.md | 49 +++++++++++++++++++++++++-- hugolib/page.go | 51 +++++++++++++++++++++++++++++ hugolib/pageSort.go | 3 +- hugolib/pageSort_test.go | 11 ++++--- hugolib/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 index c4f20d986..7408f6288 100644 --- 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 index e914f24fd..96923f9e1 100644 --- 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 "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. diff --git a/hugolib/page.go b/hugolib/page.go index e92767da0..042f1378e 100644 --- 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 index e1ea786b6..6d2431cec 100644 --- 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 index f5f28f1d7..a17f53dc6 100644 --- 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 index 90a4d1245..0fa622e33 100644 --- 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)