Fix GetPage on section/bundle name overlaps

In the internal Radix we stored the directory based nodes without a traling slash, e.g. `/blog`.

The original motivation was probably to make it easy to do prefix searching: Give me all ancestors.

This, however have lead to some ambigouty with overlapping directory names.

This particular problem was, however, not possible to work around in an easy way, so from now we store these as `/blog/`.

Fixes #7301
This commit is contained in:
Bjørn Erik Pedersen
2020-05-21 11:25:00 +02:00
parent 6c3c6686f5
commit a985efcecf
10 changed files with 212 additions and 163 deletions

View File

@@ -20,6 +20,8 @@ import (
"strings"
"sync"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/page"
"github.com/pkg/errors"
@@ -31,27 +33,26 @@ import (
)
// We store the branch nodes in either the `sections` or `taxonomies` tree
// with their path as a key; Unix style slashes, a leading slash but no
// trailing slash.
// with their path as a key; Unix style slashes, a leading and trailing slash.
//
// E.g. "/blog" or "/categories/funny"
// E.g. "/blog/" or "/categories/funny/"
//
// Pages that belongs to a section are stored in the `pages` tree below
// the section name and a branch separator, e.g. "/blog__hb_". A page is
// the section name and a branch separator, e.g. "/blog/__hb_". A page is
// given a key using the path below the section and the base filename with no extension
// with a leaf separator added.
//
// For bundled pages (/mybundle/index.md), we use the folder name.
//
// An exmple of a full page key would be "/blog__hb_/page1__hl_"
// An exmple of a full page key would be "/blog/__hb_page1__hl_"
//
// Bundled resources are stored in the `resources` having their path prefixed
// with the bundle they belong to, e.g.
// "/blog__hb_/bundle__hl_data.json".
// "/blog/__hb_bundle__hl_data.json".
//
// The weighted taxonomy entries extracted from page front matter are stored in
// the `taxonomyEntries` tree below /plural/term/page-key, e.g.
// "/categories/funny/blog__hb_/bundle__hl_".
// "/categories/funny/blog/__hb_bundle__hl_".
const (
cmBranchSeparator = "__hb_"
cmLeafSeparator = "__hl_"
@@ -105,7 +106,7 @@ func newContentMap(cfg contentMapConfig) *contentMap {
addToReverseMap(mountKey, n, m)
}
}
k := strings.TrimSuffix(path.Base(s), cmLeafSeparator)
k := strings.TrimPrefix(strings.TrimSuffix(path.Base(s), cmLeafSeparator), cmBranchSeparator)
addToReverseMap(k, n, m)
return false
})
@@ -127,18 +128,15 @@ type cmInsertKeyBuilder struct {
}
func (b cmInsertKeyBuilder) ForPage(s string) *cmInsertKeyBuilder {
// TODO2 fmt.Println("ForPage:", s, "baseKey:", b.baseKey, "key:", b.key)
//fmt.Println("ForPage:", s, "baseKey:", b.baseKey, "key:", b.key)
baseKey := b.baseKey
b.baseKey = s
if !strings.HasPrefix(s, "/") {
s = "/" + s
}
if baseKey != "/" {
// Don't repeat the section path in the key.
s = strings.TrimPrefix(s, baseKey)
}
s = strings.TrimPrefix(s, "/")
switch b.tree {
case b.m.sections:
@@ -154,10 +152,10 @@ func (b cmInsertKeyBuilder) ForPage(s string) *cmInsertKeyBuilder {
}
func (b cmInsertKeyBuilder) ForResource(s string) *cmInsertKeyBuilder {
// TODO2 fmt.Println("ForResource:", s, "baseKey:", b.baseKey, "key:", b.key)
//fmt.Println("ForResource:", s, "baseKey:", b.baseKey, "key:", b.key)
s = strings.TrimPrefix(s, "/")
s = strings.TrimPrefix(s, strings.TrimPrefix(b.baseKey, "/")+"/")
baseKey := helpers.AddTrailingSlash(b.baseKey)
s = strings.TrimPrefix(s, baseKey)
switch b.tree {
case b.m.pages:
@@ -173,14 +171,23 @@ func (b cmInsertKeyBuilder) ForResource(s string) *cmInsertKeyBuilder {
func (b *cmInsertKeyBuilder) Insert(n *contentNode) *cmInsertKeyBuilder {
if b.err == nil {
b.tree.Insert(cleanTreeKey(b.key), n)
b.tree.Insert(b.Key(), n)
}
return b
}
func (b *cmInsertKeyBuilder) Key() string {
switch b.tree {
case b.m.sections, b.m.taxonomies:
return cleanSectionTreeKey(b.key)
default:
return cleanTreeKey(b.key)
}
}
func (b *cmInsertKeyBuilder) DeleteAll() *cmInsertKeyBuilder {
if b.err == nil {
b.tree.DeletePrefix(cleanTreeKey(b.key))
b.tree.DeletePrefix(b.Key())
}
return b
}
@@ -211,15 +218,16 @@ func (b *cmInsertKeyBuilder) WithFile(fi hugofs.FileMetaInfo) *cmInsertKeyBuilde
}
func (b *cmInsertKeyBuilder) WithSection(s string) *cmInsertKeyBuilder {
s = cleanSectionTreeKey(s)
b.newTopLevel()
b.tree = b.m.sections
b.baseKey = s
b.key = s
// TODO2 fmt.Println("WithSection:", s, "baseKey:", b.baseKey, "key:", b.key)
return b
}
func (b *cmInsertKeyBuilder) WithTaxonomy(s string) *cmInsertKeyBuilder {
s = cleanSectionTreeKey(s)
b.newTopLevel()
b.tree = b.m.taxonomies
b.baseKey = s
@@ -233,20 +241,17 @@ func (b *cmInsertKeyBuilder) getBundle(s string) (string, string) {
m := b.m
section, _ := m.getSection(s)
p := s
if section != "/" {
p = strings.TrimPrefix(s, section)
}
p := strings.TrimPrefix(s, section)
bundlePathParts := strings.Split(p, "/")[1:]
bundlePathParts := strings.Split(p, "/")
basePath := section + cmBranchSeparator
// Put it into an existing bundle if found.
for i := len(bundlePathParts) - 2; i >= 0; i-- {
bundlePath := path.Join(bundlePathParts[:i]...)
searchKey := basePath + "/" + bundlePath + cmLeafSeparator
searchKey := basePath + bundlePath + cmLeafSeparator
if _, found := m.pages.Get(searchKey); found {
return section + "/" + bundlePath, searchKey
return section + bundlePath, searchKey
}
}
@@ -432,7 +437,7 @@ func (m *contentMap) CreateMissingNodes() error {
sectionPath = sectionPath[:firstSlash]
}
}
sect = cleanTreeKey(sect)
sect = cleanSectionTreeKey(sect)
_, found := m.sections.Get(sect)
if !found {
m.sections.Insert(sect, &contentNode{path: sectionPath})
@@ -440,7 +445,7 @@ func (m *contentMap) CreateMissingNodes() error {
}
for _, view := range m.cfg.taxonomyConfig {
s := cleanTreeKey(view.plural)
s := cleanSectionTreeKey(view.plural)
_, found := m.taxonomies.Get(s)
if !found {
b := &contentNode{
@@ -476,15 +481,20 @@ func (m *contentMap) newContentNodeFromFi(fi hugofs.FileMetaInfo) *contentNode {
}
func (m *contentMap) getFirstSection(s string) (string, *contentNode) {
s = helpers.AddTrailingSlash(s)
for {
k, v, found := m.sections.LongestPrefix(s)
if !found {
return "", nil
}
if strings.Count(k, "/") == 1 {
if strings.Count(k, "/") <= 2 {
return k, v.(*contentNode)
}
s = path.Dir(s)
s = helpers.AddTrailingSlash(path.Dir(strings.TrimSuffix(s, "/")))
}
}
@@ -507,10 +517,7 @@ func (m *contentMap) getOrCreateSection(n *contentNode, s string) (string, *cont
}
if mustCreate {
k = s[:strings.Index(s[1:], "/")+1]
if k == "" {
k = "/"
}
k = cleanSectionTreeKey(s[:strings.Index(s[1:], "/")+1])
b = &contentNode{
path: n.rootSection(),
@@ -523,7 +530,9 @@ func (m *contentMap) getOrCreateSection(n *contentNode, s string) (string, *cont
}
func (m *contentMap) getPage(section, name string) *contentNode {
key := section + cmBranchSeparator + "/" + name + cmLeafSeparator
section = helpers.AddTrailingSlash(section)
key := section + cmBranchSeparator + name + cmLeafSeparator
v, found := m.pages.Get(key)
if found {
return v.(*contentNode)
@@ -532,7 +541,9 @@ func (m *contentMap) getPage(section, name string) *contentNode {
}
func (m *contentMap) getSection(s string) (string, *contentNode) {
k, v, found := m.sections.LongestPrefix(path.Dir(s))
s = helpers.AddTrailingSlash(path.Dir(strings.TrimSuffix(s, "/")))
k, v, found := m.sections.LongestPrefix(s)
if found {
return k, v.(*contentNode)
@@ -541,21 +552,18 @@ func (m *contentMap) getSection(s string) (string, *contentNode) {
}
func (m *contentMap) getTaxonomyParent(s string) (string, *contentNode) {
s = path.Dir(s)
if s == "/" {
v, found := m.sections.Get(s)
if found {
return s, v.(*contentNode)
}
return "", nil
s = helpers.AddTrailingSlash(path.Dir(strings.TrimSuffix(s, "/")))
k, v, found := m.taxonomies.LongestPrefix(s)
if found {
return k, v.(*contentNode)
}
for _, tree := range []*contentTree{m.taxonomies, m.sections} {
k, v, found := tree.LongestPrefix(s)
if found {
return k, v.(*contentNode)
}
v, found = m.sections.Get("/")
if found {
return s, v.(*contentNode)
}
return "", nil
}
@@ -569,6 +577,15 @@ func cleanTreeKey(k string) string {
return k
}
func cleanSectionTreeKey(k string) string {
k = cleanTreeKey(k)
if k != "/" {
k += "/"
}
return k
}
func (m *contentMap) onSameLevel(s1, s2 string) bool {
return strings.Count(s1, "/") == strings.Count(s2, "/")
}
@@ -606,13 +623,13 @@ func (m *contentMap) deleteOrphanSections() {
return false
}
if s == "/" || strings.Count(s, "/") > 1 {
if s == "/" || strings.Count(s, "/") > 2 {
return false
}
prefixBundle := s + cmBranchSeparator
if !(m.sections.hasPrefix(s+"/") || m.pages.hasPrefix(prefixBundle) || m.resources.hasPrefix(prefixBundle)) {
if !(m.sections.hasBelow(s) || m.pages.hasBelow(prefixBundle) || m.resources.hasBelow(prefixBundle)) {
sectionsToDelete = append(sectionsToDelete, s)
}
@@ -630,13 +647,15 @@ func (m *contentMap) deletePage(s string) {
}
func (m *contentMap) deleteSectionByPath(s string) {
m.sections.Delete(s)
m.sections.DeletePrefix(s + "/")
m.pages.DeletePrefix(s + cmBranchSeparator)
m.pages.DeletePrefix(s + "/")
m.resources.DeletePrefix(s + cmBranchSeparator)
m.resources.DeletePrefix(s + cmLeafSeparator)
m.resources.DeletePrefix(s + "/")
if !strings.HasSuffix(s, "/") {
panic("section must end with a slash")
}
if !strings.HasPrefix(s, "/") {
panic("section must start with a slash")
}
m.sections.DeletePrefix(s)
m.pages.DeletePrefix(s)
m.resources.DeletePrefix(s)
}
func (m *contentMap) deletePageByPath(s string) {
@@ -648,8 +667,7 @@ func (m *contentMap) deletePageByPath(s string) {
}
func (m *contentMap) deleteTaxonomy(s string) {
m.taxonomies.Delete(s)
m.taxonomies.DeletePrefix(s + "/")
m.taxonomies.DeletePrefix(s)
}
func (m *contentMap) reduceKeyPart(dir, filename string) string {
@@ -817,7 +835,7 @@ func (c *contentTree) WalkQuery(query pageMapQuery, walkFn contentTreeNodeCallba
filter = contentTreeNoListAlwaysFilter
}
if query.Prefix != "" {
c.WalkPrefix(query.Prefix, func(s string, v interface{}) bool {
c.WalkBelow(query.Prefix, func(s string, v interface{}) bool {
n := v.(*contentNode)
if filter != nil && filter(s, n) {
return false
@@ -862,6 +880,18 @@ func (c contentTrees) WalkPrefix(prefix string, fn contentTreeNodeCallback) {
}
}
// WalkBelow walks the tree below the given prefix, i.e. it skips the
// node with the given prefix as key.
func (c *contentTree) WalkBelow(prefix string, fn radix.WalkFn) {
c.Tree.WalkPrefix(prefix, func(s string, v interface{}) bool {
if s == prefix {
return false
}
return fn(s, v)
})
}
func (c *contentTree) getMatch(matches func(b *contentNode) bool) string {
var match string
c.Walk(func(s string, v interface{}) bool {
@@ -881,9 +911,9 @@ func (c *contentTree) getMatch(matches func(b *contentNode) bool) string {
return match
}
func (c *contentTree) hasPrefix(s string) bool {
func (c *contentTree) hasBelow(s1 string) bool {
var t bool
c.Tree.WalkPrefix(s, func(s string, v interface{}) bool {
c.WalkBelow(s1, func(s2 string, v interface{}) bool {
t = true
return true
})
@@ -953,12 +983,7 @@ func (c *contentTreeRef) getPagesRecursive() page.Pages {
Filter: c.n.p.m.getListFilter(true),
}
query.Prefix = c.key + cmBranchSeparator
c.m.collectPages(query, func(c *contentNode) {
pas = append(pas, c.p)
})
query.Prefix = c.key + "/"
query.Prefix = c.key
c.m.collectPages(query, func(c *contentNode) {
pas = append(pas, c.p)
})