github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/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 "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 // MakeTitle converts the path given to a suitable title, trimming whitespace 67 // and replacing hyphens with whitespace. 68 func MakeTitle(inpath string) string { 69 return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1) 70 } 71 72 // ReplaceExtension takes a path and an extension, strips the old extension 73 // and returns the path with the new extension. 74 func ReplaceExtension(path string, newExt string) string { 75 f, _ := fileAndExt(path, fpb) 76 return f + "." + newExt 77 } 78 79 func makePathRelative(inPath string, possibleDirectories ...string) (string, error) { 80 for _, currentPath := range possibleDirectories { 81 if strings.HasPrefix(inPath, currentPath) { 82 return strings.TrimPrefix(inPath, currentPath), nil 83 } 84 } 85 return inPath, errors.New("can't extract relative path, unknown prefix") 86 } 87 88 // Should be good enough for Hugo. 89 var isFileRe = regexp.MustCompile(`.*\..{1,6}$`) 90 91 // GetDottedRelativePath expects a relative path starting after the content directory. 92 // It returns a relative path with dots ("..") navigating up the path structure. 93 func GetDottedRelativePath(inPath string) string { 94 inPath = filepath.Clean(filepath.FromSlash(inPath)) 95 96 if inPath == "." { 97 return "./" 98 } 99 100 if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, FilePathSeparator) { 101 inPath += FilePathSeparator 102 } 103 104 if !strings.HasPrefix(inPath, FilePathSeparator) { 105 inPath = FilePathSeparator + inPath 106 } 107 108 dir, _ := filepath.Split(inPath) 109 110 sectionCount := strings.Count(dir, FilePathSeparator) 111 112 if sectionCount == 0 || dir == FilePathSeparator { 113 return "./" 114 } 115 116 var dottedPath string 117 118 for i := 1; i < sectionCount; i++ { 119 dottedPath += "../" 120 } 121 122 return dottedPath 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 // PathNoExt takes a path, strips out the extension, 162 // and returns the name of the file. 163 func PathNoExt(in string) string { 164 return strings.TrimSuffix(in, path.Ext(in)) 165 } 166 167 // FileAndExt returns the filename and any extension of a file path as 168 // two separate strings. 169 // 170 // If the path, in, contains a directory name ending in a slash, 171 // then both name and ext will be empty strings. 172 // 173 // If the path, in, is either the current directory, the parent 174 // directory or the root directory, or an empty string, 175 // then both name and ext will be empty strings. 176 // 177 // If the path, in, represents the path of a file without an extension, 178 // then name will be the name of the file and ext will be an empty string. 179 // 180 // If the path, in, represents a filename with an extension, 181 // then name will be the filename minus any extension - including the dot 182 // and ext will contain the extension - minus the dot. 183 func fileAndExt(in string, b filepathPathBridge) (name string, ext string) { 184 ext = b.Ext(in) 185 base := b.Base(in) 186 187 return extractFilename(in, ext, base, b.Separator()), ext 188 } 189 190 func extractFilename(in, ext, base, pathSeparator string) (name string) { 191 // No file name cases. These are defined as: 192 // 1. any "in" path that ends in a pathSeparator 193 // 2. any "base" consisting of just an pathSeparator 194 // 3. any "base" consisting of just an empty string 195 // 4. any "base" consisting of just the current directory i.e. "." 196 // 5. any "base" consisting of just the parent directory i.e. ".." 197 if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator { 198 name = "" // there is NO filename 199 } else if ext != "" { // there was an Extension 200 // return the filename minus the extension (and the ".") 201 name = base[:strings.LastIndex(base, ".")] 202 } else { 203 // no extension case so just return base, which willi 204 // be the filename 205 name = base 206 } 207 return 208 } 209 210 // GetRelativePath returns the relative path of a given path. 211 func GetRelativePath(path, base string) (final string, err error) { 212 if filepath.IsAbs(path) && base == "" { 213 return "", errors.New("source: missing base directory") 214 } 215 name := filepath.Clean(path) 216 base = filepath.Clean(base) 217 218 name, err = filepath.Rel(base, name) 219 if err != nil { 220 return "", err 221 } 222 223 if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) { 224 name += FilePathSeparator 225 } 226 return name, nil 227 } 228 229 func prettifyPath(in string, b filepathPathBridge) string { 230 if filepath.Ext(in) == "" { 231 // /section/name/ -> /section/name/index.html 232 if len(in) < 2 { 233 return b.Separator() 234 } 235 return b.Join(in, "index.html") 236 } 237 name, ext := fileAndExt(in, b) 238 if name == "index" { 239 // /section/name/index.html -> /section/name/index.html 240 return b.Clean(in) 241 } 242 // /section/name.html -> /section/name/index.html 243 return b.Join(b.Dir(in), name, "index"+ext) 244 } 245 246 type NamedSlice struct { 247 Name string 248 Slice []string 249 } 250 251 func (n NamedSlice) String() string { 252 if len(n.Slice) == 0 { 253 return n.Name 254 } 255 return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ",")) 256 }