xml

    A document-buffered pull XML parser for MoonBit

    xml
    parser
    Download zip
    Author
    Version
    0.4.1
    License
    Apache-2.0
    Last updated
    3 hours ago
    Downloads
    24K

    Dependencies

    #xml

    A document-buffered pull XML parser for MoonBit, inspired by quick-xml.

    #Features

    • Pull-parser model - Read XML events one at a time (like StAX in Java)
    • Document-buffered input - Constructors load the full input, then callers pull events one at a time
    • Multi-backend - Works on wasm, wasm-gc, js, and native
    • XML 1.0 + Namespaces 1.0 - Unicode names plus namespace-aware events
    • Source-aware parsing - Authored ranges for events and attributes, plus contextual spans for errors

    #Usage

    // From string
    let xml = "<root><item id=\"1\">Hello</item></root>"
    let reader = @xml.Reader::from_string(xml)

    // From file
    let reader = @xml.Reader::from_file("document.xml")

    while true {
    let event = reader.read_event()
    match event.kind {
    Start(elem) => println("Start: \{elem.name}")
    End(name) => println("End: \{name}")
    Text(content) => println("Text: \{content}")
    Eof => break
    _ => continue
    }
    }

    #Namespace-aware parsing

    Use NamespaceReader when callers need namespace URI, prefix, and local-name information. The original Reader remains available for raw qualified names and namespace declaration attributes.

    let reader = @xml.NamespaceReader::from_string(
    "<p:root xmlns:p=\"urn:example\" p:id=\"1\"/>",
    )

    match reader.read_event().kind {
    Empty(element) => {
    println(element.name.local_name) // root
    println(element.name.namespace_uri) // Some("urn:example")
    }
    _ => ()
    }

    Namespace declarations are exposed through NamespaceElement::namespace_declarations and are not included in its normal attributes. Default namespaces apply to element names but not to unprefixed attribute names.

    #Checked writing

    Writer validates XML names, characters, delimiter sequences, and document structure as output is added. Its write methods and to_string raise WriterError instead of returning malformed XML; to_string also requires exactly one complete root element.

    #Source locations

    Every event returned by Reader::read_event includes its authored source range. Event and attribute spans are half-open; offsets count UTF-16 code units, so they can slice the original MoonBit String directly.

    let input = "<root id='a&amp;b'/>"
    let reader = @xml.Reader::from_string(input)
    let parsed = reader.read_event()
    let authored = input[parsed.span.start.offset:parsed.span.end.offset]
    assert_eq(authored, input)
    guard parsed.kind is Empty(element) else { abort("expected empty element") }
    assert_eq(element.attributes[0].value, "a&b")

    Each XmlAttribute contains the whole attribute span plus separate name and unquoted value spans. Parse failures raise XmlError::At, which contains an XmlErrorKind and a relevant authored source span. Syntax failures normally cover input consumed while detecting the error, while an unclosed-element error points to the unmatched opening tag. Events produced by entity expansion point to the authored entity reference.

    #Event Types

    Event contains an EventKind and a SourceSpan. The EventKind variants are:

    EventDescription
    Start(XmlElement)Opening tag <name>
    End(String)Closing tag </name>
    Empty(XmlElement)Self-closing tag <name/>
    Text(String)Text content (entities decoded)
    CData(String)CDATA section <![CDATA[...]]>
    Comment(String)Comment <!-- ... -->
    PI(target, data)Processing instruction <?target data?>
    Decl(version, encoding, standalone)XML declaration
    DocType(String)DOCTYPE declaration
    EofEnd of document

    #W3C Conformance

    This library is tested against the W3C XML Conformance Test Suite, using libxml2 (lxml) as the reference parser.

    Current status: 817/817 tests passing

    CategoryTestsDescription
    Valid (with events)448Parser produces correct event sequence
    Valid (error-only)6Parser does not error on valid XML
    Not-well-formed281Parser correctly rejects malformed XML
    Unit tests82Reader, writer, escape, namespace, source spans, conformance tests

    Coverage:
    • XML 1.0 (James Clark xmltest)
    • XML 1.0 Errata 2nd/3rd/4th edition
    • Namespaces 1.0
    • Sun Microsystems tests
    • IBM XML 1.0 tests

    #Running Conformance Tests

    # Download the W3C test suite curl -L -o xmlts.tar.gz "https://www.w3.org/XML/Test/xmlts20130923.tar.gz" tar -xzf xmlts.tar.gz && mv xmlconf . && rm xmlts.tar.gz # Run tests moon test

    #Regenerating Tests

    # Requires: libxml2 (xmllint), lxml (pip install lxml) python3 scripts/generate_conformance_tests.py

    #Excluded Tests

    The following test categories are skipped:
    • External entity references (require file I/O)
    • XML 1.1 documents (we only support XML 1.0)
    • DTD validation tests (invalid type)

    #Limitations

    • Non-validating - Does not validate against DTD
    • UTF-8 only - Other encodings not supported
    • XML 1.0 only - XML 1.1 not supported
    • Bounded entity expansion - Internal entities are limited to 32 nesting levels and 262,144 expanded characters per document

    External entity declarations are parsed, but their contents are not resolved. Referencing an external entity raises an error.

    #License

    Apache-2.0

    WriterError

    pub(all) suberror WriterError {
    InvalidName(String)
    InvalidContent(String)
    InvalidStructure(String)
    InvalidDeclaration(String)
    } derive(Eq,
    Debug
    )

    An error raised when Writer would produce an invalid XML document.

    WriterError::equal

    fn WriterError::equal(WriterError, WriterError) -> Bool

    WriterError::not_equal

    fn WriterError::not_equal(x : WriterError, y : WriterError) -> Bool

    XmlError

    pub(all) suberror XmlError {
    At(error~ : XmlErrorKind, span~ : SourceSpan)
    } derive(Eq,
    Debug
    )

    An XML parsing error paired with its relevant authored source range. Syntax errors normally cover input consumed while detecting the failure; an unclosed-element error covers the unmatched opening tag.
    impl Show for XmlError

    XmlError::equal

    fn XmlError::equal(XmlError, XmlError) -> Bool

    XmlError::not_equal

    fn XmlError::not_equal(x : XmlError, y : XmlError) -> Bool

    XmlError::output

    fn XmlError::output(self : XmlError, logger : &Logger) -> Unit

    XmlError::to_repr

    XmlError::to_string

    fn XmlError::to_string(self : XmlError) -> String

    XmlErrorKind

    pub(all) suberror XmlErrorKind {
    UnexpectedEof
    InvalidSyntax(String)
    UnmatchedTag(expected~ : String, found~ : String)
    InvalidAttribute(String)
    InvalidEntity(String)
    } derive(Eq,
    Debug
    )

    XML parsing error

    XmlErrorKind::equal

    XmlErrorKind::not_equal

    fn XmlErrorKind::not_equal(x : XmlErrorKind, y : XmlErrorKind) -> Bool

    XmlErrorKind::output

    fn XmlErrorKind::output(self : XmlErrorKind, logger : &Logger) -> Unit

    XmlErrorKind::to_string

    fn XmlErrorKind::to_string(self : XmlErrorKind) -> String

    Event

    pub struct Event {
    kind : EventKind
    span : SourceSpan
    } derive(Eq)

    An XML event together with its authored source range.
    impl Show for Event

    Event::equal

    fn Event::equal(Event, Event) -> Bool

    Event::not_equal

    fn Event::not_equal(x : Event, y : Event) -> Bool

    Event::output

    fn Event::output(self : Event, logger : &Logger) -> Unit

    Event::to_repr

    Event::to_string

    fn Event::to_string(self : Event) -> String

    EventKind

    pub(all) enum EventKind {
    Start(XmlElement)
    End(String)
    Empty(XmlElement)
    Text(String)
    CData(String)
    Comment(String)
    PI(target~ : String, data~ : String)
    Decl(version~ : String, encoding~ : String?, standalone~ : String?)
    DocType(String)
    Eof
    } derive(Eq,
    Debug
    )

    An XML event returned by the parser
    impl Show for EventKind

    EventKind::equal

    fn EventKind::equal(EventKind, EventKind) -> Bool

    EventKind::not_equal

    fn EventKind::not_equal(x : EventKind, y : EventKind) -> Bool

    EventKind::output

    fn EventKind::output(self : EventKind, logger : &Logger) -> Unit

    EventKind::to_string

    fn EventKind::to_string(self : EventKind) -> String

    NamespaceAttribute

    pub struct NamespaceAttribute {
    name : XmlName
    value : String
    span : SourceSpan
    name_span : SourceSpan
    value_span : SourceSpan
    } derive(Eq,
    Debug
    )

    An XML attribute with a namespace-resolved name.

    NamespaceAttribute::equal

    NamespaceAttribute::not_equal

    NamespaceDeclaration

    pub struct NamespaceDeclaration {
    prefix : String?
    namespace_uri : String
    span : SourceSpan
    name_span : SourceSpan
    value_span : SourceSpan
    } derive(Eq,
    Debug
    )

    A namespace declaration from an element start tag. None denotes the default namespace declaration.

    NamespaceDeclaration::equal

    NamespaceDeclaration::not_equal

    NamespaceElement

    pub struct NamespaceElement {
    name : XmlName
    attributes : Array[NamespaceAttribute]
    namespace_declarations : Array[NamespaceDeclaration]
    } derive(Eq,
    Debug
    )

    An XML element with namespace-resolved element and attribute names. Namespace declarations are exposed separately and are not normal attributes.

    NamespaceElement::equal

    NamespaceElement::not_equal

    fn NamespaceElement::not_equal(x : NamespaceElement, y : NamespaceElement) -> Bool

    NamespaceEvent

    pub struct NamespaceEvent {
    kind : NamespaceEventKind
    span : SourceSpan
    } derive(Eq,
    Debug
    )

    A namespace-aware XML event with its authored source range.

    NamespaceEvent::equal

    NamespaceEvent::not_equal

    fn NamespaceEvent::not_equal(x : NamespaceEvent, y : NamespaceEvent) -> Bool

    NamespaceEventKind

    pub(all) enum NamespaceEventKind {
    Start(NamespaceElement)
    End(XmlName)
    Empty(NamespaceElement)
    Text(String)
    CData(String)
    Comment(String)
    PI(target~ : String, data~ : String)
    Decl(version~ : String, encoding~ : String?, standalone~ : String?)
    DocType(String)
    Eof
    } derive(Eq,
    Debug
    )

    The semantic kind of a namespace-aware XML event.

    NamespaceEventKind::equal

    NamespaceEventKind::not_equal

    NamespaceReader

    pub struct NamespaceReader {
    reader : Reader
    scopes : Array[Map[String, String]]
    }

    A namespace-aware adapter over the raw XML Reader event stream.

    NamespaceReader::column

    fn NamespaceReader::column(self : NamespaceReader) -> Int

    Get the current one-indexed column number.

    NamespaceReader::from_file

    Create a namespace-aware reader from a file path.

    NamespaceReader::from_string

    fn NamespaceReader::from_string(input : String) -> NamespaceReader

    Create a namespace-aware reader from a string.

    NamespaceReader::is_eof

    fn NamespaceReader::is_eof(self : NamespaceReader) -> Bool

    Check whether the underlying reader has consumed its input.

    NamespaceReader::line

    fn NamespaceReader::line(self : NamespaceReader) -> Int

    Get the current one-indexed line number.

    NamespaceReader::read_event

    fn NamespaceReader::read_event(self : NamespaceReader) -> NamespaceEvent raise XmlError

    Read the next namespace-aware XML event.

    NamespaceReader::read_events_until_eof

    fn NamespaceReader::read_events_until_eof(self : NamespaceReader) -> Array[NamespaceEvent] raise XmlError

    Read all namespace-aware events through the final Eof event.

    Reader

    pub struct Reader {
    input : Array[Char]
    pos : Int
    offset : Int
    line : Int
    column : Int
    entities : Map[String, String]
    entity_expansion_remaining : Int
    attr_types : Map[String, String]
    tag_stack : Array[(String, SourceSpan)]
    seen_root : Bool
    root_closed : Bool
    seen_content : Bool
    just_saw_decl : Bool
    pending_events : Array[Event]
    internal_subset_events : Array[Event]
    just_saw_doctype : Bool
    had_bom : Bool
    }

    A pull XML reader over a fully buffered document

    Reader::column

    fn Reader::column(self : Reader) -> Int

    Get current column number (1-indexed)

    Reader::from_file

    fn Reader::from_file(path : String) -> Reader raise
    IOError

    Create a new reader from a file path

    Reader::from_string

    fn Reader::from_string(input : String) -> Reader

    Create a new reader from a string

    Reader::is_eof

    fn Reader::is_eof(self : Reader) -> Bool

    Check if the reader has reached the end

    Reader::line

    fn Reader::line(self : Reader) -> Int

    Get current line number (1-indexed)

    Reader::read_event

    fn Reader::read_event(self : Reader) -> Event raise XmlError

    Read the next XML event

    Reader::read_events_until_eof

    fn Reader::read_events_until_eof(self : Reader) -> Array[Event] raise XmlError

    Read all events until EOF (inclusive) as an Array. Includes the final Eof event in the result. Raises XmlError if parsing fails.

    SourcePosition

    pub struct SourcePosition {
    offset : Int
    line : Int
    column : Int
    } derive(Eq,
    Debug
    )

    A position in the original XML source. offset is a zero-indexed UTF-16 code-unit offset suitable for slicing the original MoonBit String; line and column are one-indexed and count Unicode characters.

    SourcePosition::equal

    SourcePosition::not_equal

    fn SourcePosition::not_equal(x : SourcePosition, y : SourcePosition) -> Bool

    SourceSpan

    pub struct SourceSpan {
    start : SourcePosition
    end : SourcePosition
    } derive(Eq,
    Debug
    )

    A half-open range [start, end) in the original XML source.

    SourceSpan::equal

    fn SourceSpan::equal(SourceSpan, SourceSpan) -> Bool

    SourceSpan::not_equal

    fn SourceSpan::not_equal(x : SourceSpan, y : SourceSpan) -> Bool

    Writer

    pub struct Writer {
    buffer : StringBuilder
    element_stack : Array[String]
    seen_root : Bool
    wrote_anything : Bool
    doctype_root : String?
    }

    An XML writer for generating XML output

    Writer::cdata

    fn Writer::cdata(self : Writer, content : String) -> Unit raise WriterError

    Write CDATA section

    Writer::comment

    fn Writer::comment(self : Writer, content : String) -> Unit raise WriterError

    Write a comment

    Writer::empty_element

    fn Writer::empty_element(self : Writer, name : String, attributes : Array[(String, String)]) -> Unit raise WriterError

    Write a self-closing element

    Writer::end_element

    fn Writer::end_element(self : Writer, name : String) -> Unit raise WriterError

    Write an end element

    Writer::new

    fn Writer::new() -> Writer

    Create a new writer

    Writer::start_element

    fn Writer::start_element(self : Writer, name : String, attributes : Array[(String, String)]) -> Unit raise WriterError

    Write a start element

    Writer::text

    fn Writer::text(self : Writer, content : String) -> Unit raise WriterError

    Write text content (escaped)

    Writer::to_string

    fn Writer::to_string(self : Writer) -> String raise WriterError

    Get the generated XML string

    Writer::write_event

    fn Writer::write_event(self : Writer, event : Event) -> Unit raise WriterError

    Write an XML event

    XmlAttribute

    pub struct XmlAttribute {
    name : String
    value : String
    span : SourceSpan
    name_span : SourceSpan
    value_span : SourceSpan
    } derive(Eq,
    Debug
    )

    A parsed attribute together with its authored source ranges. value_span excludes the surrounding quote characters.

    XmlAttribute::equal

    XmlAttribute::not_equal

    fn XmlAttribute::not_equal(x : XmlAttribute, y : XmlAttribute) -> Bool

    XmlElement

    pub struct XmlElement {
    name : String
    attributes : Array[XmlAttribute]
    } derive(Eq)

    An XML element with tag name and attributes
    impl Show for XmlElement

    XmlElement::equal

    fn XmlElement::equal(XmlElement, XmlElement) -> Bool

    XmlElement::get

    fn XmlElement::get(self : XmlElement, attr_name : String) -> String?

    Get an attribute value by name

    XmlElement::not_equal

    fn XmlElement::not_equal(x : XmlElement, y : XmlElement) -> Bool

    XmlElement::output

    fn XmlElement::output(self : XmlElement, logger : &Logger) -> Unit

    XmlElement::to_repr

    XmlElement::to_string

    fn XmlElement::to_string(self : XmlElement) -> String

    XmlName

    pub struct XmlName {
    qualified_name : String
    prefix : String?
    local_name : String
    namespace_uri : String?
    } derive(Eq,
    Debug
    )

    An XML qualified name resolved against the namespace declarations in scope.

    XmlName::equal

    fn XmlName::equal(XmlName, XmlName) -> Bool

    XmlName::not_equal

    fn XmlName::not_equal(x : XmlName, y : XmlName) -> Bool

    XmlName::to_repr

    escape

    fn escape(text : String) -> String

    Escape special XML characters in text Replaces: & " '

    unescape

    fn unescape(text : String) -> String raise XmlErrorKind

    Unescape XML entities Handles: < > & " ' &#NN; &#xHH;

    Powered by MoonBit

    Site sourceReport issuePackagesBuild queueSkillsStatistics

    © 2026 mooncakes.io