path.go (7824B)
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 "path" 20 "path/filepath" 21 "regexp" 22 "strings" 23 ) 24 25 // FilePathSeparator as defined by os.Separator. 26 const FilePathSeparator = string(filepath.Separator) 27 28 // filepathPathBridge is a bridge for common functionality in filepath vs path 29 type filepathPathBridge interface { 30 Base(in string) string 31 Clean(in string) string 32 Dir(in string) string 33 Ext(in string) string 34 Join(elem ...string) string 35 Separator() string 36 } 37 38 type filepathBridge struct{} 39 40 func (filepathBridge) Base(in string) string { 41 return filepath.Base(in) 42 } 43 44 func (filepathBridge) Clean(in string) string { 45 return filepath.Clean(in) 46 } 47 48 func (filepathBridge) Dir(in string) string { 49 return filepath.Dir(in) 50 } 51 52 func (filepathBridge) Ext(in string) string { 53 return filepath.Ext(in) 54 } 55 56 func (filepathBridge) Join(elem ...string) string { 57 return filepath.Join(elem...) 58 } 59 60 func (filepathBridge) Separator() string { 61 return FilePathSeparator 62 } 63 64 var fpb filepathBridge 65 66 // AbsPathify creates an absolute path if given a working dir and a relative path. 67 // If already absolute, the path is just cleaned. 68 func AbsPathify(workingDir, inPath string) string { 69 if filepath.IsAbs(inPath) { 70 return filepath.Clean(inPath) 71 } 72 return filepath.Join(workingDir, inPath) 73 } 74 75 // MakeTitle converts the path given to a suitable title, trimming whitespace 76 // and replacing hyphens with whitespace. 77 func MakeTitle(inpath string) string { 78 return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1) 79 } 80 81 // ReplaceExtension takes a path and an extension, strips the old extension 82 // and returns the path with the new extension. 83 func ReplaceExtension(path string, newExt string) string { 84 f, _ := fileAndExt(path, fpb) 85 return f + "." + newExt 86 } 87 88 func makePathRelative(inPath string, possibleDirectories ...string) (string, error) { 89 for _, currentPath := range possibleDirectories { 90 if strings.HasPrefix(inPath, currentPath) { 91 return strings.TrimPrefix(inPath, currentPath), nil 92 } 93 } 94 return inPath, errors.New("can't extract relative path, unknown prefix") 95 } 96 97 // Should be good enough for Hugo. 98 var isFileRe = regexp.MustCompile(`.*\..{1,6}$`) 99 100 // GetDottedRelativePath expects a relative path starting after the content directory. 101 // It returns a relative path with dots ("..") navigating up the path structure. 102 func GetDottedRelativePath(inPath string) string { 103 inPath = filepath.Clean(filepath.FromSlash(inPath)) 104 105 if inPath == "." { 106 return "./" 107 } 108 109 if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, FilePathSeparator) { 110 inPath += FilePathSeparator 111 } 112 113 if !strings.HasPrefix(inPath, FilePathSeparator) { 114 inPath = FilePathSeparator + inPath 115 } 116 117 dir, _ := filepath.Split(inPath) 118 119 sectionCount := strings.Count(dir, FilePathSeparator) 120 121 if sectionCount == 0 || dir == FilePathSeparator { 122 return "./" 123 } 124 125 var dottedPath string 126 127 for i := 1; i < sectionCount; i++ { 128 dottedPath += "../" 129 } 130 131 return dottedPath 132 } 133 134 // ExtNoDelimiter takes a path and returns the extension, excluding the delimiter, i.e. "md". 135 func ExtNoDelimiter(in string) string { 136 return strings.TrimPrefix(Ext(in), ".") 137 } 138 139 // Ext takes a path and returns the extension, including the delimiter, i.e. ".md". 140 func Ext(in string) string { 141 _, ext := fileAndExt(in, fpb) 142 return ext 143 } 144 145 // PathAndExt is the same as FileAndExt, but it uses the path package. 146 func PathAndExt(in string) (string, string) { 147 return fileAndExt(in, pb) 148 } 149 150 // FileAndExt takes a path and returns the file and extension separated, 151 // the extension including the delimiter, i.e. ".md". 152 func FileAndExt(in string) (string, string) { 153 return fileAndExt(in, fpb) 154 } 155 156 // FileAndExtNoDelimiter takes a path and returns the file and extension separated, 157 // the extension excluding the delimiter, e.g "md". 158 func FileAndExtNoDelimiter(in string) (string, string) { 159 file, ext := fileAndExt(in, fpb) 160 return file, strings.TrimPrefix(ext, ".") 161 } 162 163 // Filename takes a file path, strips out the extension, 164 // and returns the name of the file. 165 func Filename(in string) (name string) { 166 name, _ = fileAndExt(in, fpb) 167 return 168 } 169 170 // PathNoExt takes a path, strips out the extension, 171 // and returns the name of the file. 172 func PathNoExt(in string) string { 173 return strings.TrimSuffix(in, path.Ext(in)) 174 } 175 176 // FileAndExt returns the filename and any extension of a file path as 177 // two separate strings. 178 // 179 // If the path, in, contains a directory name ending in a slash, 180 // then both name and ext will be empty strings. 181 // 182 // If the path, in, is either the current directory, the parent 183 // directory or the root directory, or an empty string, 184 // then both name and ext will be empty strings. 185 // 186 // If the path, in, represents the path of a file without an extension, 187 // then name will be the name of the file and ext will be an empty string. 188 // 189 // If the path, in, represents a filename with an extension, 190 // then name will be the filename minus any extension - including the dot 191 // and ext will contain the extension - minus the dot. 192 func fileAndExt(in string, b filepathPathBridge) (name string, ext string) { 193 ext = b.Ext(in) 194 base := b.Base(in) 195 196 return extractFilename(in, ext, base, b.Separator()), ext 197 } 198 199 func extractFilename(in, ext, base, pathSeparator string) (name string) { 200 // No file name cases. These are defined as: 201 // 1. any "in" path that ends in a pathSeparator 202 // 2. any "base" consisting of just an pathSeparator 203 // 3. any "base" consisting of just an empty string 204 // 4. any "base" consisting of just the current directory i.e. "." 205 // 5. any "base" consisting of just the parent directory i.e. ".." 206 if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator { 207 name = "" // there is NO filename 208 } else if ext != "" { // there was an Extension 209 // return the filename minus the extension (and the ".") 210 name = base[:strings.LastIndex(base, ".")] 211 } else { 212 // no extension case so just return base, which willi 213 // be the filename 214 name = base 215 } 216 return 217 } 218 219 // GetRelativePath returns the relative path of a given path. 220 func GetRelativePath(path, base string) (final string, err error) { 221 if filepath.IsAbs(path) && base == "" { 222 return "", errors.New("source: missing base directory") 223 } 224 name := filepath.Clean(path) 225 base = filepath.Clean(base) 226 227 name, err = filepath.Rel(base, name) 228 if err != nil { 229 return "", err 230 } 231 232 if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) { 233 name += FilePathSeparator 234 } 235 return name, nil 236 } 237 238 func prettifyPath(in string, b filepathPathBridge) string { 239 if filepath.Ext(in) == "" { 240 // /section/name/ -> /section/name/index.html 241 if len(in) < 2 { 242 return b.Separator() 243 } 244 return b.Join(in, "index.html") 245 } 246 name, ext := fileAndExt(in, b) 247 if name == "index" { 248 // /section/name/index.html -> /section/name/index.html 249 return b.Clean(in) 250 } 251 // /section/name.html -> /section/name/index.html 252 return b.Join(b.Dir(in), name, "index"+ext) 253 } 254 255 type NamedSlice struct { 256 Name string 257 Slice []string 258 } 259 260 func (n NamedSlice) String() string { 261 if len(n.Slice) == 0 { 262 return n.Name 263 } 264 return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ",")) 265 }