page_paths.go (8460B)
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
21 "github.com/gohugoio/hugo/helpers"
22 "github.com/gohugoio/hugo/output"
23 )
24
25 const slash = "/"
26
27 // TargetPathDescriptor describes how a file path for a given resource
28 // should look like on the file system. The same descriptor is then later used to
29 // create both the permalinks and the relative links, paginator URLs etc.
30 //
31 // The big motivating behind this is to have only one source of truth for URLs,
32 // and by that also get rid of most of the fragile string parsing/encoding etc.
33 //
34 //
35 type TargetPathDescriptor struct {
36 PathSpec *helpers.PathSpec
37
38 Type output.Format
39 Kind string
40
41 Sections []string
42
43 // For regular content pages this is either
44 // 1) the Slug, if set,
45 // 2) the file base name (TranslationBaseName).
46 BaseName string
47
48 // Source directory.
49 Dir string
50
51 // Typically a language prefix added to file paths.
52 PrefixFilePath string
53
54 // Typically a language prefix added to links.
55 PrefixLink string
56
57 // If in multihost mode etc., every link/path needs to be prefixed, even
58 // if set in URL.
59 ForcePrefix bool
60
61 // URL from front matter if set. Will override any Slug etc.
62 URL string
63
64 // Used to create paginator links.
65 Addends string
66
67 // The expanded permalink if defined for the section, ready to use.
68 ExpandedPermalink string
69
70 // Some types cannot have uglyURLs, even if globally enabled, RSS being one example.
71 UglyURLs bool
72 }
73
74 // TODO(bep) move this type.
75 type TargetPaths struct {
76
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 string
96 var err error
97 if f.Protocol != "" {
98 baseURL, err = s.BaseURL.WithProtocol(f.Protocol)
99 if err != nil {
100 return ""
101 }
102 } else {
103 baseURL = s.BaseURL.String()
104 }
105
106 return s.PermalinkForBaseURL(p.Link, baseURL)
107 }
108
109 func isHtmlIndex(s string) bool {
110 return strings.HasSuffix(s, "/index.html")
111 }
112
113 func CreateTargetPaths(d TargetPathDescriptor) (tp TargetPaths) {
114 if d.Type.Name == "" {
115 panic("CreateTargetPath: missing type")
116 }
117
118 // Normalize all file Windows paths to simplify what's next.
119 if helpers.FilePathSeparator != slash {
120 d.Dir = filepath.ToSlash(d.Dir)
121 d.PrefixFilePath = filepath.ToSlash(d.PrefixFilePath)
122
123 }
124
125 if d.URL != "" && !strings.HasPrefix(d.URL, "/") {
126 // Treat this as a context relative URL
127 d.ForcePrefix = true
128 }
129
130 pagePath := slash
131 fullSuffix := d.Type.MediaType.FirstSuffix.FullSuffix
132
133 var (
134 pagePathDir string
135 link string
136 linkDir string
137 )
138
139 // The top level index files, i.e. the home page etc., needs
140 // the index base even when uglyURLs is enabled.
141 needsBase := true
142
143 isUgly := d.UglyURLs && !d.Type.NoUgly
144 baseNameSameAsType := d.BaseName != "" && d.BaseName == d.Type.BaseName
145
146 if d.ExpandedPermalink == "" && baseNameSameAsType {
147 isUgly = true
148 }
149
150 if d.Kind != KindPage && d.URL == "" && len(d.Sections) > 0 {
151 if d.ExpandedPermalink != "" {
152 pagePath = pjoin(pagePath, d.ExpandedPermalink)
153 } else {
154 pagePath = pjoin(d.Sections...)
155 }
156 needsBase = false
157 }
158
159 if d.Type.Path != "" {
160 pagePath = pjoin(pagePath, d.Type.Path)
161 }
162
163 if d.Kind != KindHome && d.URL != "" {
164 pagePath = pjoin(pagePath, d.URL)
165
166 if d.Addends != "" {
167 pagePath = pjoin(pagePath, d.Addends)
168 }
169
170 pagePathDir = pagePath
171 link = pagePath
172 hasDot := strings.Contains(d.URL, ".")
173 hasSlash := strings.HasSuffix(d.URL, slash)
174
175 if hasSlash || !hasDot {
176 pagePath = pjoin(pagePath, d.Type.BaseName+fullSuffix)
177 } else if hasDot {
178 pagePathDir = path.Dir(pagePathDir)
179 }
180
181 if !isHtmlIndex(pagePath) {
182 link = pagePath
183 } else if !hasSlash {
184 link += slash
185 }
186
187 linkDir = pagePathDir
188
189 if d.ForcePrefix {
190
191 // Prepend language prefix if not already set in URL
192 if d.PrefixFilePath != "" && !strings.HasPrefix(d.URL, slash+d.PrefixFilePath) {
193 pagePath = pjoin(d.PrefixFilePath, pagePath)
194 pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
195 }
196
197 if d.PrefixLink != "" && !strings.HasPrefix(d.URL, slash+d.PrefixLink) {
198 link = pjoin(d.PrefixLink, link)
199 linkDir = pjoin(d.PrefixLink, linkDir)
200 }
201 }
202
203 } else if d.Kind == KindPage {
204
205 if d.ExpandedPermalink != "" {
206 pagePath = pjoin(pagePath, d.ExpandedPermalink)
207 } else {
208 if d.Dir != "" {
209 pagePath = pjoin(pagePath, d.Dir)
210 }
211 if d.BaseName != "" {
212 pagePath = pjoin(pagePath, d.BaseName)
213 }
214 }
215
216 if d.Addends != "" {
217 pagePath = pjoin(pagePath, d.Addends)
218 }
219
220 link = pagePath
221
222 // TODO(bep) this should not happen after the fix in https://github.com/gohugoio/hugo/issues/4870
223 // but we may need some more testing before we can remove it.
224 if baseNameSameAsType {
225 link = strings.TrimSuffix(link, d.BaseName)
226 }
227
228 pagePathDir = link
229 link = link + slash
230 linkDir = pagePathDir
231
232 if isUgly {
233 pagePath = addSuffix(pagePath, fullSuffix)
234 } else {
235 pagePath = pjoin(pagePath, d.Type.BaseName+fullSuffix)
236 }
237
238 if !isHtmlIndex(pagePath) {
239 link = pagePath
240 }
241
242 if d.PrefixFilePath != "" {
243 pagePath = pjoin(d.PrefixFilePath, pagePath)
244 pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
245 }
246
247 if d.PrefixLink != "" {
248 link = pjoin(d.PrefixLink, link)
249 linkDir = pjoin(d.PrefixLink, linkDir)
250 }
251
252 } else {
253 if d.Addends != "" {
254 pagePath = pjoin(pagePath, d.Addends)
255 }
256
257 needsBase = needsBase && d.Addends == ""
258
259 // No permalink expansion etc. for node type pages (for now)
260 base := ""
261
262 if needsBase || !isUgly {
263 base = d.Type.BaseName
264 }
265
266 pagePathDir = pagePath
267 link = pagePath
268 linkDir = pagePathDir
269
270 if base != "" {
271 pagePath = path.Join(pagePath, addSuffix(base, fullSuffix))
272 } else {
273 pagePath = addSuffix(pagePath, fullSuffix)
274 }
275
276 if !isHtmlIndex(pagePath) {
277 link = pagePath
278 } else {
279 link += slash
280 }
281
282 if d.PrefixFilePath != "" {
283 pagePath = pjoin(d.PrefixFilePath, pagePath)
284 pagePathDir = pjoin(d.PrefixFilePath, pagePathDir)
285 }
286
287 if d.PrefixLink != "" {
288 link = pjoin(d.PrefixLink, link)
289 linkDir = pjoin(d.PrefixLink, linkDir)
290 }
291 }
292
293 pagePath = pjoin(slash, pagePath)
294 pagePathDir = strings.TrimSuffix(path.Join(slash, pagePathDir), slash)
295
296 hadSlash := strings.HasSuffix(link, slash)
297 link = strings.Trim(link, slash)
298 if hadSlash {
299 link += slash
300 }
301
302 if !strings.HasPrefix(link, slash) {
303 link = slash + link
304 }
305
306 linkDir = strings.TrimSuffix(path.Join(slash, linkDir), slash)
307
308 // if page URL is explicitly set in frontmatter,
309 // preserve its value without sanitization
310 if d.Kind != KindPage || d.URL == "" {
311 // Note: MakePathSanitized will lower case the path if
312 // disablePathToLower isn't set.
313 pagePath = d.PathSpec.MakePathSanitized(pagePath)
314 pagePathDir = d.PathSpec.MakePathSanitized(pagePathDir)
315 link = d.PathSpec.MakePathSanitized(link)
316 linkDir = d.PathSpec.MakePathSanitized(linkDir)
317 }
318
319 tp.TargetFilename = filepath.FromSlash(pagePath)
320 tp.SubResourceBaseTarget = filepath.FromSlash(pagePathDir)
321 tp.SubResourceBaseLink = linkDir
322 tp.Link = d.PathSpec.URLizeFilename(link)
323 if tp.Link == "" {
324 tp.Link = slash
325 }
326
327 return
328 }
329
330 func addSuffix(s, suffix string) string {
331 return strings.Trim(s, slash) + suffix
332 }
333
334 // Like path.Join, but preserves one trailing slash if present.
335 func pjoin(elem ...string) string {
336 hadSlash := strings.HasSuffix(elem[len(elem)-1], slash)
337 joined := path.Join(elem...)
338 if hadSlash && !strings.HasSuffix(joined, slash) {
339 return joined + slash
340 }
341 return joined
342 }