hugo/hugolib/pagecollections.go
2024-01-30 19:43:01 +01:00

270 lines
7.1 KiB
Go

// Copyright 2024 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"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/resources/kinds"
"github.com/gohugoio/hugo/resources/page"
)
// pageFinder provides ways to find a Page in a Site.
type pageFinder struct {
pageMap *pageMap
}
func newPageFinder(m *pageMap) *pageFinder {
if m == nil {
panic("must provide a pageMap")
}
c := &pageFinder{pageMap: m}
return c
}
// getPageRef resolves a Page from ref/relRef, with a slightly more comprehensive
// search path than getPage.
func (c *pageFinder) getPageRef(context page.Page, ref string) (page.Page, error) {
n, err := c.getContentNode(context, true, ref)
if err != nil {
return nil, err
}
if p, ok := n.(page.Page); ok {
return p, nil
}
return nil, nil
}
func (c *pageFinder) getPage(context page.Page, ref string) (page.Page, error) {
n, err := c.getContentNode(context, false, paths.ToSlashTrimTrailing(ref))
if err != nil {
return nil, err
}
if p, ok := n.(page.Page); ok {
return p, nil
}
return nil, nil
}
// Only used in tests.
func (c *pageFinder) getPageOldVersion(kind string, sections ...string) page.Page {
refs := append([]string{kind}, path.Join(sections...))
p, _ := c.getPageForRefs(refs...)
return p
}
// 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 ambiguous.
func (c *pageFinder) getPageForRefs(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] == kinds.KindHome {
key = "/"
} else if len(refs) == 1 {
if len(ref) == 2 && refs[0] == kinds.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.getPage(nil, key)
}
const defaultContentExt = ".md"
func (c *pageFinder) getContentNode(context page.Page, isReflink bool, ref string) (contentNodeI, error) {
inRef := ref
if ref == "" {
ref = "/"
}
if paths.HasExt(ref) {
return c.getContentNodeForRef(context, isReflink, true, inRef, ref)
}
var refs []string
// We are always looking for a content file and having an extension greatly simplifies the code that follows,
// even in the case where the extension does not match this one.
if ref == "/" {
refs = append(refs, "/_index"+defaultContentExt)
} else if strings.HasSuffix(ref, "/index") {
refs = append(refs, ref+"/index"+defaultContentExt)
refs = append(refs, ref+defaultContentExt)
} else {
refs = append(refs, ref+defaultContentExt)
}
for _, ref := range refs {
n, err := c.getContentNodeForRef(context, isReflink, false, inRef, ref)
if n != nil || err != nil {
return n, err
}
}
return nil, nil
}
func (c *pageFinder) getContentNodeForRef(context page.Page, isReflink, hadExtension bool, inRef, ref string) (contentNodeI, error) {
s := c.pageMap.s
contentPathParser := s.Conf.PathParser()
if context != nil && !strings.HasPrefix(ref, "/") {
// Try the page-relative path first.
// Branch pages: /mysection, "./mypage" => /mysection/mypage
// Regular pages: /mysection/mypage.md, Path=/mysection/mypage, "./someotherpage" => /mysection/mypage/../someotherpage
// Regular leaf bundles: /mysection/mypage/index.md, Path=/mysection/mypage, "./someotherpage" => /mysection/mypage/../someotherpage
// Given the above, for regular pages we use the containing folder.
var baseDir string
if pi := context.PathInfo(); pi != nil {
if pi.IsBranchBundle() || (hadExtension) {
baseDir = pi.Dir()
} else {
baseDir = pi.ContainerDir()
}
}
rel := path.Join(baseDir, inRef)
if !hadExtension && !paths.HasExt(rel) {
// See comment above.
rel += defaultContentExt
}
relPath := contentPathParser.Parse(files.ComponentFolderContent, rel)
n, err := c.getContentNodeFromPath(relPath, ref)
if n != nil || err != nil {
return n, err
}
if hadExtension && context.File() != nil {
if n, err := c.getContentNodeFromRefReverseLookup(inRef, context.File().FileInfo()); n != nil || err != nil {
return n, err
}
}
}
if strings.HasPrefix(ref, ".") {
// Page relative, no need to look further.
return nil, nil
}
refPath := contentPathParser.Parse(files.ComponentFolderContent, ref)
n, err := c.getContentNodeFromPath(refPath, ref)
if n != nil || err != nil {
return n, err
}
if hadExtension && s.home != nil && s.home.File() != nil {
if n, err := c.getContentNodeFromRefReverseLookup(inRef, s.home.File().FileInfo()); n != nil || err != nil {
return n, err
}
}
var doSimpleLookup bool
if isReflink || context == nil {
slashCount := strings.Count(inRef, "/")
if slashCount <= 1 {
doSimpleLookup = slashCount == 0 || ref[0] == '/'
}
}
if !doSimpleLookup {
return nil, nil
}
n = c.pageMap.pageReverseIndex.Get(refPath.BaseNameNoIdentifier())
if n == ambiguousContentNode {
return nil, fmt.Errorf("page reference %q is ambiguous", inRef)
}
return n, nil
}
func (c *pageFinder) getContentNodeFromRefReverseLookup(ref string, fi hugofs.FileMetaInfo) (contentNodeI, error) {
s := c.pageMap.s
meta := fi.Meta()
dir := meta.Filename
if !fi.IsDir() {
dir = filepath.Dir(meta.Filename)
}
realFilename := filepath.Join(dir, ref)
pcs, err := s.BaseFs.Content.ReverseLookup(realFilename)
if err != nil {
return nil, err
}
// There may be multiple matches, but we will only use the first one.
for _, pc := range pcs {
pi := s.Conf.PathParser().Parse(pc.Component, pc.Path)
if n := c.pageMap.treePages.Get(pi.Base()); n != nil {
return n, nil
}
}
return nil, nil
}
func (c *pageFinder) getContentNodeFromPath(refPath *paths.Path, ref string) (contentNodeI, error) {
s := refPath.Base()
n := c.pageMap.treePages.Get(s)
if n != nil {
return n, nil
}
return nil, nil
}