From 93aad3c543828efca2adeb7f96cf50ae29878593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 18 Jun 2021 10:27:27 +0200 Subject: Split out the puthe path/filepath functions into common/paths So they can be used from the config package without cyclic troubles. Updates #8654 --- helpers/path.go | 185 --------------------------------------------------- helpers/path_test.go | 135 ------------------------------------- helpers/url.go | 153 ++---------------------------------------- helpers/url_test.go | 108 +----------------------------- 4 files changed, 9 insertions(+), 572 deletions(-) (limited to 'helpers') 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("")) -} -- cgit v1.2.3