///|
fn is_ascii_letter(c : Int) -> Bool {
  (c >= 65 && c <= 90) || (c >= 97 && c <= 122)
}

///|
fn is_ascii_digit(c : Int) -> Bool {
  c >= 48 && c <= 57
}

///|
fn is_ascii_alnum(c : Int) -> Bool {
  is_ascii_letter(c) || is_ascii_digit(c)
}

///|
fn is_ascii_hexdigit(c : Int) -> Bool {
  is_ascii_digit(c) || (c >= 97 && c <= 102) || (c >= 65 && c <= 70)
}

///|
fn is_token_char(c : Int) -> Bool {
  is_ascii_alnum(c) || c == 45 || c == 46 || c == 95
}

///|
fn is_base64url(c : Int) -> Bool {
  is_ascii_alnum(c) || c == 45 || c == 95 || c == 61
}

///|
fn is_email_local(c : Int) -> Bool {
  is_ascii_alnum(c) ||
  c == 33 ||
  c == 35 ||
  c == 36 ||
  c == 37 ||
  c == 38 ||
  c == 39 ||
  c == 42 ||
  c == 43 ||
  c == 45 ||
  c == 46 ||
  c == 47 ||
  c == 63 ||
  c == 94 ||
  c == 95 ||
  c == 96 ||
  c == 123 ||
  c == 124 ||
  c == 125 ||
  c == 126
}

///|
/// Character-wise prefix test with no intermediate allocation; this sits on
/// the hottest scanning path.
fn starts_at(text : String, at : Int, prefix : String) -> Bool {
  if at < 0 || at + prefix.length() > text.length() {
    return false
  }
  for k = 0; k < prefix.length(); k = k + 1 {
    if text[at + k] != prefix[k] {
      return false
    }
  }
  true
}

///|
fn ascii_lower(text : String) -> String {
  let out = StringBuilder()
  for i = 0; i < text.length(); i = i + 1 {
    let c = text[i].to_int()
    out.write_char(
      Int::unsafe_to_char(if c >= 65 && c <= 90 { c + 32 } else { c }),
    )
  }
  out.to_string()
}

///|
fn trim_value_end(text : String, start : Int) -> Int {
  let mut end = start
  while end < text.length() {
    let c = text[end].to_int()
    if c == 32 ||
      c == 9 ||
      c == 10 ||
      c == 13 ||
      c == 44 ||
      c == 59 ||
      c == 34 ||
      c == 39 ||
      c == 41 ||
      c == 125 ||
      c == 93 {
      break
    }
    end += 1
  }
  end
}