path.go - hugo - [fork] hugo port for 9front
(HTM) git clone https://git.drkhsh.at/hugo.git
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) Submodules
(DIR) README
(DIR) LICENSE
---
path.go (12121B)
---
1 // Copyright 2021 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 paths
15
16 import (
17 "errors"
18 "fmt"
19 "net/url"
20 "path"
21 "path/filepath"
22 "strings"
23 "unicode"
24 )
25
26 // FilePathSeparator as defined by os.Separator.
27 const (
28 FilePathSeparator = string(filepath.Separator)
29 slash = "/"
30 )
31
32 // filepathPathBridge is a bridge for common functionality in filepath vs path
33 type filepathPathBridge interface {
34 Base(in string) string
35 Clean(in string) string
36 Dir(in string) string
37 Ext(in string) string
38 Join(elem ...string) string
39 Separator() string
40 }
41
42 type filepathBridge struct{}
43
44 func (filepathBridge) Base(in string) string {
45 return filepath.Base(in)
46 }
47
48 func (filepathBridge) Clean(in string) string {
49 return filepath.Clean(in)
50 }
51
52 func (filepathBridge) Dir(in string) string {
53 return filepath.Dir(in)
54 }
55
56 func (filepathBridge) Ext(in string) string {
57 return filepath.Ext(in)
58 }
59
60 func (filepathBridge) Join(elem ...string) string {
61 return filepath.Join(elem...)
62 }
63
64 func (filepathBridge) Separator() string {
65 return FilePathSeparator
66 }
67
68 var fpb filepathBridge
69
70 // AbsPathify creates an absolute path if given a working dir and a relative path.
71 // If already absolute, the path is just cleaned.
72 func AbsPathify(workingDir, inPath string) string {
73 if filepath.IsAbs(inPath) {
74 return filepath.Clean(inPath)
75 }
76 return filepath.Join(workingDir, inPath)
77 }
78
79 // AddTrailingSlash adds a trailing Unix styled slash (/) if not already
80 // there.
81 func AddTrailingSlash(path string) string {
82 if !strings.HasSuffix(path, "/") {
83 path += "/"
84 }
85 return path
86 }
87
88 // AddLeadingSlash adds a leading Unix styled slash (/) if not already
89 // there.
90 func AddLeadingSlash(path string) string {
91 if !strings.HasPrefix(path, "/") {
92 path = "/" + path
93 }
94 return path
95 }
96
97 // AddTrailingAndLeadingSlash adds a leading and trailing Unix styled slash (/) if not already
98 // there.
99 func AddLeadingAndTrailingSlash(path string) string {
100 return AddTrailingSlash(AddLeadingSlash(path))
101 }
102
103 // MakeTitle converts the path given to a suitable title, trimming whitespace
104 // and replacing hyphens with whitespace.
105 func MakeTitle(inpath string) string {
106 return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1)
107 }
108
109 // ReplaceExtension takes a path and an extension, strips the old extension
110 // and returns the path with the new extension.
111 func ReplaceExtension(path string, newExt string) string {
112 f, _ := fileAndExt(path, fpb)
113 return f + "." + newExt
114 }
115
116 func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
117 for _, currentPath := range possibleDirectories {
118 if strings.HasPrefix(inPath, currentPath) {
119 return strings.TrimPrefix(inPath, currentPath), nil
120 }
121 }
122 return inPath, errors.New("can't extract relative path, unknown prefix")
123 }
124
125 // ExtNoDelimiter takes a path and returns the extension, excluding the delimiter, i.e. "md".
126 func ExtNoDelimiter(in string) string {
127 return strings.TrimPrefix(Ext(in), ".")
128 }
129
130 // Ext takes a path and returns the extension, including the delimiter, i.e. ".md".
131 func Ext(in string) string {
132 _, ext := fileAndExt(in, fpb)
133 return ext
134 }
135
136 // PathAndExt is the same as FileAndExt, but it uses the path package.
137 func PathAndExt(in string) (string, string) {
138 return fileAndExt(in, pb)
139 }
140
141 // FileAndExt takes a path and returns the file and extension separated,
142 // the extension including the delimiter, i.e. ".md".
143 func FileAndExt(in string) (string, string) {
144 return fileAndExt(in, fpb)
145 }
146
147 // FileAndExtNoDelimiter takes a path and returns the file and extension separated,
148 // the extension excluding the delimiter, e.g "md".
149 func FileAndExtNoDelimiter(in string) (string, string) {
150 file, ext := fileAndExt(in, fpb)
151 return file, strings.TrimPrefix(ext, ".")
152 }
153
154 // Filename takes a file path, strips out the extension,
155 // and returns the name of the file.
156 func Filename(in string) (name string) {
157 name, _ = fileAndExt(in, fpb)
158 return
159 }
160
161 // FileAndExt returns the filename and any extension of a file path as
162 // two separate strings.
163 //
164 // If the path, in, contains a directory name ending in a slash,
165 // then both name and ext will be empty strings.
166 //
167 // If the path, in, is either the current directory, the parent
168 // directory or the root directory, or an empty string,
169 // then both name and ext will be empty strings.
170 //
171 // If the path, in, represents the path of a file without an extension,
172 // then name will be the name of the file and ext will be an empty string.
173 //
174 // If the path, in, represents a filename with an extension,
175 // then name will be the filename minus any extension - including the dot
176 // and ext will contain the extension - minus the dot.
177 func fileAndExt(in string, b filepathPathBridge) (name string, ext string) {
178 ext = b.Ext(in)
179 base := b.Base(in)
180
181 return extractFilename(in, ext, base, b.Separator()), ext
182 }
183
184 func extractFilename(in, ext, base, pathSeparator string) (name string) {
185 // No file name cases. These are defined as:
186 // 1. any "in" path that ends in a pathSeparator
187 // 2. any "base" consisting of just an pathSeparator
188 // 3. any "base" consisting of just an empty string
189 // 4. any "base" consisting of just the current directory i.e. "."
190 // 5. any "base" consisting of just the parent directory i.e. ".."
191 if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator {
192 name = "" // there is NO filename
193 } else if ext != "" { // there was an Extension
194 // return the filename minus the extension (and the ".")
195 name = base[:strings.LastIndex(base, ".")]
196 } else {
197 // no extension case so just return base, which will
198 // be the filename
199 name = base
200 }
201 return
202 }
203
204 // GetRelativePath returns the relative path of a given path.
205 func GetRelativePath(path, base string) (final string, err error) {
206 if filepath.IsAbs(path) && base == "" {
207 return "", errors.New("source: missing base directory")
208 }
209 name := filepath.Clean(path)
210 base = filepath.Clean(base)
211
212 name, err = filepath.Rel(base, name)
213 if err != nil {
214 return "", err
215 }
216
217 if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) {
218 name += FilePathSeparator
219 }
220 return name, nil
221 }
222
223 func prettifyPath(in string, b filepathPathBridge) string {
224 if filepath.Ext(in) == "" {
225 // /section/name/ -> /section/name/index.html
226 if len(in) < 2 {
227 return b.Separator()
228 }
229 return b.Join(in, "index.html")
230 }
231 name, ext := fileAndExt(in, b)
232 if name == "index" {
233 // /section/name/index.html -> /section/name/index.html
234 return b.Clean(in)
235 }
236 // /section/name.html -> /section/name/index.html
237 return b.Join(b.Dir(in), name, "index"+ext)
238 }
239
240 // CommonDirPath returns the common directory of the given paths.
241 func CommonDirPath(path1, path2 string) string {
242 if path1 == "" || path2 == "" {
243 return ""
244 }
245
246 hadLeadingSlash := strings.HasPrefix(path1, "/") || strings.HasPrefix(path2, "/")
247
248 path1 = TrimLeading(path1)
249 path2 = TrimLeading(path2)
250
251 p1 := strings.Split(path1, "/")
252 p2 := strings.Split(path2, "/")
253
254 var common []string
255
256 for i := 0; i < len(p1) && i < len(p2); i++ {
257 if p1[i] == p2[i] {
258 common = append(common, p1[i])
259 } else {
260 break
261 }
262 }
263
264 s := strings.Join(common, "/")
265
266 if hadLeadingSlash && s != "" {
267 s = "/" + s
268 }
269
270 return s
271 }
272
273 // Sanitize sanitizes string to be used in Hugo's file paths and URLs, allowing only
274 // a predefined set of special Unicode characters.
275 //
276 // Spaces will be replaced with a single hyphen.
277 //
278 // This function is the core function used to normalize paths in Hugo.
279 //
280 // Note that this is the first common step for URL/path sanitation,
281 // the final URL/path may end up looking differently if the user has stricter rules defined (e.g. removePathAccents=true).
282 func Sanitize(s string) string {
283 var willChange bool
284 for i, r := range s {
285 willChange = !isAllowedPathCharacter(s, i, r)
286 if willChange {
287 break
288 }
289 }
290
291 if !willChange {
292 // Prevent allocation when nothing changes.
293 return s
294 }
295
296 target := make([]rune, 0, len(s))
297 var (
298 prependHyphen bool
299 wasHyphen bool
300 )
301
302 for i, r := range s {
303 isAllowed := isAllowedPathCharacter(s, i, r)
304
305 if isAllowed {
306 // track explicit hyphen in input; no need to add a new hyphen if
307 // we just saw one.
308 wasHyphen = r == '-'
309
310 if prependHyphen {
311 // if currently have a hyphen, don't prepend an extra one
312 if !wasHyphen {
313 target = append(target, '-')
314 }
315 prependHyphen = false
316 }
317 target = append(target, r)
318 } else if len(target) > 0 && !wasHyphen && unicode.IsSpace(r) {
319 prependHyphen = true
320 }
321 }
322
323 return string(target)
324 }
325
326 func isAllowedPathCharacter(s string, i int, r rune) bool {
327 if r == ' ' {
328 return false
329 }
330 // Check for the most likely first (faster).
331 isAllowed := unicode.IsLetter(r) || unicode.IsDigit(r)
332 isAllowed = isAllowed || r == '.' || r == '/' || r == '\\' || r == '_' || r == '#' || r == '+' || r == '~' || r == '-' || r == '@'
333 isAllowed = isAllowed || unicode.IsMark(r)
334 isAllowed = isAllowed || (r == '%' && i+2 < len(s) && ishex(s[i+1]) && ishex(s[i+2]))
335 return isAllowed
336 }
337
338 // From https://golang.org/src/net/url/url.go
339 func ishex(c byte) bool {
340 switch {
341 case '0' <= c && c <= '9':
342 return true
343 case 'a' <= c && c <= 'f':
344 return true
345 case 'A' <= c && c <= 'F':
346 return true
347 }
348 return false
349 }
350
351 var slashFunc = func(r rune) bool {
352 return r == '/'
353 }
354
355 // Dir behaves like path.Dir without the path.Clean step.
356 //
357 // The returned path ends in a slash only if it is the root "/".
358 func Dir(s string) string {
359 dir, _ := path.Split(s)
360 if len(dir) > 1 && dir[len(dir)-1] == '/' {
361 return dir[:len(dir)-1]
362 }
363 return dir
364 }
365
366 // FieldsSlash cuts s into fields separated with '/'.
367 func FieldsSlash(s string) []string {
368 f := strings.FieldsFunc(s, slashFunc)
369 return f
370 }
371
372 // DirFile holds the result from path.Split.
373 type DirFile struct {
374 Dir string
375 File string
376 }
377
378 // Used in test.
379 func (df DirFile) String() string {
380 return fmt.Sprintf("%s|%s", df.Dir, df.File)
381 }
382
383 // PathEscape escapes unicode letters in pth.
384 // Use URLEscape to escape full URLs including scheme, query etc.
385 // This is slightly faster for the common case.
386 // Note, there is a url.PathEscape function, but that also
387 // escapes /.
388 func PathEscape(pth string) string {
389 u, err := url.Parse(pth)
390 if err != nil {
391 panic(err)
392 }
393 return u.EscapedPath()
394 }
395
396 // ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer.
397 func ToSlashTrimLeading(s string) string {
398 return TrimLeading(filepath.ToSlash(s))
399 }
400
401 // TrimLeading trims the leading slash from the given string.
402 func TrimLeading(s string) string {
403 return strings.TrimPrefix(s, "/")
404 }
405
406 // ToSlashTrimTrailing is just a filepath.ToSlash with an added / suffix trimmer.
407 func ToSlashTrimTrailing(s string) string {
408 return TrimTrailing(filepath.ToSlash(s))
409 }
410
411 // TrimTrailing trims the trailing slash from the given string.
412 func TrimTrailing(s string) string {
413 return strings.TrimSuffix(s, "/")
414 }
415
416 // ToSlashTrim trims any leading and trailing slashes from the given string and converts it to a forward slash separated path.
417 func ToSlashTrim(s string) string {
418 return strings.Trim(filepath.ToSlash(s), "/")
419 }
420
421 // ToSlashPreserveLeading converts the path given to a forward slash separated path
422 // and preserves the leading slash if present trimming any trailing slash.
423 func ToSlashPreserveLeading(s string) string {
424 return "/" + strings.Trim(filepath.ToSlash(s), "/")
425 }
426
427 // IsSameFilePath checks if s1 and s2 are the same file path.
428 func IsSameFilePath(s1, s2 string) bool {
429 return path.Clean(ToSlashTrim(s1)) == path.Clean(ToSlashTrim(s2))
430 }