///| Port of `vscode/src/vs/base/common/uri.ts` (checked-in submodule): the `URI` class, percent encoder/decoder, and `uriToFsPath`.
///
/// Reviewed deviations from the pinned Monaco implementation:
/// - D3: the RFC3986 appendix-B regex and `_rEncodedAsHex` become hand-rolled
///   scanners with identical accept/capture semantics.
/// - D4: JS `encodeURIComponent` is reimplemented as UTF-8 percent
///   transcoding (uppercase hex); a lone surrogate aborts the way JS throws
///   `URIError`. JS `decodeURIComponent` is `@percent.decode`, which raises on
///   exactly the inputs that make the JS builtin throw, so
///   `decode_uri_component_graceful` keeps the raw text.
/// - D5: Monaco's abstract `URI` + caching subclass `Uri` collapse into one
///   struct with `mut` cache fields; `Eq`/`Hash`/`Debug` are manual over the
///   five components so the caches never affect identity. Monaco re-derives
///   when a cached value is falsy (`''`); the `Option` caches keep it — the
///   recomputed value would be identical.
/// - D6: `[UriError]` throws become the `UriError` suberror raised by
///   `Uri::parse` / `Uri::from` / `Uri::with_`; `Uri::file` also validates
///   but its inputs cannot fail validation, so it stays non-raising;
///   `Uri::join_path` on a URI without path aborts (programmer error,
///   matching Monaco's unconditional throw).
/// - D7: `toJSON`/`revive`/`UriState` marshalling is deferred — the remote
///   protocol serializes `to_string()` strings.

///|
/// Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986.
/// This class is a simple parser which creates the basic component parts
/// (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation
/// and encoding.
///
/// ```txt
///       foo://example.com:8042/over/there?name=ferret#nose
///       \_/   \______________/\_________/ \_________/ \__/
///        |           |            |            |        |
///     scheme     authority       path        query   fragment
///        |   _____________________|__
///       / \ /                        \
///       urn:example:animal:ferret:nose
/// ```
///
/// Components are stored **decoded** (uri.ts:125-146). The two `mut` fields
/// are the `_formatted`/`_fsPath` lazy caches of the `Uri` subclass
/// (uri.ts:455-477, D5).
pub struct Uri {
  /// scheme is the 'http' part of 'http://www.example.com/some/path?query#fragment'.
  /// The part before the first colon.
  scheme : String
  /// authority is the 'www.example.com' part of 'http://www.example.com/some/path?query#fragment'.
  /// The part between the first double slashes and the next slash.
  authority : String
  /// path is the '/some/path' part of 'http://www.example.com/some/path?query#fragment'.
  path : String
  /// query is the 'query' part of 'http://www.example.com/some/path?query#fragment'.
  query : String
  /// fragment is the 'fragment' part of 'http://www.example.com/some/path?query#fragment'.
  fragment : String
  priv mut formatted_cache : String?
  priv mut fs_path_cache : String?
}

///|
/// Component equality (D5: the caches are identity-irrelevant).
pub impl Eq for Uri with fn equal(self, other) {
  self.scheme == other.scheme &&
  self.authority == other.authority &&
  self.path == other.path &&
  self.query == other.query &&
  self.fragment == other.fragment
}

///|
/// Mirrors the `debug.description` label `URI(${this.toString()})`
/// (uri.ts:421) without touching the `_formatted` cache.
pub impl Debug for Uri with fn to_repr(self) {
  Repr("URI(\{as_formatted(self, false)})")
}

///|
/// `_schemePattern` (uri.ts:11): `^\w[\w\d+.-]*$` where `\w` is
/// `[A-Za-z0-9_]`.
fn is_scheme_word_char(code : Int32) -> Bool {
  (code >= 'a'.to_int() && code <= 'z'.to_int()) ||
  (code >= 'A'.to_int() && code <= 'Z'.to_int()) ||
  (code >= '0'.to_int() && code <= '9'.to_int()) ||
  code == '_'.to_int()
}

///|
fn scheme_matches_pattern(scheme : String) -> Bool {
  if scheme.length() == 0 {
    return false
  }
  if !is_scheme_word_char(scheme[0].to_int()) {
    return false
  }
  for i in 1.. String {
  let mut count = 0
  let mut first = ""
  let mut first_index = -1
  let mut i = 0
  while i < scheme.length() {
    let unit = scheme[i].to_int()
    let mut width = 1
    let mut ch = ""
    if is_high_surrogate(unit) &&
      i + 1 < scheme.length() &&
      is_low_surrogate(scheme[i + 1].to_int()) {
      width = 2
      ch = js_slice(scheme, i, end=i + 2)
    } else {
      ch = js_slice(scheme, i, end=i + 1)
    }
    let code = unit
    let is_legal = width == 1 &&
      (
        is_scheme_word_char(code) ||
        code == '+'.to_int() ||
        code == '.'.to_int() ||
        code == '-'.to_int()
      )
    if !is_legal {
      if first_index == -1 {
        first = ch
        first_index = i
      }
      count += 1
    }
    i += width
  }
  if count > 0 {
    " Found '\{first}' at index \{first_index} (\{count} total)"
  } else {
    ""
  }
}

///|
/// `_validateUri` (uri.ts:15).
fn validate_uri(ret : Uri, strict : Bool) -> Unit raise UriError {
  // scheme, must be set
  if ret.scheme == "" && strict {
    raise UriError(
      "[UriError]: Scheme is missing: {scheme: \"\", authority: \"\{ret.authority}\", path: \"\{ret.path}\", query: \"\{ret.query}\", fragment: \"\{ret.fragment}\"}",
    )
  }

  // scheme, https://tools.ietf.org/html/rfc3986#section-3.1
  // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
  if ret.scheme != "" && !scheme_matches_pattern(ret.scheme) {
    raise UriError(
      "[UriError]: Scheme contains illegal characters.\{scheme_illegal_detail(ret.scheme)} (len:\{ret.scheme.length()})",
    )
  }

  // path, http://tools.ietf.org/html/rfc3986#section-3.3
  // If a URI contains an authority component, then the path component
  // must either be empty or begin with a slash ("/") character.  If a URI
  // does not contain an authority component, then the path cannot begin
  // with two slash characters ("//").
  if ret.path != "" {
    if ret.authority != "" {
      if char_code_at(ret.path, 0) != char_forward_slash {
        raise UriError(
          "[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash (\"/\") character",
        )
      }
    } else if char_code_at(ret.path, 0) == char_forward_slash &&
      char_code_at(ret.path, 1) == char_forward_slash {
      raise UriError(
        "[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters (\"//\")",
      )
    }
  }
}

///|
/// `_schemeFix` (uri.ts:54): for a while uris *without* schemes were allowed
/// and this is the migration for them — empty scheme + non-strict falls back
/// to the file-scheme.
fn scheme_fix(scheme : String, strict : Bool) -> String {
  if scheme == "" && !strict {
    return "file"
  }
  scheme
}

///|
/// `_referenceResolution` (uri.ts:62) — implements a bit of
/// https://tools.ietf.org/html/rfc3986#section-5: the slash-character is the
/// 'default base' as constructing URIs relative to other URIs is unsupported.
fn reference_resolution(scheme : String, path : String) -> String {
  match scheme {
    "https" | "http" | "file" =>
      if path == "" {
        "/"
      } else if char_code_at(path, 0) != char_forward_slash {
        "/\{path}"
      } else {
        path
      }
    _ => path
  }
}

///|
/// The validating string-overload constructor (uri.ts:161-180):
/// schemeFix → referenceResolution → validateUri.
fn Uri::new_(
  scheme : String,
  authority : String,
  path : String,
  query : String,
  fragment : String,
  strict : Bool,
) -> Uri raise UriError {
  let scheme = scheme_fix(scheme, strict)
  let path = reference_resolution(scheme, path)
  let uri : Uri = {
    scheme,
    authority,
    path,
    query,
    fragment,
    formatted_cache: None,
    fs_path_cache: None,
  }
  validate_uri(uri, strict)
  uri
}

///|
/// Hand-rolled scanner for `_regexp` (uri.ts:84), the RFC3986 appendix-B
/// component split `^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?`
/// (D3). Returns the raw (undecoded) components.
fn parse_components(value : String) -> (String, String, String, String, String) {
  let len = value.length()

  // scheme: `([^:/?#]+?):` — at least one class char before the first ':'.
  let scheme_end = for i in 0.. 0 {
    (js_slice(value, 0, end=scheme_end), scheme_end + 1)
  } else {
    ("", 0)
  }

  // authority: `(\/\/([^/?#]*))?`
  let (authority, pos) = if char_code_at(value, pos) == char_forward_slash &&
    char_code_at(value, pos + 1) == char_forward_slash {
    let start = pos + 2
    let end = for j in start.. Uri raise UriError {
  let (scheme, authority, path, query, fragment) = parse_components(value)
  Uri::new_(
    scheme,
    percent_decode(authority),
    percent_decode(path),
    percent_decode(query),
    percent_decode(fragment),
    strict,
  )
}

///|
/// `URI.file` (uri.ts:307): creates a new URI from a file system path, e.g.
/// `c:\my\files`, `/usr/home`, or `\\server\share\some\path`.
///
/// The *difference* between `Uri::parse` and `Uri::file` is that the latter
/// treats the argument as path, not as stringified-uri: the path might
/// contain characters that are interpreted (`#` and `?`).
///
/// Validation cannot fail on this shape (scheme `file`, slash-rooted path),
/// so unlike `parse`/`from` this stays non-raising (D6).
pub fn Uri::file(path : String) -> Uri {
  let mut path = path
  let mut authority = ""

  // normalize to fwd-slashes on windows,
  // on other systems bwd-slashes are valid
  // filename character, eg /f\oo/ba\r.txt
  if platform_is_win32() {
    path = path.replace_all(old="\\", new="/")
  }

  // check for authority as used in UNC shares
  // or use the path as given
  if char_code_at(path, 0) == char_forward_slash &&
    char_code_at(path, 1) == char_forward_slash {
    let idx = index_of_char(path, char_forward_slash, 2)
    if idx == -1 {
      authority = js_slice(path, 2)
      path = "/"
    } else {
      authority = js_slice(path, 2, end=idx)
      path = js_slice(path, idx)
      if path == "" {
        path = "/"
      }
    }
  }
  try! Uri::new_("file", authority, path, "", "", false)
}

///|
/// `URI.from` (uri.ts:341): creates a new URI from uri components with
/// validation — use for untrusted components from storage, user input,
/// command arguments etc. Unless `strict` the scheme defaults to `file`.
pub fn Uri::from(
  scheme~ : String,
  authority~ : String = "",
  path~ : String = "",
  query~ : String = "",
  fragment~ : String = "",
  strict~ : Bool = false,
) -> Uri raise UriError {
  Uri::new_(scheme, authority, path, query, fragment, strict)
}

///|
/// `URI.with` (uri.ts:218). Monaco's undefined/null distinction maps to
/// optional named args: omitted = keep, `Some("")` = clear (Monaco maps null
/// to `''` anyway, uri.ts:227-249, D6). Returns `self` unchanged when every
/// component matches (uri.ts:251).
fn Uri::with_impl(
  self : Uri,
  scheme? : String,
  authority? : String,
  path? : String,
  query? : String,
  fragment? : String,
) -> Uri raise UriError {
  let scheme = scheme.unwrap_or(self.scheme)
  let authority = authority.unwrap_or(self.authority)
  let path = path.unwrap_or(self.path)
  let query = query.unwrap_or(self.query)
  let fragment = fragment.unwrap_or(self.fragment)
  if scheme == self.scheme &&
    authority == self.authority &&
    path == self.path &&
    query == self.query &&
    fragment == self.fragment {
    return self
  }
  Uri::new_(scheme, authority, path, query, fragment, false)
}

///|
/// `URI.joinPath` (uri.ts:360): join a URI path with path fragments and
/// normalize the resulting path. Aborts on a URI without path, matching
/// Monaco's unconditional throw (programmer error, D6).
fn Uri::join_path_impl(uri : Uri, path_fragment : Array[String]) -> Uri {
  if uri.path == "" {
    abort(
      "[UriError]: cannot call joinPath on URI without path: \{uri.to_string()}",
    )
  }
  let new_path = if platform_is_win32() && uri.scheme == "file" {
    Uri::file(Win32::join([uri_to_fs_path(uri, true)] + path_fragment)).path
  } else {
    Posix::join([uri.path] + path_fragment)
  }
  // The joined path keeps the validated shape of `uri.path`, so the
  // re-validation inside `with_` cannot fail (D6).
  try! uri.with_(path=new_path)
}

///|
/// `toString(skipEncoding)` (uri.ts:386, caching override uri.ts:467-477):
/// guaranteed to round-trip through `Uri::parse` into an equal URI. The
/// result is percent-encoded (mostly ignoring scheme-specific rules) and is
/// meant for externalization or transport, not display.
pub fn Uri::to_string(self : Uri, skip_encoding~ : Bool = false) -> String {
  if !skip_encoding {
    match self.formatted_cache {
      Some(formatted) => formatted
      None => {
        let formatted = as_formatted(self, false)
        self.formatted_cache = Some(formatted)
        formatted
      }
    }
  } else {
    // we don't cache that
    as_formatted(self, true)
  }
}

///|
/// The `fsPath` getter (uri.ts:209, caching override uri.ts:460-465):
/// platform-specific file system path for this URI. Handles UNC paths
/// (`file://server/share` → `//server/share` resp. `\\server\share`) and
/// normalizes windows drive letters to lower-case. Does not validate the
/// path and does not look at the scheme.
fn Uri::fs_path_impl(self : Uri) -> String {
  match self.fs_path_cache {
    Some(fs_path) => fs_path
    None => {
      let fs_path = uri_to_fs_path(self, false)
      self.fs_path_cache = Some(fs_path)
      fs_path
    }
  }
}

///|
/// `encodeTable` (uri.ts:517) — reserved characters
/// https://tools.ietf.org/html/rfc3986#section-2.2.
fn encode_table(code : Int32) -> String? {
  match code {
    58 => Some("%3A") // gen-delims: colon
    47 => Some("%2F") // slash
    63 => Some("%3F") // question mark
    35 => Some("%23") // hash
    91 => Some("%5B") // open square bracket
    93 => Some("%5D") // close square bracket
    64 => Some("%40") // at sign
    33 => Some("%21") // sub-delims: exclamation mark
    36 => Some("%24") // dollar sign
    38 => Some("%26") // ampersand
    39 => Some("%27") // single quote
    40 => Some("%28") // open paren
    41 => Some("%29") // close paren
    42 => Some("%2A") // asterisk
    43 => Some("%2B") // plus
    44 => Some("%2C") // comma
    59 => Some("%3B") // semicolon
    61 => Some("%3D") // equals
    32 => Some("%20") // space
    _ => None
  }
}

///|
let hex_digits_upper : String = "0123456789ABCDEF"

///|
fn write_percent_byte(sb : StringBuilder, byte : Int32) -> Unit {
  sb.write_char('%')
  sb.write_char(Int32::unsafe_to_char(hex_digits_upper[byte >> 4].to_int()))
  sb.write_char(Int32::unsafe_to_char(hex_digits_upper[byte & 0xF].to_int()))
}

///|
/// The delegated-span replacement for JS `encodeURIComponent` (uri.ts:564,
/// 599; D4): UTF-8 percent-encode every code point in `[start, end)` of `s`
/// with uppercase hex. Chars reaching this path are exactly those outside
/// the fast-path sets, and `encodeURIComponent` escapes all of them. A lone
/// surrogate aborts the way JS throws `URIError`.
fn write_native_encoded(
  sb : StringBuilder,
  s : String,
  start : Int32,
  end : Int32,
) -> Unit {
  let mut i = start
  while i < end {
    let unit = s[i].to_int()
    let mut code_point = unit
    if is_high_surrogate(unit) {
      if i + 1 < end && is_low_surrogate(s[i + 1].to_int()) {
        code_point = 0x10000 +
          ((unit - 0xD800) << 10) +
          (s[i + 1].to_int() - 0xDC00)
        i += 1
      } else {
        abort("URIError: URI malformed")
      }
    } else if is_low_surrogate(unit) {
      abort("URIError: URI malformed")
    }
    if code_point < 0x80 {
      write_percent_byte(sb, code_point)
    } else if code_point < 0x800 {
      write_percent_byte(sb, 0xC0 | (code_point >> 6))
      write_percent_byte(sb, 0x80 | (code_point & 0x3F))
    } else if code_point < 0x10000 {
      write_percent_byte(sb, 0xE0 | (code_point >> 12))
      write_percent_byte(sb, 0x80 | ((code_point >> 6) & 0x3F))
      write_percent_byte(sb, 0x80 | (code_point & 0x3F))
    } else {
      write_percent_byte(sb, 0xF0 | (code_point >> 18))
      write_percent_byte(sb, 0x80 | ((code_point >> 12) & 0x3F))
      write_percent_byte(sb, 0x80 | ((code_point >> 6) & 0x3F))
      write_percent_byte(sb, 0x80 | (code_point & 0x3F))
    }
    i += 1
  }
}

///|
/// `encodeURIComponentFast` (uri.ts:541): unreserved fast path (`isPath`
/// keeps `/`, `isAuthority` keeps `[ ] :`), table escapes, delayed
/// native-encode spans, and allocate-only-on-change (`None` result means the
/// input passed through untouched).
fn encode_uri_component_fast(
  uri_component : String,
  is_path : Bool,
  is_authority : Bool,
) -> String {
  let mut res : StringBuilder? = None
  let mut native_encode_pos = -1
  for pos in 0..= 'a'.to_int() && code <= 'z'.to_int()) ||
      (code >= 'A'.to_int() && code <= 'Z'.to_int()) ||
      (code >= '0'.to_int() && code <= '9'.to_int()) ||
      code == '-'.to_int() ||
      code == '.'.to_int() ||
      code == '_'.to_int() ||
      code == '~'.to_int() ||
      (is_path && code == char_forward_slash) ||
      (is_authority && code == '['.to_int()) ||
      (is_authority && code == ']'.to_int()) ||
      (is_authority && code == char_colon) {
      // check if we are delaying native encode
      if native_encode_pos != -1 {
        // `None` is unreachable: a delayed span implies an allocation.
        if res is Some(sb) {
          write_native_encoded(sb, uri_component, native_encode_pos, pos)
        }
        native_encode_pos = -1
      }
      // check if we write into a new string (by default we try to return the param)
      if res is Some(sb) {
        sb.write_char(Int32::unsafe_to_char(code))
      }
    } else {
      // encoding needed, we need to allocate a new string
      if res is None {
        let sb = StringBuilder()
        sb.write_string(js_slice(uri_component, 0, end=pos))
        res = Some(sb)
      }

      // check with default table first
      match encode_table(code) {
        Some(escaped) => {
          // check if we are delaying native encode
          if native_encode_pos != -1 {
            if res is Some(sb) {
              write_native_encoded(sb, uri_component, native_encode_pos, pos)
            }
            native_encode_pos = -1
          }
          // append escaped variant to result
          if res is Some(sb) {
            sb.write_string(escaped)
          }
        }
        None =>
          // use native encode only when needed
          if native_encode_pos == -1 {
            native_encode_pos = pos
          }
      }
    }
  }
  if native_encode_pos != -1 {
    if res is Some(sb) {
      write_native_encoded(
        sb,
        uri_component,
        native_encode_pos,
        uri_component.length(),
      )
    }
  }
  match res {
    Some(sb) => sb.to_string()
    None => uri_component
  }
}

///|
/// `encodeURIComponentMinimal` (uri.ts:605): only `#` and `?` are escaped.
/// Literal runs are copied as substring chunks (boundaries sit on the ASCII
/// `#`/`?`, so surrogate pairs are never split).
fn encode_uri_component_minimal(path : String) -> String {
  let mut res : StringBuilder? = None
  let mut chunk_start = 0
  for pos in 0.. {
          let sb = StringBuilder()
          res = Some(sb)
          sb
        }
        Some(sb) => sb
      }
      sb.write_string(js_slice(path, chunk_start, end=pos))
      // `None` is unreachable: `#` and `?` are in the table.
      if encode_table(code) is Some(escaped) {
        sb.write_string(escaped)
      }
      chunk_start = pos + 1
    }
  }
  match res {
    None => path
    Some(sb) => {
      sb.write_string(js_slice(path, chunk_start))
      sb.to_string()
    }
  }
}

///|
/// `uriToFsPath` (uri.ts:626): computes `fsPath` for the given uri — UNC
/// form for file-URIs with authority, lower-cased drive letters unless
/// `keep_drive_letter_casing`, and backslashes on windows.
fn uri_to_fs_path_impl(uri : Uri, keep_drive_letter_casing : Bool) -> String {
  let mut value = ""
  if uri.authority != "" && uri.path.length() > 1 && uri.scheme == "file" {
    // unc path: file://shares/c$/far/boo
    value = "//\{uri.authority}\{uri.path}"
  } else if char_code_at(uri.path, 0) == char_forward_slash &&
    is_windows_device_root(char_code_at(uri.path, 1)) &&
    char_code_at(uri.path, 2) == char_colon {
    if !keep_drive_letter_casing {
      // windows drive letter: file:///c:/far/boo
      value = to_lower_js(js_slice(uri.path, 1, end=2)) + js_slice(uri.path, 2)
    } else {
      value = js_slice(uri.path, 1)
    }
  } else {
    // other path
    value = uri.path
  }
  if platform_is_win32() {
    value = value.replace_all(old="/", new="\\")
  }
  value
}

///|
/// `_asFormatted` (uri.ts:656): create the external version of a uri.
fn as_formatted(uri : Uri, skip_encoding : Bool) -> String {
  let encoder : (String, Bool, Bool) -> String = if !skip_encoding {
    encode_uri_component_fast
  } else {
    (component, _is_path, _is_authority) => {
      encode_uri_component_minimal(component)
    }
  }
  let mut res = ""
  let scheme = uri.scheme
  let mut authority = uri.authority
  let mut path = uri.path
  let query = uri.query
  let fragment = uri.fragment
  if scheme != "" {
    res = "\{res}\{scheme}:"
  }
  if authority != "" || scheme == "file" {
    res = "\{res}//"
  }
  if authority != "" {
    let mut idx = index_of_char(authority, '@'.to_int(), 0)
    if idx != -1 {
      // @
      let userinfo = js_slice(authority, 0, end=idx)
      authority = js_slice(authority, idx + 1)
      idx = last_index_of_char(userinfo, char_colon)
      if idx == -1 {
        res += encoder(userinfo, false, false)
      } else {
        // :@
        res += encoder(js_slice(userinfo, 0, end=idx), false, false)
        res = "\{res}:"
        res += encoder(js_slice(userinfo, idx + 1), false, true)
      }
      res = "\{res}@"
    }
    authority = to_lower_js(authority)
    idx = last_index_of_char(authority, char_colon)
    if idx == -1 {
      res += encoder(authority, false, true)
    } else {
      // :
      res += encoder(js_slice(authority, 0, end=idx), false, true)
      res += js_slice(authority, idx)
    }
  }
  if path != "" {
    // lower-case windows drive letters in /C:/fff or C:/fff
    if path.length() >= 3 &&
      char_code_at(path, 0) == char_forward_slash &&
      char_code_at(path, 2) == char_colon {
      let code = char_code_at(path, 1)
      if code >= 'A'.to_int() && code <= 'Z'.to_int() {
        path = "/\{Int::unsafe_to_char(code + 32).to_string()}:\{js_slice(path, 3)}" // "/c:".length === 3
      }
    } else if path.length() >= 2 && char_code_at(path, 1) == char_colon {
      let code = char_code_at(path, 0)
      if code >= 'A'.to_int() && code <= 'Z'.to_int() {
        path = "\{Int::unsafe_to_char(code + 32).to_string()}:\{js_slice(path, 2)}" // "/c:".length === 3
      }
    }
    // encode the rest of the path
    res += encoder(path, true, false)
  }
  if query != "" {
    res = "\{res}?\{encoder(query, false, false)}"
  }
  if fragment != "" {
    let encoded_fragment = if !skip_encoding {
      encode_uri_component_fast(fragment, false, false)
    } else {
      fragment
    }
    res = "\{res}#\{encoded_fragment}"
  }
  res
}

///|
fn last_index_of_char(s : String, code : Int32) -> Int32 {
  for i in s.length()>..0 {
    if s[i].to_int() == code {
      return i
    }
  }
  -1
}

///|
/// `decodeURIComponentGraceful` (uri.ts:728): decode; on failure keep the
/// raw 3-char prefix and retry the rest recursively.
fn decode_uri_component_graceful(str : String) -> String {
  @percent.decode(str) catch {
    Malformed(_) =>
      if str.length() > 3 {
        js_slice(str, 0, end=3) +
        decode_uri_component_graceful(js_slice(str, 3))
      } else {
        str
      }
  }
}

///|
/// `%XY` where X and Y are `[0-9A-Za-z]` — the (deliberately loose)
/// `_rEncodedAsHex` character class (uri.ts:740).
fn is_encoded_as_hex_at(str : String, pos : Int32) -> Bool {
  fn hexish(code : Int32) -> Bool {
    (code >= '0'.to_int() && code <= '9'.to_int()) ||
    (code >= 'A'.to_int() && code <= 'Z'.to_int()) ||
    (code >= 'a'.to_int() && code <= 'z'.to_int())
  }

  pos + 2 < str.length() &&
  str[pos].to_int() == '%'.to_int() &&
  hexish(str[pos + 1].to_int()) &&
  hexish(str[pos + 2].to_int())
}

///|
/// `percentDecode` (uri.ts:742): pass through when no `(%[0-9A-Za-z]{2})+`
/// run exists; otherwise replace each maximal run gracefully (scanner
/// replaces the regex, D3).
fn percent_decode(str : String) -> String {
  let len = str.length()
  let found = for pos in 0..