]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
tpl/urls: Add PathEscape and PathUnescape functions
authorJoe Mooring <joe.mooring@veriphor.com>
Tue, 25 Nov 2025 18:28:35 +0000 (10:28 -0800)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 25 Nov 2025 22:53:04 +0000 (23:53 +0100)
Closes #14209

tpl/urls/urls.go
tpl/urls/urls_test.go

index 8bd7514ec280dcc5d7885172747816e2cd335dd7..5db6f266d33988e923e4b274550023dfb0fd5405 100644 (file)
@@ -221,3 +221,24 @@ func (ns *Namespace) JoinPath(elements ...any) (string, error) {
        }
        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)
+}
index ae68edcf212e43931b16a7fff8ecfdc5fbc6fdca..7319a4ea951e144b0242a67bf2e5d53ac7e2f165 100644 (file)
@@ -15,6 +15,7 @@ package urls
 
 import (
        "net/url"
+       "regexp"
        "testing"
 
        "github.com/gohugoio/hugo/config/testconfig"
@@ -105,3 +106,82 @@ func TestJoinPath(t *testing.T) {
                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)
+                       }
+               })
+       }
+}