hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

path.go (12823B)

    1 // Copyright 2019 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/gohugoio/hugo/common/text"
   28 
   29 	"github.com/gohugoio/hugo/config"
   30 
   31 	"github.com/gohugoio/hugo/hugofs"
   32 
   33 	"github.com/gohugoio/hugo/common/hugio"
   34 	"github.com/spf13/afero"
   35 )
   36 
   37 // MakePath takes a string with any characters and replace it
   38 // so the string could be used in a path.
   39 // It does so by creating a Unicode-sanitized string, with the spaces replaced,
   40 // whilst preserving the original casing of the string.
   41 // E.g. Social Media -> Social-Media
   42 func (p *PathSpec) MakePath(s string) string {
   43 	return p.UnicodeSanitize(s)
   44 }
   45 
   46 // MakePathsSanitized applies MakePathSanitized on every item in the slice
   47 func (p *PathSpec) MakePathsSanitized(paths []string) {
   48 	for i, path := range paths {
   49 		paths[i] = p.MakePathSanitized(path)
   50 	}
   51 }
   52 
   53 // MakePathSanitized creates a Unicode-sanitized string, with the spaces replaced
   54 func (p *PathSpec) MakePathSanitized(s string) string {
   55 	if p.DisablePathToLower {
   56 		return p.MakePath(s)
   57 	}
   58 	return strings.ToLower(p.MakePath(s))
   59 }
   60 
   61 // ToSlashTrimLeading is just a filepath.ToSlaas with an added / prefix trimmer.
   62 func ToSlashTrimLeading(s string) string {
   63 	return strings.TrimPrefix(filepath.ToSlash(s), "/")
   64 }
   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 // From https://golang.org/src/net/url/url.go
   73 func ishex(c rune) bool {
   74 	switch {
   75 	case '0' <= c && c <= '9':
   76 		return true
   77 	case 'a' <= c && c <= 'f':
   78 		return true
   79 	case 'A' <= c && c <= 'F':
   80 		return true
   81 	}
   82 	return false
   83 }
   84 
   85 // UnicodeSanitize sanitizes string to be used in Hugo URL's, allowing only
   86 // a predefined set of special Unicode characters.
   87 // If RemovePathAccents configuration flag is enabled, Unicode accents
   88 // are also removed.
   89 // Hyphens in the original input are maintained.
   90 // Spaces will be replaced with a single hyphen, and sequential replacement hyphens will be reduced to one.
   91 func (p *PathSpec) UnicodeSanitize(s string) string {
   92 	if p.RemovePathAccents {
   93 		s = text.RemoveAccentsString(s)
   94 	}
   95 
   96 	source := []rune(s)
   97 	target := make([]rune, 0, len(source))
   98 	var (
   99 		prependHyphen bool
  100 		wasHyphen     bool
  101 	)
  102 
  103 	for i, r := range source {
  104 		isAllowed := r == '.' || r == '/' || r == '\\' || r == '_' || r == '#' || r == '+' || r == '~' || r == '-'
  105 		isAllowed = isAllowed || unicode.IsLetter(r) || unicode.IsDigit(r) || unicode.IsMark(r)
  106 		isAllowed = isAllowed || (r == '%' && i+2 < len(source) && ishex(source[i+1]) && ishex(source[i+2]))
  107 
  108 		if isAllowed {
  109 			// track explicit hyphen in input; no need to add a new hyphen if
  110 			// we just saw one.
  111 			wasHyphen = r == '-'
  112 
  113 			if prependHyphen {
  114 				// if currently have a hyphen, don't prepend an extra one
  115 				if !wasHyphen {
  116 					target = append(target, '-')
  117 				}
  118 				prependHyphen = false
  119 			}
  120 			target = append(target, r)
  121 		} else if len(target) > 0 && !wasHyphen && unicode.IsSpace(r) {
  122 			prependHyphen = true
  123 		}
  124 	}
  125 
  126 	return string(target)
  127 }
  128 
  129 func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
  130 	for _, currentPath := range possibleDirectories {
  131 		if strings.HasPrefix(inPath, currentPath) {
  132 			return strings.TrimPrefix(inPath, currentPath), nil
  133 		}
  134 	}
  135 	return inPath, errors.New("can't extract relative path, unknown prefix")
  136 }
  137 
  138 // Should be good enough for Hugo.
  139 var isFileRe = regexp.MustCompile(`.*\..{1,6}$`)
  140 
  141 // GetDottedRelativePath expects a relative path starting after the content directory.
  142 // It returns a relative path with dots ("..") navigating up the path structure.
  143 func GetDottedRelativePath(inPath string) string {
  144 	inPath = filepath.Clean(filepath.FromSlash(inPath))
  145 
  146 	if inPath == "." {
  147 		return "./"
  148 	}
  149 
  150 	if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, FilePathSeparator) {
  151 		inPath += FilePathSeparator
  152 	}
  153 
  154 	if !strings.HasPrefix(inPath, FilePathSeparator) {
  155 		inPath = FilePathSeparator + inPath
  156 	}
  157 
  158 	dir, _ := filepath.Split(inPath)
  159 
  160 	sectionCount := strings.Count(dir, FilePathSeparator)
  161 
  162 	if sectionCount == 0 || dir == FilePathSeparator {
  163 		return "./"
  164 	}
  165 
  166 	var dottedPath string
  167 
  168 	for i := 1; i < sectionCount; i++ {
  169 		dottedPath += "../"
  170 	}
  171 
  172 	return dottedPath
  173 }
  174 
  175 type NamedSlice struct {
  176 	Name  string
  177 	Slice []string
  178 }
  179 
  180 func (n NamedSlice) String() string {
  181 	if len(n.Slice) == 0 {
  182 		return n.Name
  183 	}
  184 	return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ","))
  185 }
  186 
  187 func ExtractAndGroupRootPaths(paths []string) []NamedSlice {
  188 	if len(paths) == 0 {
  189 		return nil
  190 	}
  191 
  192 	pathsCopy := make([]string, len(paths))
  193 	hadSlashPrefix := strings.HasPrefix(paths[0], FilePathSeparator)
  194 
  195 	for i, p := range paths {
  196 		pathsCopy[i] = strings.Trim(filepath.ToSlash(p), "/")
  197 	}
  198 
  199 	sort.Strings(pathsCopy)
  200 
  201 	pathsParts := make([][]string, len(pathsCopy))
  202 
  203 	for i, p := range pathsCopy {
  204 		pathsParts[i] = strings.Split(p, "/")
  205 	}
  206 
  207 	var groups [][]string
  208 
  209 	for i, p1 := range pathsParts {
  210 		c1 := -1
  211 
  212 		for j, p2 := range pathsParts {
  213 			if i == j {
  214 				continue
  215 			}
  216 
  217 			c2 := -1
  218 
  219 			for i, v := range p1 {
  220 				if i >= len(p2) {
  221 					break
  222 				}
  223 				if v != p2[i] {
  224 					break
  225 				}
  226 
  227 				c2 = i
  228 			}
  229 
  230 			if c1 == -1 || (c2 != -1 && c2 < c1) {
  231 				c1 = c2
  232 			}
  233 		}
  234 
  235 		if c1 != -1 {
  236 			groups = append(groups, p1[:c1+1])
  237 		} else {
  238 			groups = append(groups, p1)
  239 		}
  240 	}
  241 
  242 	groupsStr := make([]string, len(groups))
  243 	for i, g := range groups {
  244 		groupsStr[i] = strings.Join(g, "/")
  245 	}
  246 
  247 	groupsStr = UniqueStringsSorted(groupsStr)
  248 
  249 	var result []NamedSlice
  250 
  251 	for _, g := range groupsStr {
  252 		name := filepath.FromSlash(g)
  253 		if hadSlashPrefix {
  254 			name = FilePathSeparator + name
  255 		}
  256 		ns := NamedSlice{Name: name}
  257 		for _, p := range pathsCopy {
  258 			if !strings.HasPrefix(p, g) {
  259 				continue
  260 			}
  261 
  262 			p = strings.TrimPrefix(p, g)
  263 			if p != "" {
  264 				ns.Slice = append(ns.Slice, p)
  265 			}
  266 		}
  267 
  268 		ns.Slice = UniqueStrings(ExtractRootPaths(ns.Slice))
  269 
  270 		result = append(result, ns)
  271 	}
  272 
  273 	return result
  274 }
  275 
  276 // ExtractRootPaths extracts the root paths from the supplied list of paths.
  277 // The resulting root path will not contain any file separators, but there
  278 // may be duplicates.
  279 // So "/content/section/" becomes "content"
  280 func ExtractRootPaths(paths []string) []string {
  281 	r := make([]string, len(paths))
  282 	for i, p := range paths {
  283 		root := filepath.ToSlash(p)
  284 		sections := strings.Split(root, "/")
  285 		for _, section := range sections {
  286 			if section != "" {
  287 				root = section
  288 				break
  289 			}
  290 		}
  291 		r[i] = root
  292 	}
  293 	return r
  294 }
  295 
  296 // FindCWD returns the current working directory from where the Hugo
  297 // executable is run.
  298 func FindCWD() (string, error) {
  299 	serverFile, err := filepath.Abs(os.Args[0])
  300 	if err != nil {
  301 		return "", fmt.Errorf("can't get absolute path for executable: %v", err)
  302 	}
  303 
  304 	path := filepath.Dir(serverFile)
  305 	realFile, err := filepath.EvalSymlinks(serverFile)
  306 	if err != nil {
  307 		if _, err = os.Stat(serverFile + ".exe"); err == nil {
  308 			realFile = filepath.Clean(serverFile + ".exe")
  309 		}
  310 	}
  311 
  312 	if err == nil && realFile != serverFile {
  313 		path = filepath.Dir(realFile)
  314 	}
  315 
  316 	return path, nil
  317 }
  318 
  319 // SymbolicWalk is like filepath.Walk, but it follows symbolic links.
  320 func SymbolicWalk(fs afero.Fs, root string, walker hugofs.WalkFunc) error {
  321 	if _, isOs := fs.(*afero.OsFs); isOs {
  322 		// Mainly to track symlinks.
  323 		fs = hugofs.NewBaseFileDecorator(fs)
  324 	}
  325 
  326 	w := hugofs.NewWalkway(hugofs.WalkwayConfig{
  327 		Fs:     fs,
  328 		Root:   root,
  329 		WalkFn: walker,
  330 	})
  331 
  332 	return w.Walk()
  333 }
  334 
  335 // LstatIfPossible can be used to call Lstat if possible, else Stat.
  336 func LstatIfPossible(fs afero.Fs, path string) (os.FileInfo, error) {
  337 	if lstater, ok := fs.(afero.Lstater); ok {
  338 		fi, _, err := lstater.LstatIfPossible(path)
  339 		return fi, err
  340 	}
  341 
  342 	return fs.Stat(path)
  343 }
  344 
  345 // SafeWriteToDisk is the same as WriteToDisk
  346 // but it also checks to see if file/directory already exists.
  347 func SafeWriteToDisk(inpath string, r io.Reader, fs afero.Fs) (err error) {
  348 	return afero.SafeWriteReader(fs, inpath, r)
  349 }
  350 
  351 // WriteToDisk writes content to disk.
  352 func WriteToDisk(inpath string, r io.Reader, fs afero.Fs) (err error) {
  353 	return afero.WriteReader(fs, inpath, r)
  354 }
  355 
  356 // OpenFilesForWriting opens all the given filenames for writing.
  357 func OpenFilesForWriting(fs afero.Fs, filenames ...string) (io.WriteCloser, error) {
  358 	var writeClosers []io.WriteCloser
  359 	for _, filename := range filenames {
  360 		f, err := OpenFileForWriting(fs, filename)
  361 		if err != nil {
  362 			for _, wc := range writeClosers {
  363 				wc.Close()
  364 			}
  365 			return nil, err
  366 		}
  367 		writeClosers = append(writeClosers, f)
  368 	}
  369 
  370 	return hugio.NewMultiWriteCloser(writeClosers...), nil
  371 }
  372 
  373 // OpenFileForWriting opens or creates the given file. If the target directory
  374 // does not exist, it gets created.
  375 func OpenFileForWriting(fs afero.Fs, filename string) (afero.File, error) {
  376 	filename = filepath.Clean(filename)
  377 	// Create will truncate if file already exists.
  378 	// os.Create will create any new files with mode 0666 (before umask).
  379 	f, err := fs.Create(filename)
  380 	if err != nil {
  381 		if !os.IsNotExist(err) {
  382 			return nil, err
  383 		}
  384 		if err = fs.MkdirAll(filepath.Dir(filename), 0777); err != nil { //  before umask
  385 			return nil, err
  386 		}
  387 		f, err = fs.Create(filename)
  388 	}
  389 
  390 	return f, err
  391 }
  392 
  393 // GetCacheDir returns a cache dir from the given filesystem and config.
  394 // The dir will be created if it does not exist.
  395 func GetCacheDir(fs afero.Fs, cfg config.Provider) (string, error) {
  396 	cacheDir := getCacheDir(cfg)
  397 	if cacheDir != "" {
  398 		exists, err := DirExists(cacheDir, fs)
  399 		if err != nil {
  400 			return "", err
  401 		}
  402 		if !exists {
  403 			err := fs.MkdirAll(cacheDir, 0777) // Before umask
  404 			if err != nil {
  405 				return "", fmt.Errorf("failed to create cache dir: %w", err)
  406 			}
  407 		}
  408 		return cacheDir, nil
  409 	}
  410 
  411 	// Fall back to a cache in /tmp.
  412 	return GetTempDir("hugo_cache", fs), nil
  413 }
  414 
  415 func getCacheDir(cfg config.Provider) string {
  416 	// Always use the cacheDir config if set.
  417 	cacheDir := cfg.GetString("cacheDir")
  418 	if len(cacheDir) > 1 {
  419 		return addTrailingFileSeparator(cacheDir)
  420 	}
  421 
  422 	// See Issue #8714.
  423 	// Turns out that Cloudflare also sets NETLIFY=true in its build environment,
  424 	// but all of these 3 should not give any false positives.
  425 	if os.Getenv("NETLIFY") == "true" && os.Getenv("PULL_REQUEST") != "" && os.Getenv("DEPLOY_PRIME_URL") != "" {
  426 		// Netlify's cache behaviour is not documented, the currently best example
  427 		// is this project:
  428 		// https://github.com/philhawksworth/content-shards/blob/master/gulpfile.js
  429 		return "/opt/build/cache/hugo_cache/"
  430 	}
  431 
  432 	// This will fall back to an hugo_cache folder in the tmp dir, which should work fine for most CI
  433 	// providers. See this for a working CircleCI setup:
  434 	// https://github.com/bep/hugo-sass-test/blob/6c3960a8f4b90e8938228688bc49bdcdd6b2d99e/.circleci/config.yml
  435 	// If not, they can set the HUGO_CACHEDIR environment variable or cacheDir config key.
  436 	return ""
  437 }
  438 
  439 func addTrailingFileSeparator(s string) string {
  440 	if !strings.HasSuffix(s, FilePathSeparator) {
  441 		s = s + FilePathSeparator
  442 	}
  443 	return s
  444 }
  445 
  446 // GetTempDir returns a temporary directory with the given sub path.
  447 func GetTempDir(subPath string, fs afero.Fs) string {
  448 	return afero.GetTempDir(fs, subPath)
  449 }
  450 
  451 // DirExists checks if a path exists and is a directory.
  452 func DirExists(path string, fs afero.Fs) (bool, error) {
  453 	return afero.DirExists(fs, path)
  454 }
  455 
  456 // IsDir checks if a given path is a directory.
  457 func IsDir(path string, fs afero.Fs) (bool, error) {
  458 	return afero.IsDir(fs, path)
  459 }
  460 
  461 // IsEmpty checks if a given path is empty, meaning it doesn't contain any regular files.
  462 func IsEmpty(path string, fs afero.Fs) (bool, error) {
  463 	var hasFile bool
  464 	err := afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
  465 		if info.IsDir() {
  466 			return nil
  467 		}
  468 		hasFile = true
  469 		return filepath.SkipDir
  470 	})
  471 	return !hasFile, err
  472 }
  473 
  474 // Exists checks if a file or directory exists.
  475 func Exists(path string, fs afero.Fs) (bool, error) {
  476 	return afero.Exists(fs, path)
  477 }
  478 
  479 // AddTrailingSlash adds a trailing Unix styled slash (/) if not already
  480 // there.
  481 func AddTrailingSlash(path string) string {
  482 	if !strings.HasSuffix(path, "/") {
  483 		path += "/"
  484 	}
  485 	return path
  486 }