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 }