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 }