XML (std/xml)
from "std/xml" import { Xml, XmlNode, XmlReader, XmlEvent, XmlError }A strict XML 1.0 parser in pure Milo, in two layers over one tokenizer:
Xml.parse(text)builds an owned tree ofXmlNodes. Reach for it to read a feed, a config file or an API response whole.XmlReaderis a pull parser: eachnext()returns oneXmlEvent(start tag with attributes, end tag, text, CDATA, comment, processing instruction) and no tree is built. Use it for large documents or when only a few elements matter.
Reading a feed
from "std/xml" import { Xml }
fn main(): i32 {
let text = "<rss version=\"2.0\"><channel><title>News</title><item><title>First</title><dc:creator>ann</dc:creator></item></channel></rss>"
let root = Xml.parse(text)!
let channel = root.child("channel")!
print(channel.childText("title") ?? "")
for item in channel.children { // borrows: no copy
if item.name == "item" {
print($"{item.childText("title") ?? ""} by {item.childText("dc:creator") ?? ""}")
}
}
return 0
}An XmlNode has name, attrs, element children, and character data in the ElementTree layout: text is what comes after the start tag and before the first child, and each child's tail is what follows its end tag. In <p>a<b>b</b>c</p>, p.text is "a", b.text is "b" and b.tail is "c", so mixed content keeps its order. textContent() concatenates all of it. CDATA merges into the text; comments and processing instructions are not kept in the tree.
child(name) copies the subtree it returns, which is convenient for one step of navigation. Iterating children with for borrows and copies nothing.
Pulling events
from "std/xml" import { XmlReader, XmlEvent }
fn main(): i32 {
var r = XmlReader.new("<a x=\"1\">hi<b/></a>")
while true {
let Ok(next) = r.next() else {
return 1
}
let Some(ev) = next else {
break
}
match ev {
XmlEvent.StartTag(s) => print($"start {s.name} ({s.attrs.len} attrs)")
XmlEvent.EndTag(name) => print($"end {name}")
XmlEvent.Text(t) => print($"text {t}")
_ => {}
}
}
return 0
}A self-closing <b/> reports StartTag then EndTag, so the two spellings need no separate handling.
What it accepts
The parser is strict: the first problem is an XmlError carrying the byte offset, never a guess.
| Input | Result |
|---|---|
The five predefined entities and &#N; / &#xH; | decoded |
HTML named entities ( ), a bare & | UnknownEntity / Syntax, as XML requires |
<!DOCTYPE x SYSTEM "x.dtd"> | skipped |
<!DOCTYPE x [ ... ]> (an internal subset) | DoctypeSubset: entity declarations live there, and expanding them is the "billion laughs" attack |
UTF-8 (a BOM is fine), encoding="UTF-8" or "US-ASCII" | accepted |
| invalid UTF-8, a UTF-16 BOM, any other declared encoding | Encoding |
| mismatched end tags, two root elements, a duplicate attribute | MismatchedTag / Syntax |
elements nested more than 512 deep, in Xml.parse | TooDeep (XmlReader has no limit) |
Line endings are normalized as XML specifies: CRLF and a lone CR read as LF, and a literal tab or newline inside an attribute value reads as a space.
Namespaces are not resolved. Names keep their prefix exactly as written ("dc:creator", "atom:link"), and xmlns / xmlns:p declarations are ordinary attributes. Matching prefixed names literally is what feed readers do in practice; to resolve a prefix to its URI, read the xmlns:* attribute yourself.
API reference
Xml
pub struct XmlXml: whole-document parsing.
Xml.parse
fn Xml.parse(text: &string): Result<XmlNode, XmlError>Parse text into a tree and return its root element. Strict: the first well-formedness, encoding or entity problem is the error (see XmlError and the module comment for what is and is not accepted). Elements may nest at most 512 deep (XmlError.TooDeep); use XmlReader for deeper documents.
XmlAttr
pub struct XmlAttrXmlAttr: one attribute, value entity-decoded and whitespace-normalized as XML requires (a literal tab, LF or CR in the value reads as a space).
Fields: name: string, value: string.
XmlError
pub enum XmlErrorXmlError: why a document was rejected. The first field of every variant is the byte offset into the input where the problem was found.
Variants:
Syntax(i64, string): Malformed markup. Payload: offset and what was wrong.MismatchedTag(i64, string, string): An end tag that does not close the open element. Payload: offset, the name that was open, the name the end tag gave.UnknownEntity(i64, string):&name;where name is not one of XML's five predefined entities, or a numeric reference to a code point XML does not allow. Payload: offset and the reference text between∧.DoctypeSubset(i64): A<!DOCTYPE>with an internal subset, refused rather than expanded (see the module comment). Payload: offset of the<!DOCTYPE.Encoding(i64, string): Input that is not UTF-8: an invalid byte sequence, a UTF-16 byte order mark, or a declared encoding other than UTF-8/US-ASCII. Payload: offset and detail.TooDeep(i64): Elements nested deeper than Xml.parse allows (512). Payload: offset of the start tag that went over. XmlReader has no depth limit.
XmlError.message
fn XmlError.message(self: &XmlError): stringOne-line description, ending with the byte offset.
XmlError.offset
fn XmlError.offset(self: &XmlError): i64The byte offset into the input where the error was found.
XmlEvent
pub enum XmlEventXmlEvent: one step of a pull parse.
Variants:
StartTag(XmlStart):<name attr="v">. A self-closing<name/>reports StartTag then EndTag, so a consumer never needs to treat the two spellings differently.EndTag(string):</name>, already checked against the matching StartTag.Text(string): Character data, entity references decoded, CRLF and lone CR read as LF. May be whitespace only (indentation between elements is text inside an element). Text outside the root element is whitespace and is never reported.CData(string): The contents of<![CDATA[ ... ]]>, verbatim.Comment(string): The contents of<!-- ... -->.Pi(XmlPi):<?target data?>.
XmlNode
pub struct XmlNodeXmlNode: an element of a parsed document, as an owned tree.
Character data uses the ElementTree layout so mixed content keeps its order with only element children in children: text is the character data after the start tag and before the first child element, and each child's tail is the character data after that child's end tag and before the next sibling. In <p>a<b>b</b>c</p>, p.text is "a", b.text is "b" and b.tail is "c". CDATA is merged into the surrounding text; comments and processing instructions are dropped. Iterate children with for to visit elements without copying.
Fields: name: string, attrs: Vec<XmlAttr>, children: Vec<XmlNode>, text: string, tail: string.
XmlNode.attr
fn XmlNode.attr(self: &XmlNode, name: &string): Option<string>The value of attribute name (matched exactly, prefix included), or None.
XmlNode.child
fn XmlNode.child(self: &XmlNode, name: &string): Option<XmlNode>A copy of the first child element named name, or None. This deep-copies the subtree; to walk a large document, iterate children with for, which borrows.
XmlNode.childText
fn XmlNode.childText(self: &XmlNode, name: &string): Option<string>The textContent of the first child element named name, or None when there is no such child. The text is not trimmed.
XmlNode.textContent
fn XmlNode.textContent(self: &XmlNode): stringAll character data inside this element, descendants included, in document order (this element's own tail is not part of it). Not trimmed.
XmlPi
pub struct XmlPiXmlPi: a processing instruction <?target data?>. The XML declaration (<?xml version="1.0"?>) is not one and is never reported.
Fields: target: string, data: string.
XmlReader
pub struct XmlReaderXmlReader: a pull parser over a document it owns.
Fields: src: string, scan: Scan.
XmlReader.new
fn XmlReader.new(text: string): XmlReaderA reader over text. Nothing is checked until the first next().
XmlReader.next
fn XmlReader.next(self: &mut XmlReader): Result<Option<XmlEvent>, XmlError>The next event, or None once the root element has closed and only whitespace, comments or PIs remain. An error is final: calling next() again after one is unspecified.
XmlStart
pub struct XmlStartXmlStart: a start tag. name keeps any namespace prefix ("atom:link").
Fields: name: string, attrs: Vec<XmlAttr>.