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 }