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)
+ }
}
}
}