Skip to content

Commit 1a1b062

Browse files
jmooringbep
authored andcommitted
tpl/urls: Add PathEscape and PathUnescape functions
Closes #14209
1 parent 7a43b92 commit 1a1b062

File tree

2 files changed

+101
-0
lines changed

2 files changed

+101
-0
lines changed

‎tpl/urls/urls.go‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,24 @@ func (ns *Namespace) JoinPath(elements ...any) (string, error) {
221221
}
222222
return result, nil
223223
}
224+
225+
// PathEscape returns the given string, applying percent-encoding to special
226+
// characters and reserved delimiters so it can be safely used as a segment
227+
// within a URL path.
228+
func (ns *Namespace) PathEscape(s any) (string, error) {
229+
ss, err := cast.ToStringE(s)
230+
if err != nil {
231+
return "", err
232+
}
233+
return url.PathEscape(ss), nil
234+
}
235+
236+
// PathUnescape returns the given string, replacing all percent-encoded
237+
// sequences with the corresponding unescaped characters.
238+
func (ns *Namespace) PathUnescape(s any) (string, error) {
239+
ss, err := cast.ToStringE(s)
240+
if err != nil {
241+
return "", err
242+
}
243+
return url.PathUnescape(ss)
244+
}

‎tpl/urls/urls_test.go‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package urls
1515

1616
import (
1717
"net/url"
18+
"regexp"
1819
"testing"
1920

2021
"github.com/gohugoio/hugo/config/testconfig"
@@ -105,3 +106,82 @@ func TestJoinPath(t *testing.T) {
105106
c.Assert(result, qt.Equals, test.expect)
106107
}
107108
}
109+
110+
func TestPathEscape(t *testing.T) {
111+
t.Parallel()
112+
c := qt.New(t)
113+
ns := newNs()
114+
115+
tests := []struct {
116+
name string
117+
input any
118+
want string
119+
wantErr bool
120+
errCheck string
121+
}{
122+
{"string", "A/b/c?d=é&f=g+h", "A%2Fb%2Fc%3Fd=%C3%A9&f=g+h", false, ""},
123+
{"empty string", "", "", false, ""},
124+
{"integer", 6, "6", false, ""},
125+
{"float", 7.42, "7.42", false, ""},
126+
{"nil", nil, "", false, ""},
127+
{"slice", []int{}, "", true, "unable to cast"},
128+
{"map", map[string]string{}, "", true, "unable to cast"},
129+
{"struct", tstNoStringer{}, "", true, "unable to cast"},
130+
}
131+
132+
for _, tt := range tests {
133+
c.Run(tt.name, func(c *qt.C) {
134+
got, err := ns.PathEscape(tt.input)
135+
if tt.wantErr {
136+
c.Assert(err, qt.IsNotNil, qt.Commentf("PathEscape(%v) should have failed", tt.input))
137+
if tt.errCheck != "" {
138+
c.Assert(err, qt.ErrorMatches, ".*"+regexp.QuoteMeta(tt.errCheck)+".*")
139+
}
140+
} else {
141+
c.Assert(err, qt.IsNil)
142+
c.Assert(got, qt.Equals, tt.want)
143+
}
144+
})
145+
}
146+
}
147+
148+
func TestPathUnescape(t *testing.T) {
149+
t.Parallel()
150+
c := qt.New(t)
151+
ns := newNs()
152+
153+
tests := []struct {
154+
name string
155+
input any
156+
want string
157+
wantErr bool
158+
errCheck string
159+
}{
160+
{"string", "A%2Fb%2Fc%3Fd=%C3%A9&f=g+h", "A/b/c?d=é&f=g+h", false, ""},
161+
{"empty string", "", "", false, ""},
162+
{"integer", 6, "6", false, ""},
163+
{"float", 7.42, "7.42", false, ""},
164+
{"nil", nil, "", false, ""},
165+
{"slice", []int{}, "", true, "unable to cast"},
166+
{"map", map[string]string{}, "", true, "unable to cast"},
167+
{"struct", tstNoStringer{}, "", true, "unable to cast"},
168+
{"malformed hex", "bad%g0escape", "", true, "invalid URL escape"},
169+
{"incomplete hex", "trailing%", "", true, "invalid URL escape"},
170+
{"single hex digit", "trail%1", "", true, "invalid URL escape"},
171+
}
172+
173+
for _, tt := range tests {
174+
c.Run(tt.name, func(c *qt.C) {
175+
got, err := ns.PathUnescape(tt.input)
176+
if tt.wantErr {
177+
c.Assert(err, qt.Not(qt.IsNil), qt.Commentf("PathUnescape(%v) should have failed", tt.input))
178+
if tt.errCheck != "" {
179+
c.Assert(err, qt.ErrorMatches, ".*"+regexp.QuoteMeta(tt.errCheck)+".*")
180+
}
181+
} else {
182+
c.Assert(err, qt.IsNil)
183+
c.Assert(got, qt.Equals, tt.want)
184+
}
185+
})
186+
}
187+
}

0 commit comments

Comments
 (0)