Split out the puthe path/filepath functions into common/paths
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 18 Jun 2021 08:27:27 +0000 (10:27 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 18 Jun 2021 08:55:00 +0000 (10:55 +0200)
So they can be used from the config package without cyclic troubles.

Updates #8654

21 files changed:
commands/server.go
common/paths/path.go [new file with mode: 0644]
common/paths/path_test.go [new file with mode: 0644]
common/paths/url.go [new file with mode: 0644]
common/paths/url_test.go [new file with mode: 0644]
create/content.go
create/content_template_handler.go
helpers/path.go
helpers/path_test.go
helpers/url.go
helpers/url_test.go
hugolib/config.go
hugolib/content_map_test.go
hugolib/pagecollections.go
hugolib/site.go
langs/i18n/translationProvider.go
resources/image.go
resources/image_test.go
resources/page/pagemeta/page_frontmatter.go
resources/transform.go
source/fileInfo.go

index 5cb43470b10411cdc6ea316efcd4d43c916f17a1..02db354ba56fd840a2aec46b348841756e37714c 100644 (file)
@@ -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 (file)
index 0000000..0237dd9
--- /dev/null
@@ -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 (file)
index 0000000..e55493c
--- /dev/null
@@ -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 (file)
index 0000000..600e8d2
--- /dev/null
@@ -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 (file)
index 0000000..3e8391e
--- /dev/null
@@ -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(""))
+}
index 26eda20310b4fbfac665dbff37d916d404c13819..797ffe75862e12d49d4259beba82294cbe31a877 100644 (file)
@@ -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
index 3e9701e4bde2ae00173eb6e4d57ec34aadc5a7f1..09cf4c0a50ccdb6df34d89340c9c8b408461e2ad 100644 (file)
@@ -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)
        }
index 17a513cecd9706a05aba43c442dbf7f54a99bcf9..fd35fafc9cbe1488bccdf9b2c60723516800eec4 100644 (file)
@@ -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
index c9595183253dc6cf40c9d8e373397167fe111ee9..1d2dc118431bb0a6df19ea3a71e99dec3a5db43e 100644 (file)
@@ -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"),
index 8c39bc4fa10ef7f6043a993ea28417dcb72afbae..193dd3c864156c7ae332f62df5ef03490afb86ee 100644 (file)
 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)
-}
index 4c16208f19e757a777ca996fbfb9c20619c1b337..f899e1cdbb9c2213991f3d396bc027a55d0dac23 100644 (file)
@@ -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(""))
-}
index deba8abe63f504baff5cfc45f324fd19feb935b7..091827660445072a32e2bf25aea9d8ad5d34244c 100644 (file)
@@ -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}
 
index e5ba983a42e2e12a708f69949c43281989da702e..a62380efd4bd3968125c963d59a410ab74b60cfa 100644 (file)
@@ -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())
                        })
        }
 
index 2e4287612122dc820ff8b8ec59d20b53189d36dd..623d5de42fca55a8ef4987820c5eee288c667c68 100644 (file)
@@ -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
index 9921dcc976edd29cbdaabfcc0fd740a7c5c59143..2e23368d723830968cf3548bc34d30f19d7bc108 100644 (file)
@@ -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
 }
index 1151e6e1d0977883c69ce5d55b34942ed19f9b43..70bf3b5f3c2ce086e2e64440a47e9c88021efed3 100644 (file)
@@ -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
index 282f008edfd404d4469796e1323a19befe2c4fd1..4aec6bed577d976951d8140e114aed944396772c 100644 (file)
@@ -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()
        }
index cca961ee007a7e221d5847717bcc2593448bc653..e39e889925f8db46178b55ca3f1b74a30d7ebf11 100644 (file)
@@ -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)
        }
index a32fc588443e72f0e144281cdd123a9f6fdca8f2..3184b444d89dc1189c6e3f2b098827364f7f1267 100644 (file)
@@ -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.
index ad2485716ffff4c91527a9a0f8cffa493ba0d612..3586a8bfaeb0d4c3dbc742ab431f1f66e424a4a8 100644 (file)
@@ -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))
 }
 
index 9e7e6df53c90f9e75384495da09448f768cbb63a..7b20f5f29bb7d748c9f99e672da153932f1015fa 100644 (file)
@@ -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