///|
fn hex_digit(value : Int) -> Char {
  if value < 10 {
    Int::unsafe_to_char('0'.to_int() + value)
  } else {
    Int::unsafe_to_char('A'.to_int() + value - 10)
  }
}

///|
fn write_hex2(out : StringBuilder, value : Int) -> Unit {
  out.write_char(hex_digit((value >> 4) & 0x0f))
  out.write_char(hex_digit(value & 0x0f))
}

///|
fn write_hex4(out : StringBuilder, value : Int) -> Unit {
  out.write_char(hex_digit((value >> 12) & 0x0f))
  out.write_char(hex_digit((value >> 8) & 0x0f))
  out.write_char(hex_digit((value >> 4) & 0x0f))
  out.write_char(hex_digit(value & 0x0f))
}

///|
/// Render non-printing characters as stable ASCII. The result is safe to paste
/// into terminals and useful in audit reports.
pub fn visible(input : String) -> String {
  let out = StringBuilder::new(size_hint=input.length())
  for c in input {
    let n = c.to_int()
    if n == 0x1b {
      out.write_string("\\x1B")
    } else if n == 0x07 {
      out.write_string("\\a")
    } else if n == 0x08 {
      out.write_string("\\b")
    } else if n == 0x09 {
      out.write_string("\\t")
    } else if n == 0x0a {
      out.write_string("\\n")
    } else if n == 0x0d {
      out.write_string("\\r")
    } else if n < 0x20 || n == 0x7f || (n >= 0x80 && n <= 0x9f) {
      out.write_string("\\x")
      write_hex2(out, n & 0xff)
    } else if is_bidi_control(c) {
      out.write_string("\\u")
      write_hex4(out, n)
    } else {
      out.write_char(c)
    }
  }
  out.to_string()
}

///|
pub fn strip_bidi_controls(input : String) -> String {
  let out = StringBuilder::new(size_hint=input.length())
  for c in input {
    if !is_bidi_control(c) {
      out.write_char(c)
    }
  }
  out.to_string()
}

///|
fn json_escape_char(out : StringBuilder, c : Char) -> Unit {
  let n = c.to_int()
  if c == '"' {
    out.write_string("\\\"")
  } else if c == '\\' {
    out.write_string("\\\\")
  } else if c == '\n' {
    out.write_string("\\n")
  } else if c == '\r' {
    out.write_string("\\r")
  } else if c == '\t' {
    out.write_string("\\t")
  } else if n < 0x20 || is_bidi_control(c) {
    out.write_string("\\u")
    write_hex4(out, n)
  } else {
    out.write_char(c)
  }
}

///|
pub fn json_quote(input : String) -> String {
  let out = StringBuilder::new(size_hint=input.length() + 2)
  out.write_char('"')
  for c in input {
    json_escape_char(out, c)
  }
  out.write_char('"')
  out.to_string()
}

///|
pub fn markdown_escape(input : String) -> String {
  let out = StringBuilder::new(size_hint=input.length())
  for c in input {
    if c == '|' || c == '`' || c == '\\' {
      out.write_char('\\')
    }
    if c == '\n' || c == '\r' {
      out.write_char(' ')
    } else {
      out.write_char(c)
    }
  }
  out.to_string()
}

///|
/// GitHub workflow command escaping as documented for annotation properties.
pub fn github_property_escape(input : String) -> String {
  let out = StringBuilder::new()
  for c in input {
    if c == '%' {
      out.write_string("%25")
    } else if c == '\r' {
      out.write_string("%0D")
    } else if c == '\n' {
      out.write_string("%0A")
    } else if c == ':' {
      out.write_string("%3A")
    } else if c == ',' {
      out.write_string("%2C")
    } else {
      out.write_char(c)
    }
  }
  out.to_string()
}

///|
pub fn github_message_escape(input : String) -> String {
  let out = StringBuilder::new()
  for c in input {
    if c == '%' {
      out.write_string("%25")
    } else if c == '\r' {
      out.write_string("%0D")
    } else if c == '\n' {
      out.write_string("%0A")
    } else {
      out.write_char(c)
    }
  }
  out.to_string()
}