3

What would be the most readable way to parse a URL query string into a { 'param': 'value' } map in XSLT/XPath 3.0?

Note: this is the inverse function of the one described in Building a URL query string from a map of parameters with XPath.

Update: I neglected to mention that the function should support multi-value parameters such as a=1&a=2, and ideally parse them as an xs:string* sequence.

3 Answers3

2
declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
let $querystring := "a=1&b=2&c=3"
return 
  ( tokenize($querystring, "&") 
    ! (let $param := tokenize(., "=") 
      return map:entry($param[1], $param[2]) ) 
  ) => map:merge()

In order to support multiple values, you could can apply the $options parameter specifying what to do with duplicates:

declare namespace map = "http://www.w3.org/2005/xpath-functions/map";
let $querystring := "a=1&b=2&a=3"
return 
  ( tokenize($querystring, "&") 
    ! (let $param := tokenize(., "=") 
      return map:entry($param[1], $param[2]) ) 
  ) => map:merge(map:entry('duplicates', 'combine'))
Mads Hansen
  • 63,927
  • 12
  • 112
  • 147
  • The first thing that comes to mind - what about duplicate param names? :) The version of the inverse URI building function I'm using supports query params as `map(xs:string, xs:string*)`, meaning that one param can have multiple values. So in this case would it be possible to parse them as a `xs:string*` sequence? – Martynas Jusevičius Aug 26 '21 at 21:18
  • 1
    Yeah, if handling multiple param with the same name, then would need to have a map available, and append values as you are putting new entries. `map:put($map, $key, (map:get($map, $key), $value) )` – Mads Hansen Aug 26 '21 at 21:21
1

2 more answers by Christian Grün:

let $querystring := "a=1&b=2&a=3"
return map:merge(
  for $query in tokenize($querystring, "&")
  let $param := tokenize($query, "=")
  return map:entry(head($param), tail($param)),
  map { 'duplicates': 'combine' }
)

One more solution (if you don’t wanna use the for clause):

let $querystring := "a=1&b=2&a=3"
return map:merge(
  tokenize($querystring, "&")
  ! array { tokenize(., "=") }
  ! map:entry(.(1), .(2)),
  map { 'duplicates': 'combine' }
)
0

let's see - substring to get ? and strip any trailing #... fragment identifier then tokenize on [&;] (actually [;&] to get name=value pairs, which are separated by & or (less commonly) ; then substring-before and after, or tokenize again, to get before and after the = (name value) then uridecode the name and the value separately

let $query := substring-after($uri, '?'),
     $beforefrag := substring-before($query || '#', '#')
return
  tokenize($beforefrag, '[;&]')
  ! [substring-before(., '='), substring-after(., '=') ]
  ! map:entry(local:uridecode(.(1), local:uridecode(.(2))

might give us a sequene of map entries, and we can use map:merge on that.

If we know our input is plausibly encoded, we could use

declare function local:uridecode($input as xs:string?) as xs:string?
{
   parse-xml-fragment(replace($input, '=(..)', '&x$1;'))
};

but a better version would just replace the two hex characters. It's really unfortunate we don't have a version of replace() that takes a function argument to be called for each matching subexpression, ala perl's e flag.```

and of course you can put that into

(...) => map:merge()
barefootliam
  • 619
  • 3
  • 7