github.com/dominikszabo/hugo-ds-clean@v0.47.1/helpers/url.go (about) 1 // Copyright 2015 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 helpers 15 16 import ( 17 "fmt" 18 "net/url" 19 "path" 20 "path/filepath" 21 "strings" 22 23 "github.com/PuerkitoBio/purell" 24 ) 25 26 type pathBridge struct { 27 } 28 29 func (pathBridge) Base(in string) string { 30 return path.Base(in) 31 } 32 33 func (pathBridge) Clean(in string) string { 34 return path.Clean(in) 35 } 36 37 func (pathBridge) Dir(in string) string { 38 return path.Dir(in) 39 } 40 41 func (pathBridge) Ext(in string) string { 42 return path.Ext(in) 43 } 44 45 func (pathBridge) Join(elem ...string) string { 46 return path.Join(elem...) 47 } 48 49 func (pathBridge) Separator() string { 50 return "/" 51 } 52 53 var pb pathBridge 54 55 func sanitizeURLWithFlags(in string, f purell.NormalizationFlags) string { 56 s, err := purell.NormalizeURLString(in, f) 57 if err != nil { 58 return in 59 } 60 61 // Temporary workaround for the bug fix and resulting 62 // behavioral change in purell.NormalizeURLString(): 63 // a leading '/' was inadvertently added to relative links, 64 // but no longer, see #878. 65 // 66 // I think the real solution is to allow Hugo to 67 // make relative URL with relative path, 68 // e.g. "../../post/hello-again/", as wished by users 69 // in issues #157, #622, etc., without forcing 70 // relative URLs to begin with '/'. 71 // Once the fixes are in, let's remove this kludge 72 // and restore SanitizeURL() to the way it was. 73 // -- @anthonyfok, 2015-02-16 74 // 75 // Begin temporary kludge 76 u, err := url.Parse(s) 77 if err != nil { 78 panic(err) 79 } 80 if len(u.Path) > 0 && !strings.HasPrefix(u.Path, "/") { 81 u.Path = "/" + u.Path 82 } 83 return u.String() 84 // End temporary kludge 85 86 //return s 87 88 } 89 90 // SanitizeURL sanitizes the input URL string. 91 func SanitizeURL(in string) string { 92 return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveTrailingSlash|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator) 93 } 94 95 // SanitizeURLKeepTrailingSlash is the same as SanitizeURL, but will keep any trailing slash. 96 func SanitizeURLKeepTrailingSlash(in string) string { 97 return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator) 98 } 99 100 // URLize is similar to MakePath, but with Unicode handling 101 // Example: 102 // uri: Vim (text editor) 103 // urlize: vim-text-editor 104 func (p *PathSpec) URLize(uri string) string { 105 return p.URLEscape(p.MakePathSanitized(uri)) 106 107 } 108 109 // URLizeFilename creates an URL from a filename by esacaping unicode letters 110 // and turn any filepath separator into forward slashes. 111 func (p *PathSpec) URLizeFilename(filename string) string { 112 return p.URLEscape(filepath.ToSlash(filename)) 113 } 114 115 // URLEscape escapes unicode letters. 116 func (p *PathSpec) URLEscape(uri string) string { 117 // escape unicode letters 118 parsedURI, err := url.Parse(uri) 119 if err != nil { 120 // if net/url can not parse URL it means Sanitize works incorrectly 121 panic(err) 122 } 123 x := parsedURI.String() 124 return x 125 } 126 127 // MakePermalink combines base URL with content path to create full URL paths. 128 // Example 129 // base: http://spf13.com/ 130 // path: post/how-i-blog 131 // result: http://spf13.com/post/how-i-blog 132 func MakePermalink(host, plink string) *url.URL { 133 134 base, err := url.Parse(host) 135 if err != nil { 136 panic(err) 137 } 138 139 p, err := url.Parse(plink) 140 if err != nil { 141 panic(err) 142 } 143 144 if p.Host != "" { 145 panic(fmt.Errorf("Can't make permalink from absolute link %q", plink)) 146 } 147 148 base.Path = path.Join(base.Path, p.Path) 149 150 // path.Join will strip off the last /, so put it back if it was there. 151 hadTrailingSlash := (plink == "" && strings.HasSuffix(host, "/")) || strings.HasSuffix(p.Path, "/") 152 if hadTrailingSlash && !strings.HasSuffix(base.Path, "/") { 153 base.Path = base.Path + "/" 154 } 155 156 return base 157 } 158 159 // AbsURL creates an absolute URL from the relative path given and the BaseURL set in config. 160 func (p *PathSpec) AbsURL(in string, addLanguage bool) string { 161 url, err := url.Parse(in) 162 if err != nil { 163 return in 164 } 165 166 if url.IsAbs() || strings.HasPrefix(in, "//") { 167 return in 168 } 169 170 var baseURL string 171 if strings.HasPrefix(in, "/") { 172 u := p.BaseURL.URL() 173 u.Path = "" 174 baseURL = u.String() 175 } else { 176 baseURL = p.BaseURL.String() 177 } 178 179 if addLanguage { 180 prefix := p.GetLanguagePrefix() 181 if prefix != "" { 182 hasPrefix := false 183 // avoid adding language prefix if already present 184 if strings.HasPrefix(in, "/") { 185 hasPrefix = strings.HasPrefix(in[1:], prefix) 186 } else { 187 hasPrefix = strings.HasPrefix(in, prefix) 188 } 189 190 if !hasPrefix { 191 addSlash := in == "" || strings.HasSuffix(in, "/") 192 in = path.Join(prefix, in) 193 194 if addSlash { 195 in += "/" 196 } 197 } 198 } 199 } 200 return MakePermalink(baseURL, in).String() 201 } 202 203 // IsAbsURL determines whether the given path points to an absolute URL. 204 func IsAbsURL(path string) bool { 205 url, err := url.Parse(path) 206 if err != nil { 207 return false 208 } 209 210 return url.IsAbs() || strings.HasPrefix(path, "//") 211 } 212 213 // RelURL creates a URL relative to the BaseURL root. 214 // Note: The result URL will not include the context root if canonifyURLs is enabled. 215 func (p *PathSpec) RelURL(in string, addLanguage bool) string { 216 baseURL := p.BaseURL.String() 217 canonifyURLs := p.CanonifyURLs 218 if (!strings.HasPrefix(in, baseURL) && strings.HasPrefix(in, "http")) || strings.HasPrefix(in, "//") { 219 return in 220 } 221 222 u := in 223 224 if strings.HasPrefix(in, baseURL) { 225 u = strings.TrimPrefix(u, baseURL) 226 } 227 228 if addLanguage { 229 prefix := p.GetLanguagePrefix() 230 if prefix != "" { 231 hasPrefix := false 232 // avoid adding language prefix if already present 233 if strings.HasPrefix(in, "/") { 234 hasPrefix = strings.HasPrefix(in[1:], prefix) 235 } else { 236 hasPrefix = strings.HasPrefix(in, prefix) 237 } 238 239 if !hasPrefix { 240 hadSlash := strings.HasSuffix(u, "/") 241 242 u = path.Join(prefix, u) 243 244 if hadSlash { 245 u += "/" 246 } 247 } 248 } 249 } 250 251 if !canonifyURLs { 252 u = AddContextRoot(baseURL, u) 253 } 254 255 if in == "" && !strings.HasSuffix(u, "/") && strings.HasSuffix(baseURL, "/") { 256 u += "/" 257 } 258 259 if !strings.HasPrefix(u, "/") { 260 u = "/" + u 261 } 262 263 return u 264 } 265 266 // AddContextRoot adds the context root to an URL if it's not already set. 267 // For relative URL entries on sites with a base url with a context root set (i.e. http://example.com/mysite), 268 // relative URLs must not include the context root if canonifyURLs is enabled. But if it's disabled, it must be set. 269 func AddContextRoot(baseURL, relativePath string) string { 270 271 url, err := url.Parse(baseURL) 272 if err != nil { 273 panic(err) 274 } 275 276 newPath := path.Join(url.Path, relativePath) 277 278 // path strips traling slash, ignore root path. 279 if newPath != "/" && strings.HasSuffix(relativePath, "/") { 280 newPath += "/" 281 } 282 return newPath 283 } 284 285 // PrependBasePath prepends any baseURL sub-folder to the given resource 286 // if canonifyURLs is disabled. 287 // If canonifyURLs is set, we will globally prepend the absURL with any sub-folder, 288 // so avoid doing anything here to avoid getting double paths. 289 func (p *PathSpec) PrependBasePath(rel string) string { 290 if p.BasePath != "" { 291 rel = filepath.ToSlash(rel) 292 // Need to prepend any path from the baseURL 293 hadSlash := strings.HasSuffix(rel, "/") 294 rel = path.Join(p.BasePath, rel) 295 if hadSlash { 296 rel += "/" 297 } 298 } 299 return rel 300 } 301 302 // URLizeAndPrep applies misc sanitation to the given URL to get it in line 303 // with the Hugo standard. 304 func (p *PathSpec) URLizeAndPrep(in string) string { 305 return p.URLPrep(p.URLize(in)) 306 } 307 308 // URLPrep applies misc sanitation to the given URL. 309 func (p *PathSpec) URLPrep(in string) string { 310 if p.UglyURLs { 311 return Uglify(SanitizeURL(in)) 312 } 313 pretty := PrettifyURL(SanitizeURL(in)) 314 if path.Ext(pretty) == ".xml" { 315 return pretty 316 } 317 url, err := purell.NormalizeURLString(pretty, purell.FlagAddTrailingSlash) 318 if err != nil { 319 return pretty 320 } 321 return url 322 } 323 324 // PrettifyURL takes a URL string and returns a semantic, clean URL. 325 func PrettifyURL(in string) string { 326 x := PrettifyURLPath(in) 327 328 if path.Base(x) == "index.html" { 329 return path.Dir(x) 330 } 331 332 if in == "" { 333 return "/" 334 } 335 336 return x 337 } 338 339 // PrettifyURLPath takes a URL path to a content and converts it 340 // to enable pretty URLs. 341 // /section/name.html becomes /section/name/index.html 342 // /section/name/ becomes /section/name/index.html 343 // /section/name/index.html becomes /section/name/index.html 344 func PrettifyURLPath(in string) string { 345 return prettifyPath(in, pb) 346 } 347 348 // Uglify does the opposite of PrettifyURLPath(). 349 // /section/name/index.html becomes /section/name.html 350 // /section/name/ becomes /section/name.html 351 // /section/name.html becomes /section/name.html 352 func Uglify(in string) string { 353 if path.Ext(in) == "" { 354 if len(in) < 2 { 355 return "/" 356 } 357 // /section/name/ -> /section/name.html 358 return path.Clean(in) + ".html" 359 } 360 361 name, ext := fileAndExt(in, pb) 362 if name == "index" { 363 // /section/name/index.html -> /section/name.html 364 d := path.Dir(in) 365 if len(d) > 1 { 366 return d + ext 367 } 368 return in 369 } 370 // /.xml -> /index.xml 371 if name == "" { 372 return path.Dir(in) + "index" + ext 373 } 374 // /section/name.html -> /section/name.html 375 return path.Clean(in) 376 }