From 5b3e165bad1318a1764ee534770fd2eda8a03feb Mon Sep 17 00:00:00 2001 From: Joe Mooring Date: Fri, 14 Apr 2023 13:27:16 -0700 Subject: [PATCH] tpl/urls: Add JoinPath template function See https://pkg.go.dev/net/url#JoinPath Closes #9694 --- docs/content/en/functions/urls.JoinPath.md | 24 +++++++++++++++ tpl/urls/init.go | 8 +++++ tpl/urls/urls.go | 35 ++++++++++++++++++++++ tpl/urls/urls_test.go | 35 ++++++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 docs/content/en/functions/urls.JoinPath.md diff --git a/docs/content/en/functions/urls.JoinPath.md b/docs/content/en/functions/urls.JoinPath.md new file mode 100644 index 00000000000..c0e0464b636 --- /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 ec954640d29..cdc597f3444 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 551b5387504..5de2342d47b 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 f33e128be5b..1567875c070 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) + } +}