Add config.cascade

This commit adds support for using the `cascade` keyword in your configuration file(s), e.g. `config.toml`.

Note that

* Every feature of `cascade` is available, e.g. `_target` to target specific page sets.
* Pages, e.g. the home page, can overwrite the cascade defined in config.

Fixes #8741
This commit is contained in:
Bjørn Erik Pedersen 2021-07-09 11:52:03 +02:00
parent 30eea3915b
commit 5cb52c2315
6 changed files with 129 additions and 32 deletions

View File

@ -27,10 +27,10 @@ import (
var ( var (
// ConfigRootKeysSet contains all of the config map root keys. // ConfigRootKeysSet contains all of the config map root keys.
// TODO(bep) use this for something (docs etc.)
ConfigRootKeysSet = map[string]bool{ ConfigRootKeysSet = map[string]bool{
"build": true, "build": true,
"caches": true, "caches": true,
"cascade": true,
"frontmatter": true, "frontmatter": true,
"languages": true, "languages": true,
"imaging": true, "imaging": true,

View File

@ -20,6 +20,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/gohugoio/hugo/common/maps"
qt "github.com/frankban/quicktest" qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/parser" "github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders" "github.com/gohugoio/hugo/parser/metadecoders"
@ -50,6 +52,69 @@ func BenchmarkCascade(b *testing.B) {
} }
} }
func TestCascadeConfig(t *testing.T) {
c := qt.New(t)
// Make sure the cascade from config gets applied even if we're not
// having a content file for the home page.
for _, withHomeContent := range []bool{true, false} {
testName := "Home content file"
if !withHomeContent {
testName = "No home content file"
}
c.Run(testName, func(c *qt.C) {
b := newTestSitesBuilder(c)
b.WithConfigFile("toml", `
baseURL="https://example.org"
[cascade]
img1 = "img1-config.jpg"
imgconfig = "img-config.jpg"
`)
if withHomeContent {
b.WithContent("_index.md", `
---
title: "Home"
cascade:
img1: "img1-home.jpg"
img2: "img2-home.jpg"
---
`)
}
b.WithContent("p1.md", ``)
b.Build(BuildCfg{})
p1 := b.H.Sites[0].getPage("p1")
if withHomeContent {
b.Assert(p1.Params(), qt.DeepEquals, maps.Params{
"imgconfig": "img-config.jpg",
"draft": bool(false),
"iscjklanguage": bool(false),
"img1": "img1-home.jpg",
"img2": "img2-home.jpg",
})
} else {
b.Assert(p1.Params(), qt.DeepEquals, maps.Params{
"img1": "img1-config.jpg",
"imgconfig": "img-config.jpg",
"draft": bool(false),
"iscjklanguage": bool(false),
})
}
})
}
}
func TestCascade(t *testing.T) { func TestCascade(t *testing.T) {
allLangs := []string{"en", "nn", "nb", "sv"} allLangs := []string{"en", "nn", "nb", "sv"}

View File

@ -462,10 +462,13 @@ func (m *pageMap) assembleSections() error {
if parent != nil { if parent != nil {
parentBucket = parent.p.bucket parentBucket = parent.p.bucket
} else if s == "/" {
parentBucket = m.s.siteBucket
} }
kind := page.KindSection kind := page.KindSection
if s == "/" { if s == "/" {
kind = page.KindHome kind = page.KindHome
} }

View File

@ -340,34 +340,10 @@ func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, fron
if p.bucket != nil { if p.bucket != nil {
// Check for any cascade define on itself. // Check for any cascade define on itself.
if cv, found := frontmatter["cascade"]; found { if cv, found := frontmatter["cascade"]; found {
if v, err := maps.ToSliceStringMap(cv); err == nil { var err error
p.bucket.cascade = make(map[page.PageMatcher]maps.Params) p.bucket.cascade, err = page.DecodeCascade(cv)
if err != nil {
for _, vv := range v { return err
var m page.PageMatcher
if mv, found := vv["_target"]; found {
err := page.DecodePageMatcher(mv, &m)
if err != nil {
return err
}
}
c, found := p.bucket.cascade[m]
if found {
// Merge
for k, v := range vv {
if _, found := c[k]; !found {
c[k] = v
}
}
} else {
p.bucket.cascade[m] = vv
}
}
} else {
p.bucket.cascade = map[page.PageMatcher]maps.Params{
{}: maps.ToStringMap(cv),
}
} }
} }
} }

View File

@ -103,7 +103,7 @@ import (
type Site struct { type Site struct {
// The owning container. When multiple languages, there will be multiple // The owning container. When multiple languages, there will be multiple
// sites. // sites .
h *HugoSites h *HugoSites
*PageCollections *PageCollections
@ -113,7 +113,8 @@ type Site struct {
Sections Taxonomy Sections Taxonomy
Info *SiteInfo Info *SiteInfo
language *langs.Language language *langs.Language
siteBucket *pagesMapBucket
siteCfg siteConfigHolder siteCfg siteConfigHolder
@ -388,6 +389,7 @@ func (s *Site) reset() *Site {
frontmatterHandler: s.frontmatterHandler, frontmatterHandler: s.frontmatterHandler,
mediaTypesConfig: s.mediaTypesConfig, mediaTypesConfig: s.mediaTypesConfig,
language: s.language, language: s.language,
siteBucket: s.siteBucket,
h: s.h, h: s.h,
publisher: s.publisher, publisher: s.publisher,
siteConfigConfig: s.siteConfigConfig, siteConfigConfig: s.siteConfigConfig,
@ -539,9 +541,23 @@ But this also means that your site configuration may not do what you expect. If
enableEmoji: cfg.Language.Cfg.GetBool("enableEmoji"), enableEmoji: cfg.Language.Cfg.GetBool("enableEmoji"),
} }
s := &Site{ var siteBucket *pagesMapBucket
if cfg.Language.IsSet("cascade") {
var err error
cascade, err := page.DecodeCascade(cfg.Language.Get("cascade"))
if err != nil {
return nil, errors.Errorf("failed to decode cascade config: %s", err)
}
siteBucket = &pagesMapBucket{
cascade: cascade,
}
}
s := &Site{
language: cfg.Language, language: cfg.Language,
siteBucket: siteBucket,
disabledKinds: disabledKinds, disabledKinds: disabledKinds,
outputFormats: outputFormats, outputFormats: outputFormats,

View File

@ -19,6 +19,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/hugofs/glob" "github.com/gohugoio/hugo/hugofs/glob"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
@ -70,6 +71,42 @@ func (m PageMatcher) Matches(p Page) bool {
return true return true
} }
// DecodeCascade decodes in which could be eiter a map or a slice of maps.
func DecodeCascade(in interface{}) (map[PageMatcher]maps.Params, error) {
m, err := maps.ToSliceStringMap(in)
if err != nil {
return map[PageMatcher]maps.Params{
{}: maps.ToStringMap(in),
}, nil
}
cascade := make(map[PageMatcher]maps.Params)
for _, vv := range m {
var m PageMatcher
if mv, found := vv["_target"]; found {
err := DecodePageMatcher(mv, &m)
if err != nil {
return nil, err
}
}
c, found := cascade[m]
if found {
// Merge
for k, v := range vv {
if _, found := c[k]; !found {
c[k] = v
}
}
} else {
cascade[m] = vv
}
}
return cascade, nil
}
// DecodePageMatcher decodes m into v. // DecodePageMatcher decodes m into v.
func DecodePageMatcher(m interface{}, v *PageMatcher) error { func DecodePageMatcher(m interface{}, v *PageMatcher) error {
if err := mapstructure.WeakDecode(m, v); err != nil { if err := mapstructure.WeakDecode(m, v); err != nil {