summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoe Mooring <joe.mooring@veriphor.com>2023-04-14 13:27:16 -0700
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2023-05-19 10:31:17 +0200
commit5b3e165bad1318a1764ee534770fd2eda8a03feb (patch)
tree8abe6854567c6bb5ccda945982ad8d3dd51903fb
parent03cb38e6c6d60e95df1f40a56fe432e1747ea7ac (diff)
tpl/urls: Add JoinPath template function
See https://pkg.go.dev/net/url#JoinPath Closes #9694
-rw-r--r--docs/content/en/functions/urls.JoinPath.md24
-rw-r--r--tpl/urls/init.go8
-rw-r--r--tpl/urls/urls.go35
-rw-r--r--tpl/urls/urls_test.go35
4 files changed, 102 insertions, 0 deletions
diff --git a/docs/content/en/functions/urls.JoinPath.md b/docs/content/en/functions/urls.JoinPath.md
new file mode 100644
index 000000000..c0e0464b6
--- /dev/null
+++ b/docs/content/en/functions/urls.JoinPath.md
@@ -0,0 +1,24 @@
+---
+title: urls.JoinPath
+description: Joins the provided elements into a URL string and cleans the result of any ./ or ../ elements.
+categories: [functions]
+menu:
+ docs:
+ parent: functions
+keywords: [urls,path,join]
+signature: ["urls.JoinPath ELEMENT..."]
+---
+
+```go-html-template
+{{ urls.JoinPath "" }} → "/"
+{{ urls.JoinPath "a" }} → "a"
+{{ urls.JoinPath "a" "b" }} → "a/b"
+{{ urls.JoinPath "/a" "b" }} → "/a/b"
+{{ urls.JoinPath "https://example.org" "b" }} → "https://example.org/b"
+
+{{ urls.JoinPath (slice "a" "b") }} → "a/b"
+```
+
+Unlike the [`path.Join`] function, `urls.JoinPath` retains consecutive leading slashes.
+
+[`path.Join`]: /functions/path.join/
diff --git a/tpl/urls/init.go b/tpl/urls/init.go
index ec954640d..cdc597f34 100644
--- a/tpl/urls/init.go
+++ b/tpl/urls/init.go
@@ -68,6 +68,14 @@ func init() {
},
)
+ ns.AddMethodMapping(ctx.JoinPath,
+ nil,
+ [][2]string{
+ {`{{ urls.JoinPath "https://example.org" "foo" }}`, `https://example.org/foo`},
+ {`{{ urls.JoinPath (slice "a" "b") }}`, `a/b`},
+ },
+ )
+
return ns
}
diff --git a/tpl/urls/urls.go b/tpl/urls/urls.go
index 551b53875..5de2342d4 100644
--- a/tpl/urls/urls.go
+++ b/tpl/urls/urls.go
@@ -185,3 +185,38 @@ func (ns *Namespace) AbsLangURL(s any) (template.HTML, error) {
return template.HTML(ns.deps.PathSpec.AbsURL(ss, !ns.multihost)), nil
}
+
+// JoinPath joins the provided elements into a URL string and cleans the result
+// of any ./ or ../ elements.
+func (ns *Namespace) JoinPath(elements ...any) (string, error) {
+
+ var selements []string
+ for _, e := range elements {
+ switch v := e.(type) {
+ case []string:
+ for _, e := range v {
+ selements = append(selements, e)
+ }
+ case []any:
+ for _, e := range v {
+ se, err := cast.ToStringE(e)
+ if err != nil {
+ return "", err
+ }
+ selements = append(selements, se)
+ }
+ default:
+ se, err := cast.ToStringE(e)
+ if err != nil {
+ return "", err
+ }
+ selements = append(selements, se)
+ }
+ }
+
+ result, err := url.JoinPath(selements[0], selements[1:]...)
+ if err != nil {
+ return "", err
+ }
+ return result, nil
+}
diff --git a/tpl/urls/urls_test.go b/tpl/urls/urls_test.go
index f33e128be..1567875c0 100644
--- a/tpl/urls/urls_test.go
+++ b/tpl/urls/urls_test.go
@@ -69,3 +69,38 @@ func TestParse(t *testing.T) {
qt.CmpEquals(hqt.DeepAllowUnexported(&url.URL{}, url.Userinfo{})), test.expect)
}
}
+
+func TestJoinPath(t *testing.T) {
+ t.Parallel()
+ c := qt.New(t)
+
+ for _, test := range []struct {
+ elements any
+ expect any
+ }{
+ {"", `/`},
+ {"a", `a`},
+ {"/a/b", `/a/b`},
+ {"./../a/b", `a/b`},
+ {[]any{""}, `/`},
+ {[]any{"a"}, `a`},
+ {[]any{"/a", "b"}, `/a/b`},
+ {[]any{".", "..", "/a", "b"}, `a/b`},
+ {[]any{"https://example.org", "a"}, `https://example.org/a`},
+ {[]any{nil}, `/`},
+ // errors
+ {tstNoStringer{}, false},
+ {[]any{tstNoStringer{}}, false},
+ } {
+
+ result, err := ns.JoinPath(test.elements)
+
+ if b, ok := test.expect.(bool); ok && !b {
+ c.Assert(err, qt.Not(qt.IsNil))
+ continue
+ }
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(result, qt.Equals, test.expect)
+ }
+}