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 }