///|
/// 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.buf.write_string(tag)
self.buf.write_char('>')
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.buf.write_string(tag)
self.buf.write_char('>')
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.buf.write_string(tag)
self.buf.write_char('>')
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.buf.write_string(tag)
self.buf.write_char('>')
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.buf.write_string(tag)
self.buf.write_char('>')
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 => ()
}
}