///|
fn xml_char(n : Int32) -> Bool {
  n == 9 ||
  n == 10 ||
  n == 13 ||
  (n >= 32 && n <= 0xd7ff) ||
  (n >= 0xe000 && n <= 0xfffd) ||
  (n >= 0x10000 && n <= 0x10ffff)
}

///|
pub fn escape_xml_text(text : String) -> String raise PlistError {
  let out = StringBuilder()
  for c in text {
    if !xml_char(c.to_int()) {
      raise Invalid("character forbidden by XML 1.0")
    }
    match c {
      '&' => out.write_string("&")
      '<' => out.write_string("<")
      '>' => out.write_string(">")
      '\r' => out.write_string("
")
      _ => out.write_char(c)
    }
  }
  out.to_string()
}

///|
pub fn unescape_xml_text(text : String) -> String raise PlistError {
  let text = text
    .replace_all(old="\r\n", new="\n")
    .replace_all(old="\r", new="\n")
  let out = StringBuilder()
  let mut pos = 0
  while pos < text.length() {
    if text[pos] == 38 {
      let start = pos + 1
      pos = start
      while pos < text.length() && text[pos] != 59 {
        pos += 1
      }
      if pos >= text.length() {
        raise Invalid("unterminated XML entity")
      }
      let name = text[start:pos].to_owned()
      let code = match name {
        "amp" => 38
        "lt" => 60
        "gt" => 62
        "apos" => 39
        "quot" => 34
        _ => {
          if !name.has_prefix("#") {
            raise Invalid("unknown XML entity")
          }
          let hex = name.has_prefix("#x")
          let from = if hex { 2 } else { 1 }
          let base = if hex { 16 } else { 10 }
          if name.length() <= from {
            raise Invalid("empty character reference")
          }
          let mut n = 0
          for i = from; i < name.length(); i = i + 1 {
            let c = name[i].to_int()
            let d = if c >= 48 && c <= 57 {
              c - 48
            } else if hex && c >= 65 && c <= 70 {
              c - 55
            } else if hex && c >= 97 && c <= 102 {
              c - 87
            } else {
              -1
            }
            if d < 0 || n > (0x10ffff - d) / base {
              raise Invalid("invalid character reference")
            }
            n = n * base + d
          }
          n
        }
      }
      if !xml_char(code) {
        raise Invalid("XML character reference out of range")
      }
      match code.to_char() {
        Some(c) => out.write_char(c)
        None => raise Invalid("XML surrogate reference")
      }
      pos += 1
    } else {
      let next = match text.get_char(pos) {
        Some(c) => c
        None => raise Invalid("invalid XML Unicode")
      }
      if !xml_char(next.to_int()) {
        raise Invalid("XML character out of range")
      }
      if next == '<' {
        raise Invalid("unescaped less-than in XML text")
      }
      out.write_char(next)
      pos += if next.to_int() > 0xffff { 2 } else { 1 }
    }
  }
  if text.contains("]]>") {
    raise Invalid("CDATA terminator in ordinary text")
  }
  out.to_string()
}