tpl/urls: Add PathEscape and PathUnescape functions

Closes #14209
This commit is contained in:
Joe Mooring
2025-11-25 10:28:35 -08:00
committed by Bjørn Erik Pedersen
parent 7a43b928a6
commit 1a1b062a01
2 changed files with 101 additions and 0 deletions
+21
View File
@@ -221,3 +221,24 @@ func (ns *Namespace) JoinPath(elements ...any) (string, error) {
} }
return result, nil return result, nil
} }
// PathEscape returns the given string, applying percent-encoding to special
// characters and reserved delimiters so it can be safely used as a segment
// within a URL path.
func (ns *Namespace) PathEscape(s any) (string, error) {
ss, err := cast.ToStringE(s)
if err != nil {
return "", err
}
return url.PathEscape(ss), nil
}
// PathUnescape returns the given string, replacing all percent-encoded
// sequences with the corresponding unescaped characters.
func (ns *Namespace) PathUnescape(s any) (string, error) {
ss, err := cast.ToStringE(s)
if err != nil {
return "", err
}
return url.PathUnescape(ss)
}
+80
View File
@@ -15,6 +15,7 @@ package urls
import ( import (
"net/url" "net/url"
"regexp"
"testing" "testing"
"github.com/gohugoio/hugo/config/testconfig" "github.com/gohugoio/hugo/config/testconfig"
@@ -105,3 +106,82 @@ func TestJoinPath(t *testing.T) {
c.Assert(result, qt.Equals, test.expect) c.Assert(result, qt.Equals, test.expect)
} }
} }
func TestPathEscape(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := newNs()
tests := []struct {
name string
input any
want string
wantErr bool
errCheck string
}{
{"string", "A/b/c?d=é&f=g+h", "A%2Fb%2Fc%3Fd=%C3%A9&f=g+h", false, ""},
{"empty string", "", "", false, ""},
{"integer", 6, "6", false, ""},
{"float", 7.42, "7.42", false, ""},
{"nil", nil, "", false, ""},
{"slice", []int{}, "", true, "unable to cast"},
{"map", map[string]string{}, "", true, "unable to cast"},
{"struct", tstNoStringer{}, "", true, "unable to cast"},
}
for _, tt := range tests {
c.Run(tt.name, func(c *qt.C) {
got, err := ns.PathEscape(tt.input)
if tt.wantErr {
c.Assert(err, qt.IsNotNil, qt.Commentf("PathEscape(%v) should have failed", tt.input))
if tt.errCheck != "" {
c.Assert(err, qt.ErrorMatches, ".*"+regexp.QuoteMeta(tt.errCheck)+".*")
}
} else {
c.Assert(err, qt.IsNil)
c.Assert(got, qt.Equals, tt.want)
}
})
}
}
func TestPathUnescape(t *testing.T) {
t.Parallel()
c := qt.New(t)
ns := newNs()
tests := []struct {
name string
input any
want string
wantErr bool
errCheck string
}{
{"string", "A%2Fb%2Fc%3Fd=%C3%A9&f=g+h", "A/b/c?d=é&f=g+h", false, ""},
{"empty string", "", "", false, ""},
{"integer", 6, "6", false, ""},
{"float", 7.42, "7.42", false, ""},
{"nil", nil, "", false, ""},
{"slice", []int{}, "", true, "unable to cast"},
{"map", map[string]string{}, "", true, "unable to cast"},
{"struct", tstNoStringer{}, "", true, "unable to cast"},
{"malformed hex", "bad%g0escape", "", true, "invalid URL escape"},
{"incomplete hex", "trailing%", "", true, "invalid URL escape"},
{"single hex digit", "trail%1", "", true, "invalid URL escape"},
}
for _, tt := range tests {
c.Run(tt.name, func(c *qt.C) {
got, err := ns.PathUnescape(tt.input)
if tt.wantErr {
c.Assert(err, qt.Not(qt.IsNil), qt.Commentf("PathUnescape(%v) should have failed", tt.input))
if tt.errCheck != "" {
c.Assert(err, qt.ErrorMatches, ".*"+regexp.QuoteMeta(tt.errCheck)+".*")
}
} else {
c.Assert(err, qt.IsNil)
c.Assert(got, qt.Equals, tt.want)
}
})
}
}