/common/paths/path.go

https://gitlab.com/gohugo/hugo · Go · 265 lines · 159 code · 43 blank · 63 comment · 41 complexity · b01dc437a9b2c231069abf0412ef1aa7 MD5 · raw file

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