github.com/neohugo/neohugo@v0.123.8/hugolib/pagecollections.go (about) 1 // Copyright 2024 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package hugolib 15 16 import ( 17 "fmt" 18 "path" 19 "path/filepath" 20 "strings" 21 22 "github.com/neohugo/neohugo/hugofs" 23 "github.com/neohugo/neohugo/hugofs/files" 24 25 "github.com/neohugo/neohugo/common/paths" 26 27 "github.com/neohugo/neohugo/resources/kinds" 28 "github.com/neohugo/neohugo/resources/page" 29 ) 30 31 // pageFinder provides ways to find a Page in a Site. 32 type pageFinder struct { 33 pageMap *pageMap 34 } 35 36 func newPageFinder(m *pageMap) *pageFinder { 37 if m == nil { 38 panic("must provide a pageMap") 39 } 40 c := &pageFinder{pageMap: m} 41 return c 42 } 43 44 // getPageRef resolves a Page from ref/relRef, with a slightly more comprehensive 45 // search path than getPage. 46 func (c *pageFinder) getPageRef(context page.Page, ref string) (page.Page, error) { 47 n, err := c.getContentNode(context, true, ref) 48 if err != nil { 49 return nil, err 50 } 51 52 if p, ok := n.(page.Page); ok { 53 return p, nil 54 } 55 return nil, nil 56 } 57 58 func (c *pageFinder) getPage(context page.Page, ref string) (page.Page, error) { 59 n, err := c.getContentNode(context, false, ref) 60 if err != nil { 61 return nil, err 62 } 63 if p, ok := n.(page.Page); ok { 64 return p, nil 65 } 66 return nil, nil 67 } 68 69 // Only used in tests. 70 func (c *pageFinder) getPageOldVersion(kind string, sections ...string) page.Page { 71 refs := append([]string{kind}, path.Join(sections...)) 72 p, _ := c.getPageForRefs(refs...) 73 return p 74 } 75 76 // This is an adapter func for the old API with Kind as first argument. 77 // This is invoked when you do .Site.GetPage. We drop the Kind and fails 78 // if there are more than 2 arguments, which would be ambiguous. 79 func (c *pageFinder) getPageForRefs(ref ...string) (page.Page, error) { 80 var refs []string 81 for _, r := range ref { 82 // A common construct in the wild is 83 // .Site.GetPage "home" "" or 84 // .Site.GetPage "home" "/" 85 if r != "" && r != "/" { 86 refs = append(refs, r) 87 } 88 } 89 90 var key string 91 92 if len(refs) > 2 { 93 // This was allowed in Hugo <= 0.44, but we cannot support this with the 94 // new API. This should be the most unusual case. 95 return nil, fmt.Errorf(`too many arguments to .Site.GetPage: %v. Use lookups on the form {{ .Site.GetPage "/posts/mypage-md" }}`, ref) 96 } 97 98 if len(refs) == 0 || refs[0] == kinds.KindHome { 99 key = "/" 100 } else if len(refs) == 1 { 101 if len(ref) == 2 && refs[0] == kinds.KindSection { 102 // This is an old style reference to the "Home Page section". 103 // Typically fetched via {{ .Site.GetPage "section" .Section }} 104 // See https://github.com/gohugoio/hugo/issues/4989 105 key = "/" 106 } else { 107 key = refs[0] 108 } 109 } else { 110 key = refs[1] 111 } 112 113 key = filepath.ToSlash(key) 114 if !strings.HasPrefix(key, "/") { 115 key = "/" + key 116 } 117 118 return c.getPage(nil, key) 119 } 120 121 const defaultContentExt = ".md" 122 123 func (c *pageFinder) getContentNode(context page.Page, isReflink bool, ref string) (contentNodeI, error) { 124 ref = paths.ToSlashTrimTrailing(ref) 125 inRef := ref 126 if ref == "" { 127 ref = "/" 128 } 129 130 if paths.HasExt(ref) { 131 return c.getContentNodeForRef(context, isReflink, true, inRef, ref) 132 } 133 134 // We are always looking for a content file and having an extension greatly simplifies the code that follows, 135 // even in the case where the extension does not match this one. 136 if ref == "/" { 137 if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, "/_index"+defaultContentExt); n != nil || err != nil { 138 return n, err 139 } 140 } else if strings.HasSuffix(ref, "/index") { 141 if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, ref+"/index"+defaultContentExt); n != nil || err != nil { 142 return n, err 143 } 144 if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, ref+defaultContentExt); n != nil || err != nil { 145 return n, err 146 } 147 } else { 148 if n, err := c.getContentNodeForRef(context, isReflink, false, inRef, ref+defaultContentExt); n != nil || err != nil { 149 return n, err 150 } 151 } 152 153 return nil, nil 154 } 155 156 func (c *pageFinder) getContentNodeForRef(context page.Page, isReflink, hadExtension bool, inRef, ref string) (contentNodeI, error) { 157 s := c.pageMap.s 158 contentPathParser := s.Conf.PathParser() 159 160 if context != nil && !strings.HasPrefix(ref, "/") { 161 // Try the page-relative path first. 162 // Branch pages: /mysection, "./mypage" => /mysection/mypage 163 // Regular pages: /mysection/mypage.md, Path=/mysection/mypage, "./someotherpage" => /mysection/mypage/../someotherpage 164 // Regular leaf bundles: /mysection/mypage/index.md, Path=/mysection/mypage, "./someotherpage" => /mysection/mypage/../someotherpage 165 // Given the above, for regular pages we use the containing folder. 166 var baseDir string 167 if pi := context.PathInfo(); pi != nil { 168 if pi.IsBranchBundle() || (hadExtension && strings.HasPrefix(ref, "../")) { 169 baseDir = pi.Dir() 170 } else { 171 baseDir = pi.ContainerDir() 172 } 173 } 174 175 rel := path.Join(baseDir, ref) 176 177 relPath, _ := contentPathParser.ParseBaseAndBaseNameNoIdentifier(files.ComponentFolderContent, rel) 178 179 n, err := c.getContentNodeFromPath(relPath, ref) 180 if n != nil || err != nil { 181 return n, err 182 } 183 184 if hadExtension && context.File() != nil { 185 if n, err := c.getContentNodeFromRefReverseLookup(inRef, context.File().FileInfo()); n != nil || err != nil { 186 return n, err 187 } 188 } 189 190 } 191 192 if strings.HasPrefix(ref, ".") { 193 // Page relative, no need to look further. 194 return nil, nil 195 } 196 197 relPath, nameNoIdentifier := contentPathParser.ParseBaseAndBaseNameNoIdentifier(files.ComponentFolderContent, ref) 198 199 n, err := c.getContentNodeFromPath(relPath, ref) 200 201 if n != nil || err != nil { 202 return n, err 203 } 204 205 if hadExtension && s.home != nil && s.home.File() != nil { 206 if n, err := c.getContentNodeFromRefReverseLookup(inRef, s.home.File().FileInfo()); n != nil || err != nil { 207 return n, err 208 } 209 } 210 211 var doSimpleLookup bool 212 if isReflink || context == nil { 213 slashCount := strings.Count(inRef, "/") 214 if slashCount <= 1 { 215 doSimpleLookup = slashCount == 0 || ref[0] == '/' 216 } 217 } 218 219 if !doSimpleLookup { 220 return nil, nil 221 } 222 223 n = c.pageMap.pageReverseIndex.Get(nameNoIdentifier) 224 if n == ambiguousContentNode { 225 return nil, fmt.Errorf("page reference %q is ambiguous", inRef) 226 } 227 228 return n, nil 229 } 230 231 func (c *pageFinder) getContentNodeFromRefReverseLookup(ref string, fi hugofs.FileMetaInfo) (contentNodeI, error) { 232 s := c.pageMap.s 233 meta := fi.Meta() 234 dir := meta.Filename 235 if !fi.IsDir() { 236 dir = filepath.Dir(meta.Filename) 237 } 238 239 realFilename := filepath.Join(dir, ref) 240 241 pcs, err := s.BaseFs.Content.ReverseLookup(realFilename, true) 242 if err != nil { 243 return nil, err 244 } 245 246 // There may be multiple matches, but we will only use the first one. 247 for _, pc := range pcs { 248 pi := s.Conf.PathParser().Parse(pc.Component, pc.Path) 249 if n := c.pageMap.treePages.Get(pi.Base()); n != nil { 250 return n, nil 251 } 252 } 253 return nil, nil 254 } 255 256 func (c *pageFinder) getContentNodeFromPath(s string, ref string) (contentNodeI, error) { 257 n := c.pageMap.treePages.Get(s) 258 if n != nil { 259 return n, nil 260 } 261 262 return nil, nil 263 }