From bd98182dbde893a8a809661c70633741bbf63911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 9 Aug 2019 10:05:22 +0200 Subject: [PATCH] Implement cascading front matter Fixes #6041 --- hugolib/cascade_test.go | 252 ++++++++++++++++++++++++++++++++++++ hugolib/collections_test.go | 3 +- hugolib/hugo_sites_build.go | 48 +++++-- hugolib/page.go | 4 +- hugolib/page__common.go | 3 + hugolib/page__meta.go | 48 +++++-- hugolib/page__new.go | 40 ++++-- hugolib/pagecollections.go | 15 ++- hugolib/pages_map.go | 118 ++++++++++++++--- hugolib/site.go | 25 ++-- hugolib/testhelpers_test.go | 13 +- 11 files changed, 496 insertions(+), 73 deletions(-) create mode 100644 hugolib/cascade_test.go diff --git a/hugolib/cascade_test.go b/hugolib/cascade_test.go new file mode 100644 index 000000000..aebd7a825 --- /dev/null +++ b/hugolib/cascade_test.go @@ -0,0 +1,252 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hugolib + +import ( + "bytes" + "fmt" + "path" + "testing" + + "github.com/alecthomas/assert" + "github.com/gohugoio/hugo/parser" + "github.com/gohugoio/hugo/parser/metadecoders" + "github.com/stretchr/testify/require" +) + +func BenchmarkCascade(b *testing.B) { + allLangs := []string{"en", "nn", "nb", "sv", "ab", "aa", "af", "sq", "kw", "da"} + + for i := 1; i <= len(allLangs); i += 2 { + langs := allLangs[0:i] + b.Run(fmt.Sprintf("langs-%d", len(langs)), func(b *testing.B) { + assert := require.New(b) + b.StopTimer() + builders := make([]*sitesBuilder, b.N) + for i := 0; i < b.N; i++ { + builders[i] = newCascadeTestBuilder(b, langs) + } + b.StartTimer() + + for i := 0; i < b.N; i++ { + builder := builders[i] + err := builder.BuildE(BuildCfg{}) + assert.NoError(err) + first := builder.H.Sites[0] + assert.NotNil(first) + } + }) + } +} + +func TestCascade(t *testing.T) { + assert := assert.New(t) + + allLangs := []string{"en", "nn", "nb", "sv"} + + langs := allLangs[:3] + + t.Run(fmt.Sprintf("langs-%d", len(langs)), func(t *testing.T) { + b := newCascadeTestBuilder(t, langs) + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", ` + 12|taxonomy|categories/cool/_index.md|Cascade Category|cat.png|categories|HTML-| + 12|taxonomy|categories/catsect1|catsect1|cat.png|categories|HTML-| + 12|taxonomy|categories/funny|funny|cat.png|categories|HTML-| + 12|taxonomyTerm|categories/_index.md|My Categories|cat.png|categories|HTML-| + 32|taxonomy|categories/sad/_index.md|Cascade Category|sad.png|categories|HTML-| + 42|taxonomy|tags/blue|blue|home.png|tags|HTML-| + 42|section|sect3|Cascade Home|home.png|sect3|HTML-| + 42|taxonomyTerm|tags|Cascade Home|home.png|tags|HTML-| + 42|page|p2.md|Cascade Home|home.png|page|HTML-| + 42|page|sect2/p2.md|Cascade Home|home.png|sect2|HTML-| + 42|page|sect3/p1.md|Cascade Home|home.png|sect3|HTML-| + 42|taxonomy|tags/green|green|home.png|tags|HTML-| + 42|home|_index.md|Home|home.png|page|HTML-| + 42|page|p1.md|p1|home.png|page|HTML-| + 42|section|sect1/_index.md|Sect1|sect1.png|stype|HTML-| + 42|section|sect1/s1_2/_index.md|Sect1_2|sect1.png|stype|HTML-| + 42|page|sect1/s1_2/p1.md|Sect1_2_p1|sect1.png|stype|HTML-| + 42|page|sect1/s1_2/p2.md|Sect1_2_p2|sect1.png|stype|HTML-| + 42|section|sect2/_index.md|Sect2|home.png|sect2|HTML-| + 42|page|sect2/p1.md|Sect2_p1|home.png|sect2|HTML-| + 52|page|sect4/p1.md|Cascade Home|home.png|sect4|RSS-| + 52|section|sect4/_index.md|Sect4|home.png|sect4|RSS-| +`) + + // Check that type set in cascade gets the correct layout. + b.AssertFileContent("public/sect1/index.html", `stype list: Sect1`) + b.AssertFileContent("public/sect1/s1_2/p2/index.html", `stype single: Sect1_2_p2`) + + // Check output formats set in cascade + b.AssertFileContent("public/sect4/index.xml", `https://example.org/sect4/index.xml`) + b.AssertFileContent("public/sect4/p1/index.xml", `https://example.org/sect4/p1/index.xml`) + assert.False(b.CheckExists("public/sect2/index.xml")) + + // Check cascade into bundled page + b.AssertFileContent("public/bundle1/index.html", `Resources: bp1.md|home.png|`) + + }) + +} + +func newCascadeTestBuilder(t testing.TB, langs []string) *sitesBuilder { + p := func(m map[string]interface{}) string { + var yamlStr string + + if len(m) > 0 { + var b bytes.Buffer + + parser.InterfaceToConfig(m, metadecoders.YAML, &b) + yamlStr = b.String() + } + + metaStr := "---\n" + yamlStr + "\n---" + + return metaStr + + } + + createLangConfig := func(lang string) string { + const langEntry = ` +[languages.%s] +` + return fmt.Sprintf(langEntry, lang) + } + + createMount := func(lang string) string { + const mountsTempl = ` +[[module.mounts]] +source="content/%s" +target="content" +lang="%s" +` + return fmt.Sprintf(mountsTempl, lang, lang) + } + + config := ` +baseURL = "https://example.org" +defaultContentLanguage = "en" +defaultContentLanguageInSubDir = false + +[languages]` + for _, lang := range langs { + config += createLangConfig(lang) + } + + config += "\n\n[module]\n" + for _, lang := range langs { + config += createMount(lang) + } + + b := newTestSitesBuilder(t).WithConfigFile("toml", config) + + createContentFiles := func(lang string) { + + withContent := func(filenameContent ...string) { + for i := 0; i < len(filenameContent); i += 2 { + b.WithContent(path.Join(lang, filenameContent[i]), filenameContent[i+1]) + } + } + + withContent( + "_index.md", p(map[string]interface{}{ + "title": "Home", + "cascade": map[string]interface{}{ + "title": "Cascade Home", + "ICoN": "home.png", + "outputs": []string{"HTML"}, + "weight": 42, + }, + }), + "p1.md", p(map[string]interface{}{ + "title": "p1", + }), + "p2.md", p(map[string]interface{}{}), + "sect1/_index.md", p(map[string]interface{}{ + "title": "Sect1", + "type": "stype", + "cascade": map[string]interface{}{ + "title": "Cascade Sect1", + "icon": "sect1.png", + "type": "stype", + "categories": []string{"catsect1"}, + }, + }), + "sect1/s1_2/_index.md", p(map[string]interface{}{ + "title": "Sect1_2", + }), + "sect1/s1_2/p1.md", p(map[string]interface{}{ + "title": "Sect1_2_p1", + }), + "sect1/s1_2/p2.md", p(map[string]interface{}{ + "title": "Sect1_2_p2", + }), + "sect2/_index.md", p(map[string]interface{}{ + "title": "Sect2", + }), + "sect2/p1.md", p(map[string]interface{}{ + "title": "Sect2_p1", + "categories": []string{"cool", "funny", "sad"}, + "tags": []string{"blue", "green"}, + }), + "sect2/p2.md", p(map[string]interface{}{}), + "sect3/p1.md", p(map[string]interface{}{}), + "sect4/_index.md", p(map[string]interface{}{ + "title": "Sect4", + "cascade": map[string]interface{}{ + "weight": 52, + "outputs": []string{"RSS"}, + }, + }), + "sect4/p1.md", p(map[string]interface{}{}), + "p2.md", p(map[string]interface{}{}), + "bundle1/index.md", p(map[string]interface{}{}), + "bundle1/bp1.md", p(map[string]interface{}{}), + "categories/_index.md", p(map[string]interface{}{ + "title": "My Categories", + "cascade": map[string]interface{}{ + "title": "Cascade Category", + "icoN": "cat.png", + "weight": 12, + }, + }), + "categories/cool/_index.md", p(map[string]interface{}{}), + "categories/sad/_index.md", p(map[string]interface{}{ + "cascade": map[string]interface{}{ + "icon": "sad.png", + "weight": 32, + }, + }), + ) + } + + createContentFiles("en") + + b.WithTemplates("index.html", ` + +{{ range .Site.Pages }} +{{- .Weight }}|{{ .Kind }}|{{ path.Join .Path }}|{{ .Title }}|{{ .Params.icon }}|{{ .Type }}|{{ range .OutputFormats }}{{ .Name }}-{{ end }}| +{{ end }} +`, + + "_default/single.html", "default single: {{ .Title }}|{{ .RelPermalink }}|{{ .Content }}|Resources: {{ range .Resources }}{{ .Name }}|{{ .Params.icon }}|{{ .Content }}{{ end }}", + "_default/list.html", "default list: {{ .Title }}", + "stype/single.html", "stype single: {{ .Title }}|{{ .RelPermalink }}|{{ .Content }}", + "stype/list.html", "stype list: {{ .Title }}", + ) + + return b +} diff --git a/hugolib/collections_test.go b/hugolib/collections_test.go index 1a261260e..804c0cae1 100644 --- a/hugolib/collections_test.go +++ b/hugolib/collections_test.go @@ -178,7 +178,6 @@ tags_weight: %d b.WithSimpleConfigFile(). WithContent("page1.md", fmt.Sprintf(pageContent, 10), "page2.md", fmt.Sprintf(pageContent, 20)). WithTemplatesAdded("index.html", ` - {{ $p1 := index .Site.RegularPages 0 }}{{ $p2 := index .Site.RegularPages 1 }} {{ $pages := slice }} @@ -205,7 +204,7 @@ tags_weight: %d b.CreateSites().Build(BuildCfg{}) assert.Equal(1, len(b.H.Sites)) - require.Len(t, b.H.Sites[0].RegularPages(), 2) + assert.Len(b.H.Sites[0].RegularPages(), 2) b.AssertFileContent("public/index.html", "pages:2:page.Pages:Page(/page2.md)/Page(/page1.md)", diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index 82a189a50..a70a19e7c 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -19,7 +19,10 @@ import ( "fmt" "runtime/trace" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/output" + "golang.org/x/sync/errgroup" + "golang.org/x/sync/semaphore" "github.com/pkg/errors" @@ -226,7 +229,7 @@ func (h *HugoSites) process(config *BuildCfg, init func(config *BuildCfg) error, } -func (h *HugoSites) assemble(config *BuildCfg) error { +func (h *HugoSites) assemble(bcfg *BuildCfg) error { if len(h.Sites) > 1 { // The first is initialized during process; initialize the rest @@ -237,23 +240,46 @@ func (h *HugoSites) assemble(config *BuildCfg) error { } } - if !config.whatChanged.source { + if !bcfg.whatChanged.source { return nil } + numWorkers := config.GetNumWorkerMultiplier() + sem := semaphore.NewWeighted(int64(numWorkers)) + g, ctx := errgroup.WithContext(context.Background()) + for _, s := range h.Sites { - if err := s.assemblePagesMap(s); err != nil { - return err - } + s := s + g.Go(func() error { + err := sem.Acquire(ctx, 1) + if err != nil { + return err + } + defer sem.Release(1) - if err := s.pagesMap.assembleTaxonomies(s); err != nil { - return err - } + if err := s.assemblePagesMap(s); err != nil { + return err + } - if err := s.createWorkAllPages(); err != nil { - return err - } + if err := s.pagesMap.assemblePageMeta(); err != nil { + return err + } + if err := s.pagesMap.assembleTaxonomies(s); err != nil { + return err + } + + if err := s.createWorkAllPages(); err != nil { + return err + } + + return nil + + }) + } + + if err := g.Wait(); err != nil { + return err } if err := h.createPageCollections(); err != nil { diff --git a/hugolib/page.go b/hugolib/page.go index 8dda33009..306ca7b0f 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -520,7 +520,7 @@ func (p *pageState) addResources(r ...resource.Resource) { p.resources = append(p.resources, r...) } -func (p *pageState) mapContent(meta *pageMeta) error { +func (p *pageState) mapContent(bucket *pagesMapBucket, meta *pageMeta) error { s := p.shortcodeState @@ -563,7 +563,7 @@ Loop: } } - if err := meta.setMetadata(p, m); err != nil { + if err := meta.setMetadata(bucket, p, m); err != nil { return err } diff --git a/hugolib/page__common.go b/hugolib/page__common.go index cf554bb40..b13a71a40 100644 --- a/hugolib/page__common.go +++ b/hugolib/page__common.go @@ -35,6 +35,9 @@ type pageCommon struct { // Laziliy initialized dependencies. init *lazy.Init + metaInit sync.Once + metaInitFn func(bucket *pagesMapBucket) error + // All of these represents the common parts of a page.Page maps.Scratcher navigation.PageMenusProvider diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index 551f47977..e8ef13bfd 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -306,19 +306,51 @@ func (p *pageMeta) Weight() int { return p.weight } -func (pm *pageMeta) setMetadata(p *pageState, frontmatter map[string]interface{}) error { - if frontmatter == nil { +func (pm *pageMeta) setMetadata(bucket *pagesMapBucket, p *pageState, frontmatter map[string]interface{}) error { + if frontmatter == nil && bucket.cascade == nil { return errors.New("missing frontmatter data") } pm.params = make(map[string]interface{}) - // Needed for case insensitive fetching of params values - maps.ToLower(frontmatter) + if frontmatter != nil { + // Needed for case insensitive fetching of params values + maps.ToLower(frontmatter) + if p.IsNode() { + // Check for any cascade define on itself. + if cv, found := frontmatter["cascade"]; found { + cvm := cast.ToStringMap(cv) + if bucket.cascade == nil { + bucket.cascade = cvm + } else { + for k, v := range cvm { + bucket.cascade[k] = v + } + } + } + } + + if bucket != nil && bucket.cascade != nil { + for k, v := range bucket.cascade { + if _, found := frontmatter[k]; !found { + frontmatter[k] = v + } + } + } + } else { + frontmatter = make(map[string]interface{}) + for k, v := range bucket.cascade { + frontmatter[k] = v + } + } var mtime time.Time - if p.File().FileInfo() != nil { - mtime = p.File().FileInfo().ModTime() + var contentBaseName string + if !p.File().IsZero() { + contentBaseName = p.File().ContentBaseName() + if p.File().FileInfo() != nil { + mtime = p.File().FileInfo().ModTime() + } } var gitAuthorDate time.Time @@ -331,7 +363,7 @@ func (pm *pageMeta) setMetadata(p *pageState, frontmatter map[string]interface{} Params: pm.params, Dates: &pm.Dates, PageURLs: &pm.urlPaths, - BaseFilename: p.File().ContentBaseName(), + BaseFilename: contentBaseName, ModTime: mtime, GitAuthorDate: gitAuthorDate, } @@ -546,7 +578,7 @@ func (pm *pageMeta) setMetadata(p *pageState, frontmatter map[string]interface{} if isCJKLanguage != nil { pm.isCJKLanguage = *isCJKLanguage - } else if p.s.siteCfg.hasCJKLanguage { + } else if p.s.siteCfg.hasCJKLanguage && p.source.parsed != nil { if cjkRe.Match(p.source.parsed.Input()) { pm.isCJKLanguage = true } else { diff --git a/hugolib/page__new.go b/hugolib/page__new.go index 64c84b0f8..99bf305aa 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -95,7 +95,7 @@ func newPageBase(metaProvider *pageMeta) (*pageState, error) { } -func newPageFromMeta(metaProvider *pageMeta) (*pageState, error) { +func newPageFromMeta(meta map[string]interface{}, metaProvider *pageMeta) (*pageState, error) { if metaProvider.f == nil { metaProvider.f = page.NewZeroFile(metaProvider.s.DistinctWarningLog) } @@ -105,8 +105,26 @@ func newPageFromMeta(metaProvider *pageMeta) (*pageState, error) { return nil, err } - if err := metaProvider.applyDefaultValues(); err != nil { - return nil, err + initMeta := func(bucket *pagesMapBucket) error { + if meta != nil || bucket != nil { + if err := metaProvider.setMetadata(bucket, ps, meta); err != nil { + return ps.wrapError(err) + } + } + + if err := metaProvider.applyDefaultValues(); err != nil { + return err + } + + return nil + } + + if metaProvider.standalone { + initMeta(nil) + } else { + // Because of possible cascade keywords, we need to delay this + // until we have the complete page graph. + ps.metaInitFn = initMeta } ps.init.Add(func() (interface{}, error) { @@ -152,7 +170,7 @@ func newPageFromMeta(metaProvider *pageMeta) (*pageState, error) { func newPageStandalone(m *pageMeta, f output.Format) (*pageState, error) { m.configuredOutputFormats = output.Formats{f} m.standalone = true - p, err := newPageFromMeta(m) + p, err := newPageFromMeta(nil, m) if err != nil { return nil, err @@ -211,12 +229,16 @@ func newPageWithContent(f *fileInfo, s *Site, bundled bool, content resource.Ope ps.shortcodeState = newShortcodeHandler(ps, ps.s, nil) - if err := ps.mapContent(metaProvider); err != nil { - return nil, ps.wrapError(err) - } + ps.metaInitFn = func(bucket *pagesMapBucket) error { + if err := ps.mapContent(bucket, metaProvider); err != nil { + return ps.wrapError(err) + } - if err := metaProvider.applyDefaultValues(); err != nil { - return nil, err + if err := metaProvider.applyDefaultValues(); err != nil { + return err + } + + return nil } ps.init.Add(func() (interface{}, error) { diff --git a/hugolib/pagecollections.go b/hugolib/pagecollections.go index 1c8bed9d9..52eb66156 100644 --- a/hugolib/pagecollections.go +++ b/hugolib/pagecollections.go @@ -387,6 +387,7 @@ func (c *PageCollections) clearResourceCacheForPage(page *pageState) { } func (c *PageCollections) assemblePagesMap(s *Site) error { + c.pagesMap = newPagesMap(s) rootSections := make(map[string]bool) @@ -437,18 +438,14 @@ func (c *PageCollections) createWorkAllPages() error { var ( bucketsToRemove []string rootBuckets []*pagesMapBucket + walkErr error ) c.pagesMap.r.Walk(func(s string, v interface{}) bool { bucket := v.(*pagesMapBucket) - var parentBucket *pagesMapBucket + parentBucket := c.pagesMap.parentBucket(s) - if s != "/" { - _, parentv, found := c.pagesMap.r.LongestPrefix(path.Dir(s)) - if !found { - panic(fmt.Sprintf("[BUG] parent bucket not found for %q", s)) - } - parentBucket = parentv.(*pagesMapBucket) + if parentBucket != nil { if !mainSectionsFound && strings.Count(s, "/") == 1 { // Root section @@ -536,6 +533,10 @@ func (c *PageCollections) createWorkAllPages() error { return false }) + if walkErr != nil { + return walkErr + } + c.pagesMap.s.lastmod = siteLastmod if !mainSectionsFound { diff --git a/hugolib/pages_map.go b/hugolib/pages_map.go index 26e937fd2..26bbedec6 100644 --- a/hugolib/pages_map.go +++ b/hugolib/pages_map.go @@ -68,6 +68,43 @@ func (m *pagesMap) getOrCreateHome() *pageState { return home } +func (m *pagesMap) initPageMeta(p *pageState, bucket *pagesMapBucket) error { + var err error + p.metaInit.Do(func() { + if p.metaInitFn != nil { + err = p.metaInitFn(bucket) + } + }) + return err +} + +func (m *pagesMap) initPageMetaFor(prefix string, bucket *pagesMapBucket) error { + parentBucket := m.parentBucket(prefix) + + m.mergeCascades(bucket, parentBucket) + + if err := m.initPageMeta(bucket.owner, bucket); err != nil { + return err + } + + if !bucket.view { + for _, p := range bucket.pages { + ps := p.(*pageState) + if err := m.initPageMeta(ps, bucket); err != nil { + return err + } + + for _, p := range ps.resources.ByType(pageResourceType) { + if err := m.initPageMeta(p.(*pageState), bucket); err != nil { + return err + } + } + } + } + + return nil +} + func (m *pagesMap) createSectionIfNotExists(section string) { key := m.cleanKey(section) _, found := m.r.Get(key) @@ -126,18 +163,19 @@ func (m *pagesMap) addPage(p *pageState) { bucket.pages = append(bucket.pages, p) } -func (m *pagesMap) withEveryPage(f func(p *pageState)) { - m.r.Walk(func(k string, v interface{}) bool { - b := v.(*pagesMapBucket) - f(b.owner) - if !b.view { - for _, p := range b.pages { - f(p.(*pageState)) - } - } +func (m *pagesMap) assemblePageMeta() error { + var walkErr error + m.r.Walk(func(s string, v interface{}) bool { + bucket := v.(*pagesMapBucket) + if err := m.initPageMetaFor(s, bucket); err != nil { + walkErr = err + return true + } return false }) + + return walkErr } func (m *pagesMap) assembleTaxonomies(s *Site) error { @@ -165,6 +203,9 @@ func (m *pagesMap) assembleTaxonomies(s *Site) error { key := m.cleanKey(plural) bucket = m.addBucketFor(key, n, nil) + if err := m.initPageMetaFor(key, bucket); err != nil { + return err + } } if bucket.meta == nil { @@ -201,7 +242,7 @@ func (m *pagesMap) assembleTaxonomies(s *Site) error { } - addTaxonomy := func(singular, plural, term string, weight int, p page.Page) { + addTaxonomy := func(singular, plural, term string, weight int, p page.Page) error { bkey := bucketKey{ plural: plural, } @@ -228,6 +269,9 @@ func (m *pagesMap) assembleTaxonomies(s *Site) error { key := m.cleanKey(path.Join(plural, termKey)) b2 = m.addBucketFor(key, n, meta) + if err := m.initPageMetaFor(key, b2); err != nil { + return err + } b1.pages = append(b1.pages, b2.owner) taxonomyBuckets[bkey] = b2 @@ -239,6 +283,8 @@ func (m *pagesMap) assembleTaxonomies(s *Site) error { b1.owner.m.Dates.UpdateDateAndLastmodIfAfter(p) b2.owner.m.Dates.UpdateDateAndLastmodIfAfter(p) + + return nil } m.r.Walk(func(k string, v interface{}) bool { @@ -262,10 +308,14 @@ func (m *pagesMap) assembleTaxonomies(s *Site) error { if vals != nil { if v, ok := vals.([]string); ok { for _, idx := range v { - addTaxonomy(singular, plural, idx, weight, p) + if err := addTaxonomy(singular, plural, idx, weight, p); err != nil { + m.s.Log.ERROR.Printf("Failed to add taxonomy %q for %q: %s", plural, p.Path(), err) + } } } else if v, ok := vals.(string); ok { - addTaxonomy(singular, plural, v, weight, p) + if err := addTaxonomy(singular, plural, v, weight, p); err != nil { + m.s.Log.ERROR.Printf("Failed to add taxonomy %q for %q: %s", plural, p.Path(), err) + } } else { m.s.Log.ERROR.Printf("Invalid %s in %q\n", plural, p.Path()) } @@ -291,16 +341,41 @@ func (m *pagesMap) cleanKey(key string) string { return "/" + key } -func (m *pagesMap) dump() { - m.r.Walk(func(s string, v interface{}) bool { +func (m *pagesMap) mergeCascades(b1, b2 *pagesMapBucket) { + if b1.cascade == nil { + b1.cascade = make(map[string]interface{}) + } + if b2 != nil && b2.cascade != nil { + for k, v := range b2.cascade { + if _, found := b1.cascade[k]; !found { + b1.cascade[k] = v + } + } + } +} + +func (m *pagesMap) parentBucket(prefix string) *pagesMapBucket { + if prefix == "/" { + return nil + } + _, parentv, found := m.r.LongestPrefix(path.Dir(prefix)) + if !found { + panic(fmt.Sprintf("[BUG] parent bucket not found for %q", prefix)) + } + return parentv.(*pagesMapBucket) + +} + +func (m *pagesMap) withEveryPage(f func(p *pageState)) { + m.r.Walk(func(k string, v interface{}) bool { b := v.(*pagesMapBucket) - fmt.Println("-------\n", s, ":", b.owner.Kind(), ":") - if b.owner != nil { - fmt.Println("Owner:", b.owner.Path()) - } - for _, p := range b.pages { - fmt.Println(p.Path()) + f(b.owner) + if !b.view { + for _, p := range b.pages { + f(p.(*pageState)) + } } + return false }) } @@ -312,6 +387,9 @@ type pagesMapBucket struct { // Some additional metatadata attached to this node. meta map[string]interface{} + // Cascading front matter. + cascade map[string]interface{} + owner *pageState // The branch node // When disableKinds is enabled for this node. diff --git a/hugolib/site.go b/hugolib/site.go index 2b8a7285a..bf07d52b1 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -1650,12 +1650,13 @@ func (s *Site) kindFromSectionPath(sectionPath string) string { } func (s *Site) newTaxonomyPage(title string, sections ...string) *pageState { - p, err := newPageFromMeta(&pageMeta{ - title: title, - s: s, - kind: page.KindTaxonomy, - sections: sections, - }) + p, err := newPageFromMeta( + map[string]interface{}{"title": title}, + &pageMeta{ + s: s, + kind: page.KindTaxonomy, + sections: sections, + }) if err != nil { panic(err) @@ -1666,11 +1667,13 @@ func (s *Site) newTaxonomyPage(title string, sections ...string) *pageState { } func (s *Site) newPage(kind string, sections ...string) *pageState { - p, err := newPageFromMeta(&pageMeta{ - s: s, - kind: kind, - sections: sections, - }) + p, err := newPageFromMeta( + map[string]interface{}{}, + &pageMeta{ + s: s, + kind: kind, + sections: sections, + }) if err != nil { panic(err) diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index d7e0d5c85..25ebbf125 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -649,9 +649,16 @@ func (s *sitesBuilder) AssertHome(matches ...string) { func (s *sitesBuilder) AssertFileContent(filename string, matches ...string) { s.T.Helper() content := s.FileContent(filename) - for _, match := range matches { - if !strings.Contains(content, match) { - s.Fatalf("No match for %q in content for %s\n%s\n%q", match, filename, content, content) + for _, m := range matches { + lines := strings.Split(m, "\n") + for _, match := range lines { + match = strings.TrimSpace(match) + if match == "" { + continue + } + if !strings.Contains(content, match) { + s.Fatalf("No match for %q in content for %s\n%s\n%q", match, filename, content, content) + } } } }