///|
fn is_url_escape_target(code : UInt16) -> Bool {
  code == '#' ||
  code == '$' ||
  code == '%' ||
  code == '&' ||
  code == '~' ||
  code == '_' ||
  code == '^' ||
  code == '{' ||
  code == '}'
}

///|
fn unescape_url(text : String) -> String {
  let builder = StringBuilder()
  let mut segment_start = 0
  for index = 0; index < text.length(); {
    if text[index] == '\\' &&
      index + 1 < text.length() &&
      is_url_escape_target(text[index + 1]) {
      builder.write_string(
        text.unsafe_substring(start=segment_start, end=index),
      )
      builder.write_string(
        text.unsafe_substring(start=index + 1, end=index + 2),
      )
      segment_start = index + 2
      continue index + 2
    }
    continue index + 1
  } nobreak {
    builder.write_string(
      text.unsafe_substring(start=segment_start, end=text.length()),
    )
    builder.to_string()
  }
}

///|
fn Parser::parse_url_group(
  self : Parser,
  optional : Bool,
) -> ParseNode? raise ParseFailure {
  self.gullet.lexer.set_catcode("%", 13)
  self.gullet.lexer.set_catcode("~", 12)
  let parsed : Result[Token?, ParseFailure] = capture_parse_result(() => {
    self.parse_string_group(optional)
  })
  self.gullet.lexer.set_catcode("%", 14)
  self.gullet.lexer.set_catcode("~", 13)
  match parsed {
    Err(err) => raise err
    Ok(None) => None
    Ok(Some(token)) => Some(Url(mode=self.mode, url=unescape_url(token.text)))
  }
}