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  }