///|
/// XML has no null or UID representation; these fail instead of losing types.
pub fn encode_xml(
  value : Value,
  pretty~ : Bool = true,
  limits~ : Limits = Limits::default(),
) -> String raise PlistError {
  validate(value, limits~)
  let out = StringBuilder()
  let bytes = Ref(0)
  fn put(text : String) -> Unit raise PlistError {
    let n = @utf8.encode(text[:]).length()
    if n > limits.max_bytes - bytes.val {
      raise Limit("XML output byte budget")
    }
    bytes.val += n
    out.write_string(text)
  }
  fn indent(depth : Int32) -> Unit raise PlistError {
    if pretty {
      for i = 0; i < depth; i = i + 1 {
        put("  ")
      }
    }
  }
  fn newline() -> Unit raise PlistError {
    if pretty {
      put("\n")
    }
  }
  fn scalar(
    tag : String,
    text : String,
    depth : Int32,
  ) -> Unit raise PlistError {
    indent(depth)
    put("<" + tag + ">")
    put(text)
    put("")
    newline()
  }
  fn write(v : Value, depth : Int32) -> Unit raise PlistError {
    match v {
      Null | Uid(_) =>
        raise Unsupported("null and UID are binary-only plist types")
      Boolean(b) => {
        indent(depth)
        put(if b { "" } else { "" })
        newline()
      }
      Integer(n) => scalar("integer", n.to_string(), depth)
      Unsigned(n) => scalar("integer", n.to_string(), depth)
      Real(d) => {
        let text = if d.is_nan() {
          "nan"
        } else if d == 0.0 && d.reinterpret_as_uint64() == 0x8000000000000000UL {
          "-0.0"
        } else if d.is_pos_inf() {
          "inf"
        } else if d.is_neg_inf() {
          "-inf"
        } else {
          d.to_string()
        }
        scalar("real", text, depth)
      }
      Text(text) => scalar("string", escape_xml_text(text), depth)
      Data(data) => scalar("data", encode_base64(data), depth)
      Date(d) => scalar("date", format_date(d), depth)
      List(items) => {
        indent(depth)
        put("")
        newline()
        for item in items {
          write(item, depth + 1)
        }
        indent(depth)
        put("")
        newline()
      }
      Dict(entries) => {
        indent(depth)
        put("")
        newline()
        for (key, item) in entries {
          scalar("key", escape_xml_text(key), depth + 1)
          write(item, depth + 1)
        }
        indent(depth)
        put("")
        newline()
      }
    }
  }
  put("")
  newline()
  put("")
  newline()
  write(value, 1)
  put("")
  newline()
  out.to_string()
}