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
This commit is contained in:
John Feminella 2017-02-19 02:50:08 -05:00 committed by Bjørn Erik Pedersen
parent 6d2281c8ea
commit b2e3748a4e
6 changed files with 142 additions and 11 deletions

View file

@ -238,6 +238,7 @@ your list templates:
{{ end }} {{ end }}
### Order by Parameter ### Order by Parameter
Order based on the specified frontmatter parameter. Pages without that Order based on the specified frontmatter parameter. Pages without that
parameter will use the site's `.Site.Params` default. If the parameter is not 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 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 }} {{ 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 ### Reverse Order
Can be applied to any of the above. Using Date for an example. Can be applied to any of the above. Using Date for an example.

View file

@ -103,10 +103,55 @@ which would render
**See also:** [Archetypes]({{% ref "content/archetypes.md" %}}) for consistency of `Params` across pieces of content. **See also:** [Archetypes]({{% ref "content/archetypes.md" %}}) for consistency of `Params` across pieces of content.
### Param method ### 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 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. [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.

View file

@ -314,13 +314,64 @@ func (p *Page) Param(key interface{}) (interface{}, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
keyStr = strings.ToLower(keyStr) 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 { if val, ok := p.Params[keyStr]; ok {
return val, nil return val, nil
} }
return p.Site.Params[keyStr], 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 { func (p *Page) Author() Author {
authors := p.Authors() authors := p.Authors()

View file

@ -14,9 +14,8 @@
package hugolib package hugolib
import ( import (
"sort"
"github.com/spf13/cast" "github.com/spf13/cast"
"sort"
) )
var spc = newPageCache() var spc = newPageCache()

View file

@ -20,7 +20,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/spf13/cast"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -121,11 +120,11 @@ func TestPageSortReverse(t *testing.T) {
func TestPageSortByParam(t *testing.T) { func TestPageSortByParam(t *testing.T) {
t.Parallel() t.Parallel()
var k interface{} = "arbitrary" var k interface{} = "arbitrarily.nested"
s := newTestSite(t) s := newTestSite(t)
unsorted := createSortTestPages(s, 10) unsorted := createSortTestPages(s, 10)
delete(unsorted[9].Params, cast.ToString(k)) delete(unsorted[9].Params, "arbitrarily")
firstSetValue, _ := unsorted[0].Param(k) firstSetValue, _ := unsorted[0].Param(k)
secondSetValue, _ := unsorted[1].Param(k) secondSetValue, _ := unsorted[1].Param(k)
@ -137,7 +136,7 @@ func TestPageSortByParam(t *testing.T) {
assert.Equal(t, "xyz92", lastSetValue) assert.Equal(t, "xyz92", lastSetValue)
assert.Equal(t, nil, unsetValue) assert.Equal(t, nil, unsetValue)
sorted := unsorted.ByParam("arbitrary") sorted := unsorted.ByParam("arbitrarily.nested")
firstSetSortedValue, _ := sorted[0].Param(k) firstSetSortedValue, _ := sorted[0].Param(k)
secondSetSortedValue, _ := sorted[1].Param(k) secondSetSortedValue, _ := sorted[1].Param(k)
lastSetSortedValue, _ := sorted[8].Param(k) lastSetSortedValue, _ := sorted[8].Param(k)
@ -182,7 +181,9 @@ func createSortTestPages(s *Site, num int) Pages {
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
p := s.newPage(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i))) p := s.newPage(filepath.FromSlash(fmt.Sprintf("/x/y/p%d.md", i)))
p.Params = map[string]interface{}{ 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 w := 5

View file

@ -1336,7 +1336,7 @@ some content
func TestPageParams(t *testing.T) { func TestPageParams(t *testing.T) {
t.Parallel() t.Parallel()
s := newTestSite(t) s := newTestSite(t)
want := map[string]interface{}{ wantedMap := map[string]interface{}{
"tags": []string{"hugo", "web"}, "tags": []string{"hugo", "web"},
// Issue #2752 // Issue #2752
"social": []interface{}{ "social": []interface{}{
@ -1348,10 +1348,37 @@ func TestPageParams(t *testing.T) {
for i, c := range pagesParamsTemplate { for i, c := range pagesParamsTemplate {
p, err := s.NewPageFrom(strings.NewReader(c), "content/post/params.md") p, err := s.NewPageFrom(strings.NewReader(c), "content/post/params.md")
require.NoError(t, err, "err during parse", "#%d", i) 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) { func TestPageSimpleMethods(t *testing.T) {
t.Parallel() t.Parallel()
s := newTestSite(t) s := newTestSite(t)