summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--commands/server.go4
-rw-r--r--common/paths/path.go312
-rw-r--r--common/paths/path_test.go252
-rw-r--r--common/paths/url.go212
-rw-r--r--common/paths/url_test.go129
-rw-r--r--create/content.go4
-rw-r--r--create/content_template_handler.go4
-rw-r--r--helpers/path.go185
-rw-r--r--helpers/path_test.go135
-rw-r--r--helpers/url.go153
-rw-r--r--helpers/url_test.go108
-rw-r--r--hugolib/config.go7
-rw-r--r--hugolib/content_map_test.go4
-rw-r--r--hugolib/pagecollections.go4
-rw-r--r--hugolib/site.go4
-rw-r--r--langs/i18n/translationProvider.go6
-rw-r--r--resources/image.go6
-rw-r--r--resources/image_test.go4
-rw-r--r--resources/page/pagemeta/page_frontmatter.go4
-rw-r--r--resources/transform.go6
-rw-r--r--source/fileInfo.go4
21 files changed, 956 insertions, 591 deletions
diff --git a/commands/server.go b/commands/server.go
index 5cb43470b..02db354ba 100644
--- a/commands/server.go
+++ b/commands/server.go
@@ -31,6 +31,8 @@ import (
"syscall"
"time"
+ "github.com/gohugoio/hugo/common/paths"
+
"github.com/pkg/errors"
"github.com/gohugoio/hugo/livereload"
@@ -275,7 +277,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
func getRootWatchDirsStr(baseDir string, watchDirs []string) string {
relWatchDirs := make([]string, len(watchDirs))
for i, dir := range watchDirs {
- relWatchDirs[i], _ = helpers.GetRelativePath(dir, baseDir)
+ relWatchDirs[i], _ = paths.GetRelativePath(dir, baseDir)
}
return strings.Join(helpers.UniqueStringsSorted(helpers.ExtractRootPaths(relWatchDirs)), ",")
diff --git a/common/paths/path.go b/common/paths/path.go
new file mode 100644
index 000000000..0237dd9f2
--- /dev/null
+++ b/common/paths/path.go
@@ -0,0 +1,312 @@
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package paths
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+// FilePathSeparator as defined by os.Separator.
+const FilePathSeparator = string(filepath.Separator)
+
+// filepathPathBridge is a bridge for common functionality in filepath vs path
+type filepathPathBridge interface {
+ Base(in string) string
+ Clean(in string) string
+ Dir(in string) string
+ Ext(in string) string
+ Join(elem ...string) string
+ Separator() string
+}
+
+type filepathBridge struct {
+}
+
+func (filepathBridge) Base(in string) string {
+ return filepath.Base(in)
+}
+
+func (filepathBridge) Clean(in string) string {
+ return filepath.Clean(in)
+}
+
+func (filepathBridge) Dir(in string) string {
+ return filepath.Dir(in)
+}
+
+func (filepathBridge) Ext(in string) string {
+ return filepath.Ext(in)
+}
+
+func (filepathBridge) Join(elem ...string) string {
+ return filepath.Join(elem...)
+}
+
+func (filepathBridge) Separator() string {
+ return FilePathSeparator
+}
+
+var fpb filepathBridge
+
+// ToSlashTrimLeading is just a filepath.ToSlash with an added / prefix trimmer.
+func ToSlashTrimLeading(s string) string {
+ return strings.TrimPrefix(filepath.ToSlash(s), "/")
+}
+
+// MakeTitle converts the path given to a suitable title, trimming whitespace
+// and replacing hyphens with whitespace.
+func MakeTitle(inpath string) string {
+ return strings.Replace(strings.TrimSpace(inpath), "-", " ", -1)
+}
+
+// ReplaceExtension takes a path and an extension, strips the old extension
+// and returns the path with the new extension.
+func ReplaceExtension(path string, newExt string) string {
+ f, _ := fileAndExt(path, fpb)
+ return f + "." + newExt
+}
+
+func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
+ for _, currentPath := range possibleDirectories {
+ if strings.HasPrefix(inPath, currentPath) {
+ return strings.TrimPrefix(inPath, currentPath), nil
+ }
+ }
+ return inPath, errors.New("can't extract relative path, unknown prefix")
+}
+
+// Should be good enough for Hugo.
+var isFileRe = regexp.MustCompile(`.*\..{1,6}$`)
+
+// GetDottedRelativePath expects a relative path starting after the content directory.
+// It returns a relative path with dots ("..") navigating up the path structure.
+func GetDottedRelativePath(inPath string) string {
+ inPath = filepath.Clean(filepath.FromSlash(inPath))
+
+ if inPath == "." {
+ return "./"
+ }
+
+ if !isFileRe.MatchString(inPath) && !strings.HasSuffix(inPath, FilePathSeparator) {
+ inPath += FilePathSeparator
+ }
+
+ if !strings.HasPrefix(inPath, FilePathSeparator) {
+ inPath = FilePathSeparator + inPath
+ }
+
+ dir, _ := filepath.Split(inPath)
+
+ sectionCount := strings.Count(dir, FilePathSeparator)
+
+ if sectionCount == 0 || dir == FilePathSeparator {
+ return "./"
+ }
+
+ var dottedPath string
+
+ for i := 1; i < sectionCount; i++ {
+ dottedPath += "../"
+ }
+
+ return dottedPath
+}
+
+// ExtNoDelimiter takes a path and returns the extension, excluding the delimiter, i.e. "md".
+func ExtNoDelimiter(in string) string {
+ return strings.TrimPrefix(Ext(in), ".")
+}
+
+// Ext takes a path and returns the extension, including the delimiter, i.e. ".md".
+func Ext(in string) string {
+ _, ext := fileAndExt(in, fpb)
+ return ext
+}
+
+// PathAndExt is the same as FileAndExt, but it uses the path package.
+func PathAndExt(in string) (string, string) {
+ return fileAndExt(in, pb)
+}
+
+// FileAndExt takes a path and returns the file and extension separated,
+// the extension including the delimiter, i.e. ".md".
+func FileAndExt(in string) (string, string) {
+ return fileAndExt(in, fpb)
+}
+
+// FileAndExtNoDelimiter takes a path and returns the file and extension separated,
+// the extension excluding the delimiter, e.g "md".
+func FileAndExtNoDelimiter(in string) (string, string) {
+ file, ext := fileAndExt(in, fpb)
+ return file, strings.TrimPrefix(ext, ".")
+}
+
+// Filename takes a file path, strips out the extension,
+// and returns the name of the file.
+func Filename(in string) (name string) {
+ name, _ = fileAndExt(in, fpb)
+ return
+}
+
+// PathNoExt takes a path, strips out the extension,
+// and returns the name of the file.
+func PathNoExt(in string) string {
+ return strings.TrimSuffix(in, path.Ext(in))
+}
+
+// FileAndExt returns the filename and any extension of a file path as
+// two separate strings.
+//
+// If the path, in, contains a directory name ending in a slash,
+// then both name and ext will be empty strings.
+//
+// If the path, in, is either the current directory, the parent
+// directory or the root directory, or an empty string,
+// then both name and ext will be empty strings.
+//
+// If the path, in, represents the path of a file without an extension,
+// then name will be the name of the file and ext will be an empty string.
+//
+// If the path, in, represents a filename with an extension,
+// then name will be the filename minus any extension - including the dot
+// and ext will contain the extension - minus the dot.
+func fileAndExt(in string, b filepathPathBridge) (name string, ext string) {
+ ext = b.Ext(in)
+ base := b.Base(in)
+
+ return extractFilename(in, ext, base, b.Separator()), ext
+}
+
+func extractFilename(in, ext, base, pathSeparator string) (name string) {
+ // No file name cases. These are defined as:
+ // 1. any "in" path that ends in a pathSeparator
+ // 2. any "base" consisting of just an pathSeparator
+ // 3. any "base" consisting of just an empty string
+ // 4. any "base" consisting of just the current directory i.e. "."
+ // 5. any "base" consisting of just the parent directory i.e. ".."
+ if (strings.LastIndex(in, pathSeparator) == len(in)-1) || base == "" || base == "." || base == ".." || base == pathSeparator {
+ name = "" // there is NO filename
+ } else if ext != "" { // there was an Extension
+ // return the filename minus the extension (and the ".")
+ name = base[:strings.LastIndex(base, ".")]
+ } else {
+ // no extension case so just return base, which willi
+ // be the filename
+ name = base
+ }
+ return
+}
+
+// GetRelativePath returns the relative path of a given path.
+func GetRelativePath(path, base string) (final string, err error) {
+ if filepath.IsAbs(path) && base == "" {
+ return "", errors.New("source: missing base directory")
+ }
+ name := filepath.Clean(path)
+ base = filepath.Clean(base)
+
+ name, err = filepath.Rel(base, name)
+ if err != nil {
+ return "", err
+ }
+
+ if strings.HasSuffix(filepath.FromSlash(path), FilePathSeparator) && !strings.HasSuffix(name, FilePathSeparator) {
+ name += FilePathSeparator
+ }
+ return name, nil
+}
+
+// PathPrep prepares the path using the uglify setting to create paths on
+// either the form /section/name/index.html or /section/name.html.
+func PathPrep(ugly bool, in string) string {
+ if ugly {
+ return Uglify(in)
+ }
+ return PrettifyPath(in)
+}
+
+// PrettifyPath is the same as PrettifyURLPath but for file paths.
+// /section/name.html becomes /section/name/index.html
+// /section/name/ becomes /section/name/index.html
+// /section/name/index.html becomes /section/name/index.html
+func PrettifyPath(in string) string {
+ return prettifyPath(in, fpb)
+}
+
+func prettifyPath(in string, b filepathPathBridge) string {
+ if filepath.Ext(in) == "" {
+ // /section/name/ -> /section/name/index.html
+ if len(in) < 2 {
+ return b.Separator()
+ }
+ return b.Join(in, "index.html")
+ }
+ name, ext := fileAndExt(in, b)
+ if name == "index" {
+ // /section/name/index.html -> /section/name/index.html
+ return b.Clean(in)
+ }
+ // /section/name.html -> /section/name/index.html
+ return b.Join(b.Dir(in), name, "index"+ext)
+}
+
+type NamedSlice struct {
+ Name string
+ Slice []string
+}
+
+func (n NamedSlice) String() string {
+ if len(n.Slice) == 0 {
+ return n.Name
+ }
+ return fmt.Sprintf("%s%s{%s}", n.Name, FilePathSeparator, strings.Join(n.Slice, ","))
+}
+
+// FindCWD returns the current working directory from where the Hugo
+// executable is run.
+func FindCWD() (string, error) {
+ serverFile, err := filepath.Abs(os.Args[0])
+ if err != nil {
+ return "", fmt.Errorf("can't get absolute path for executable: %v", err)
+ }
+
+ path := filepath.Dir(serverFile)
+ realFile, err := filepath.EvalSymlinks(serverFile)
+ if err != nil {
+ if _, err = os.Stat(serverFile + ".exe"); err == nil {
+ realFile = filepath.Clean(serverFile + ".exe")
+ }
+ }
+
+ if err == nil && realFile != serverFile {
+ path = filepath.Dir(realFile)
+ }
+
+ return path, nil
+}
+
+// AddTrailingSlash adds a trailing Unix styled slash (/) if not already
+// there.
+func AddTrailingSlash(path string) string {
+ if !strings.HasSuffix(path, "/") {
+ path += "/"
+ }
+ return path
+}
diff --git a/common/paths/path_test.go b/common/paths/path_test.go
new file mode 100644
index 000000000..e55493c7d
--- /dev/null
+++ b/common/paths/path_test.go
@@ -0,0 +1,252 @@
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package paths
+
+import (
+ "path/filepath"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestGetRelativePath(t *testing.T) {
+ tests := []struct {
+ path string
+ base string
+ expect interface{}
+ }{
+ {filepath.FromSlash("/a/b"), filepath.FromSlash("/a"), filepath.FromSlash("b")},
+ {filepath.FromSlash("/a/b/c/"), filepath.FromSlash("/a"), filepath.FromSlash("b/c/")},
+ {filepath.FromSlash("/c"), filepath.FromSlash("/a/b"), filepath.FromSlash("../../c")},
+ {filepath.FromSlash("/c"), "", false},
+ }
+ for i, this := range tests {
+ // ultimately a fancy wrapper around filepath.Rel
+ result, err := GetRelativePath(this.path, this.base)
+
+ if b, ok := this.expect.(bool); ok && !b {
+ if err == nil {
+ t.Errorf("[%d] GetRelativePath didn't return an expected error", i)
+ }
+ } else {
+ if err != nil {
+ t.Errorf("[%d] GetRelativePath failed: %s", i, err)
+ continue
+ }
+ if result != this.expect {
+ t.Errorf("[%d] GetRelativePath got %v but expected %v", i, result, this.expect)
+ }
+ }
+
+ }
+}
+
+func TestMakePathRelative(t *testing.T) {
+ type test struct {
+ inPath, path1, path2, output string
+ }
+
+ data := []test{
+ {"/abc/bcd/ab.css", "/abc/bcd", "/bbc/bcd", "/ab.css"},
+ {"/abc/bcd/ab.css", "/abcd/bcd", "/abc/bcd", "/ab.css"},
+ }
+
+ for i, d := range data {
+ output, _ := makePathRelative(d.inPath, d.path1, d.path2)
+ if d.output != output {
+ t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output)
+ }
+ }
+ _, error := makePathRelative("a/b/c.ss", "/a/c", "/d/c", "/e/f")
+
+ if error == nil {
+ t.Errorf("Test failed, expected error")
+ }
+}
+
+func TestGetDottedRelativePath(t *testing.T) {
+ // on Windows this will receive both kinds, both country and western ...
+ for _, f := range []func(string) string{filepath.FromSlash, func(s string) string { return s }} {
+ doTestGetDottedRelativePath(f, t)
+ }
+}
+
+func doTestGetDottedRelativePath(urlFixer func(string) string, t *testing.T) {
+ type test struct {
+ input, expected string
+ }
+ data := []test{
+ {"", "./"},
+ {urlFixer("/"), "./"},
+ {urlFixer("post"), "../"},
+ {urlFixer("/post"), "../"},
+ {urlFixer("post/"), "../"},
+ {urlFixer("tags/foo.html"), "../"},
+ {urlFixer("/tags/foo.html"), "../"},
+ {urlFixer("/post/"), "../"},
+ {urlFixer("////post/////"), "../"},
+ {urlFixer("/foo/bar/index.html"), "../../"},
+ {urlFixer("/foo/bar/foo/"), "../../../"},
+ {urlFixer("/foo/bar/foo"), "../../../"},
+ {urlFixer("foo/bar/foo/"), "../../../"},
+ {urlFixer("foo/bar/foo/bar"), "../../../../"},
+ {"404.html", "./"},
+ {"404.xml", "./"},
+ {"/404.html", "./"},
+ }
+ for i, d := range data {
+ output := GetDottedRelativePath(d.input)
+ if d.expected != output {
+ t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
+ }
+ }
+}
+
+func TestMakeTitle(t *testing.T) {
+ type test struct {
+ input, expected string
+ }
+ data := []test{
+ {"Make-Title", "Make Title"},
+ {"MakeTitle", "MakeTitle"},
+ {"make_title", "make_title"},
+ }
+ for i, d := range data {
+ output := MakeTitle(d.input)
+ if d.expected != output {
+ t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
+ }
+ }
+}
+
+// Replace Extension is probably poorly named, but the intent of the
+// function is to accept a path and return only the file name with a
+// new extension. It's intentionally designed to strip out the path
+// and only provide the name. We should probably rename the function to
+// be more explicit at some point.
+func TestReplaceExtension(t *testing.T) {
+ type test struct {
+ input, newext, expected string
+ }
+ data := []test{
+ // These work according to the above definition
+ {"/some/random/path/file.xml", "html", "file.html"},
+ {"/banana.html", "xml", "banana.xml"},
+ {"./banana.html", "xml", "banana.xml"},
+ {"banana/pie/index.html", "xml", "index.xml"},
+ {"../pies/fish/index.html", "xml", "index.xml"},
+ // but these all fail
+ {"filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
+ {"/filename-without-an-ext", "ext", "filename-without-an-ext.ext"},
+ {"/directory/mydir/", "ext", ".ext"},
+ {"mydir/", "ext", ".ext"},
+ }
+
+ for i, d := range data {
+ output := ReplaceExtension(filepath.FromSlash(d.input), d.newext)
+ if d.expected != output {
+ t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
+ }
+ }
+}
+
+func TestExtNoDelimiter(t *testing.T) {
+ c := qt.New(t)
+ c.Assert(ExtNoDelimiter(filepath.FromSlash("/my/data.json")), qt.Equals, "json")
+}
+
+func TestFilename(t *testing.T) {
+ type test struct {
+ input, expected string
+ }
+ data := []test{
+ {"index.html", "index"},
+ {"./index.html", "index"},
+ {"/index.html", "index"},
+ {"index", "index"},
+ {"/tmp/index.html", "index"},
+ {"./filename-no-ext", "filename-no-ext"},
+ {"/filename-no-ext", "filename-no-ext"},
+ {"filename-no-ext", "filename-no-ext"},
+ {"directory/", ""}, // no filename case??
+ {"directory/.hidden.ext", ".hidden"},
+ {"./directory/../~/banana/gold.fish", "gold"},
+ {"../directory/banana.man", "banana"},
+ {"~/mydir/filename.ext", "filename"},
+ {"./directory//tmp/filename.ext", "filename"},
+ }
+
+ for i, d := range data {
+ output := Filename(filepath.FromSlash(d.input))
+ if d.expected != output {
+ t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
+ }
+ }
+}
+
+func TestFileAndExt(t *testing.T) {
+ type test struct {
+ input, expectedFile, expectedExt string
+ }
+ data := []test{
+ {"index.html", "index", ".html"},
+ {"./index.html", "index", ".html"},
+ {"/index.html", "index", ".html"},
+ {"index", "index", ""},
+ {"/tmp/index.html", "index", ".html"},
+ {"./filename-no-ext", "filename-no-ext", ""},
+ {"/filename-no-ext", "filename-no-ext", ""},
+ {"filename-no-ext", "filename-no-ext", ""},
+ {"directory/", "", ""}, // no filename case??
+ {"directory/.hidden.ext", ".hidden", ".ext"},
+ {"./directory/../~/banana/gold.fish", "gold", ".fish"},
+ {"../directory/banana.man", "banana", ".man"},
+ {"~/mydir/filename.ext", "filename", ".ext"},
+ {"./directory//tmp/filename.ext", "filename", ".ext"},
+ }
+
+ for i, d := range data {
+ file, ext := fileAndExt(filepath.FromSlash(d.input), fpb)
+ if d.expectedFile != file {
+ t.Errorf("Test %d failed. Expected filename %q got %q.", i, d.expectedFile, file)
+ }
+ if d.expectedExt != ext {
+ t.Errorf("Test %d failed. Expected extension %q got %q.", i, d.expectedExt, ext)
+ }
+ }
+}
+
+func TestFindCWD(t *testing.T) {
+ type test struct {
+ expectedDir string
+ expectedErr error
+ }
+
+ // cwd, _ := os.Getwd()
+ data := []test{
+ //{cwd, nil},
+ // Commenting this out. It doesn't work properly.
+ // There's a good reason why we don't use os.Getwd(), it doesn't actually work the way we want it to.
+ // I really don't know a better way to test this function. - SPF 2014.11.04
+ }
+ for i, d := range data {
+ dir, err := FindCWD()
+ if d.expectedDir != dir {
+ t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedDir, dir)
+ }
+ if d.expectedErr != err {
+ t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedErr, err)
+ }
+ }
+}
diff --git a/common/paths/url.go b/common/paths/url.go
new file mode 100644
index 000000000..600e8d22d
--- /dev/null
+++ b/common/paths/url.go
@@ -0,0 +1,212 @@
+// Copyright 2021 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package paths
+
+import (
+ "fmt"
+ "net/url"
+ "path"
+ "strings"
+
+ "github.com/PuerkitoBio/purell"
+)
+
+type pathBridge struct {
+}
+
+func (pathBridge) Base(in string) string {
+ return path.Base(in)
+}
+
+func (pathBridge) Clean(in string) string {
+ return path.Clean(in)
+}
+
+func (pathBridge) Dir(in string) string {
+ return path.Dir(in)
+}
+
+func (pathBridge) Ext(in string) string {
+ return path.Ext(in)
+}
+
+func (pathBridge) Join(elem ...string) string {
+ return path.Join(elem...)
+}
+
+func (pathBridge) Separator() string {
+ return "/"
+}
+
+var pb pathBridge
+
+func sanitizeURLWithFlags(in string, f purell.NormalizationFlags) string {
+ s, err := purell.NormalizeURLString(in, f)
+ if err != nil {
+ return in
+ }
+
+ // Temporary workaround for the bug fix and resulting
+ // behavioral change in purell.NormalizeURLString():
+ // a leading '/' was inadvertently added to relative links,
+ // but no longer, see #878.
+ //
+ // I think the real solution is to allow Hugo to
+ // make relative URL with relative path,
+ // e.g. "../../post/hello-again/", as wished by users
+ // in issues #157, #622, etc., without forcing
+ // relative URLs to begin with '/'.
+ // Once the fixes are in, let's remove this kludge
+ // and restore SanitizeURL() to the way it was.
+ // -- @anthonyfok, 2015-02-16
+ //
+ // Begin temporary kludge
+ u, err := url.Parse(s)
+ if err != nil {
+ panic(err)
+ }
+ if len(u.Path) > 0 && !strings.HasPrefix(u.Path, "/") {
+ u.Path = "/" + u.Path
+ }
+ return u.String()
+ // End temporary kludge
+
+ // return s
+
+}
+
+// SanitizeURL sanitizes the input URL string.
+func SanitizeURL(in string) string {
+ return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveTrailingSlash|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator)
+}
+
+// SanitizeURLKeepTrailingSlash is the same as SanitizeURL, but will keep any trailing slash.
+func SanitizeURLKeepTrailingSlash(in string) string {
+ return sanitizeURLWithFlags(in, purell.FlagsSafe|purell.FlagRemoveDotSegments|purell.FlagRemoveDuplicateSlashes|purell.FlagRemoveUnnecessaryHostDots|purell.FlagRemoveEmptyPortSeparator)
+}
+
+// MakePermalink combines base URL with content path to create full URL paths.
+// Example
+// base: http://spf13.com/
+// path: post/how-i-blog
+// result: http://spf13.com/post/how-i-blog
+func MakePermalink(host, plink string) *url.URL {
+ base, err := url.Parse(host)
+ if err != nil {
+ panic(err)
+ }
+
+ p, err := url.Parse(plink)
+ if err != nil {
+ panic(err)
+ }
+
+ if p.Host != "" {
+ panic(fmt.Errorf("can't make permalink from absolute link %q", plink))
+ }
+
+ base.Path = path.Join(base.Path, p.Path)
+
+ // path.Join will strip off the last /, so put it back if it was there.
+ hadTrailingSlash := (plink == "" && strings.HasSuffix(host, "/")) || strings.HasSuffix(p.Path, "/")
+ if hadTrailingSlash && !strings.HasSuffix(base.Path, "/") {
+ base.Path = base.Path + "/"
+ }
+
+ return base
+}
+
+// IsAbsURL determines whether the given path points to an absolute URL.
+func IsAbsURL(path string) bool {
+ url, err := url.Parse(path)
+ if err != nil {
+ return false
+ }
+
+ return url.IsAbs() || strings.HasPrefix(path, "//")
+}
+
+// AddContextRoot adds the context root to an URL if it's not already set.
+// For relative URL entries on sites with a base url with a context root set (i.e. http://example.com/mysite),
+// relative URLs must not include the context root if canonifyURLs is enabled. But if it's disabled, it must be set.
+func AddContextRoot(baseURL, relativePath string) string {
+ url, err := url.Parse(baseURL)
+ if err != nil {
+ panic(err)
+ }
+
+ newPath := path.Join(url.Path, relativePath)
+
+ // path strips trailing slash, ignore root path.
+ if newPath != "/" && strings.HasSuffix(relativePath, "/") {
+ newPath += "/"
+ }
+ return newPath
+}
+
+// URLizeAn
+
+// PrettifyURL takes a URL string and returns a semantic, clean URL.
+func PrettifyURL(in string) string {
+ x := PrettifyURLPath(in)
+
+ if path.Base(x) == "index.html" {
+ return path.Dir(x)
+ }
+
+ if in == "" {
+ return "/"
+ }
+
+ return x
+}
+
+// PrettifyURLPath takes a URL path to a content and converts it
+// to enable pretty URLs.
+// /section/name.html becomes /section/name/index.html
+// /section/name/ becomes /section/name/index.html
+// /section/name/index.html becomes /section/name/index.html
+func PrettifyURLPath(in string) string {
+ return prettifyPath(in, pb)
+}
+
+// Uglify does the opposite of PrettifyURLPath().
+// /section/name/index.html becomes /section/name.html
+// /section/name/ becomes /section/name.html
+// /section/name.html becomes /section/name.html
+func Uglify(in string) string {
+ if path.Ext(in) == "" {
+ if len(in) < 2 {
+ return "/"
+ }
+ // /section/name/ -> /section/name.html
+ return path.Clean(in) + ".html"
+ }
+
+ name, ext := fileAndExt(in, pb)
+ if name == "index" {
+ // /section/name/index.html -> /section/name.html
+ d := path.Dir(in)
+ if len(d) > 1 {
+ return d + ext
+ }
+ return in
+ }
+ // /.xml -> /index.xml
+ if name == "" {
+ return path.Dir(in) + "index" + ext
+ }
+ // /section/name.html -> /section/name.html
+ return path.Clean(in)
+}
diff --git a/common/paths/url_test.go b/common/paths/url_test.go
new file mode 100644