diff options
-rw-r--r-- | commands/server.go | 4 | ||||
-rw-r--r-- | common/paths/path.go | 312 | ||||
-rw-r--r-- | common/paths/path_test.go | 252 | ||||
-rw-r--r-- | common/paths/url.go | 212 | ||||
-rw-r--r-- | common/paths/url_test.go | 129 | ||||
-rw-r--r-- | create/content.go | 4 | ||||
-rw-r--r-- | create/content_template_handler.go | 4 | ||||
-rw-r--r-- | helpers/path.go | 185 | ||||
-rw-r--r-- | helpers/path_test.go | 135 | ||||
-rw-r--r-- | helpers/url.go | 153 | ||||
-rw-r--r-- | helpers/url_test.go | 108 | ||||
-rw-r--r-- | hugolib/config.go | 7 | ||||
-rw-r--r-- | hugolib/content_map_test.go | 4 | ||||
-rw-r--r-- | hugolib/pagecollections.go | 4 | ||||
-rw-r--r-- | hugolib/site.go | 4 | ||||
-rw-r--r-- | langs/i18n/translationProvider.go | 6 | ||||
-rw-r--r-- | resources/image.go | 6 | ||||
-rw-r--r-- | resources/image_test.go | 4 | ||||
-rw-r--r-- | resources/page/pagemeta/page_frontmatter.go | 4 | ||||
-rw-r--r-- | resources/transform.go | 6 | ||||
-rw-r--r-- | source/fileInfo.go | 4 |
21 files changed, 956 insertions, 591 deletions
diff --git a/commands/server.go b/commands/server.go index 5cb43470b..02db354ba 100644 --- a/commands/server.go +++ b/commands/server.go @@ -31,6 +31,8 @@ import ( "syscall" "time" + "github.com/gohugoio/hugo/common/paths" + "github.com/pkg/errors" "github.com/gohugoio/hugo/livereload" @@ -275,7 +277,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error { func getRootWatchDirsStr(baseDir string, watchDirs []string) string { relWatchDirs := make([]string, len(watchDirs)) for i, dir := range watchDirs { - relWatchDirs[i], _ = helpers.GetRelativePath(dir, baseDir) + relWatchDirs[i], _ = paths.GetRelativePath(dir, baseDir) } return strings.Join(helpers.UniqueStringsSorted(helpers.ExtractRootPaths(relWatchDirs)), ",") diff --git a/common/paths/path.go b/common/paths/path.go new file mode 100644 index 000000000..0237dd9f2 --- /dev/null +++ b/common/paths/path.go @@ -0,0 +1,312 @@ +// Copyright 2021 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package paths + +import ( + "errors" + "fmt" + "os" + "path" + "path/filepath" + "regexp" + "strings" +) + +// FilePathSeparator as defined by os.Separator. +const FilePathSeparator = string(filepath.Separator) + +// filepathPathBridge is a bridge for common functionality in filepath vs path +type filepathPathBridge interface { + Base(in string) string + Clean(in string) string + Dir(in string) string + Ext(in string) string + Join(elem ...string) string + Separator() string +} + +type filepathBridge struct { +} + +func (filepathBridge) Base(in string) string { + return filepath.Base(in) +} + +func (filepathBridge) Clean(in string) string { + return filepath.Clean(in) +} + +func (filepathBridge) Dir(in string) string { + return filepath.Dir(in) +} + +func (filepathBridge) Ext(in string) string { + return filepath.Ext(in) +} + +func (filepathBridge) Join(elem ...string) string { + return filepath.Join(elem...) +} + +func (filepathBridge) Separator() string { + return FilePathSeparator +} + +var fpb filepathBridge + +// ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer. +func ToSlashTrimLeading(s string) string { + return strings.TrimPrefix(filepath.ToSlash(s), "/") +} + +// MakeTitle converts the path given to a suitable title, trimming whitespace +// and replacing hyphens with whitespace. +func MakeTitle(inpath string) string { + return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1) +} + +// ReplaceExtension takes a path and an extension, strips the old extension +// and returns the path with the new extension. +func ReplaceExtension(path string, newExt string) string { + f, _ := fileAndExt(path, fpb) + return f + "." + newExt +} + +func makePathRelative(inPath string, possibleDirectories ...string) (string, error) { + for _, currentPath := range possibleDirectories { + if strings.HasPrefix(inPath, currentPath) { + return strings.TrimPrefix(inPath, currentPath), nil + } + } + return inPath, errors.New("can't extract relative path, unknown prefix") +} + +// Should be good enough for Hugo. +var isFileRe = regexp.MustCompile(`.*\..{1,6}$`) + +// GetDottedRelativePath expects a relative path starting after the content directory. +// It returns a relative path with dots ("..") navigating up the path structure. +func GetDottedRelativePath(inPath string) string { + inPath = filepath.Clean(filepath.FromSlash(inPath)) + + if inPath == "." { + return "./" + } + + if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, FilePathSeparator) { + inPath += FilePathSeparator + } + + if !strings.HasPrefix(inPath, FilePathSeparator) { + inPath = FilePathSeparator + inPath + } + + dir, _ := filepath.Split(inPath) + + sectionCount := strings.Count(dir, FilePathSeparator) + + if sectionCount == 0 || dir == FilePathSeparator { + return "./" + } + + var dottedPath string + + for i := 1; i < sectionCount; i++ { + dottedPath += "../" + } + + return dottedPath +} + +// ExtNoDelimiter takes a path and returns the extension, excluding the delimiter, i.e. "md". +func ExtNoDelimiter(in string) string { + return strings.TrimPrefix(Ext(in), ".") +} + +// Ext takes a path and returns the extension, including the delimiter, i.e. ".md". +func Ext(in string) string { + _, ext := fileAndExt(in, fpb) + return ext +} + +// PathAndExt is the same as FileAndExt, but it uses the path package. +func PathAndExt(in string) (string, string) { + return fileAndExt(in, pb) +} + +// FileAndExt takes a path and returns the file and extension separated, +// the extension including the delimiter, i.e. ".md". +func FileAndExt(in string) (string, string) { + return fileAndExt(in, fpb) +} + +// FileAndExtNoDelimiter takes a path and returns the file and extension separated, +// the extension excluding the delimiter, e.g "md". +func FileAndExtNoDelimiter(in string) (string, string) { + file, ext := fileAndExt(in, fpb) + return file, strings.TrimPrefix(ext, ".") +} + +// Filename takes a file path, strips out the extension, +// and returns the name of the file. +func Filename(in string) (name string) { + name, _ = fileAndExt(in, fpb) + return +} + +// PathNoExt takes a path, strips out the extension, +// and returns the name of the file. +func PathNoExt(in string) string { + return strings.TrimSuffix(in, path.Ext(in)) +} + +// FileAndExt returns the filename and any extension of a file path as +// two separate strings. +// +// If the path, in, contains a directory name ending in a slash, +// then both name and ext will be empty strings. +// +// If the path, in, is either the current directory, the parent +// directory or the root directory, or an empty string, +// then both name and ext will be empty strings. +// +// If the path, in, represents the path of a file without an extension, +// then name will be the name of the file and ext will be an empty string. +// +// If the path, in, represents a filename with an extension, +// then name will be the filename minus any extension - including the dot +// and ext will contain the extension - minus the dot. +func fileAndExt(in string, b filepathPathBridge) (name string, ext string) { + ext = b.Ext(in) + base := b.Base(in) + + return extractFilename(in, ext, base, b.Separator()), ext +} + +func extractFilename(in, ext, base, pathSeparator string) (name string) { + // No file name cases. These are defined as: + // 1. any "in" path that ends in a pathSeparator + // 2. any "base" consisting of just an pathSeparator + // 3. any "base" consisting of just an empty string + // 4. any "base" consisting of just the current directory i.e. "." + // 5. any "base" consisting of just the parent directory i.e. ".." + if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator { + name = "" // there is NO filename + } else if ext != "" { // there was an Extension + // return the filename minus the extension (and the ".") + name = base[:strings.LastIndex(base, ".")] + } else { + // no extension case so just return base, which willi + // be the filename + name = base + } + return +} + +// GetRelativePath returns the relative path of a given path. +func GetRelativePath(path, base string) (final string, err error) { + if filepath.IsAbs(path) && base == "" { + return "", errors.New("source: missing base directory") + } + name := filepath.Clean(path) + base = filepath.Clean(base) + + name, err = filepath.Rel(base, name) + if err != nil { + return "", err + } + + if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) { + name += FilePathSeparator + } + return name, nil +} + +// PathPrep prepares the path using the uglify setting to create paths on +// either the form /section/name/index.html or /section/name.html. +func PathPrep(ugly bool, in string) string { + if ugly { + return Uglify(in) + } + return PrettifyPath(in) +} + +// PrettifyPath is the same as PrettifyURLPath but for file paths. +// /section/name.html becomes /section/name/index.html +// /section/name/ becomes /section/name/index.html +// /section/name/index.html becomes /section/name/index.html +func PrettifyPath(in string) string { + return prettifyPath(in, fpb) +} + +func prettifyPath(in string, b filepathPathBridge) string { + if filepath.Ext(in) == "" { + // /section/name/ -> /section/name/index.html + if len(in) < 2 { + return b.Separator() + } + return b.Join(in, "index.html") + } + name, ext := fileAndExt(in, b) + if name == "index" { + // /section/name/index.html -> /section/name/index.html + return b.Clean(in) + } + // /section/name.html -> /section/name/index.html + return b.Join(b.Dir(in), name, "index"+ext) +} + +type NamedSlice struct { + Name string + Slice []string +} + +func (n NamedSlice) String() string { + if len(n.Slice) == 0 { + return n.Name + } + return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ",")) +} + +// FindCWD returns the current working directory from where the Hugo +// executable is run. +func FindCWD() (string, error) { + serverFile, err := filepath.Abs(os.Args[0]) + if err != nil { + return "", fmt.Errorf("can't get absolute path for executable: %v", err) + } + + path := filepath.Dir(serverFile) + realFile, err := filepath.EvalSymlinks(serverFile) + if err != nil { + if _, err = os.Stat(serverFile + ".exe"); err == nil { + realFile = filepath.Clean(serverFile + ".exe") + } + } + + if err == nil && realFile != serverFile { + path = filepath.Dir(realFile) + } + + return path, nil +} + +// AddTrailingSlash adds a trailing Unix styled slash (/) if not already +// there. +func AddTrailingSlash(path string) string { + if !strings.HasSuffix(path, "/") { + path += "/" + } + return path +} diff --git a/common/paths/path_test.go b/common/paths/path_test.go new file mode 100644 index 000000000..e55493c7d --- /dev/null +++ b/common/paths/path_test.go @@ -0,0 +1,252 @@ +// Copyright 2021 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package paths + +import ( + "path/filepath" + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestGetRelativePath(t *testing.T) { + tests := []struct { + path string + base string + expect interface{} + }{ + {filepath.FromSlash("/a/b"), filepath.FromSlash("/a"), filepath.FromSlash("b")}, + {filepath.FromSlash("/a/b/c/"), filepath.FromSlash("/a"), filepath.FromSlash("b/c/")}, + {filepath.FromSlash("/c"), filepath.FromSlash("/a/b"), filepath.FromSlash("../../c")}, + {filepath.FromSlash("/c"), "", false}, + } + for i, this := range tests { + // ultimately a fancy wrapper around filepath.Rel + result, err := GetRelativePath(this.path, this.base) + + if b, ok := this.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] GetRelativePath didn't return an expected error", i) + } + } else { + if err != nil { + t.Errorf("[%d] GetRelativePath failed: %s", i, err) + continue + } + if result != this.expect { + t.Errorf("[%d] GetRelativePath got %v but expected %v", i, result, this.expect) + } + } + + } +} + +func TestMakePathRelative(t *testing.T) { + type test struct { + inPath, path1, path2, output string + } + + data := []test{ + {"/abc/bcd/ab.css", "/abc/bcd", "/bbc/bcd", "/ab.css"}, + {"/abc/bcd/ab.css", "/abcd/bcd", "/abc/bcd", "/ab.css"}, + } + + for i, d := range data { + output, _ := makePathRelative(d.inPath, d.path1, d.path2) + if d.output != output { + t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output) + } + } + _, error := makePathRelative("a/b/c.ss", "/a/c", "/d/c", "/e/f") + + if error == nil { + t.Errorf("Test failed, expected error") + } +} + +func TestGetDottedRelativePath(t *testing.T) { + // on Windows this will receive both kinds, both country and western ... + for _, f := range []func(string) string{filepath.FromSlash, func(s string) string { return s }} { + doTestGetDottedRelativePath(f, t) + } +} + +func doTestGetDottedRelativePath(urlFixer func(string) string, t *testing.T) { + type test struct { + input, expected string + } + data := []test{ + {"", "./"}, + {urlFixer("/"), "./"}, + {urlFixer("post"), "../"}, + {urlFixer("/post"), "../"}, + {urlFixer("post/"), "../"}, + {urlFixer("tags/foo.html"), "../"}, + {urlFixer("/tags/foo.html"), "../"}, + {urlFixer("/post/"), "../"}, + {urlFixer("////post/////"), "../"}, + {urlFixer("/foo/bar/index.html"), "../../"}, + {urlFixer("/foo/bar/foo/"), "../../../"}, + {urlFixer("/foo/bar/foo"), "../../../"}, + {urlFixer("foo/bar/foo/"), "../../../"}, + {urlFixer("foo/bar/foo/bar"), "../../../../"}, + {"404.html", "./"}, + {"404.xml", "./"}, + {"/404.html", "./"}, + } + for i, d := range data { + output := GetDottedRelativePath(d.input) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + } +} + +func TestMakeTitle(t *testing.T) { + type test struct { + input, expected string + } + data := []test{ + {"Make-Title", "Make Title"}, + {"MakeTitle", "MakeTitle"}, + {"make_title", "make_title"}, + } + for i, d := range data { + output := MakeTitle(d.input) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + } +} + +// Replace Extension is probably poorly named, but the intent of the +// function is to accept a path and return only the file name with a +// new extension. It's intentionally designed to strip out the path +// and only provide the name. We should probably rename the function to +// be more explicit at some point. +func TestReplaceExtension(t *testing.T) { + type test struct { + input, newext, expected string + } + data := []test{ + // These work according to the above definition + {"/some/random/path/file.xml", "html", "file.html"}, + {"/banana.html", "xml", "banana.xml"}, + {"./banana.html", "xml", "banana.xml"}, + {"banana/pie/index.html", "xml", "index.xml"}, + {"../pies/fish/index.html", "xml", "index.xml"}, + // but these all fail + {"filename-without-an-ext", "ext", "filename-without-an-ext.ext"}, + {"/filename-without-an-ext", "ext", "filename-without-an-ext.ext"}, + {"/directory/mydir/", "ext", ".ext"}, + {"mydir/", "ext", ".ext"}, + } + + for i, d := range data { + output := ReplaceExtension(filepath.FromSlash(d.input), d.newext) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + } +} + +func TestExtNoDelimiter(t *testing.T) { + c := qt.New(t) + c.Assert(ExtNoDelimiter(filepath.FromSlash("/my/data.json")), qt.Equals, "json") +} + +func TestFilename(t *testing.T) { + type test struct { + input, expected string + } + data := []test{ + {"index.html", "index"}, + {"./index.html", "index"}, + {"/index.html", "index"}, + {"index", "index"}, + {"/tmp/index.html", "index"}, + {"./filename-no-ext", "filename-no-ext"}, + {"/filename-no-ext", "filename-no-ext"}, + {"filename-no-ext", "filename-no-ext"}, + {"directory/", ""}, // no filename case?? + {"directory/.hidden.ext", ".hidden"}, + {"./directory/../~/banana/gold.fish", "gold"}, + {"../directory/banana.man", "banana"}, + {"~/mydir/filename.ext", "filename"}, + {"./directory//tmp/filename.ext", "filename"}, + } + + for i, d := range data { + output := Filename(filepath.FromSlash(d.input)) + if d.expected != output { + t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) + } + } +} + +func TestFileAndExt(t *testing.T) { + type test struct { + input, expectedFile, expectedExt string + } + data := []test{ + {"index.html", "index", ".html"}, + {"./index.html", "index", ".html"}, + {"/index.html", "index", ".html"}, + {"index", "index", ""}, + {"/tmp/index.html", "index", ".html"}, + {"./filename-no-ext", "filename-no-ext", ""}, + {"/filename-no-ext", "filename-no-ext", ""}, + {"filename-no-ext", "filename-no-ext", ""}, + {"directory/", "", ""}, // no filename case?? + {"directory/.hidden.ext", ".hidden", ".ext"}, + {"./directory/../~/banana/gold.fish", "gold", ".fish"}, + {"../directory/banana.man", "banana", ".man"}, + {"~/mydir/filename.ext", "filename", ".ext"}, + {"./directory//tmp/filename.ext", "filename", ".ext"}, + } + + for i, d := range data { + file, ext := fileAndExt(filepath.FromSlash(d.input), fpb) + if d.expectedFile != file { + t.Errorf("Test %d failed. Expected filename %q got %q.", i, d.expectedFile, file) + } + if d.expectedExt != ext { + t.Errorf("Test %d failed. Expected extension %q got %q.", i, d.expectedExt, ext) + } + } +} + +func TestFindCWD(t *testing.T) { + type test struct { + expectedDir string + expectedErr error + } + + // cwd, _ := os.Getwd() + data := []test{ + //{cwd, nil}, + // Commenting this out. It doesn't work properly. + // There's a good reason why we don't use os.Getwd(), it doesn't actually work the way we want it to. + // I really don't know a better way to test this function. - SPF 2014.11.04 + } + for i, d := range data { + dir, err := FindCWD() + if d.expectedDir != dir { + t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedDir, dir) + } + if d.expectedErr != err { + t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedErr, err) + } + } +} diff --git a/common/paths/url.go b/common/paths/url.go new file mode 100644 index 000000000..600e8d22d --- /dev/null +++ b/common/paths/url.go @@ -0,0 +1,212 @@ +// Copyright 2021 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package paths + +import ( + "fmt" + "net/url" + "path" + "strings" + + "github.com/PuerkitoBio/purell" +) + +type pathBridge struct { +} + +func (pathBridge) Base(in string) string { + return path.Base(in) +} + +func (pathBridge) Clean(in string) string { + return path.Clean(in) +} + +func (pathBridge) Dir(in string) string { + return path.Dir(in) +} + +func (pathBridge) Ext(in string) string { + return path.Ext(in) +} + +func (pathBridge) Join(elem ...string) string { + return path.Join(elem...) +} + +func (pathBridge) Separator() string { + return "/" +} + +var pb pathBridge + +func sanitizeURLWithFlags(in string, f purell.NormalizationFlags) string { + s, err := purell.NormalizeURLString(in, f) + if err != nil { + return in + } + + // Temporary workaround for the bug fix and resulting + // behavioral change in purell.NormalizeURLString(): + // a leading '/' was inadvertently added to relative links, + // but no longer, see #878. + // + // I think the real solution is to allow Hugo to + // make relative URL with relative path, + // e.g. "../../post/hello-again/", as wished by users + // in issues #157, #622, etc., without forcing + // relative URLs to begin with '/'. + // Once the fixes are in, let's remove this kludge + // and restore SanitizeURL() to the way it was. + // -- @anthonyfok, 2015-02-16 + // + // Begin temporary kludge + u, err := url.Parse(s) + if err != nil { + panic(err) + } + if len(u.Path) > 0 && !strings.HasPrefix(u.Path, "/") { + u.Path = "/" + u.Path + } + return u.String() + // End temporary kludge + + // return s + +} + +// SanitizeURL sanitizes the input URL string. +func SanitizeURL(in string) string { + return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveTrailingSlash|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator) +} + +// SanitizeURLKeepTrailingSlash is the same as SanitizeURL, but will keep any trailing slash. +func SanitizeURLKeepTrailingSlash(in string) string { + return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator) +} + +// MakePermalink combines base URL with content path to create full URL paths. +// Example +// base: http://spf13.com/ +// path: post/how-i-blog +// result: http://spf13.com/post/how-i-blog +func MakePermalink(host, plink string) *url.URL { + base, err := url.Parse(host) + if err != nil { + panic(err) + } + + p, err := url.Parse(plink) + if err != nil { + panic(err) + } + + if p.Host != "" { + panic(fmt.Errorf("can't make permalink from absolute link %q", plink)) + } + + base.Path = path.Join(base.Path, p.Path) + + // path.Join will strip off the last /, so put it back if it was there. + hadTrailingSlash := (plink == "" && strings.HasSuffix(host, "/")) || strings.HasSuffix(p.Path, "/") + if hadTrailingSlash && !strings.HasSuffix(base.Path, "/") { + base.Path = base.Path + "/" + } + + return base +} + +// IsAbsURL determines whether the given path points to an absolute URL. +func IsAbsURL(path string) bool { + url, err := url.Parse(path) + if err != nil { + return false + } + + return url.IsAbs() || strings.HasPrefix(path, "//") +} + +// AddContextRoot adds the context root to an URL if it's not already set. +// For relative URL entries on sites with a base url with a context root set (i.e. http://example.com/mysite), +// relative URLs must not include the context root if canonifyURLs is enabled. But if it's disabled, it must be set. +func AddContextRoot(baseURL, relativePath string) string { + url, err := url.Parse(baseURL) + if err != nil { + panic(err) + } + + newPath := path.Join(url.Path, relativePath) + + // path strips trailing slash, ignore root path. + if newPath != "/" && strings.HasSuffix(relativePath, "/") { + newPath += "/" + } + return newPath +} + +// URLizeAn + +// PrettifyURL takes a URL string and returns a semantic, clean URL. +func PrettifyURL(in string) string { + x := PrettifyURLPath(in) + + if path.Base(x) == "index.html" { + return path.Dir(x) + } + + if in == "" { + return "/" + } + + return x +} + +// PrettifyURLPath takes a URL path to a content and converts it +// to enable pretty URLs. +// /section/name.html becomes /section/name/index.html +// /section/name/ becomes /section/name/index.html +// /section/name/index.html becomes /section/name/index.html +func PrettifyURLPath(in string) string { + return prettifyPath(in, pb) +} + +// Uglify does the opposite of PrettifyURLPath(). +// /section/name/index.html becomes /section/name.html +// /section/name/ becomes /section/name.html +// /section/name.html becomes /section/name.html +func Uglify(in string) string { + if path.Ext(in) == "" { + if len(in) < 2 { + return "/" + } + // /section/name/ -> /section/name.html + return path.Clean(in) + ".html" + } + + name, ext := fileAndExt(in, pb) + if name == "index" { + // /section/name/index.html -> /section/name.html + d := path.Dir(in) + if len(d) > 1 { + return d + ext + } + return in + } + // /.xml -> /index.xml + if name == "" { + return path.Dir(in) + "index" + ext + } + // /section/name.html -> /section/name.html + return path.Clean(in) +} diff --git a/common/paths/url_test.go b/common/paths/url_test.go new file mode 100644 index 000000000..3e8391ef5 --- /dev/null +++ b/common/paths/url_test.go @@ -0,0 +1,129 @@ +// Copyright 2021 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package paths + +import ( + "strings" + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestSanitizeURL(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"http://foo.bar/", "http://foo.bar"}, + {"http://foo.bar", "http://foo.bar"}, // issue #1105 + {"http://foo.bar/zoo/", "http://foo.bar/zoo"}, // issue #931 + } + + for i, test := range tests { + o1 := SanitizeURL(test.input) + o2 := SanitizeURLKeepTrailingSlash(test.input) + + expected2 := test.expected + + if strings.HasSuffix(test.input, "/") && !strings.HasSuffix(expected2, "/") { + expected2 += "/" + } + + if o1 != test.expected { + t.Errorf("[%d] 1: Expected %#v, got %#v\n", i, test.expected, o1) + } + if o2 != expected2 { + t.Errorf("[%d] 2: Expected %#v, got %#v\n", i, expected2, o2) + } + } +} + +func TestMakePermalink(t *testing.T) { + type test struct { + host, link, output string + } + + data := []test{ + {"http://abc.com/foo", "post/bar", "http://abc.com/foo/post/bar"}, + {"http://abc.com/foo/", "post/bar", "http://abc.com/foo/post/bar"}, + {"http://abc.com", "post/bar", "http://abc.com/post/bar"}, + {"http://abc.com", "bar", "http://abc.com/bar"}, + {"http://abc.com/foo/bar", "post/bar", "http://abc.com/foo/bar/post/bar"}, + {"http://abc.com/foo/bar", "post/bar/", "http://abc.com/foo/bar/post/bar/"}, + } + + for i, d := range data { + output := MakePermalink(d.host, d.link).String() + if d.output != output { + t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output) + } + } +} + +func TestAddContextRoot(t *testing.T) { + tests := []struct { + baseURL string + url string + expected string + }{ + {"http://example.com/sub/", "/foo", "/sub/foo"}, + {"http://example.com/sub/", "/foo/index.html", "/sub/foo/index.html"}, + {"http://example.com/sub1/sub2", "/foo", "/sub1/sub2/foo"}, + {"http://example.com", "/foo", "/foo"}, + // cannot guess that the context root is already added int the example below + {"http://example.com/sub/", "/sub/foo", "/sub/sub/foo"}, + {"http://example.com/тря", "/трям/", "/тря/трям/"}, + {"http://example.com", "/", "/"}, + {"http://example.com/bar", "//", "/bar/"}, + } + + for _, test := range tests { + output := AddContextRoot(test.baseURL, test.url) + if output != test.expected { + t.Errorf("Expected %#v, got %#v\n", test.expected, output) + } + } +} + +func TestPretty(t *testing.T) { + c := qt.New(t) + c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name.html")) + c.Assert("/section/sub/name/index.html", qt.Equals, PrettifyURLPath("/section/sub/name.html")) + c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/")) + c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/index.html")) + c.Assert("/index.html", qt.Equals, PrettifyURLPath("/index.html")) + c.Assert("/name/index.xml", qt.Equals, PrettifyURLPath("/name.xml")) + c.Assert("/", qt.Equals, PrettifyURLPath("/")) + c.Assert("/", qt.Equals, PrettifyURLPath("")) + c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name.html")) + c.Assert("/section/sub/name", qt.Equals, PrettifyURL("/section/sub/name.html")) + c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/")) + c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/index.html")) + c.Assert("/", qt.Equals, PrettifyURL("/index.html")) + c.Assert("/name/index.xml", qt.Equals, PrettifyURL("/name.xml")) + c.Assert("/", qt.Equals, PrettifyURL("/")) + c.Assert("/", qt.Equals, PrettifyURL("")) +} + +func TestUgly(t *testing.T) { + c := qt.New(t) + c.Assert("/section/name.html", qt.Equals, Uglify("/section/name.html")) + c.Assert("/section/sub/name.html", qt.Equals, Uglify("/section/sub/name.html")) + c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/")) + c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/index.html")) + c.Assert("/index.html", qt.Equals, Uglify("/index.html")) + c.Assert("/name.xml", qt.Equals, Uglify("/name.xml")) + c.Assert("/", qt.Equals, Uglify("/")) + c.Assert("/", qt.Equals, Uglify("")) +} diff --git a/create/content.go b/create/content.go index 26eda2031..797ffe758 100644 --- a/create/content.go +++ b/create/content.go @@ -21,6 +21,8 @@ import ( "path/filepath" "strings" + "github.com/gohugoio/hugo/common/paths" + "github.com/pkg/errors" "github.com/gohugoio/hugo/common/hexec" @@ -39,7 +41,7 @@ import ( func NewContent( sites *hugolib.HugoSites, kind, targetPath string) error { targetPath = filepath.Clean(targetPath) - ext := helpers.Ext(targetPath) + ext := paths.Ext(targetPath) ps := sites.PathSpec archetypeFs := ps.BaseFs.SourceFilesystems.Archetypes.Fs sourceFs := ps.Fs.Source diff --git a/create/content_template_handler.go b/create/content_template_handler.go index 3e9701e4b..09cf4c0a5 100644 --- a/create/content_template_handler.go +++ b/create/content_template_handler.go @@ -20,6 +20,8 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/common/paths" + "github.com/pkg/errors" "github.com/gohugoio/hugo/helpers" @@ -129,7 +131,7 @@ func executeArcheTypeAsTemplate(s *hugolib.Site, name, kind, targetPath, archety // Reuse the Hugo template setup to get the template funcs properly set up. templateHandler := s.Deps.Tmpl().(tpl.TemplateManager) - templateName := helpers.Filename(archetypeFilename) + templateName := paths.Filename(archetypeFilename) if err := templateHandler.AddTemplate("_text/"+templateName, string(archetypeTemplate)); err != nil { return nil, errors.Wrapf(err, "Failed to parse archetype file %q:", archetypeFilename) } diff --git a/helpers/path.go b/helpers/path.go index 17a513cec..fd35fafc9 100644 --- a/helpers/path.go +++ b/helpers/path.go @@ -18,7 +18,6 @@ import ( "fmt" "io" "os" - "path" "path/filepath" "regexp" "sort" @@ -39,45 +38,6 @@ import ( // ErrThemeUndefined is returned when a theme has not be defined by the user. var ErrThemeUndefined = errors.New("no theme set") -// filepathPathBridge is a bridge for common functionality in filepath vs path -type filepathPathBridge interface { - Base(in string) string - Clean(in string) string - Dir(in string) string - Ext(in string) string - Join(elem ...string) string - Separator() string -} - -type filepathBridge struct { -} - -func (filepathBridge) Base(in string) string { - return filepath.Base(in) -} - -func (filepathBridge) Clean(in string) string { - return filepath.Clean(in) -} - -func (filepathBridge) Dir(in string) string { - return filepath.Dir(in) -} - -func (filepathBridge) Ext(in string) string { - return filepath.Ext(in) -} - -func (filepathBridge) Join(elem ...string) string { - return filepath.Join(elem...) -} - -func (filepathBridge) Separator() string { - return FilePathSeparator -} - -var fpb filepathBridge - // MakePath takes a string with any characters and replace it // so the string could be used in a path. // It does so by creating a Unicode-sanitized string, with the spaces replaced, @@ -159,13 +119,6 @@ func (p *PathSpec) UnicodeSanitize(s string) string { return string(target) } -// ReplaceExtension takes a path and an extension, strips the old extension -// and returns the path with the new extension. -func ReplaceExtension(path string, newExt string) string { - f, _ := fileAndExt(path, fpb) - return f + "." + newExt -} - func makePathRelative(inPath string, possibleDirectories ...string) (string, error) { for _, currentPath := range possibleDirectories { if strings.HasPrefix(inPath, currentPath) { @@ -212,144 +165,6 @@ func GetDottedRelativePath(inPath string) string { return dottedPath } -// ExtNoDelimiter takes a path and returns the extension, excluding the delimiter, i.e. "md". -func ExtNoDelimiter(in string) string { - return strings.TrimPrefix(Ext(in), ".") -} - -// Ext takes a path and returns the extension, including the delimiter, i.e. ".md". -func Ext(in string) string { - _, ext := fileAndExt(in, fpb) - return ext -} - -// PathAndExt is the same as FileAndExt, but it uses the path package. -func PathAndExt(in string) (string, string) { - return fileAndExt(in, pb) -} - -// FileAndExt takes a path and returns the file and extension separated, -// the extension including the delimiter, i.e. ".md". -func FileAndExt(in string) (string, string) { - return fileAndExt(in, fpb) -} - -// FileAndExtNoDelimiter takes a path and returns the file and extension separated, -// the extension excluding the delimiter, e.g "md". -func FileAndExtNoDelimiter(in string) (string, string) { - file, ext := fileAndExt(in, fpb) - return file, strings.TrimPrefix(ext, ".") -} - -// Filename takes a file path, strips out the extension, -// and returns the name of the file. -func Filename(in string) (name string) { - name, _ = fileAndExt(in, fpb) - return -} - -// PathNoExt takes a path, strips out the extension, -// and returns the name of the file. -func PathNoExt(in string) string { - return strings.TrimSuffix(in, path.Ext(in)) -} - -// FileAndExt returns the filename and any extension of a file path as -// two separate strings. -// -// If the path, in, contains a directory name ending in a slash, -// then both name and ext will be empty strings. -// -// If the path, in, is either the current directory, the parent -// directory or the root directory, or an empty string, -// then both name and ext will be empty strings. -// -// If the path, in, represents the path of a file without an extension, -// then name will be the name of the file and ext will be an empty string. -// -// If the path, in, represents a filename with an extension, -// then name will be the filename minus any extension - including the dot -// and ext will contain the extension - minus the dot. -func fileAndExt(in string, b filepathPathBridge) (name string, ext string) { - ext = b.Ext(in) - base := b.Base(in) - - return extractFilename(in, ext, base, b.Separator()), ext -} - -func extractFilename(in, ext, base, pathSeparator string) (name string) { - // No file name cases. These are defined as: - // 1. any "in" path that ends in a pathSeparator - // 2. any "base" consisting of just an pathSeparator - // 3. any "base" consisting of just an empty string - // 4. any "base" consisting of just the current directory i.e. "." - // 5. any "base" consisting of just the parent directory i.e. ".." - if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator { - name = "" // there is NO filename - } else if ext != "" { // there was an Extension - // return the filename minus the extension (and the ".") - name = base[:strings.LastIndex(base, ".")] - } else { - // no extension case so just return base, which willi - // be the filename - name = base - } - return -} - -// GetRelativePath returns the relative path of a given path. -func GetRelativePath(path, base string) (final string, err error) { - if filepath.IsAbs(path) && base == "" { - return "", errors.New("source: missing base directory") - } - name := filepath.Clean(path) - base = filepath.Clean(base) - - name, err = filepath.Rel(base, name) - if err != nil { - return "", err - } - - if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) { - name += FilePathSeparator - } - return name, nil -} - -// PathPrep prepares the path using the uglify setting to create paths on -// either the form /section/name/index.html or /section/name.html. -func PathPrep(ugly bool, in string) string { - if ugly { - return Uglify(in) - } - return PrettifyPath(in) -} - -// PrettifyPath is the same as PrettifyURLPath but for file paths. -// /section/name.html becomes /section/name/index.html -// /section/name/ becomes /section/name/index.html -// /section/name/index.html becomes /section/name/index.html -func PrettifyPath(in string) string { - return prettifyPath(in, fpb) -} - -func prettifyPath(in string, b filepathPathBridge) string { - if filepath.Ext(in) == "" { - // /section/name/ -> /section/name/index.html - if len(in) < 2 { - return b.Separator() - } - return b.Join(in, "index.html") - } - name, ext := fileAndExt(in, b) - if name == "index" { - // /section/name/index.html -> /section/name/index.html - return b.Clean(in) - } - // /section/name.html -> /section/name/index.html - return b.Join(b.Dir(in), name, "index"+ext) -} - type NamedSlice struct { Name string Slice []string diff --git a/helpers/path_test.go b/helpers/path_test.go index c95951832..1d2dc1184 100644 --- a/helpers/path_test.go +++ b/helpers/path_test.go @@ -123,38 +123,6 @@ func TestMakePathSanitizedDisablePathToLower(t *testing.T) { } } -func TestGetRelativePath(t *testing.T) { - tests := []struct { - path string - base string - expect interface{} - }{ - {filepath.FromSlash("/a/b"), filepath.FromSlash("/a"), filepath.FromSlash("b")}, - {filepath.FromSlash("/a/b/c/"), filepath.FromSlash("/a"), filepath.FromSlash("b/c/")}, - {filepath.FromSlash("/c"), filepath.FromSlash("/a/b"), filepath.FromSlash("../../c")}, - {filepath.FromSlash("/c"), "", false}, - } - for i, this := range tests { - // ultimately a fancy wrapper around filepath.Rel - result, err := GetRelativePath(this.path, this.base) - - if b, ok := this.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] GetRelativePath didn't return an expected error", i) - } - } else { - if err != nil { - t.Errorf("[%d] GetRelativePath failed: %s", i, err) - continue - } - if result != this.expect { - t.Errorf("[%d] GetRelativePath got %v but expected %v", i, result, this.expect) - } - } - - } -} - func TestMakePathRelative(t *testing.T) { type test struct { inPath, path1, path2, output string @@ -233,37 +201,6 @@ func TestMakeTitle(t *testing.T) { } } -// Replace Extension is probably poorly named, but the intent of the -// function is to accept a path and return only the file name with a -// new extension. It's intentionally designed to strip out the path -// and only provide the name. We should probably rename the function to -// be more explicit at some point. -func TestReplaceExtension(t *testing.T) { - type test struct { - input, newext, expected string - } - data := []test{ - // These work according to the above definition - {"/some/random/path/file.xml", "html", "file.html"}, - {"/banana.html", "xml", "banana.xml"}, - {"./banana.html", "xml", "banana.xml"}, - {"banana/pie/index.html", "xml", "index.xml"}, - {"../pies/fish/index.html", "xml", "index.xml"}, - // but these all fail - {"filename-without-an-ext", "ext", "filename-without-an-ext.ext"}, - {"/filename-without-an-ext", "ext", "filename-without-an-ext.ext"}, - {"/directory/mydir/", "ext", ".ext"}, - {"mydir/", "ext", ".ext"}, - } - - for i, d := range data { - output := ReplaceExtension(filepath.FromSlash(d.input), d.newext) - if d.expected != output { - t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) - } - } -} - func TestDirExists(t *testing.T) { type test struct { input string @@ -538,78 +475,6 @@ func TestAbsPathify(t *testing.T) { } } -func TestExtNoDelimiter(t *testing.T) { - c := qt.New(t) - c.Assert(ExtNoDelimiter(filepath.FromSlash("/my/data.json")), qt.Equals, "json") -} - -func TestFilename(t *testing.T) { - type test struct { - input, expected string - } - data := []test{ - {"index.html", "index"}, - {"./index.html", "index"}, - {"/index.html", "index"}, - {"index", "index"}, - {"/tmp/index.html", "index"}, - {"./filename-no-ext", "filename-no-ext"}, - {"/filename-no-ext", "filename-no-ext"}, - {"filename-no-ext", "filename-no-ext"}, - {"directory/", ""}, // no filename case?? - {"directory/.hidden.ext", ".hidden"}, - {"./directory/../~/banana/gold.fish", "gold"}, - {"../directory/banana.man", "banana"}, - {"~/mydir/filename.ext", "filename"}, - {"./directory//tmp/filename.ext", "filename"}, - } - - for i, d := range data { - output := Filename(filepath.FromSlash(d.input)) - if d.expected != output { - t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output) - } - } -} - -func TestFileAndExt(t *testing.T) { - type test struct { - input, expectedFile, expectedExt string - } - data := []test{ - {"index.html", "index", ".html"}, - {"./index.html", "index", ".html"}, - {"/index.html", "index", ".html"}, - {"index", "index", ""}, - {"/tmp/index.html", "index", ".html"}, - {"./filename-no-ext", "filename-no-ext", ""}, - {"/filename-no-ext", "filename-no-ext", ""}, - {"filename-no-ext", "filename-no-ext", ""}, - {"directory/", "", ""}, // no filename case?? - {"directory/.hidden.ext", ".hidden", ".ext"}, - {"./directory/../~/banana/gold.fish", "gold", ".fish"}, - {"../directory/banana.man", "banana", ".man"}, - {"~/mydir/filename.ext", "filename", ".ext"}, - {"./directory//tmp/filename.ext", "filename", ".ext"}, - } - - for i, d := range data { - file, ext := fileAndExt(filepath.FromSlash(d.input), fpb) - if d.expectedFile != file { - t.Errorf("Test %d failed. Expected filename %q got %q.", i, d.expectedFile, file) - } - if d.expectedExt != ext { - t.Errorf("Test %d failed. Expected extension %q got %q.", i, d.expectedExt, ext) - } - } -} - -func TestPathPrep(t *testing.T) { -} - -func TestPrettifyPath(t *testing.T) { -} - func TestExtractAndGroupRootPaths(t *testing.T) { in := []string{ filepath.FromSlash("/a/b/c/d"), diff --git a/helpers/url.go b/helpers/url.go index 8c39bc4fa..193dd3c86 100644 --- a/helpers/url.go +++ b/helpers/url.go @@ -14,44 +14,16 @@ package helpers import ( - "fmt" "net/url" "path" "path/filepath" "strings" + "github.com/gohugoio/hugo/common/paths" + "github.com/PuerkitoBio/purell" ) -type pathBridge struct { -} - -func (pathBridge) Base(in string) string { - return path.Base(in) -} - -func (pathBridge) Clean(in string) string { - return path.Clean(in) -} - -func (pathBridge) Dir(in string) string { - return path.Dir(in) -} - -func (pathBridge) Ext(in string) string { - return path.Ext(in) -} - -func (pathBridge) Join(elem ...string) string { - return path.Join(elem...) -} - -func (pathBridge) Separator() string { - return "/" -} - -var pb pathBridge - func sanitizeURLWithFlags(in string, f purell.NormalizationFlags) string { s, err := purell.NormalizeURLString(in, f) if err != nil { @@ -123,37 +95,6 @@ func (p *PathSpec) URLEscape(uri string) string { return x } -// MakePermalink combines base URL with content path to create full URL paths. -// Example -// base: http://spf13.com/ -// path: post/how-i-blog -// result: http://spf13.com/post/how-i-blog -func MakePermalink(host, plink string) *url.URL { - base, err := url.Parse(host) - if err != nil { - panic(err) - } - - p, err := url.Parse(plink) - if err != nil { - panic(err) - } - - if p.Host != "" { - panic(fmt.Errorf("can't make permalink from absolute link %q", plink)) - } - - base.Path = path.Join(base.Path, p.Path) - - // path.Join will strip off the last /, so put it back if it was there. - hadTrailingSlash := (plink == "" && strings.HasSuffix(host, "/")) || strings.HasSuffix(p.Path, "/") - if hadTrailingSlash && !strings.HasSuffix(base.Path, "/") { - base.Path = base.Path + "/" - } - - return base -} - // AbsURL creates an absolute URL from the relative path given and the BaseURL set in config. func (p *PathSpec) AbsURL(in string, addLanguage bool) string { url, err := url.Parse(in) @@ -199,17 +140,7 @@ func (p *PathSpec) AbsURL(in string, addLanguage bool) string { } } } - return MakePermalink(baseURL, in).String() -} - -// IsAbsURL determines whether the given path points to an absolute URL. -func IsAbsURL(path string) bool { - url, err := url.Parse(path) - if err != nil { - return false - } - - return url.IsAbs() || strings.HasPrefix(path, "//") + return paths.MakePermalink(baseURL, in).String() } // RelURL creates a URL relative to the BaseURL root. @@ -255,7 +186,7 @@ func (p *PathSpec) RelURL(in string, addLanguage bool) string { } if !canonifyURLs { - u = AddContextRoot(baseURL, u) + u = paths.AddContextRoot(baseURL, u) } if in == "" && !strings.HasSuffix(u, "/") && strings.HasSuffix(baseURL, "/") { @@ -269,24 +200,6 @@ func (p *PathSpec) RelURL(in string, addLanguage bool) string { return u } -// AddContextRoot adds the context root to an URL if it's not already set. -// For relative URL entries on sites with a base url with a context root set (i.e. http://example.com/mysite), -// relative URLs must not include the context root if canonifyURLs is enabled. But if it's disabled, it must be set. -func AddContextRoot(baseURL, relativePath string) string { - url, err := url.Parse(baseURL) - if err != nil { - panic(err) - } - - newPath := path.Join(url.Path, relativePath) - - // path strips trailing slash, ignore root path. - if newPath != "/" && strings.HasSuffix(relativePath, "/") { - newPath += "/" - } - return newPath -} - // PrependBasePath prepends any baseURL sub-folder to the given resource func (p *PathSpec) PrependBasePath(rel string, isAbs bool) string { basePath := p.GetBasePath(!isAbs) @@ -311,9 +224,9 @@ func (p *PathSpec) URLizeAndPrep(in string) string { // URLPrep applies misc sanitation to the given URL. func (p *PathSpec) URLPrep(in string) string { if p.UglyURLs { - return Uglify(SanitizeURL(in)) + return paths.Uglify(SanitizeURL(in)) } - pretty := PrettifyURL(SanitizeURL(in)) + pretty := paths.PrettifyURL(SanitizeURL(in)) if path.Ext(pretty) == ".xml" { return pretty } @@ -323,57 +236,3 @@ func (p *PathSpec) URLPrep(in string) string { } return url } - -// PrettifyURL takes a URL string and returns a semantic, clean URL. -func PrettifyURL(in string) string { - x := PrettifyURLPath(in) - - if path.Base(x) == "index.html" { - return path.Dir(x) - } - - if in == "" { - return "/" - } - - return x -} - -// PrettifyURLPath takes a URL path to a content and converts it -// to enable pretty URLs. -// /section/name.html becomes /section/name/index.html -// /section/name/ becomes /section/name/index.html -// /section/name/index.html becomes /section/name/index.html -func PrettifyURLPath(in string) string { - return prettifyPath(in, pb) -} - -// Uglify does the opposite of PrettifyURLPath(). -// /section/name/index.html becomes /section/name.html -// /section/name/ becomes /section/name.html -// /section/name.html becomes /section/name.html -func Uglify(in string) string { - if path.Ext(in) == "" { - if len(in) < 2 { - return "/" - } - // /section/name/ -> /section/name.html - return path.Clean(in) + ".html" - } - - name, ext := fileAndExt(in, pb) - if name == "index" { - // /section/name/index.html -> /section/name.html - d := path.Dir(in) - if len(d) > 1 { - return d + ext - } - return in - } - // /.xml -> /index.xml - if name == "" { - return path.Dir(in) + "index" + ext - } - // /section/name.html -> /section/name.html - return path.Clean(in) -} diff --git a/helpers/url_test.go b/helpers/url_test.go index 4c16208f1..f899e1cdb 100644 --- a/helpers/url_test.go +++ b/helpers/url_test.go @@ -17,7 +17,6 @@ import ( "strings" "testing" - qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/langs" ) @@ -93,9 +92,8 @@ func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, {"/" + lang + "test", "http://base/", "http://base/" + lang + "/" + lang + "test"}, } - for _, test := range newTests { - tests = append(tests, test) - } + tests = append(tests, newTests...) + } for _, test := range tests { @@ -121,24 +119,6 @@ func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, } } -func TestIsAbsURL(t *testing.T) { - c := qt.New(t) - - for _, this := range []struct { - a string - b bool - }{ - {"http://gohugo.io", true}, - {"https://gohugo.io", true}, - {"//gohugo.io", true}, - {"http//gohugo.io", false}, - {"/content", false}, - {"content", false}, - } { - c.Assert(IsAbsURL(this.a) == this.b, qt.Equals, true) - } -} - func TestRelURL(t *testing.T) { for _, defaultInSubDir := range []bool{true, false} { for _, addLanguage := range []bool{true, false} { @@ -187,10 +167,7 @@ func doTestRelURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, {lang + "test", "http://base/", false, "/" + lang + "/" + lang + "test"}, {"/" + lang + "test", "http://base/", false, "/" + lang + "/" + lang + "test"}, } - - for _, test := range newTests { - tests = append(tests, test) - } + tests = append(tests, newTests...) } for i, test := range tests { @@ -247,28 +224,6 @@ func TestSanitizeURL(t *testing.T) { } } -func TestMakePermalink(t *testing.T) { - type test struct { - host, link, output string - } - - data := []test{ - {"http://abc.com/foo", "post/bar", "http://abc.com/foo/post/bar"}, - {"http://abc.com/foo/", "post/bar", "http://abc.com/foo/post/bar"}, - {"http://abc.com", "post/bar", "http://abc.com/post/bar"}, - {"http://abc.com", "bar", "http://abc.com/bar"}, - {"http://abc.com/foo/bar", "post/bar", "http://abc.com/foo/bar/post/bar"}, - {"http://abc.com/foo/bar", "post/bar/", "http://abc.com/foo/bar/post/bar/"}, - } - - for i, d := range data { - output := MakePermalink(d.host, d.link).String() - if d.output != output { - t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output) - } - } -} - func TestURLPrep(t *testing.T) { type test struct { ugly bool @@ -293,60 +248,3 @@ func TestURLPrep(t *testing.T) { } } } - -func TestAddContextRoot(t *testing.T) { - tests := []struct { - baseURL string - url string - expected string - }{ - {"http://example.com/sub/", "/foo", "/sub/foo"}, - {"http://example.com/sub/", "/foo/index.html", "/sub/foo/index.html"}, - {"http://example.com/sub1/sub2", "/foo", "/sub1/sub2/foo"}, - {"http://example.com", "/foo", "/foo"}, - // cannot guess that the context root is already added int the example below - {"http://example.com/sub/", "/sub/foo", "/sub/sub/foo"}, - {"http://example.com/тря", "/трям/", "/тря/трям/"}, - {"http://example.com", "/", "/"}, - {"http://example.com/bar", "//", "/bar/"}, - } - - for _, test := range tests { - output := AddContextRoot(test.baseURL, test.url) - if output != test.expected { - t.Errorf("Expected %#v, got %#v\n", test.expected, output) - } - } -} - -func TestPretty(t *testing.T) { - c := qt.New(t) - c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name.html")) - c.Assert("/section/sub/name/index.html", qt.Equals, PrettifyURLPath("/section/sub/name.html")) - c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/")) - c.Assert("/section/name/index.html", qt.Equals, PrettifyURLPath("/section/name/index.html")) - c.Assert("/index.html", qt.Equals, PrettifyURLPath("/index.html")) - c.Assert("/name/index.xml", qt.Equals, PrettifyURLPath("/name.xml")) - c.Assert("/", qt.Equals, PrettifyURLPath("/")) - c.Assert("/", qt.Equals, PrettifyURLPath("")) - c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name.html")) - c.Assert("/section/sub/name", qt.Equals, PrettifyURL("/section/sub/name.html")) - c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/")) - c.Assert("/section/name", qt.Equals, PrettifyURL("/section/name/index.html")) - c.Assert("/", qt.Equals, PrettifyURL("/index.html")) - c.Assert("/name/index.xml", qt.Equals, PrettifyURL("/name.xml")) - c.Assert("/", qt.Equals, PrettifyURL("/")) - c.Assert("/", qt.Equals, PrettifyURL("")) -} - -func TestUgly(t *testing.T) { - c := qt.New(t) - c.Assert("/section/name.html", qt.Equals, Uglify("/section/name.html")) - c.Assert("/section/sub/name.html", qt.Equals, Uglify("/section/sub/name.html")) - c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/")) - c.Assert("/section/name.html", qt.Equals, Uglify("/section/name/index.html")) - c.Assert("/index.html", qt.Equals, Uglify("/index.html")) - c.Assert("/name.xml", qt.Equals, Uglify("/name.xml")) - c.Assert("/", qt.Equals, Uglify("/")) - c.Assert("/", qt.Equals, Uglify("")) -} diff --git a/hugolib/config.go b/hugolib/config.go index deba8abe6..091827660 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -21,6 +21,7 @@ import ( "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/maps" + cpaths "github.com/gohugoio/hugo/common/paths" "github.com/gobwas/glob" hglob "github.com/gohugoio/hugo/hugofs/glob" @@ -436,7 +437,7 @@ func (l configLoader) loadConfig(configName string) (string, error) { } var filename string - if helpers.ExtNoDelimiter(configName) != "" { + if cpaths.ExtNoDelimiter(configName) != "" { exists, _ := helpers.Exists(baseFilename, l.Fs) if exists { filename = baseFilename @@ -509,7 +510,7 @@ func (l configLoader) loadConfigFromConfigDir() ([]string, error) { return nil } - name := helpers.Filename(filepath.Base(path)) + name := cpaths.Filename(filepath.Base(path)) item, err := metadecoders.Default.UnmarshalFileToMap(sourceFs, path) if err != nil { @@ -520,7 +521,7 @@ func (l configLoader) loadConfigFromConfigDir() ([]string, error) { if name != "config" { // Can be params.jp, menus.en etc. - name, lang := helpers.FileAndExtNoDelimiter(name) + name, lang := cpaths.FileAndExtNoDelimiter(name) keyPath = []string{name} diff --git a/hugolib/content_map_test.go b/hugolib/content_map_test.go index e5ba983a4..a62380efd 100644 --- a/hugolib/content_map_test.go +++ b/hugolib/content_map_test.go @@ -19,7 +19,7 @@ import ( "strings" "testing" - "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/htesting/hqt" @@ -112,7 +112,7 @@ func TestContentMap(t *testing.T) { meta["lang"] = lang meta["path"] = meta.Filename() meta["classifier"] = files.ClassifyContentFile(fi.Name(), meta.GetOpener()) - meta["translationBaseName"] = helpers.Filename(fi.Name()) + meta["translationBaseName"] = paths.Filename(fi.Name()) }) } diff --git a/hugolib/pagecollections.go b/hugolib/pagecollections.go index 2e4287612..623d5de42 100644 --- a/hugolib/pagecollections.go +++ b/hugolib/pagecollections.go @@ -20,6 +20,8 @@ import ( "strings" "sync" + "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/helpers" @@ -187,7 +189,7 @@ func (c *PageCollections) getSectionOrPage(ref string) (*contentNode, string) { langSuffix := "." + m.s.Lang() // Trim both extension and any language code. - name := helpers.PathNoExt(filename) + name := paths.PathNoExt(filename) name = strings.TrimSuffix(name, langSuffix) // These are reserved bundle names and will always be stored by their owning diff --git a/hugolib/site.go b/hugolib/site.go index 9921dcc97..2e23368d7 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -29,6 +29,8 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/common/constants" "github.com/gohugoio/hugo/common/loggers" @@ -1418,7 +1420,7 @@ func (s *SiteInfo) createNodeMenuEntryURL(in string) string { menuEntryURL := in menuEntryURL = helpers.SanitizeURLKeepTrailingSlash(s.s.PathSpec.URLize(menuEntryURL)) if !s.canonifyURLs { - menuEntryURL = helpers.AddContextRoot(s.s.PathSpec.BaseURL.String(), menuEntryURL) + menuEntryURL = paths.AddContextRoot(s.s.PathSpec.BaseURL.String(), menuEntryURL) } return menuEntryURL } diff --git a/langs/i18n/translationProvider.go b/langs/i18n/translationProvider.go index 1151e6e1d..70bf3b5f3 100644 --- a/langs/i18n/translationProvider.go +++ b/langs/i18n/translationProvider.go @@ -17,12 +17,14 @@ import ( "encoding/json" "strings" + "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/common/herrors" "golang.org/x/text/language" yaml "gopkg.in/yaml.v2" - "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/go-i18n/v2/i18n" + "github.com/gohugoio/hugo/helpers" toml "github.com/pelletier/go-toml" "github.com/gohugoio/hugo/deps" @@ -88,7 +90,7 @@ func addTranslationFile(bundle *i18n.Bundle, r source.File) error { f.Close() name := r.LogicalName() - lang := helpers.Filename(name) + lang := paths.Filename(name) tag := language.Make(lang) if tag == language.Und { name = artificialLangTagPrefix + name diff --git a/resources/image.go b/resources/image.go index 282f008ed..4aec6bed5 100644 --- a/resources/image.go +++ b/resources/image.go @@ -29,6 +29,8 @@ import ( "strings" "sync" + "github.com/gohugoio/hugo/common/paths" + "github.com/disintegration/gift" "github.com/gohugoio/hugo/cache/filecache" @@ -365,7 +367,7 @@ func (i *imageResource) getImageMetaCacheTargetPath() string { if fi := i.getFileInfo(); fi != nil { df.dir = filepath.Dir(fi.Meta().Path()) } - p1, _ := helpers.FileAndExt(df.file) + p1, _ := paths.FileAndExt(df.file) h, _ := i.hash() idStr := helpers.HashString(h, i.size(), imageMetaVersionNumber, cfgHash) p := path.Join(df.dir, fmt.Sprintf("%s_%s.json", p1, idStr)) @@ -373,7 +375,7 @@ func (i *imageResource) getImageMetaCacheTargetPath() string { } func (i *imageResource) relTargetPathFromConfig(conf images.ImageConfig) dirFile { - p1, p2 := helpers.FileAndExt(i.getResourcePaths().relTargetDirFile.file) + p1, p2 := paths.FileAndExt(i.getResourcePaths().relTargetDirFile.file) if conf.TargetFormat != i.Format { p2 = conf.TargetFormat.DefaultExtension() } diff --git a/resources/image_test.go b/resources/image_test.go index cca961ee0..e39e88992 100644 --- a/resources/image_test.go +++ b/resources/image_test.go @@ -28,6 +28,8 @@ import ( "testing" "time" + "github.com/gohugoio/hugo/common/paths" + "github.com/spf13/afero" "github.com/disintegration/gift" @@ -145,7 +147,7 @@ func TestImageTransformFormat(t *testing.T) { assertExtWidthHeight := func(img resource.Image, ext string, w, h int) { c.Helper() c.Assert(img, qt.Not(qt.IsNil)) - c.Assert(helpers.Ext(img.RelPermalink()), qt.Equals, ext) + c.Assert(paths.Ext(img.RelPermalink()), qt.Equals, ext) c.Assert(img.Width(), qt.Equals, w) c.Assert(img.Height(), qt.Equals, h) } diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go index a32fc5884..3184b444d 100644 --- a/resources/page/pagemeta/page_frontmatter.go +++ b/resources/page/pagemeta/page_frontmatter.go @@ -17,6 +17,8 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/resources/resource" @@ -118,7 +120,7 @@ func (f FrontMatterHandler) IsDateKey(key string) bool { // This follows the format as outlined in Jekyll, https://jekyllrb.com/docs/posts/: // "Where YEAR is a four-digit number, MONTH and DAY are both two-digit numbers" func dateAndSlugFromBaseFilename(name string) (time.Time, string) { - withoutExt, _ := helpers.FileAndExt(name) + withoutExt, _ := paths.FileAndExt(name) if len(withoutExt) < 10 { // This can not be a date. diff --git a/resources/transform.go b/resources/transform.go index ad2485716..3586a8bfa 100644 --- a/resources/transform.go +++ b/resources/transform.go @@ -22,6 +22,8 @@ import ( "strings" "sync" + "github.com/gohugoio/hugo/common/paths" + "github.com/pkg/errors" "github.com/gohugoio/hugo/resources/images/exif" @@ -136,13 +138,13 @@ func (ctx *ResourceTransformationCtx) PublishSourceMap(content string) error { // extension, e.g. ".scss" func (ctx *ResourceTransformationCtx) ReplaceOutPathExtension(newExt string) { dir, file := path.Split(ctx.InPath) - base, _ := helpers.PathAndExt(file) + base, _ := paths.PathAndExt(file) ctx.OutPath = path.Join(dir, (base + newExt)) } func (ctx *ResourceTransformationCtx) addPathIdentifier(inPath, identifier string) string { dir, file := path.Split(inPath) - base, ext := helpers.PathAndExt(file) + base, ext := paths.PathAndExt(file) return path.Join(dir, (base + identifier + ext)) } diff --git a/source/fileInfo.go b/source/fileInfo.go index 9e7e6df53..7b20f5f29 100644 --- a/source/fileInfo.go +++ b/source/fileInfo.go @@ -18,6 +18,8 @@ import ( "strings" "sync" + "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/hugofs/files" "github.com/pkg/errors" @@ -263,7 +265,7 @@ func (sp *SourceSpec) NewFileInfo(fi hugofs.FileMetaInfo) (*FileInfo, error) { } ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(name), ".")) - baseName := helpers.Filename(name) + baseName := paths.Filename(name) if translationBaseName == "" { // This is usually provided by the filesystem. But this FileInfo is also |