page_paths.go - hugo - [fork] hugo port for 9front
HTML git clone https://git.drkhsh.at/hugo.git
DIR Log
DIR Files
DIR Refs
DIR Submodules
DIR README
DIR LICENSE
---
page_paths.go (10476B)
---
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 page
15
16 import (
17 "path"
18 "path/filepath"
19 "strings"
20 "sync"
21
22 "github.com/gohugoio/hugo/common/paths"
23 "github.com/gohugoio/hugo/common/urls"
24 "github.com/gohugoio/hugo/helpers"
25 "github.com/gohugoio/hugo/output"
26 "github.com/gohugoio/hugo/resources/kinds"
27 )
28
29 const slash = "/"
30
31 // TargetPathDescriptor describes how a file path for a given resource
32 // should look like on the file system. The same descriptor is then later used to
33 // create both the permalinks and the relative links, paginator URLs etc.
34 //
35 // The big motivating behind this is to have only one source of truth for URLs,
36 // and by that also get rid of most of the fragile string parsing/encoding etc.
37
38 type TargetPathDescriptor struct {
39 PathSpec *helpers.PathSpec
40
41 Type output.Format
42 Kind string
43
44 Path *paths.Path
45 Section *paths.Path
46
47 // For regular content pages this is either
48 // 1) the Slug, if set,
49 // 2) the file base name (TranslationBaseName).
50 BaseName string
51
52 // Typically a language prefix added to file paths.
53 PrefixFilePath string
54
55 // Typically a language prefix added to links.
56 PrefixLink string
57
58 // If in multihost mode etc., every link/path needs to be prefixed, even
59 // if set in URL.
60 ForcePrefix bool
61
62 // URL from front matter if set. Will override any Slug etc.
63 URL string
64
65 // Used to create paginator links.
66 Addends string
67
68 // The expanded permalink if defined for the section, ready to use.
69 ExpandedPermalink string
70
71 // Some types cannot have uglyURLs, even if globally enabled, RSS being one example.
72 UglyURLs bool
73 }
74
75 // TODO(bep) move this type.
76 type TargetPaths struct {
77 // Where to store the file on disk relative to the publish dir. OS slashes.
78 TargetFilename string
79
80 // The directory to write sub-resources of the above.
81 SubResourceBaseTarget string
82
83 // The base for creating links to sub-resources of the above.
84 SubResourceBaseLink string
85
86 // The relative permalink to this resources. Unix slashes.
87 Link string
88 }
89
90 func (p TargetPaths) RelPermalink(s *helpers.PathSpec) string {
91 return s.PrependBasePath(p.Link, false)
92 }
93
94 func (p TargetPaths) PermalinkForOutputFormat(s *helpers.PathSpec, f output.Format) string {
95 var baseURL urls.BaseURL
96 var err error
97 if f.Protocol != "" {
98 baseURL, err = s.Cfg.BaseURL().WithProtocol(f.Protocol)
99 if err != nil {
100 return ""
101 }
102 } else {
103 baseURL = s.Cfg.BaseURL()
104 }
105 baseURLstr := baseURL.String()
106 return s.PermalinkForBaseURL(p.Link, baseURLstr)
107 }
108
109 func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
110 // Normalize all file Windows paths to simplify what's next.
111 if helpers.FilePathSeparator != "/" {
112 d.PrefixFilePath = filepath.ToSlash(d.PrefixFilePath)
113 }
114
115 if !d.Type.Root && d.URL != "" && !strings.HasPrefix(d.URL, "/") {
116 // Treat this as a context relative URL
117 d.ForcePrefix = true
118 }
119
120 if d.URL != "" {
121 d.URL = filepath.ToSlash(d.URL)
122 if strings.Contains(d.URL, "..") {
123 d.URL = path.Join("/", d.URL)
124 }
125 }
126
127 if d.Type.Root && !d.ForcePrefix {
128 d.PrefixFilePath = ""
129 d.PrefixLink = ""
130 }
131
132 pb := getPagePathBuilder(d)
133 defer putPagePathBuilder(pb)
134
135 pb.fullSuffix = d.Type.MediaType.FirstSuffix.FullSuffix
136
137 // The top level index files, i.e. the home page etc., needs
138 // the index base even when uglyURLs is enabled.
139 needsBase := true
140
141 pb.isUgly = (d.UglyURLs || d.Type.Ugly) && !d.Type.NoUgly
142 pb.baseNameSameAsType = !d.Path.IsBundle() && d.BaseName != "" && d.BaseName == d.Type.BaseName
143 indexIsUglyKind := d.Kind == kinds.KindHome || d.Kind == kinds.KindSection || d.Kind == kinds.KindTaxonomy
144 indexIsUglyKind = indexIsUglyKind && pb.isUgly
145
146 if d.ExpandedPermalink == "" && pb.baseNameSameAsType {
147 pb.isUgly = true
148 }
149
150 if d.Type.Path != "" {
151 pb.Add(d.Type.Path)
152 }
153
154 if d.Type == output.HTTPStatus404HTMLFormat || d.Type == output.SitemapFormat || d.Type == output.RobotsTxtFormat {
155 pb.noSubResources = true
156 } else if d.Kind != kinds.KindPage && d.URL == "" && d.Section.Base() != "/" {
157 if d.ExpandedPermalink != "" {
158 pb.Add(d.ExpandedPermalink)
159 } else {
160 pb.Add(d.Section.Base())
161 }
162 needsBase = false
163 }
164
165 if d.Kind != kinds.KindHome && d.URL != "" {
166 pb.Add(paths.FieldsSlash(d.URL)...)
167
168 if d.Addends != "" {
169 pb.Add(d.Addends)
170 }
171
172 hasDot := strings.Contains(d.URL, ".")
173 hasSlash := strings.HasSuffix(d.URL, "/")
174
175 if hasSlash || !hasDot {
176 pb.Add(d.Type.BaseName + pb.fullSuffix)
177 } else if hasDot {
178 pb.fullSuffix = paths.Ext(d.URL)
179 }
180
181 if pb.IsHtmlIndex() {
182 pb.linkUpperOffset = 1
183 }
184
185 if d.ForcePrefix {
186
187 // Prepend language prefix if not already set in URL
188 if d.PrefixFilePath != "" && !strings.HasPrefix(d.URL, "/"+d.PrefixFilePath) {
189 pb.prefixPath = d.PrefixFilePath
190 }
191
192 if d.PrefixLink != "" && !strings.HasPrefix(d.URL, "/"+d.PrefixLink) {
193 pb.prefixLink = d.PrefixLink
194 }
195 }
196 } else if !kinds.IsBranch(d.Kind) {
197 if d.ExpandedPermalink != "" {
198 pb.Add(d.ExpandedPermalink)
199 } else {
200 if dir := d.Path.ContainerDir(); dir != "" {
201 pb.Add(dir)
202 }
203 if d.BaseName != "" {
204 pb.Add(d.BaseName)
205 } else {
206 pb.Add(d.Path.BaseNameNoIdentifier())
207 }
208 }
209
210 if d.Addends != "" {
211 pb.Add(d.Addends)
212 }
213
214 if pb.isUgly {
215 pb.ConcatLast(pb.fullSuffix)
216 } else {
217 pb.Add(d.Type.BaseName + pb.fullSuffix)
218 }
219
220 if pb.IsHtmlIndex() {
221 pb.linkUpperOffset = 1
222 }
223
224 if d.PrefixFilePath != "" {
225 pb.prefixPath = d.PrefixFilePath
226 }
227
228 if d.PrefixLink != "" {
229 pb.prefixLink = d.PrefixLink
230 }
231 } else {
232 if d.Addends != "" {
233 pb.Add(d.Addends)
234 }
235
236 needsBase = needsBase && d.Addends == ""
237
238 if needsBase || (!pb.isUgly || indexIsUglyKind) {
239 pb.Add(d.Type.BaseName + pb.fullSuffix)
240 } else {
241 pb.ConcatLast(pb.fullSuffix)
242 }
243
244 if !indexIsUglyKind && pb.IsHtmlIndex() {
245 pb.linkUpperOffset = 1
246 }
247
248 if d.PrefixFilePath != "" {
249 pb.prefixPath = d.PrefixFilePath
250 }
251
252 if d.PrefixLink != "" {
253 pb.prefixLink = d.PrefixLink
254 }
255 }
256
257 // if page URL is explicitly set in frontmatter,
258 // preserve its value without sanitization
259 if d.URL == "" {
260 // Note: MakePathSanitized will lower case the path if
261 // disablePathToLower isn't set.
262 pb.Sanitize()
263 }
264
265 link := pb.Link()
266
267 pagePath := pb.PathFile()
268
269 tp.TargetFilename = filepath.FromSlash(pagePath)
270 if !pb.noSubResources {
271 tp.SubResourceBaseTarget = pb.PathDir()
272 tp.SubResourceBaseLink = pb.LinkDir()
273 }
274
275 // paths.{URL,Path}Escape rely on url.Parse which
276 // will consider # a fragment identifier, so it and
277 // and everything after it will be stripped from
278 // `link`, so we need to escape it first.
279 link = strings.ReplaceAll(link, "#", "%23")
280
281 if d.URL != "" {
282 tp.Link = paths.URLEscape(link)
283 } else {
284 // This is slightly faster for when we know we don't have any
285 // query or scheme etc.
286 tp.Link = paths.PathEscape(link)
287 }
288 if tp.Link == "" {
289 tp.Link = "/"
290 }
291
292 return
293 }
294
295 // When adding state here, remember to update putPagePathBuilder.
296 type pagePathBuilder struct {
297 els []string
298
299 d TargetPathDescriptor
300
301 // Builder state.
302 isUgly bool
303 baseNameSameAsType bool
304 noSubResources bool
305 fullSuffix string // File suffix including any ".".
306 prefixLink string
307 prefixPath string
308 linkUpperOffset int
309 }
310
311 func (p *pagePathBuilder) Add(el ...string) {
312 // Filter empty and slashes.
313 n := 0
314 for _, e := range el {
315 if e != "" && e != slash {
316 el[n] = e
317 n++
318 }
319 }
320 el = el[:n]
321
322 p.els = append(p.els, el...)
323 }
324
325 func (p *pagePathBuilder) ConcatLast(s string) {
326 if len(p.els) == 0 {
327 p.Add(s)
328 return
329 }
330 old := p.els[len(p.els)-1]
331 if old == "" {
332 p.els[len(p.els)-1] = s
333 return
334 }
335 if old[len(old)-1] == '/' {
336 old = old[:len(old)-1]
337 }
338 p.els[len(p.els)-1] = old + s
339 }
340
341 func (p *pagePathBuilder) IsHtmlIndex() bool {
342 return p.Last() == "index.html"
343 }
344
345 func (p *pagePathBuilder) Last() string {
346 if p.els == nil {
347 return ""
348 }
349 return p.els[len(p.els)-1]
350 }
351
352 func (p *pagePathBuilder) Link() string {
353 link := p.Path(p.linkUpperOffset)
354
355 if p.baseNameSameAsType {
356 link = strings.TrimSuffix(link, p.d.BaseName)
357 }
358
359 if p.prefixLink != "" {
360 link = "/" + p.prefixLink + link
361 }
362
363 if p.linkUpperOffset > 0 && !strings.HasSuffix(link, "/") {
364 link += "/"
365 }
366
367 return link
368 }
369
370 func (p *pagePathBuilder) LinkDir() string {
371 if p.noSubResources {
372 return ""
373 }
374
375 pathDir := p.PathDirBase()
376
377 if p.prefixLink != "" {
378 pathDir = "/" + p.prefixLink + pathDir
379 }
380
381 return pathDir
382 }
383
384 func (p *pagePathBuilder) Path(upperOffset int) string {
385 upper := len(p.els)
386 if upperOffset > 0 {
387 upper -= upperOffset
388 }
389 pth := path.Join(p.els[:upper]...)
390 return paths.AddLeadingSlash(pth)
391 }
392
393 func (p *pagePathBuilder) PathDir() string {
394 dir := p.PathDirBase()
395 if p.prefixPath != "" {
396 dir = "/" + p.prefixPath + dir
397 }
398 return dir
399 }
400
401 func (p *pagePathBuilder) PathDirBase() string {
402 if p.noSubResources {
403 return ""
404 }
405
406 dir := p.Path(0)
407 isIndex := strings.HasPrefix(p.Last(), p.d.Type.BaseName+".")
408
409 if isIndex {
410 dir = paths.Dir(dir)
411 } else {
412 dir = strings.TrimSuffix(dir, p.fullSuffix)
413 }
414
415 if dir == "/" {
416 dir = ""
417 }
418
419 return dir
420 }
421
422 func (p *pagePathBuilder) PathFile() string {
423 dir := p.Path(0)
424 if p.prefixPath != "" {
425 dir = "/" + p.prefixPath + dir
426 }
427 return dir
428 }
429
430 func (p *pagePathBuilder) Prepend(el ...string) {
431 p.els = append(p.els[:0], append(el, p.els[0:]...)...)
432 }
433
434 func (p *pagePathBuilder) Sanitize() {
435 for i, el := range p.els {
436 p.els[i] = p.d.PathSpec.MakePathSanitized(el)
437 }
438 }
439
440 var pagePathBuilderPool = &sync.Pool{
441 New: func() any {
442 return &pagePathBuilder{}
443 },
444 }
445
446 func getPagePathBuilder(d TargetPathDescriptor) *pagePathBuilder {
447 b := pagePathBuilderPool.Get().(*pagePathBuilder)
448 b.d = d
449 return b
450 }
451
452 func putPagePathBuilder(b *pagePathBuilder) {
453 b.els = b.els[:0]
454 b.fullSuffix = ""
455 b.baseNameSameAsType = false
456 b.isUgly = false
457 b.noSubResources = false
458 b.prefixLink = ""
459 b.prefixPath = ""
460 b.linkUpperOffset = 0
461 pagePathBuilderPool.Put(b)
462 }