pagecollections.go (8447B)
1 // Copyright 2019 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 "sync" 22 23 "github.com/gohugoio/hugo/common/paths" 24 25 "github.com/gohugoio/hugo/hugofs/files" 26 27 "github.com/gohugoio/hugo/helpers" 28 29 "github.com/gohugoio/hugo/resources/page" 30 ) 31 32 // PageCollections contains the page collections for a site. 33 type PageCollections struct { 34 pageMap *pageMap 35 36 // Lazy initialized page collections 37 pages *lazyPagesFactory 38 regularPages *lazyPagesFactory 39 allPages *lazyPagesFactory 40 allRegularPages *lazyPagesFactory 41 } 42 43 // Pages returns all pages. 44 // This is for the current language only. 45 func (c *PageCollections) Pages() page.Pages { 46 return c.pages.get() 47 } 48 49 // RegularPages returns all the regular pages. 50 // This is for the current language only. 51 func (c *PageCollections) RegularPages() page.Pages { 52 return c.regularPages.get() 53 } 54 55 // AllPages returns all pages for all languages. 56 func (c *PageCollections) AllPages() page.Pages { 57 return c.allPages.get() 58 } 59 60 // AllPages returns all regular pages for all languages. 61 func (c *PageCollections) AllRegularPages() page.Pages { 62 return c.allRegularPages.get() 63 } 64 65 type lazyPagesFactory struct { 66 pages page.Pages 67 68 init sync.Once 69 factory page.PagesFactory 70 } 71 72 func (l *lazyPagesFactory) get() page.Pages { 73 l.init.Do(func() { 74 l.pages = l.factory() 75 }) 76 return l.pages 77 } 78 79 func newLazyPagesFactory(factory page.PagesFactory) *lazyPagesFactory { 80 return &lazyPagesFactory{factory: factory} 81 } 82 83 func newPageCollections(m *pageMap) *PageCollections { 84 if m == nil { 85 panic("must provide a pageMap") 86 } 87 88 c := &PageCollections{pageMap: m} 89 90 c.pages = newLazyPagesFactory(func() page.Pages { 91 return m.createListAllPages() 92 }) 93 94 c.regularPages = newLazyPagesFactory(func() page.Pages { 95 return c.findPagesByKindIn(page.KindPage, c.pages.get()) 96 }) 97 98 return c 99 } 100 101 // This is an adapter func for the old API with Kind as first argument. 102 // This is invoked when you do .Site.GetPage. We drop the Kind and fails 103 // if there are more than 2 arguments, which would be ambiguous. 104 func (c *PageCollections) getPageOldVersion(ref ...string) (page.Page, error) { 105 var refs []string 106 for _, r := range ref { 107 // A common construct in the wild is 108 // .Site.GetPage "home" "" or 109 // .Site.GetPage "home" "/" 110 if r != "" && r != "/" { 111 refs = append(refs, r) 112 } 113 } 114 115 var key string 116 117 if len(refs) > 2 { 118 // This was allowed in Hugo <= 0.44, but we cannot support this with the 119 // new API. This should be the most unusual case. 120 return nil, fmt.Errorf(`too many arguments to .Site.GetPage: %v. Use lookups on the form {{ .Site.GetPage "/posts/mypage-md" }}`, ref) 121 } 122 123 if len(refs) == 0 || refs[0] == page.KindHome { 124 key = "/" 125 } else if len(refs) == 1 { 126 if len(ref) == 2 && refs[0] == page.KindSection { 127 // This is an old style reference to the "Home Page section". 128 // Typically fetched via {{ .Site.GetPage "section" .Section }} 129 // See https://github.com/gohugoio/hugo/issues/4989 130 key = "/" 131 } else { 132 key = refs[0] 133 } 134 } else { 135 key = refs[1] 136 } 137 138 key = filepath.ToSlash(key) 139 if !strings.HasPrefix(key, "/") { 140 key = "/" + key 141 } 142 143 return c.getPageNew(nil, key) 144 } 145 146 // Only used in tests. 147 func (c *PageCollections) getPage(typ string, sections ...string) page.Page { 148 refs := append([]string{typ}, path.Join(sections...)) 149 p, _ := c.getPageOldVersion(refs...) 150 return p 151 } 152 153 // getPageRef resolves a Page from ref/relRef, with a slightly more comprehensive 154 // search path than getPageNew. 155 func (c *PageCollections) getPageRef(context page.Page, ref string) (page.Page, error) { 156 n, err := c.getContentNode(context, true, ref) 157 if err != nil || n == nil || n.p == nil { 158 return nil, err 159 } 160 return n.p, nil 161 } 162 163 func (c *PageCollections) getPageNew(context page.Page, ref string) (page.Page, error) { 164 n, err := c.getContentNode(context, false, ref) 165 if err != nil || n == nil || n.p == nil { 166 return nil, err 167 } 168 return n.p, nil 169 } 170 171 func (c *PageCollections) getSectionOrPage(ref string) (*contentNode, string) { 172 var n *contentNode 173 174 pref := helpers.AddTrailingSlash(ref) 175 s, v, found := c.pageMap.sections.LongestPrefix(pref) 176 177 if found { 178 n = v.(*contentNode) 179 } 180 181 if found && s == pref { 182 // A section 183 return n, "" 184 } 185 186 m := c.pageMap 187 188 filename := strings.TrimPrefix(strings.TrimPrefix(ref, s), "/") 189 langSuffix := "." + m.s.Lang() 190 191 // Trim both extension and any language code. 192 name := paths.PathNoExt(filename) 193 name = strings.TrimSuffix(name, langSuffix) 194 195 // These are reserved bundle names and will always be stored by their owning 196 // folder name. 197 name = strings.TrimSuffix(name, "/index") 198 name = strings.TrimSuffix(name, "/_index") 199 200 if !found { 201 return nil, name 202 } 203 204 // Check if it's a section with filename provided. 205 if !n.p.File().IsZero() && n.p.File().LogicalName() == filename { 206 return n, name 207 } 208 209 return m.getPage(s, name), name 210 } 211 212 // For Ref/Reflink and .Site.GetPage do simple name lookups for the potentially ambigous myarticle.md and /myarticle.md, 213 // but not when we get ./myarticle*, section/myarticle. 214 func shouldDoSimpleLookup(ref string) bool { 215 if ref[0] == '.' { 216 return false 217 } 218 219 slashCount := strings.Count(ref, "/") 220 221 if slashCount > 1 { 222 return false 223 } 224 225 return slashCount == 0 || ref[0] == '/' 226 } 227 228 func (c *PageCollections) getContentNode(context page.Page, isReflink bool, ref string) (*contentNode, error) { 229 ref = filepath.ToSlash(strings.ToLower(strings.TrimSpace(ref))) 230 231 if ref == "" { 232 ref = "/" 233 } 234 235 inRef := ref 236 navUp := strings.HasPrefix(ref, "..") 237 var doSimpleLookup bool 238 if isReflink || context == nil { 239 doSimpleLookup = shouldDoSimpleLookup(ref) 240 } 241 242 if context != nil && !strings.HasPrefix(ref, "/") { 243 // Try the page-relative path. 244 var base string 245 if context.File().IsZero() { 246 base = context.SectionsPath() 247 } else { 248 meta := context.File().FileInfo().Meta() 249 base = filepath.ToSlash(filepath.Dir(meta.Path)) 250 if meta.Classifier == files.ContentClassLeaf { 251 // Bundles are stored in subfolders e.g. blog/mybundle/index.md, 252 // so if the user has not explicitly asked to go up, 253 // look on the "blog" level. 254 if !navUp { 255 base = path.Dir(base) 256 } 257 } 258 } 259 ref = path.Join("/", strings.ToLower(base), ref) 260 } 261 262 if !strings.HasPrefix(ref, "/") { 263 ref = "/" + ref 264 } 265 266 m := c.pageMap 267 268 // It's either a section, a page in a section or a taxonomy node. 269 // Start with the most likely: 270 n, name := c.getSectionOrPage(ref) 271 if n != nil { 272 return n, nil 273 } 274 275 if !strings.HasPrefix(inRef, "/") { 276 // Many people will have "post/foo.md" in their content files. 277 if n, _ := c.getSectionOrPage("/" + inRef); n != nil { 278 return n, nil 279 } 280 } 281 282 // Check if it's a taxonomy node 283 pref := helpers.AddTrailingSlash(ref) 284 s, v, found := m.taxonomies.LongestPrefix(pref) 285 286 if found { 287 if !m.onSameLevel(pref, s) { 288 return nil, nil 289 } 290 return v.(*contentNode), nil 291 } 292 293 getByName := func(s string) (*contentNode, error) { 294 n := m.pageReverseIndex.Get(s) 295 if n != nil { 296 if n == ambiguousContentNode { 297 return nil, fmt.Errorf("page reference %q is ambiguous", ref) 298 } 299 return n, nil 300 } 301 302 return nil, nil 303 } 304 305 var module string 306 if context != nil && !context.File().IsZero() { 307 module = context.File().FileInfo().Meta().Module 308 } 309 310 if module == "" && !c.pageMap.s.home.File().IsZero() { 311 module = c.pageMap.s.home.File().FileInfo().Meta().Module 312 } 313 314 if module != "" { 315 n, err := getByName(module + ref) 316 if err != nil { 317 return nil, err 318 } 319 if n != nil { 320 return n, nil 321 } 322 } 323 324 if !doSimpleLookup { 325 return nil, nil 326 } 327 328 // Ref/relref supports this potentially ambigous lookup. 329 return getByName(path.Base(name)) 330 } 331 332 func (*PageCollections) findPagesByKindIn(kind string, inPages page.Pages) page.Pages { 333 var pages page.Pages 334 for _, p := range inPages { 335 if p.Kind() == kind { 336 pages = append(pages, p) 337 } 338 } 339 return pages 340 }