Skip to content

XML (std/xml) ​

milo
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 of XmlNodes. Reach for it to read a feed, a config file or an API response whole.
  • XmlReader is a pull parser: each next() returns one XmlEvent (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 ​

milo
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 ​

milo
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.

InputResult
The five predefined entities and &#N; / &#xH;decoded
HTML named entities (&nbsp;), 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 encodingEncoding
mismatched end tags, two root elements, a duplicate attributeMismatchedTag / Syntax
elements nested more than 512 deep, in Xml.parseTooDeep (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 ​

milo
pub struct Xml

Xml: whole-document parsing.

Xml.parse ​

milo
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 ​

milo
pub struct XmlAttr

XmlAttr: 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 ​

milo
pub enum XmlError

XmlError: 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 & and ;.
  • 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 ​

milo
fn XmlError.message(self: &XmlError): string

One-line description, ending with the byte offset.

XmlError.offset ​

milo
fn XmlError.offset(self: &XmlError): i64

The byte offset into the input where the error was found.

XmlEvent ​

milo
pub enum XmlEvent

XmlEvent: 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 ​

milo
pub struct XmlNode

XmlNode: 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 ​

milo
fn XmlNode.attr(self: &XmlNode, name: &string): Option<string>

The value of attribute name (matched exactly, prefix included), or None.

XmlNode.child ​

milo
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 ​

milo
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 ​

milo
fn XmlNode.textContent(self: &XmlNode): string

All character data inside this element, descendants included, in document order (this element's own tail is not part of it). Not trimmed.

XmlPi ​

milo
pub struct XmlPi

XmlPi: 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 ​

milo
pub struct XmlReader

XmlReader: a pull parser over a document it owns.

Fields: src: string, scan: Scan.

XmlReader.new ​

milo
fn XmlReader.new(text: string): XmlReader

A reader over text. Nothing is checked until the first next().

XmlReader.next ​

milo
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 ​

milo
pub struct XmlStart

XmlStart: a start tag. name keeps any namespace prefix ("atom:link").

Fields: name: string, attrs: Vec<XmlAttr>.