///|
/// Parse a complete XML document into a [`Document`] tree.
///
/// The input must contain exactly one root element; character data is only
/// allowed inside it (top-level whitespace is ignored). A mismatched or
/// unexpected tag raises [`XmlError::Syntax`].
///
/// # Example
/// ```mbt check
/// test {
/// let doc = @xml.Document::read_from("hi")
/// inspect(doc.root.name, content="a")
/// }
/// ```
pub fn Document::read_from(input : String) -> Document raise XmlError {
let reader = Reader::new(input)
let stack : Array[Element] = []
let mut root : Element? = None
for ;; {
match reader.next() {
Eof => break
Start(name~, attrs~) => {
if stack.length() == 0 && root is Some(_) {
raise Syntax(
pos=reader.position(),
msg="unexpected second root element '\{name}'",
)
}
stack.push({ name, attrs, children: [] })
}
Empty(name~, attrs~) =>
if stack.length() == 0 {
guard root is None else {
raise Syntax(
pos=reader.position(),
msg="unexpected second root element '\{name}'",
)
}
root = Some({ name, attrs, children: [] })
} else {
stack[stack.length() - 1].children.push(
Element({ name, attrs, children: [] }),
)
}
End(name) => {
guard stack.length() > 0 else {
raise Syntax(
pos=reader.position(),
msg="unexpected end tag \{name}>",
)
}
let el = stack.pop().unwrap()
if el.name != name {
raise Syntax(
pos=reader.position(),
msg="end tag \{name}> does not match start tag <\{el.name}>",
)
}
if stack.length() == 0 {
guard root is None else {
raise Syntax(
pos=reader.position(),
msg="unexpected second root element '\{name}'",
)
}
root = Some(el)
} else {
stack[stack.length() - 1].children.push(Element(el))
}
}
Text(s) =>
if stack.length() == 0 {
guard is_all_ws(s) else {
raise Syntax(
pos=reader.position(),
msg="character data outside the root element",
)
}
} else {
append_text(stack[stack.length() - 1].children, s)
}
CData(s) => {
guard stack.length() > 0 else {
raise Syntax(
pos=reader.position(),
msg="CDATA section outside the root element",
)
}
stack[stack.length() - 1].children.push(CData(s))
}
// The reader currently skips comments; kept for exhaustiveness.
Comment(_) => ()
}
}
guard root is Some(doc_root) else {
raise Syntax(pos=0, msg="no root element found")
}
{ root: doc_root }
}
///|
/// Append character data, merging it with an adjacent `Text` node the way
/// common DOM implementations normalize text.
fn append_text(nodes : Array[Node], s : String) -> Unit {
let last = nodes.length() - 1
match nodes.get(last) {
Some(Text(prev)) => nodes[last] = Text(prev + s)
_ => nodes.push(Text(s))
}
}
///|
fn is_all_ws(s : String) -> Bool {
for c in s {
if !is_ws(c.to_int()) {
return false
}
}
true
}