Code change:
Looks good except for minor details and potentially a bug (encoding vs decoding). I'd also like to see the spec enforce a consistent form of percent encoding (lower vs upper)
I am sceptical wrt "use the URI percent encoder". The reason being: percent encoding rules depend on what part of the URI needs encoding (and many APIs do not get that) calling the API for a single character multiple times usually requires converting a char to a string, and that might not be good for perf if we really care about that
I've removed the reliance on URI for percent encoding; was clean as NAME said it would be. Regarding case -- NAME RFC4648 Section 8 is explicitly case-insensitive. Given that and the discussion in the group yesterday, I've left this case-insensitive. Say if you still disagree.
NAME , I was suggesting an inline definition. The fact that uppercase is base16 would just be an observation (and not a particularly useful one). Lowercase tends to be the default mode in several places.
NAME it wasn't at all clear that's what you were suggesting. I'll make an attempt.
Looking at it, I think I prefer referring to 4648. This this addresses the original issue, I'll merge; if someone wants to propose an alternate way to specify this, please feel free to open a PR.
As currently defined, the two serialized DisplayStrings %"foo\"bla%22bar" and %"foo%22bla\"bar" are semantically identical. We should either use %-escapes or \-escapes, mixing them this way is asking for smuggling-attacks.
I think probably -escapes; will work up a PR.
I don't believe that relying on a different RFC for the hex encoding makes this much better (so I sort of disagree with Martin's proposal). Just define the escaping exactly inline, and then we can choose upper/lower.

Related discussion:
text: Given an ASCII string as input_string, return a string of Unicode characters. input_string is modified to remove the parsed value. <del> If the first character of input_string is not "%", fail parsing. </del> <ins> If the first two characters of input_string are not "%" followed by DQUOTE, fail parsing. </ins> <del> Discard the first character of input_string. </del> <ins> Discard the first two characters of input_string. </ins> <del> Let parsed_string be the result of running Parsing a String (parse-string) with input_string. </del> <ins> Let byte_array be an empty byte array. </ins> <del> Let byte_array be the result of applying ASCII encoding to input_string. </del> <ins> While input_string is not empty: </ins> <del> For each sigil_byte in byte_array which is %25 ("%"): </del> <ins> Let char be the result of consuming the first character of input_string. If char is in the range %x00-1f or %x7f-ff (i.e., it is not in VCHAR or SP), fail parsing. If char is "%": Let octet_hex be the result of consuming two characters from input_string.  If there are not two characters, fail parsing. </ins> <del> Let octet_hex be the two bytes after sigil_byte in byte_string. If there are not two bytes, fail parsing. </del> <ins> Let octet be the result of hex decoding octet_hex (Section 8 of RFC4648), in a case-insensitive fashion.  If decoding fails, fail parsing. </ins> <del> Let octet be the result of decoding octet_hex as hexidecimal, in a case-insensitive fashion. </del> <ins> Append octet to byte_array. </ins> <del> Replace sigil_byte and octet_hex in byte_array with octet. </del> <ins> If char is DQUOTE: </ins> <del> Let unicode_string be the result of decoding byte_array as a UTF-8 string UTF8.  Fail parsing if decoding fails. </del> <ins> Let unicode_string be the result of decoding byte_array as a UTF-8 string UTF8.  Fail parsing if decoding fails. </ins> <del> Return unicode_string. </del> <ins> Return unicode_string. Otherwise, if char is not "%" or DQUOTE: Let byte be the result of applying ASCII encoding to char. Append byte to byte_array. Reached the end of input_string without finding a closing DQUOTE; fail parsing. </ins> 5.