diff options
-rw-r--r-- | docs/content/en/content-management/front-matter.md | 35 | ||||
-rw-r--r-- | hugofs/glob/glob.go | 34 | ||||
-rw-r--r-- | hugofs/glob/glob_test.go | 9 | ||||
-rw-r--r-- | hugolib/cascade_test.go | 70 | ||||
-rw-r--r-- | hugolib/content_map_page.go | 2 | ||||
-rw-r--r-- | hugolib/page__meta.go | 61 | ||||
-rw-r--r-- | hugolib/page_test.go | 21 | ||||
-rw-r--r-- | hugolib/pages_capture.go | 9 | ||||
-rw-r--r-- | resources/page/page_matcher.go | 91 | ||||
-rw-r--r-- | resources/page/page_matcher_test.go | 63 | ||||
-rw-r--r-- | resources/page/testhelpers_test.go | 9 |
11 files changed, 375 insertions, 29 deletions
diff --git a/docs/content/en/content-management/front-matter.md b/docs/content/en/content-management/front-matter.md index a14cb3065..097cf0598 100644 --- a/docs/content/en/content-management/front-matter.md +++ b/docs/content/en/content-management/front-matter.md @@ -159,6 +159,39 @@ show_comments: false Any node or section can pass down to descendents a set of Front Matter values as long as defined underneath the reserved `cascade` Front Matter key. +### Target Specific Pages + +{{< new-in "0.76.0" >}} + +Since Hugo 0.76 the `cascade` block can be a slice with a optional `_target` keyword, allowing for multiple `cascade` values targeting different page sets. + +{{< code-toggle copy="false" >}} +title ="Blog" +[[cascade]] +background = "yosemite.jpg" +[cascade._target] +path="/blog/**" +lang="en" +kind="page" +[[cascade]] +background = "goldenbridge.jpg" +[cascade._target] +kind="section" +{{</ code-toggle >}} + +Keywords available for `_target`: + +path +: A [Glob](https://github.com/gobwas/glob) pattern matching the content path below /content. Expects Unix-styled slashes. Note that this is the virtual path, so it starts at the mount root. + +kind +: A Glob pattern matching the Page's Kind(s), e.g. "{home,section}". + +lang +: A Glob pattern matching the Page's language, e.g. "{en,sv}". + +Any of the above can be omitted. + ### Example In `content/blog/_index.md` @@ -174,6 +207,8 @@ With the above example the Blog section page and its descendents will return `im - Said descendent has its own `banner` value set - Or a closer ancestor node has its own `cascade.banner` value set. + + ## Order Content Through Front Matter You can assign content-specific `weight` in the front matter of your content. These values are especially useful for [ordering][ordering] in list views. You can use `weight` for ordering of content and the convention of [`<TAXONOMY>_weight`][taxweight] for ordering content within a taxonomy. See [Ordering and Grouping Hugo Lists][lists] to see how `weight` can be used to organize your content in list views. diff --git a/hugofs/glob/glob.go b/hugofs/glob/glob.go index 124a3d50e..88485e1f0 100644 --- a/hugofs/glob/glob.go +++ b/hugofs/glob/glob.go @@ -23,30 +23,36 @@ import ( "github.com/gobwas/glob/syntax" ) +type globErr struct { + glob glob.Glob + err error +} + var ( - globCache = make(map[string]glob.Glob) + globCache = make(map[string]globErr) globMu sync.RWMutex ) func GetGlob(pattern string) (glob.Glob, error) { - var g glob.Glob + var eg globErr globMu.RLock() - g, found := globCache[pattern] + var found bool + eg, found = globCache[pattern] globMu.RUnlock() - if !found { - var err error - g, err = glob.Compile(strings.ToLower(pattern), '/') - if err != nil { - return nil, err - } - - globMu.Lock() - globCache[pattern] = g - globMu.Unlock() + if found { + return eg.glob, eg.err } - return g, nil + var err error + g, err := glob.Compile(strings.ToLower(pattern), '/') + eg = globErr{g, err} + + globMu.Lock() + globCache[pattern] = eg + globMu.Unlock() + + return eg.glob, eg.err } diff --git a/hugofs/glob/glob_test.go b/hugofs/glob/glob_test.go index cca8e4e0f..5a6ef5b7e 100644 --- a/hugofs/glob/glob_test.go +++ b/hugofs/glob/glob_test.go @@ -73,5 +73,14 @@ func TestGetGlob(t *testing.T) { g, err := GetGlob("**.JSON") c.Assert(err, qt.IsNil) c.Assert(g.Match("data/my.json"), qt.Equals, true) +} + +func BenchmarkGetGlob(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := GetGlob("**/foo") + if err != nil { + b.Fatal(err) + } + } } diff --git a/hugolib/cascade_test.go b/hugolib/cascade_test.go index 33fc7ceec..336acdcf3 100644 --- a/hugolib/cascade_test.go +++ b/hugolib/cascade_test.go @@ -229,7 +229,7 @@ Banner: post.jpg`, counters := &testCounters{} b.Build(BuildCfg{testCounters: counters}) - // As we only changed the content, not the cascade front matter, make + // As we only changed the content, not the cascade front matter, // only the home page is re-rendered. b.Assert(int(counters.contentRenderCounter), qt.Equals, 1) @@ -392,3 +392,71 @@ defaultContentLanguageInSubDir = false return b } + +func TestCascadeTarget(t *testing.T) { + t.Parallel() + + c := qt.New(t) + + newBuilder := func(c *qt.C) *sitesBuilder { + b := newTestSitesBuilder(c) + + b.WithTemplates("index.html", ` +{{ $p1 := site.GetPage "s1/p1" }} +{{ $s1 := site.GetPage "s1" }} + +P1|p1:{{ $p1.Params.p1 }}|p2:{{ $p1.Params.p2 }}| +S1|p1:{{ $s1.Params.p1 }}|p2:{{ $s1.Params.p2 }}| +`) + b.WithContent("s1/_index.md", "---\ntitle: s1 section\n---") + b.WithContent("s1/p1/index.md", "---\ntitle: p1\n---") + b.WithContent("s1/p2/index.md", "---\ntitle: p2\n---") + b.WithContent("s2/p1/index.md", "---\ntitle: p1_2\n---") + + return b + + } + + c.Run("slice", func(c *qt.C) { + b := newBuilder(c) + b.WithContent("_index.md", `+++ +title = "Home" +[[cascade]] +p1 = "p1" +[[cascade]] +p2 = "p2" ++++ +`) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", "P1|p1:p1|p2:p2") + + }) + + c.Run("slice with _target", func(c *qt.C) { + b := newBuilder(c) + + b.WithContent("_index.md", `+++ +title = "Home" +[[cascade]] +p1 = "p1" +[cascade._target] +path="**p1**" +[[cascade]] +p2 = "p2" +[cascade._target] +kind="section" ++++ +`) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", ` +P1|p1:p1|p2:| +S1|p1:|p2:p2| +`) + + }) + +} diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index b32f808c9..b2a8fda7e 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -789,7 +789,7 @@ func (m *pageMaps) withMaps(fn func(pm *pageMap) error) error { type pagesMapBucket struct { // Cascading front matter. - cascade maps.Params + cascade map[page.PageMatcher]maps.Params owner *pageState // The branch node diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index 0e16292f0..c7a8db3d5 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -308,12 +308,22 @@ func (p *pageMeta) Weight() int { func (pm *pageMeta) mergeBucketCascades(b1, b2 *pagesMapBucket) { if b1.cascade == nil { - b1.cascade = make(map[string]interface{}) + b1.cascade = make(map[page.PageMatcher]maps.Params) } + if b2 != nil && b2.cascade != nil { for k, v := range b2.cascade { - if _, found := b1.cascade[k]; !found { + + vv, found := b1.cascade[k] + if !found { b1.cascade[k] = v + } else { + // Merge + for ck, cv := range v { + if _, found := vv[ck]; !found { + vv[ck] = cv + } + } } } } @@ -332,14 +342,44 @@ func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, fron if p.bucket != nil { // Check for any cascade define on itself. if cv, found := frontmatter["cascade"]; found { - p.bucket.cascade = maps.ToStringMap(cv) + switch v := cv.(type) { + case []map[string]interface{}: + p.bucket.cascade = make(map[page.PageMatcher]maps.Params) + + for _, vv := range v { + var m page.PageMatcher + if mv, found := vv["_target"]; found { + err := page.DecodePageMatcher(mv, &m) + if err != nil { + return err + } + } + c, found := p.bucket.cascade[m] + if found { + // Merge + for k, v := range vv { + if _, found := c[k]; !found { + c[k] = v + } + } + } else { + p.bucket.cascade[m] = vv + } + + } + default: + p.bucket.cascade = map[page.PageMatcher]maps.Params{ + page.PageMatcher{}: maps.ToStringMap(cv), + } + } + } } } else { frontmatter = make(map[string]interface{}) } - var cascade map[string]interface{} + var cascade map[page.PageMatcher]maps.Params if p.bucket != nil { if parentBucket != nil { @@ -351,9 +391,14 @@ func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, fron cascade = parentBucket.cascade } - for k, v := range cascade { - if _, found := frontmatter[k]; !found { - frontmatter[k] = v + for m, v := range cascade { + if !m.Matches(p) { + continue + } + for kk, vv := range v { + if _, found := frontmatter[kk]; !found { + frontmatter[kk] = vv + } } } @@ -466,7 +511,7 @@ func (pm *pageMeta) setMetadata(parentBucket *pagesMapBucket, p *pageState, fron case "outputs": o := cast.ToStringSlice(v) if len(o) > 0 { - // Output formats are exlicitly set in front matter, use those. + // Output formats are explicitly set in front matter, use those. outFormats, err := p.s.outputFormatsConfig.GetByNames(o...) if err != nil { diff --git a/hugolib/page_test.go b/hugolib/page_test.go index 2df679a74..3a30216ed 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -1757,3 +1757,24 @@ $$$ `<pre><code class="language-bash {hl_lines=[1]}" data-lang="bash {hl_lines=[1]}">SHORT`, ) } + +func TestPageCaseIssues(t *testing.T) { + t.Parallel() + + b := newTestSitesBuilder(t) + b.WithConfigFile("toml", `defaultContentLanguage = "no" +[languages] +[languages.NO] +title = "Norsk" +`) + b.WithContent("a/B/C/Page1.md", "---\ntitle: Page1\n---") + b.WithTemplates("index.html", ` +{{ $p1 := site.GetPage "a/B/C/Page1" }} +Lang: {{ .Lang }} +Page1: {{ $p1.Path }} +`) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", "Lang: no", filepath.FromSlash("Page1: a/B/C/Page1.md")) +} diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go index ddb4bc495..4ee33992d 100644 --- a/hugolib/pages_capture.go +++ b/hugolib/pages_capture.go @@ -137,6 +137,7 @@ func (c *pagesCollector) isCascadingEdit(dir contentDirKey) (bool, string) { hasCascade := n.p.bucket.cascade != nil && len(n.p.bucket.cascade) > 0 if !ok { isCascade = hasCascade + return true } @@ -145,7 +146,12 @@ func (c *pagesCollector) isCascadingEdit(dir contentDirKey) (bool, string) { return true } - isCascade = !reflect.DeepEqual(cascade1, n.p.bucket.cascade) + for _, v := range n.p.bucket.cascade { + isCascade = !reflect.DeepEqual(cascade1, v) + if isCascade { + break + } + } return true @@ -187,6 +193,7 @@ func (c *pagesCollector) Collect() (collectErr error) { collectErr = c.collectDir(dir.dirname, true, nil) case bundleBranch: isCascading, section := c.isCascadingEdit(dir) + if isCascading { c.contentMap.deleteSection(section) } diff --git a/resources/page/page_matcher.go b/resources/page/page_matcher.go new file mode 100644 index 000000000..881f76e33 --- /dev/null +++ b/resources/page/page_matcher.go @@ -0,0 +1,91 @@ +// Copyright 2020 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 page + +import ( + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "github.com/gohugoio/hugo/hugofs/glob" + "github.com/mitchellh/mapstructure" +) + +// A PageMatcher can be used to match a Page with Glob patterns. +// Note that the pattern matching is case insensitive. +type PageMatcher struct { + // A Glob pattern matching the content path below /content. + // Expects Unix-styled slashes. + // Note that this is the virtual path, so it starts at the mount root + // with a leading "/". + Path string + + // A Glob pattern matching the Page's Kind(s), e.g. "{home,section}" + Kind string + + // A Glob pattern matching the Page's language, e.g. "{en,sv}". + Lang string +} + +// Matches returns whether p matches this matcher. +func (m PageMatcher) Matches(p Page) bool { + + if m.Kind != "" { + g, err := glob.GetGlob(m.Kind) + if err == nil && !g.Match(p.Kind()) { + return false + } + } + + if m.Lang != "" { + g, err := glob.GetGlob(m.Lang) + if err == nil && !g.Match(p.Lang()) { + return false + } + } + + if m.Path != "" { + g, err := glob.GetGlob(m.Path) + // TODO(bep) Path() vs filepath vs leading slash. + p := strings.ToLower(filepath.ToSlash(p.Path())) + if !(strings.HasPrefix(p, "/")) { + p = "/" + p + } + if err == nil && !g.Match(p) { + return false + } + } + + return true +} + +// DecodePageMatcher decodes m into v. +func DecodePageMatcher(m interface{}, v *PageMatcher) error { + if err := mapstructure.WeakDecode(m, v); err != nil { + return err + } + + v.Kind = strings.ToLower(v.Kind) + if v.Kind != "" { + if _, found := kindMap[v.Kind]; !found { + return errors.Errorf("%q is not a valid Page Kind", v.Kind) + } + } + + v.Path = filepath.ToSlash(strings.ToLower(v.Path)) + + return nil + +} diff --git a/resources/page/page_matcher_test.go b/resources/page/page_matcher_test.go new file mode 100644 index 000000000..24b304772 --- /dev/null +++ b/resources/page/page_matcher_test.go @@ -0,0 +1,63 @@ +// Copyright 2020 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 page + +import ( + "path/filepath" + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestPageMatcher(t *testing.T) { + c := qt.New(t) + + p1, p2, p3 := &testPage{path: "/p1", kind: "section", lang: "en"}, &testPage{path: "p2", kind: "page", lang: "no"}, &testPage{path: "p3", kind: "page", lang: "en"} + + c.Run("Matches", func(c *qt.C) { + m := PageMatcher{Kind: "section"} + + c.Assert(m.Matches(p1), qt.Equals, true) + c.Assert(m.Matches(p2), qt.Equals, false) + + m = PageMatcher{Kind: "page"} + c.Assert(m.Matches(p1), qt.Equals, false) + c.Assert(m.Matches(p2), qt.Equals, true) + c.Assert(m.Matches(p3), qt.Equals, true) + + m = PageMatcher{Kind: "page", Path: "/p2"} + c.Assert(m.Matches(p1), qt.Equals, false) + c.Assert(m.Matches(p2), qt.Equals, true) + c.Assert(m.Matches(p3), qt.Equals, false) + + m = PageMatcher{Path: "/p*"} + c.Assert(m.Matches(p1), qt.Equals, true) + c.Assert(m.Matches(p2), qt.Equals, true) + c.Assert(m.Matches(p3), qt.Equals, true) + + m = PageMatcher{Lang: "en"} + c.Assert(m.Matches(p1), qt.Equals, true) + c.Assert(m.Matches(p2), qt.Equals, false) + c.Assert(m.Matches(p3), qt.Equals, true) + + }) + + c.Run("Decode", func(c *qt.C) { + var v PageMatcher + c.Assert(DecodePageMatcher(map[string]interface{}{"kind": "foo"}, &v), qt.Not((qt.IsNil))) + c.Assert(DecodePageMatcher(map[string]interface{}{"kind": "home", "path": filepath.FromSlash("/a/b/**")}, &v), qt.IsNil) + c.Assert(v, qt.Equals, PageMatcher{Kind: "home", Path: "/a/b/**"}) + }) + +} diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index 17a795a20..54a908d3b 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -85,11 +85,12 @@ func newTestPathSpecFor(cfg config.Provider) *helpers.PathSpec { } type testPage struct { + kind string description string title string linkTitle string - - section string + lang string + section string content string @@ -297,11 +298,11 @@ func (p *testPage) Keywords() []string { } func (p *testPage) Kind() string { - panic("not implemented") + return p.kind } func (p *testPage) Lang() string { - panic("not implemented") + return p.lang } func (p *testPage) Language() *langs.Language { |