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