From 5cb52c23150032b3fdb211a095745c512369b463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 9 Jul 2021 11:52:03 +0200 Subject: [PATCH] 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 --- config/defaultConfigProvider.go | 2 +- hugolib/cascade_test.go | 65 +++++++++++++++++++++++++++++++++ hugolib/content_map_page.go | 3 ++ hugolib/page__meta.go | 32 ++-------------- hugolib/site.go | 22 +++++++++-- resources/page/page_matcher.go | 37 +++++++++++++++++++ 6 files changed, 129 insertions(+), 32 deletions(-) diff --git a/config/defaultConfigProvider.go b/config/defaultConfigProvider.go index 9f1c44ee..05c3b912 100644 --- a/config/defaultConfigProvider.go +++ b/config/defaultConfigProvider.go @@ -27,10 +27,10 @@ import ( var ( // ConfigRootKeysSet contains all of the config map root keys. - // TODO(bep) use this for something (docs etc.) ConfigRootKeysSet = map[string]bool{ "build": true, "caches": true, + "cascade": true, "frontmatter": true, "languages": true, "imaging": true, diff --git a/hugolib/cascade_test.go b/hugolib/cascade_test.go index 78409a4b..000b641e 100644 --- a/hugolib/cascade_test.go +++ b/hugolib/cascade_test.go @@ -20,6 +20,8 @@ import ( "strings" "testing" + "github.com/gohugoio/hugo/common/maps" + qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/parser" "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) { allLangs := []string{"en", "nn", "nb", "sv"} diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index 74dd0e02..278d2476 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -462,10 +462,13 @@ func (m *pageMap) assembleSections() error { if parent != nil { parentBucket = parent.p.bucket + } else if s == "/" { + parentBucket = m.s.siteBucket } kind := page.KindSection if s == "/" { + kind = page.KindHome } diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index 759fadd2..3b31cb29 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -340,34 +340,10 @@ func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, fron if p.bucket != nil { // Check for any cascade define on itself. if cv, found := frontmatter["cascade"]; found { - if v, err := maps.ToSliceStringMap(cv); err == nil { - p.bucket.cascade = make(map[page.PageMatcher]maps.Params) - - for _, vv := range v { - 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), - } + var err error + p.bucket.cascade, err = page.DecodeCascade(cv) + if err != nil { + return err } } } diff --git a/hugolib/site.go b/hugolib/site.go index 2e7e1d7f..fe7305b9 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -103,7 +103,7 @@ import ( type Site struct { // The owning container. When multiple languages, there will be multiple - // sites. + // sites . h *HugoSites *PageCollections @@ -113,7 +113,8 @@ type Site struct { Sections Taxonomy Info *SiteInfo - language *langs.Language + language *langs.Language + siteBucket *pagesMapBucket siteCfg siteConfigHolder @@ -388,6 +389,7 @@ func (s *Site) reset() *Site { frontmatterHandler: s.frontmatterHandler, mediaTypesConfig: s.mediaTypesConfig, language: s.language, + siteBucket: s.siteBucket, h: s.h, publisher: s.publisher, 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"), } - 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, + siteBucket: siteBucket, disabledKinds: disabledKinds, outputFormats: outputFormats, diff --git a/resources/page/page_matcher.go b/resources/page/page_matcher.go index 7d3db373..8e81f810 100644 --- a/resources/page/page_matcher.go +++ b/resources/page/page_matcher.go @@ -19,6 +19,7 @@ import ( "github.com/pkg/errors" + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/hugofs/glob" "github.com/mitchellh/mapstructure" ) @@ -70,6 +71,42 @@ func (m PageMatcher) Matches(p Page) bool { 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. func DecodePageMatcher(m interface{}, v *PageMatcher) error { if err := mapstructure.WeakDecode(m, v); err != nil {