ref: bd98182dbde893a8a809661c70633741bbf63911
parent: e88d7989907108b656eccd92bccc076be72a5c03
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Fri Aug 9 06:05:22 EDT 2019
Implement cascading front matter Fixes #6041
--- /dev/null
+++ b/hugolib/cascade_test.go
@@ -1,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
+}
--- a/hugolib/collections_test.go
+++ b/hugolib/collections_test.go
@@ -178,7 +178,6 @@
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 @@
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)",
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -19,7 +19,10 @@
"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) 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 @@
}
}
- 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 {--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -520,7 +520,7 @@
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 @@
}
}
- if err := meta.setMetadata(p, m); err != nil {+ if err := meta.setMetadata(bucket, p, m); err != nil {return err
}
--- a/hugolib/page__common.go
+++ b/hugolib/page__common.go
@@ -35,6 +35,9 @@
// 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
--- a/hugolib/page__meta.go
+++ b/hugolib/page__meta.go
@@ -306,19 +306,51 @@
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 @@
Params: pm.params,
Dates: &pm.Dates,
PageURLs: &pm.urlPaths,
- BaseFilename: p.File().ContentBaseName(),
+ BaseFilename: contentBaseName,
ModTime: mtime,
GitAuthorDate: gitAuthorDate,
}
@@ -546,7 +578,7 @@
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 {--- a/hugolib/page__new.go
+++ b/hugolib/page__new.go
@@ -95,7 +95,7 @@
}
-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,10 +105,28 @@
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) {pp, err := newPagePaths(metaProvider.s, ps, metaProvider)
if err != nil {@@ -152,7 +170,7 @@
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 @@
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) {--- a/hugolib/pagecollections.go
+++ b/hugolib/pagecollections.go
@@ -387,6 +387,7 @@
}
func (c *PageCollections) assemblePagesMap(s *Site) error {+
c.pagesMap = newPagesMap(s)
rootSections := make(map[string]bool)
@@ -437,18 +438,14 @@
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
@@ -535,6 +532,10 @@
return false
})
+
+ if walkErr != nil {+ return walkErr
+ }
c.pagesMap.s.lastmod = siteLastmod
--- a/hugolib/pages_map.go
+++ b/hugolib/pages_map.go
@@ -68,6 +68,43 @@
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 @@
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 @@
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 @@
}
- 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 @@
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 @@
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 @@
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 @@
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())+ f(b.owner)
+ if !b.view {+ for _, p := range b.pages {+ f(p.(*pageState))
+ }
}
- for _, p := range b.pages {- fmt.Println(p.Path())
- }
+
return false
})
}
@@ -311,6 +386,9 @@
// Some additional metatadata attached to this node.
meta map[string]interface{}+
+ // Cascading front matter.
+ cascade map[string]interface{}owner *pageState // The branch node
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -1650,12 +1650,13 @@
}
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) 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)
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -649,9 +649,16 @@
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)+ }
}
}
}
--
⑨