 
 PRIVATE>
 
+: query-decode ( str -- decoded )
+    "+" split "%20" join url-decode ;
+
+: query>assoc ( query -- assoc )
+    dup [
+        "&;" split <linked-hash> [
+            [
+                [ "=" split1 [ dup [ query-decode ] when ] bi@ swap ] dip
+                add-query-param
+            ] curry each
+        ] keep
+    ] when ;
+
+: assoc>query ( assoc -- str )
+    [
+        [
+            [ url-encode-full ] dip [
+                dup array? [ 1array ] unless
+                [ url-encode-full "=" glue , ] with each
+            ] [ , ] if*
+        ] assoc-each
+    ] { } make "&" join ;
+
 : escape-uri-component-char? ( ch -- ? )
-    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.!~*'()" member? not ; inline
+    {
+        [ letter? ]
+        [ LETTER? ]
+        [ digit? ]
+        [ "-_.!~*'()" member? ]
+    } 1|| not ; foldable
 
 : encode-uri-component ( str -- str' )
     [


: decode-uri ( str -- decoded )
    [ 0 swap [ escape-uri-char? ] decode-uri-iter ] "" make utf8 decode ;

: url-decode ( str -- decoded )
    [ 0 swap url-decode-iter ] "" make utf8 decode ;

: query-decode ( str -- decoded )
    "+" split "%20" join url-decode ;

: query>assoc ( query -- assoc )
    dup [
        "&;" split <linked-hash> [
            [
                [ "=" split1 [ dup [ query-decode ] when ] bi@ swap ] dip
                add-query-param
            ] curry each
        ] keep
    ] when ;

: assoc>query ( assoc -- str )
    [
        [
            [ url-encode-full ] dip [
                dup array? [ 1array ] unless
                [ url-encode-full "=" glue , ] with each
            ] [ , ] if*
        ] assoc-each
    ] { } make "&" join ;

    2dup length 2 - >= [
        2drop
    ] [
        [ 1 + dup 2 + ] dip <slice> hex> [ , ] when*
    ] if ;

: url-decode-iter ( index str -- )
