<moonxml/>

how it works

Four packages,
one direction of dependence.

Nothing here imports upward. The event vocabulary knows nothing; the tree builder knows the vocabulary; the parsers know the tree; the chain knows all of it and is known by none.

four packages· one direction· nothing imports upward
01

The shape of the module

moonbitstack/moonxml Node · Element · Document · Selection css · xpath · writer · chain moonxml/event Event · Sink — depends on nothing moonxml/html @html.loads — tolerant moonxml/xml @xml.loads — strict both build the same tree

A selector engine generic over a trait view of the tree would be a second representation of the tree, and two representations drift. So the engines live beside the tree, and the parsers sit above it.

02

HTML: the spec's algorithm, written down

The tokenizer folds states that share a shape — there are no separate states for every attribute-value flavour — but the tree construction is the specification's list of insertion modes, in the specification's order, with the specification's repairs.

Why it is not "lenient XML"

A lenient XML parser produces one repaired tree. WHATWG produces the tree: a specific one, which is why two browsers agree about <table><div>x<div> and a lenient parser does not.

What "repair" means concretely

Implied <html>, <head>, <body> and <tbody>; cells that close themselves; stray content fostered in front of a table; formatting reopened after a block closed it; misnested markup rearranged by the adoption agency.

Judged by the corpus. html5lib's tree-construction tests are part of the test suite — every file, every case, documents and fragments alike, and every one of them reproduces. scripts/gen_html5lib.py writes the fixture: the inputs, the contexts, and the trees they are expected to produce.
03

The chain: one shape, narrowing

A Selection is the nodes and the document they came from. Every query takes a selection and answers a selection, so the type never changes and the chain never stops.

Document 1 node .css("li") n nodes .xpath("..") m nodes .selector(x) k nodes .items[0] a Node .css("b")… and on Selection ──────────────────────────────────────────────▶ Node, then Selection again · results in document order · deduplicated by identity · empty stays empty

Identity, not equality

Nodes are compared with physical_equal, never by value: two <li>coffee</li> elements are different nodes, and a set that contains both must keep both.

XPath's two answers

XPath can answer a number. A chain cannot: it is always a set of nodes. So xpath() answers the node-set, and xpath_value() answers the typed XPathValue — nodes, string, number or boolean — for the queries that are questions rather than selections.

04

Writing back

XML: exact

Attribute order, CDATA as CDATA, comments, processing instructions with their raw data, the doctype including an internal subset, <a/> versus <a></a>. Names are Clark-spelled in the tree and prefixed on the way out, using the prefix the document declared; a namespace nothing declared gets ns1 and a declaration on the element that needed it.

HTML: by the rules

Void elements take no close tag, raw-text elements are written raw, & becomes &amp; and a no-break space becomes &nbsp; — WHATWG's own serialization, so what you write is what a browser would read back as the same tree.

05

What the tests hold down

SuiteWhat it pins
Tree and chainEvery verb, on both sides — the node it creates and the parent pointers it leaves behind.
CSS engineEvery combinator, every attribute operator, every pseudo-class implemented — and the parse errors for the ones refused.
XPath 1.0Axes including the reverse ones, predicate positions per context node, the comparison rules of §3.4, number formatting of §4.2.
XMLByte-for-byte round trips, namespaces, and the well-formedness errors with their offsets.
HTMLhtml5lib's tree-construction corpus — every case of it — and a hand-written case for every repair above.
htmxTriggers and swaps as they are spelled, inheritance and hx-disinherit, the values a form sends, and the answer landing where the page said.

All of it runs on all four backends — wasm, wasm-gc, js, native — in CI, with --deny-warn and a formatting check that fails the build.

❦