hugo/hugolib/pagecollections.go
Bjørn Erik Pedersen a985efcecf 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
2020-05-24 12:35:45 +02:00

341 lines
8.2 KiB
Go

// 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 (
"fmt"
"path"
"path/filepath"
"strings"
"sync"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resources/page"
)
// PageCollections contains the page collections for a site.
type PageCollections struct {
pageMap *pageMap
// Lazy initialized page collections
pages *lazyPagesFactory
regularPages *lazyPagesFactory
allPages *lazyPagesFactory
allRegularPages *lazyPagesFactory
}
// Pages returns all pages.
// This is for the current language only.
func (c *PageCollections) Pages() page.Pages {
return c.pages.get()
}
// RegularPages returns all the regular pages.
// This is for the current language only.
func (c *PageCollections) RegularPages() page.Pages {
return c.regularPages.get()
}
// AllPages returns all pages for all languages.
func (c *PageCollections) AllPages() page.Pages {
return c.allPages.get()
}
// AllPages returns all regular pages for all languages.
func (c *PageCollections) AllRegularPages() page.Pages {
return c.allRegularPages.get()
}
type lazyPagesFactory struct {
pages page.Pages
init sync.Once
factory page.PagesFactory
}
func (l *lazyPagesFactory) get() page.Pages {
l.init.Do(func() {
l.pages = l.factory()
})
return l.pages
}
func newLazyPagesFactory(factory page.PagesFactory) *lazyPagesFactory {
return &lazyPagesFactory{factory: factory}
}
func newPageCollections(m *pageMap) *PageCollections {
if m == nil {
panic("must provide a pageMap")
}
c := &PageCollections{pageMap: m}
c.pages = newLazyPagesFactory(func() page.Pages {
return m.createListAllPages()
})
c.regularPages = newLazyPagesFactory(func() page.Pages {
return c.findPagesByKindIn(page.KindPage, c.pages.get())
})
return c
}
// This is an adapter func for the old API with Kind as first argument.
// This is invoked when you do .Site.GetPage. We drop the Kind and fails
// if there are more than 2 arguments, which would be ambigous.
func (c *PageCollections) getPageOldVersion(ref ...string) (page.Page, error) {
var refs []string
for _, r := range ref {
// A common construct in the wild is
// .Site.GetPage "home" "" or
// .Site.GetPage "home" "/"
if r != "" && r != "/" {
refs = append(refs, r)
}
}
var key string
if len(refs) > 2 {
// This was allowed in Hugo <= 0.44, but we cannot support this with the
// new API. This should be the most unusual case.
return nil, fmt.Errorf(`too many arguments to .Site.GetPage: %v. Use lookups on the form {{ .Site.GetPage "/posts/mypage-md" }}`, ref)
}
if len(refs) == 0 || refs[0] == page.KindHome {
key = "/"
} else if len(refs) == 1 {
if len(ref) == 2 && refs[0] == page.KindSection {
// This is an old style reference to the "Home Page section".
// Typically fetched via {{ .Site.GetPage "section" .Section }}
// See https://github.com/gohugoio/hugo/issues/4989
key = "/"
} else {
key = refs[0]
}
} else {
key = refs[1]
}
key = filepath.ToSlash(key)
if !strings.HasPrefix(key, "/") {
key = "/" + key
}
return c.getPageNew(nil, key)
}
// Only used in tests.
func (c *PageCollections) getPage(typ string, sections ...string) page.Page {
refs := append([]string{typ}, path.Join(sections...))
p, _ := c.getPageOldVersion(refs...)
return p
}
// getPageRef resolves a Page from ref/relRef, with a slightly more comprehensive
// search path than getPageNew.
func (c *PageCollections) getPageRef(context page.Page, ref string) (page.Page, error) {
n, err := c.getContentNode(context, true, ref)
if err != nil || n == nil || n.p == nil {
return nil, err
}
return n.p, nil
}
func (c *PageCollections) getPageNew(context page.Page, ref string) (page.Page, error) {
n, err := c.getContentNode(context, false, ref)
if err != nil || n == nil || n.p == nil {
return nil, err
}
return n.p, nil
}
func (c *PageCollections) getSectionOrPage(ref string) (*contentNode, string) {
var n *contentNode
pref := helpers.AddTrailingSlash(ref)
s, v, found := c.pageMap.sections.LongestPrefix(pref)
if found {
n = v.(*contentNode)
}
if found && s == pref {
// A section
return n, ""
}
m := c.pageMap
filename := strings.TrimPrefix(strings.TrimPrefix(ref, s), "/")
langSuffix := "." + m.s.Lang()
// Trim both extension and any language code.
name := helpers.PathNoExt(filename)
name = strings.TrimSuffix(name, langSuffix)
// These are reserved bundle names and will always be stored by their owning
// folder name.
name = strings.TrimSuffix(name, "/index")
name = strings.TrimSuffix(name, "/_index")
if !found {
return nil, name
}
// Check if it's a section with filename provided.
if !n.p.File().IsZero() && n.p.File().LogicalName() == filename {
return n, name
}
return m.getPage(s, name), name
}
// For Ref/Reflink and .Site.GetPage do simple name lookups for the potentially ambigous myarticle.md and /myarticle.md,
// but not when we get ./myarticle*, section/myarticle.
func shouldDoSimpleLookup(ref string) bool {
if ref[0] == '.' {
return false
}
slashCount := strings.Count(ref, "/")
if slashCount > 1 {
return false
}
return slashCount == 0 || ref[0] == '/'
}
func (c *PageCollections) getContentNode(context page.Page, isReflink bool, ref string) (*contentNode, error) {
ref = filepath.ToSlash(strings.ToLower(strings.TrimSpace(ref)))
if ref == "" {
ref = "/"
}
inRef := ref
navUp := strings.HasPrefix(ref, "..")
var doSimpleLookup bool
if isReflink || context == nil {
doSimpleLookup = shouldDoSimpleLookup(ref)
}
if context != nil && !strings.HasPrefix(ref, "/") {
// Try the page-relative path.
var base string
if context.File().IsZero() {
base = context.SectionsPath()
} else {
meta := context.File().FileInfo().Meta()
base = filepath.ToSlash(filepath.Dir(meta.Path()))
if meta.Classifier() == files.ContentClassLeaf {
// Bundles are stored in subfolders e.g. blog/mybundle/index.md,
// so if the user has not explicitly asked to go up,
// look on the "blog" level.
if !navUp {
base = path.Dir(base)
}
}
}
ref = path.Join("/", strings.ToLower(base), ref)
}
if !strings.HasPrefix(ref, "/") {
ref = "/" + ref
}
m := c.pageMap
// It's either a section, a page in a section or a taxonomy node.
// Start with the most likely:
n, name := c.getSectionOrPage(ref)
if n != nil {
return n, nil
}
if !strings.HasPrefix(inRef, "/") {
// Many people will have "post/foo.md" in their content files.
if n, _ := c.getSectionOrPage("/" + inRef); n != nil {
return n, nil
}
}
// Check if it's a taxonomy node
pref := helpers.AddTrailingSlash(ref)
s, v, found := m.taxonomies.LongestPrefix(pref)
if found {
if !m.onSameLevel(pref, s) {
return nil, nil
}
return v.(*contentNode), nil
}
getByName := func(s string) (*contentNode, error) {
n := m.pageReverseIndex.Get(s)
if n != nil {
if n == ambigousContentNode {
return nil, fmt.Errorf("page reference %q is ambiguous", ref)
}
return n, nil
}
return nil, nil
}
var module string
if context != nil && !context.File().IsZero() {
module = context.File().FileInfo().Meta().Module()
}
if module == "" && !c.pageMap.s.home.File().IsZero() {
module = c.pageMap.s.home.File().FileInfo().Meta().Module()
}
if module != "" {
n, err := getByName(module + ref)
if err != nil {
return nil, err
}
if n != nil {
return n, nil
}
}
if !doSimpleLookup {
return nil, nil
}
// Ref/relref supports this potentially ambigous lookup.
return getByName(path.Base(name))
}
func (*PageCollections) findPagesByKindIn(kind string, inPages page.Pages) page.Pages {
var pages page.Pages
for _, p := range inPages {
if p.Kind() == kind {
pages = append(pages, p)
}
}
return pages
}