github.com/olliephillips/hugo@v0.42.2/helpers/path.go (about)

     1  // Copyright 2015 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 helpers
    15  
    16  import (
    17  	"errors"
    18  	"fmt"
    19  	"io"
    20  	"os"
    21  	"path/filepath"
    22  	"regexp"
    23  	"sort"
    24  	"strings"
    25  	"unicode"
    26  
    27  	"github.com/spf13/afero"
    28  	"golang.org/x/text/transform"
    29  	"golang.org/x/text/unicode/norm"
    30  )
    31  
    32  var (
    33  	// ErrThemeUndefined is returned when a theme has not be defined by the user.
    34  	ErrThemeUndefined = errors.New("no theme set")
    35  )
    36  
    37  // filepathPathBridge is a bridge for common functionality in filepath vs path
    38  type filepathPathBridge interface {
    39  	Base(in string) string
    40  	Clean(in string) string
    41  	Dir(in string) string
    42  	Ext(in string) string
    43  	Join(elem ...string) string
    44  	Separator() string
    45  }
    46  
    47  type filepathBridge struct {
    48  }
    49  
    50  func (filepathBridge) Base(in string) string {
    51  	return filepath.Base(in)
    52  }
    53  
    54  func (filepathBridge) Clean(in string) string {
    55  	return filepath.Clean(in)
    56  }
    57  
    58  func (filepathBridge) Dir(in string) string {
    59  	return filepath.Dir(in)
    60  }
    61  
    62  func (filepathBridge) Ext(in string) string {
    63  	return filepath.Ext(in)
    64  }
    65  
    66  func (filepathBridge) Join(elem ...string) string {
    67  	return filepath.Join(elem...)
    68  }
    69  
    70  func (filepathBridge) Separator() string {
    71  	return FilePathSeparator
    72  }
    73  
    74  var fpb filepathBridge
    75  
    76  // MakePath takes a string with any characters and replace it
    77  // so the string could be used in a path.
    78  // It does so by creating a Unicode-sanitized string, with the spaces replaced,
    79  // whilst preserving the original casing of the string.
    80  // E.g. Social Media -> Social-Media
    81  func (p *PathSpec) MakePath(s string) string {
    82  	return p.UnicodeSanitize(strings.Replace(strings.TrimSpace(s), " ", "-", -1))
    83  }
    84  
    85  // MakePathSanitized creates a Unicode-sanitized string, with the spaces replaced
    86  func (p *PathSpec) MakePathSanitized(s string) string {
    87  	if p.DisablePathToLower {
    88  		return p.MakePath(s)
    89  	}
    90  	return strings.ToLower(p.MakePath(s))
    91  }
    92  
    93  // MakeTitle converts the path given to a suitable title, trimming whitespace
    94  // and replacing hyphens with whitespace.
    95  func MakeTitle(inpath string) string {
    96  	return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1)
    97  }
    98  
    99  // From https://golang.org/src/net/url/url.go
   100  func ishex(c rune) bool {
   101  	switch {
   102  	case '0' <= c && c <= '9':
   103  		return true
   104  	case 'a' <= c && c <= 'f':
   105  		return true
   106  	case 'A' <= c && c <= 'F':
   107  		return true
   108  	}
   109  	return false
   110  }
   111  
   112  // UnicodeSanitize sanitizes string to be used in Hugo URL's, allowing only
   113  // a predefined set of special Unicode characters.
   114  // If RemovePathAccents configuration flag is enabled, Uniccode accents
   115  // are also removed.
   116  func (p *PathSpec) UnicodeSanitize(s string) string {
   117  	source := []rune(s)
   118  	target := make([]rune, 0, len(source))
   119  
   120  	for i, r := range source {
   121  		if r == '%' && i+2 < len(source) && ishex(source[i+1]) && ishex(source[i+2]) {
   122  			target = append(target, r)
   123  		} else if unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsMark(r) || r == '.' || r == '/' || r == '\\' || r == '_' || r == '-' || r == '#' || r == '+' || r == '~' {
   124  			target = append(target, r)
   125  		}
   126  	}
   127  
   128  	var result string
   129  
   130  	if p.RemovePathAccents {
   131  		// remove accents - see https://blog.golang.org/normalization
   132  		t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC)
   133  		result, _, _ = transform.String(t, string(target))
   134  	} else {
   135  		result = string(target)
   136  	}
   137  
   138  	return result
   139  }
   140  
   141  func isMn(r rune) bool {
   142  	return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks
   143  }
   144  
   145  // ReplaceExtension takes a path and an extension, strips the old extension
   146  // and returns the path with the new extension.
   147  func ReplaceExtension(path string, newExt string) string {
   148  	f, _ := fileAndExt(path, fpb)
   149  	return f + "." + newExt
   150  }
   151  
   152  // GetFirstThemeDir gets the root directory of the first theme, if there is one.
   153  // If there is no theme, returns the empty string.
   154  func (p *PathSpec) GetFirstThemeDir() string {
   155  	if p.ThemeSet() {
   156  		return p.AbsPathify(filepath.Join(p.ThemesDir, p.Themes()[0]))
   157  	}
   158  	return ""
   159  }
   160  
   161  // GetThemesDir gets the absolute root theme dir path.
   162  func (p *PathSpec) GetThemesDir() string {
   163  	if p.ThemeSet() {
   164  		return p.AbsPathify(p.ThemesDir)
   165  	}
   166  	return ""
   167  }
   168  
   169  // GetRelativeThemeDir gets the relative root directory of the current theme, if there is one.
   170  // If there is no theme, returns the empty string.
   171  func (p *PathSpec) GetRelativeThemeDir() string {
   172  	if p.ThemeSet() {
   173  		return strings.TrimPrefix(filepath.Join(p.ThemesDir, p.Themes()[0]), FilePathSeparator)
   174  	}
   175  	return ""
   176  }
   177  
   178  func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
   179  
   180  	for _, currentPath := range possibleDirectories {
   181  		if strings.HasPrefix(inPath, currentPath) {
   182  			return strings.TrimPrefix(inPath, currentPath), nil
   183  		}
   184  	}
   185  	return inPath, errors.New("Can't extract relative path, unknown prefix")
   186  }
   187  
   188  // Should be good enough for Hugo.
   189  var isFileRe = regexp.MustCompile(`.*\..{1,6}$`)
   190  
   191  // GetDottedRelativePath expects a relative path starting after the content directory.
   192  // It returns a relative path with dots ("..") navigating up the path structure.
   193  func GetDottedRelativePath(inPath string) string {
   194  	inPath = filepath.Clean(filepath.FromSlash(inPath))
   195  
   196  	if inPath == "." {
   197  		return "./"
   198  	}
   199  
   200  	if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, FilePathSeparator) {
   201  		inPath += FilePathSeparator
   202  	}
   203  
   204  	if !strings.HasPrefix(inPath, FilePathSeparator) {
   205  		inPath = FilePathSeparator + inPath
   206  	}
   207  
   208  	dir, _ := filepath.Split(inPath)
   209  
   210  	sectionCount := strings.Count(dir, FilePathSeparator)
   211  
   212  	if sectionCount == 0 || dir == FilePathSeparator {
   213  		return "./"
   214  	}
   215  
   216  	var dottedPath string
   217  
   218  	for i := 1; i < sectionCount; i++ {
   219  		dottedPath += "../"
   220  	}
   221  
   222  	return dottedPath
   223  }
   224  
   225  // Ext takes a path and returns the extension, including the delmiter, i.e. ".md".
   226  func Ext(in string) string {
   227  	_, ext := fileAndExt(in, fpb)
   228  	return ext
   229  }
   230  
   231  // FileAndExt takes a path and returns the file and extension separated,
   232  // the extension including the delmiter, i.e. ".md".
   233  func FileAndExt(in string) (string, string) {
   234  	return fileAndExt(in, fpb)
   235  }
   236  
   237  // Filename takes a path, strips out the extension,
   238  // and returns the name of the file.
   239  func Filename(in string) (name string) {
   240  	name, _ = fileAndExt(in, fpb)
   241  	return
   242  }
   243  
   244  // FileAndExt returns the filename and any extension of a file path as
   245  // two separate strings.
   246  //
   247  // If the path, in, contains a directory name ending in a slash,
   248  // then both name and ext will be empty strings.
   249  //
   250  // If the path, in, is either the current directory, the parent
   251  // directory or the root directory, or an empty string,
   252  // then both name and ext will be empty strings.
   253  //
   254  // If the path, in, represents the path of a file without an extension,
   255  // then name will be the name of the file and ext will be an empty string.
   256  //
   257  // If the path, in, represents a filename with an extension,
   258  // then name will be the filename minus any extension - including the dot
   259  // and ext will contain the extension - minus the dot.
   260  func fileAndExt(in string, b filepathPathBridge) (name string, ext string) {
   261  	ext = b.Ext(in)
   262  	base := b.Base(in)
   263  
   264  	return extractFilename(in, ext, base, b.Separator()), ext
   265  }
   266  
   267  func extractFilename(in, ext, base, pathSeparator string) (name string) {
   268  
   269  	// No file name cases. These are defined as:
   270  	// 1. any "in" path that ends in a pathSeparator
   271  	// 2. any "base" consisting of just an pathSeparator
   272  	// 3. any "base" consisting of just an empty string
   273  	// 4. any "base" consisting of just the current directory i.e. "."
   274  	// 5. any "base" consisting of just the parent directory i.e. ".."
   275  	if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator {
   276  		name = "" // there is NO filename
   277  	} else if ext != "" { // there was an Extension
   278  		// return the filename minus the extension (and the ".")
   279  		name = base[:strings.LastIndex(base, ".")]
   280  	} else {
   281  		// no extension case so just return base, which willi
   282  		// be the filename
   283  		name = base
   284  	}
   285  	return
   286  
   287  }
   288  
   289  // GetRelativePath returns the relative path of a given path.
   290  func GetRelativePath(path, base string) (final string, err error) {
   291  	if filepath.IsAbs(path) && base == "" {
   292  		return "", errors.New("source: missing base directory")
   293  	}
   294  	name := filepath.Clean(path)
   295  	base = filepath.Clean(base)
   296  
   297  	name, err = filepath.Rel(base, name)
   298  	if err != nil {
   299  		return "", err
   300  	}
   301  
   302  	if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) {
   303  		name += FilePathSeparator
   304  	}
   305  	return name, nil
   306  }
   307  
   308  // PathPrep prepares the path using the uglify setting to create paths on
   309  // either the form /section/name/index.html or /section/name.html.
   310  func PathPrep(ugly bool, in string) string {
   311  	if ugly {
   312  		return Uglify(in)
   313  	}
   314  	return PrettifyPath(in)
   315  }
   316  
   317  // PrettifyPath is the same as PrettifyURLPath but for file paths.
   318  //     /section/name.html       becomes /section/name/index.html
   319  //     /section/name/           becomes /section/name/index.html
   320  //     /section/name/index.html becomes /section/name/index.html
   321  func PrettifyPath(in string) string {
   322  	return prettifyPath(in, fpb)
   323  }
   324  
   325  func prettifyPath(in string, b filepathPathBridge) string {
   326  	if filepath.Ext(in) == "" {
   327  		// /section/name/  -> /section/name/index.html
   328  		if len(in) < 2 {
   329  			return b.Separator()
   330  		}
   331  		return b.Join(in, "index.html")
   332  	}
   333  	name, ext := fileAndExt(in, b)
   334  	if name == "index" {
   335  		// /section/name/index.html -> /section/name/index.html
   336  		return b.Clean(in)
   337  	}
   338  	// /section/name.html -> /section/name/index.html
   339  	return b.Join(b.Dir(in), name, "index"+ext)
   340  }
   341  
   342  // ExtractRootPaths extracts the root paths from the supplied list of paths.
   343  // The resulting root path will not contain any file separators, but there
   344  // may be duplicates.
   345  // So "/content/section/" becomes "content"
   346  func ExtractRootPaths(paths []string) []string {
   347  	r := make([]string, len(paths))
   348  	for i, p := range paths {
   349  		root := filepath.ToSlash(p)
   350  		sections := strings.Split(root, "/")
   351  		for _, section := range sections {
   352  			if section != "" {
   353  				root = section
   354  				break
   355  			}
   356  		}
   357  		r[i] = root
   358  	}
   359  	return r
   360  
   361  }
   362  
   363  // FindCWD returns the current working directory from where the Hugo
   364  // executable is run.
   365  func FindCWD() (string, error) {
   366  	serverFile, err := filepath.Abs(os.Args[0])
   367  
   368  	if err != nil {
   369  		return "", fmt.Errorf("Can't get absolute path for executable: %v", err)
   370  	}
   371  
   372  	path := filepath.Dir(serverFile)
   373  	realFile, err := filepath.EvalSymlinks(serverFile)
   374  
   375  	if err != nil {
   376  		if _, err = os.Stat(serverFile + ".exe"); err == nil {
   377  			realFile = filepath.Clean(serverFile + ".exe")
   378  		}
   379  	}
   380  
   381  	if err == nil && realFile != serverFile {
   382  		path = filepath.Dir(realFile)
   383  	}
   384  
   385  	return path, nil
   386  }
   387  
   388  // SymbolicWalk is like filepath.Walk, but it supports the root being a
   389  // symbolic link. It will still not follow symbolic links deeper down in
   390  // the file structure.
   391  func SymbolicWalk(fs afero.Fs, root string, walker filepath.WalkFunc) error {
   392  
   393  	// Sanity check
   394  	if root != "" && len(root) < 4 {
   395  		return errors.New("Path is too short")
   396  	}
   397  
   398  	// Handle the root first
   399  	fileInfo, realPath, err := getRealFileInfo(fs, root)
   400  
   401  	if err != nil {
   402  		return walker(root, nil, err)
   403  	}
   404  
   405  	if !fileInfo.IsDir() {
   406  		return fmt.Errorf("Cannot walk regular file %s", root)
   407  	}
   408  
   409  	if err := walker(realPath, fileInfo, err); err != nil && err != filepath.SkipDir {
   410  		return err
   411  	}
   412  
   413  	// Some of Hugo's filesystems represents an ordered root folder, i.e. project first, then theme folders.
   414  	// Make sure that order is preserved. afero.Walk will sort the directories down in the file tree,
   415  	// but we don't care about that.
   416  	rootContent, err := readDir(fs, root, false)
   417  
   418  	if err != nil {
   419  		return walker(root, nil, err)
   420  	}
   421  
   422  	for _, fi := range rootContent {
   423  		if err := afero.Walk(fs, filepath.Join(root, fi.Name()), walker); err != nil {
   424  			return err
   425  		}
   426  	}
   427  
   428  	return nil
   429  
   430  }
   431  
   432  func readDir(fs afero.Fs, dirname string, doSort bool) ([]os.FileInfo, error) {
   433  	f, err := fs.Open(dirname)
   434  	if err != nil {
   435  		return nil, err
   436  	}
   437  	list, err := f.Readdir(-1)
   438  	f.Close()
   439  	if err != nil {
   440  		return nil, err
   441  	}
   442  	if doSort {
   443  		sort.Slice(list, func(i, j int) bool { return list[i].Name() < list[j].Name() })
   444  	}
   445  	return list, nil
   446  }
   447  
   448  func getRealFileInfo(fs afero.Fs, path string) (os.FileInfo, string, error) {
   449  	fileInfo, err := LstatIfPossible(fs, path)
   450  	realPath := path
   451  
   452  	if err != nil {
   453  		return nil, "", err
   454  	}
   455  
   456  	if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
   457  		link, err := filepath.EvalSymlinks(path)
   458  		if err != nil {
   459  			return nil, "", fmt.Errorf("Cannot read symbolic link '%s', error was: %s", path, err)
   460  		}
   461  		fileInfo, err = LstatIfPossible(fs, link)
   462  		if err != nil {
   463  			return nil, "", fmt.Errorf("Cannot stat '%s', error was: %s", link, err)
   464  		}
   465  		realPath = link
   466  	}
   467  	return fileInfo, realPath, nil
   468  }
   469  
   470  // GetRealPath returns the real file path for the given path, whether it is a
   471  // symlink or not.
   472  func GetRealPath(fs afero.Fs, path string) (string, error) {
   473  	_, realPath, err := getRealFileInfo(fs, path)
   474  
   475  	if err != nil {
   476  		return "", err
   477  	}
   478  
   479  	return realPath, nil
   480  }
   481  
   482  // LstatIfPossible can be used to call Lstat if possible, else Stat.
   483  func LstatIfPossible(fs afero.Fs, path string) (os.FileInfo, error) {
   484  	if lstater, ok := fs.(afero.Lstater); ok {
   485  		fi, _, err := lstater.LstatIfPossible(path)
   486  		return fi, err
   487  	}
   488  
   489  	return fs.Stat(path)
   490  }
   491  
   492  // SafeWriteToDisk is the same as WriteToDisk
   493  // but it also checks to see if file/directory already exists.
   494  func SafeWriteToDisk(inpath string, r io.Reader, fs afero.Fs) (err error) {
   495  	return afero.SafeWriteReader(fs, inpath, r)
   496  }
   497  
   498  // WriteToDisk writes content to disk.
   499  func WriteToDisk(inpath string, r io.Reader, fs afero.Fs) (err error) {
   500  	return afero.WriteReader(fs, inpath, r)
   501  }
   502  
   503  // GetTempDir returns a temporary directory with the given sub path.
   504  func GetTempDir(subPath string, fs afero.Fs) string {
   505  	return afero.GetTempDir(fs, subPath)
   506  }
   507  
   508  // DirExists checks if a path exists and is a directory.
   509  func DirExists(path string, fs afero.Fs) (bool, error) {
   510  	return afero.DirExists(fs, path)
   511  }
   512  
   513  // IsDir checks if a given path is a directory.
   514  func IsDir(path string, fs afero.Fs) (bool, error) {
   515  	return afero.IsDir(fs, path)
   516  }
   517  
   518  // IsEmpty checks if a given path is empty.
   519  func IsEmpty(path string, fs afero.Fs) (bool, error) {
   520  	return afero.IsEmpty(fs, path)
   521  }
   522  
   523  // FileContains checks if a file contains a specified string.
   524  func FileContains(filename string, subslice []byte, fs afero.Fs) (bool, error) {
   525  	return afero.FileContainsBytes(fs, filename, subslice)
   526  }
   527  
   528  // FileContainsAny checks if a file contains any of the specified strings.
   529  func FileContainsAny(filename string, subslices [][]byte, fs afero.Fs) (bool, error) {
   530  	return afero.FileContainsAnyBytes(fs, filename, subslices)
   531  }
   532  
   533  // Exists checks if a file or directory exists.
   534  func Exists(path string, fs afero.Fs) (bool, error) {
   535  	return afero.Exists(fs, path)
   536  }