///|
/// Remove characters that are not legal in XML 1.0 text. Tab, LF, and CR are
/// kept; all other characters below 0x20 are dropped. (Silently stripped
/// because such characters cannot be represented in XML at all.)
fn is_valid_xml_char(c : Char) -> Bool {
  let cp = c.to_int()
  c == '\t' ||
  c == '\n' ||
  c == '\r' ||
  (cp >= 0x20 && cp <= 0xD7FF) ||
  (cp >= 0xE000 && cp <= 0xFFFD) ||
  (cp >= 0x10000 && cp <= 0x10FFFF)
}

///|
fn strip_invalid_xml_chars(s : String) -> String {
  let buf = StringBuilder::new()
  for c in s {
    if is_valid_xml_char(c) {
      buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
/// Escape XML special characters (`&`, `<`, `>`, `"`, `'`) to their entity
/// references.
fn escape_xml(s : String) -> String {
  let buf = StringBuilder::new()
  for c in s {
    if is_valid_xml_char(c) {
      match c {
        '&' => buf.write_string("&")
        '<' => buf.write_string("<")
        '>' => buf.write_string(">")
        '"' => buf.write_string(""")
        '\'' => buf.write_string("'")
        _ => buf.write_char(c)
      }
    }
  }
  buf.to_string()
}

///|
fn is_xml_name_start_char(c : Char) -> Bool {
  c.is_ascii_alphabetic() || c == '_'
}

///|
fn is_xml_name_char(c : Char) -> Bool {
  c.is_ascii_alphabetic() ||
  c.is_ascii_digit() ||
  c == '-' ||
  c == '_' ||
  c == '.'
}

///|
fn validate_xml_name_segment(
  name : String,
  err : (String) -> FeedError,
) -> Unit raise FeedError {
  guard name is [c, ..] && is_xml_name_start_char(c) else { raise err(name) }
  for c in name {
    guard is_xml_name_char(c) else { raise err(name) }
  }
}

///|
fn validate_xml_name(
  name : String,
  err : (String) -> FeedError,
) -> Unit raise FeedError {
  if name.to_lower().has_prefix("xml") {
    raise err(name)
  }
  let mut colon_count = 0
  for c in name {
    if c == ':' {
      colon_count += 1
    }
  }
  guard colon_count <= 1 else { raise err(name) }
  match name[:].find(":") {
    Some(i) => {
      guard i > 0 && i + 1 < name.length() else { raise err(name) }
      let sv = name[:]
      validate_xml_name_segment(sv.sub(start=0, end=i).to_owned(), fn(_) {
        err(name)
      })
      validate_xml_name_segment(sv.sub(start=i + 1).to_owned(), fn(_) {
        err(name)
      })
    }
    None => validate_xml_name_segment(name, err)
  }
}

///|
fn validate_tag(tag : String) -> Unit raise FeedError {
  validate_xml_name(tag, fn(t) { InvalidTag(t) })
}

///|
fn validate_attr_name(name : String) -> Unit raise FeedError {
  validate_xml_name(name, fn(n) { InvalidAttrName(n) })
}

///|
/// Validate that `name` is a valid XML NCName (no colon allowed).
/// Used for namespace prefixes, which must not contain `:`.
fn validate_ncname(
  name : String,
  err : (String) -> FeedError,
) -> Unit raise FeedError {
  guard !name.contains(":") else { raise err(name) }
  validate_xml_name_segment(name, err)
  if name.to_lower().has_prefix("xml") {
    raise err(name)
  }
}

///|
fn write_attrs(buf : StringBuilder, attrs : Array[(String, String)]) -> Unit {
  for elem in attrs {
    let (k, v) = elem
    buf.write_stringview(" ")
    buf.write_stringview(k)
    buf.write_stringview("=\"")
    buf.write_stringview(escape_xml(v))
    buf.write_stringview("\"")
  }
}

///|
fn write_namespace_attrs(
  buf : StringBuilder,
  namespaces : Array[(String, String)],
) -> Unit {
  for ns in namespaces {
    let (name, uri) = ns
    buf.write_stringview(" xmlns:")
    buf.write_stringview(name)
    buf.write_stringview("=\"")
    buf.write_stringview(escape_xml(uri))
    buf.write_stringview("\"")
  }
}

///|
/// Write XML namespace declarations using the writer's buffer.
fn XmlWriter::write_namespace_attrs(
  self : XmlWriter,
  namespaces : Array[(String, String)],
) -> Unit {
  write_namespace_attrs(self.buf, namespaces)
}

///|
/// A stateful XML writer that tracks indentation depth and produces either
/// pretty-printed or minified output.
priv struct XmlWriter {
  buf : StringBuilder
  /// Spaces per indentation level; `0` means minified (no whitespace added).
  indent_size : Int
  /// Current nesting depth.
  mut depth : Int
}

///|
/// Create an `XmlWriter` with 2 spaces per indent level when `indent` is
/// `true`, or no indentation when `false`.
fn XmlWriter::new(indent : Bool) -> XmlWriter {
  {
    buf: StringBuilder::new(),
    indent_size: if indent {
      2
    } else {
      0
    },
    depth: 0,
  }
}

///|
/// Create an `XmlWriter` with a custom number of spaces per indent level.
/// When `indent` is `false`, `size` is ignored and output is minified.
fn XmlWriter::new_with_size(indent : Bool, size : Int) -> XmlWriter {
  {
    buf: StringBuilder::new(),
    indent_size: if indent {
      size
    } else {
      0
    },
    depth: 0,
  }
}

///|
/// Append a raw string to the output buffer without any transformation.
fn XmlWriter::write_raw(self : XmlWriter, s : String) -> Unit {
  self.buf.write_string(s)
}

///|
/// Write a newline character if pretty-printing is enabled.
fn XmlWriter::newline(self : XmlWriter) -> Unit {
  if self.indent_size > 0 {
    self.buf.write_char('\n')
  }
}

///|
/// Write the current indentation padding (depth × indent_size spaces).
fn XmlWriter::pad(self : XmlWriter) -> Unit {
  if self.indent_size > 0 {
    let total = self.depth * self.indent_size
    for _ in 0.. Unit {
  self.depth = self.depth + 1
}

///|
/// Decrease the indentation depth by one level.
fn XmlWriter::dedent(self : XmlWriter) -> Unit {
  self.depth = self.depth - 1
}

///|
/// Return the accumulated output as a string.
fn XmlWriter::to_string(self : XmlWriter) -> String {
  self.buf.to_string()
}

///|
/// Write `escaped(content)` at the current depth, followed by a
/// newline.
fn XmlWriter::leaf(self : XmlWriter, tag : String, content : String) -> Unit {
  self.pad()
  self.buf.write_char('<')
  self.buf.write_string(tag)
  self.buf.write_char('>')
  self.buf.write_string(escape_xml(content))
  self.buf.write_string("')
  self.newline()
}

///|
/// Write `` at the current depth, stripping
/// illegal XML characters and escaping any embedded `]]>` sequences.
fn XmlWriter::leaf_cdata(
  self : XmlWriter,
  tag : String,
  content : String,
) -> Unit {
  self.pad()
  self.buf.write_char('<')
  self.buf.write_string(tag)
  self.buf.write_string(">",
      new="]]]]>",
    ),
  )
  self.buf.write_string("]]>')
  self.newline()
}

///|
/// Write `escaped(content)` at the current depth.
fn XmlWriter::leaf_with_attrs(
  self : XmlWriter,
  tag : String,
  attrs : Array[(String, String)],
  content : String,
) -> Unit {
  self.pad()
  self.buf.write_char('<')
  self.buf.write_string(tag)
  write_attrs(self.buf, attrs)
  self.buf.write_char('>')
  self.buf.write_string(escape_xml(content))
  self.buf.write_string("')
  self.newline()
}

///|
/// Write `` at the current depth.
fn XmlWriter::leaf_cdata_with_attrs(
  self : XmlWriter,
  tag : String,
  attrs : Array[(String, String)],
  content : String,
) -> Unit {
  self.pad()
  self.buf.write_char('<')
  self.buf.write_string(tag)
  write_attrs(self.buf, attrs)
  self.buf.write_string(">",
      new="]]]]>",
    ),
  )
  self.buf.write_string("]]>')
  self.newline()
}

///|
/// Write `` (self-closing element) at the current depth.
fn XmlWriter::self_closing(
  self : XmlWriter,
  tag : String,
  attrs : Array[(String, String)],
) -> Unit {
  self.pad()
  self.buf.write_char('<')
  self.buf.write_string(tag)
  write_attrs(self.buf, attrs)
  self.buf.write_string("/>")
  self.newline()
}

///|
/// Write the opening tag `` at the current depth, then a newline,
/// then increment the depth so children are indented one level deeper.
fn XmlWriter::open(
  self : XmlWriter,
  tag : String,
  attrs : Array[(String, String)],
) -> Unit {
  self.pad()
  self.buf.write_char('<')
  self.buf.write_string(tag)
  write_attrs(self.buf, attrs)
  self.buf.write_char('>')
  self.newline()
  self.indent()
}

///|
/// Decrement the depth then write `` at the new (parent) depth,
/// followed by a newline.
fn XmlWriter::close(self : XmlWriter, tag : String) -> Unit {
  self.dedent()
  self.pad()
  self.buf.write_string("')
  self.newline()
}

///|
/// Write a leaf element only when `value` is `Some`; do nothing for `None`.
fn XmlWriter::opt_leaf(self : XmlWriter, tag : String, value : String?) -> Unit {
  match value {
    Some(v) => self.leaf(tag, v)
    None => ()
  }
}

///|
/// Write a CDATA leaf element only when `value` is `Some`.
fn XmlWriter::opt_leaf_cdata(
  self : XmlWriter,
  tag : String,
  value : String?,
) -> Unit {
  match value {
    Some(v) => self.leaf_cdata(tag, v)
    None => ()
  }
}

///|
/// Write an integer leaf element only when `value` is `Some`.
fn XmlWriter::opt_int_leaf(
  self : XmlWriter,
  tag : String,
  value : Int?,
) -> Unit {
  match value {
    Some(v) => self.leaf(tag, v.to_string())
    None => ()
  }
}