Implement cascading front matter
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 9 Aug 2019 08:05:22 +0000 (10:05 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 10 Aug 2019 18:07:42 +0000 (20:07 +0200)
Fixes #6041

hugolib/cascade_test.go [new file with mode: 0644]
hugolib/collections_test.go
hugolib/hugo_sites_build.go
hugolib/page.go
hugolib/page__common.go
hugolib/page__meta.go
hugolib/page__new.go
hugolib/pagecollections.go
hugolib/pages_map.go
hugolib/site.go
hugolib/testhelpers_test.go

diff --git a/hugolib/cascade_test.go b/hugolib/cascade_test.go
new file mode 100644 (file)
index 0000000..aebd7a8
--- /dev/null
@@ -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", `<link>https://example.org/sect4/index.xml</link>`)
+               b.AssertFileContent("public/sect4/p1/index.xml", `<link>https://example.org/sect4/p1/index.xml</link>`)
+               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
+}
index 1a261260ec0d7d63e9e0e0254b0feaa9aa5c1713..804c0cae19f257e8a40ef77ce8e8ae12e9ba5794 100644 (file)
@@ -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)",
index 82a189a50b223f266ca521f84b760034d78aa173..a70a19e7c31724acfc685fe1c7505109dfab4a40 100644 (file)
@@ -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 {
index 8dda33009599b7cd3450042aa5a73058f0b9904c..306ca7b0fbe5de0545eca3ad4e8c88d05c372df4 100644 (file)
@@ -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
                        }
 
index cf554bb40ad5a58193aa9b0f1642fad884f3a384..b13a71a401f23c07b339a47f8de6e0814b775fb1 100644 (file)
@@ -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
index 551f479774456e616fe6862d0da2615dfe40d318..e8ef13bfde2c513046d68ac2fdd5c9e3ba8a4f77 100644 (file)
@@ -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 {
index 64c84b0f8b01dcb5b0180bcaa70c207273bcf5c3..99bf305aa58d474f244fca6b77f05885361d55eb 100644 (file)
@@ -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) {
index 1c8bed9d9ea9e4d42e13316f743c4db64985e655..52eb661569f24fa1937f56dc694431d64ba00de0 100644 (file)
@@ -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 {
index 26e937fd2e4ffcc00f87781b8be181d759d70425..26bbedec6df39b37f2f174f403ee61358a7c2e9d 100644 (file)
@@ -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 {
-               b := v.(*pagesMapBucket)
-               fmt.Println("-------\n", s, ":", b.owner.Kind(), ":")
-               if b.owner != nil {
-                       fmt.Println("Owner:", b.owner.Path())
+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
+                       }
                }
-               for _, p := range b.pages {
-                       fmt.Println(p.Path())
+       }
+}
+
+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)
+               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.
index 2b8a7285a5216f22ce24968587f8d83e1ad57a6b..bf07d52b15195ae065ad5858194b47b0fbf5cb8b 100644 (file)
@@ -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)
index d7e0d5c85c5db75bad8957f00e738b073e6b0e17..25ebbf1257852b1ad3c04393d492c3c39e8c7fb2 100644 (file)
@@ -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)
+                       }
                }
        }
 }