hanki

xml

stdlib/extra/xml.hk: XML 1.0 pull reader (layer 1 of the xml library).

Secure by design and never by mitigation: the parser defines no custom entities and performs no I/O of any kind, which puts XXE and entity-expansion attacks outside what it can express. Only the five predefined references (& < > ' ") and numeric character references decode; any other named reference is a typed UndefinedEntity error. A DOCTYPE is tolerated and skipped inert; its contents are never interpreted.

Two layers. The internal pull core (_RawReader, advanced by _next through the _RawStep sum) enforces well-formedness over raw qnames. The public face is the namespace-aware reader per Namespaces in XML 1.0: reader/reader_bytes build an [XmlReader], next pulls one [XmlEvent] whose element/attribute names arrive resolved as [XmlName] (uri + local + original prefix). Both readers are plain values: each pull returns the successor state alongside the event. Namespace declarations (xmlns, xmlns:p) are scope and no data: the reader consumes them (scoped, shadowing, default un-bindable via xmlns="") and never surfaces them as attributes. The reserved xml prefix is always bound and cannot change; xmlns is never a usable prefix; an unbound prefix is a typed UnboundPrefix error. An unprefixed attribute has no namespace even under a default declaration, which is the spec's element/attribute asymmetry.

Scope: XML 1.0 (5th ed) well-formedness over complete in-memory UTF-8 input. Not here (by design): DTD/entity machinery, XML 1.1, validation, non-UTF-8 encodings, incremental input.

Whitespace and line endings: text events deliver element content verbatim, with no trimming and no whitespace collapsing, save for the line-end normalization XML 1.0 §2.11 requires (a literal \r\n pair or lone \r becomes \n). Attribute values additionally normalize each literal whitespace character to a space (§3.3.3); whitespace written as a character reference remains literal in both.

_RawAttribute

struct _RawAttribute
  name: string
  value: string
end

One attribute of an element-start tag, in written order. name is the raw qname as written (prefix:local, unresolved; the namespace layer resolves prefixes).

_RawEvent

type _RawEvent
  RawDeclaration(string, Option<string>, Option<bool>)
  RawStart(string, List<_RawAttribute>)
  RawEnd(string)
  RawText(string)
  RawCData(string)
  RawComment(string)
  RawPi(string, string)
end

One event of the pull stream, in document order. Names are raw qnames (prefix:local, unresolved at this layer). A self-closing tag <a/> yields RawStart then RawEnd as two pulls. A DOCTYPE yields no event: it is skipped inert.

XmlErrorKind

type XmlErrorKind
  Syntax
  UnexpectedEof
  MismatchedTag
  DuplicateAttribute
  UndefinedEntity
  UnsupportedEncoding
  InvalidChar
  TooDeep
  MultipleRoots
  UnboundPrefix
  ReservedNamespace
end

What went wrong, structurally. Every kind pairs with the position fields on [XmlError]; MismatchedTag and friends carry the human detail in the error's message.

XmlError

struct XmlError
  kind: XmlErrorKind
  position: int
  line: int
  column: int
  message: string
end

A parse failure: the kind, where (byte offset into the input, plus the 1-based line and column derived from it), and a human-readable message.

_RawStep

type _RawStep
  RawNext(_RawReader, _RawEvent)
  RawDone
  RawFail(XmlError)
end

The result of one pull: the next event alongside the successor reader state, the end of the document, or a failure. The reader is a plain value RawNext returns the advanced reader, and a drive loop rebinds it each pull.

_RawReader

struct _RawReader
  input: bytes
  pos: int
  start: int
  stack: List<string>
  max_depth: int
  seen_root: bool
  doctype_seen: bool
  pending_end: Option<string>
end

The pull reader's state: a cursor over the complete input plus the open element name stack and the document-level flags well-formedness needs. A plain value with value semantics: advancing never mutates a caller's copy. Internal-shaped for now (constructed via _reader); the namespace layer becomes the public face.

defaultmax_depth

def _default_max_depth() -> int

The default element-nesting limit: far past any realistic document, and still bounded, which leaves untrusted input unable to exhaust the stack.

_reader

def _reader(input: string) -> _RawReader

readerdepth

def _reader_depth(input: string, max_depth: int) -> _RawReader

readerbytes

def _reader_bytes(input: bytes) -> Result<_RawReader, XmlError>

The bytes entry: validate UTF-8 (a byte-order mark is allowed and stripped), then read as _reader does. The UTF-8 check is up-front and whole-input, which leaves every later slice-to-string inside the reader infallible.

afterbom

def _after_bom(b: bytes) -> int

The offset past a leading UTF-8 byte-order mark (EF BB BF), or 0.

errat

def _err_at(input: bytes, pos: int, kind: XmlErrorKind, message: string) -> XmlError

Build an error at byte offset pos, deriving 1-based line/column by counting newlines up to it. O(pos), paid only on the failure path.

lineat

def _line_at(input: bytes, pos: int) -> int

columnat

def _column_at(input: bytes, pos: int) -> int

isws?

def _is_ws?(b: u8) -> bool

skipws

def _skip_ws(input: bytes, pos: int) -> int

_Cp

struct _Cp
  cp: i32
  width: int
end

Decoded code point at pos plus its encoded width. The input is known valid UTF-8 (string entry, or _reader_bytes validated), and the lead byte's class is trusted.

cpat

def _cp_at(input: bytes, pos: int) -> _Cp

_byte

def _byte(input: bytes, pos: int) -> u8

_cont

def _cont(input: bytes, pos: int) -> i32

_i

def _i(b: u8) -> i32

isxml_char?

def _is_xml_char?(cp: i32) -> bool

XML 1.0 Char: #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]. Surrogates cannot occur in valid UTF-8, and the checks that matter here are the control range and the two non-characters at the end of the BMP that UTF-8 can express.

isname_start?

def _is_name_start?(cp: i32) -> bool

XML 1.0 (5th ed) NameStartChar.

isname_char?

def _is_name_char?(cp: i32) -> bool

XML 1.0 (5th ed) NameChar: NameStartChar plus digits, -, ., #xB7, and two combining/extender ranges.

_Tok

struct _Tok
  text: string
  end_pos: int
end

A scanned lexeme and the position just past it.

_Attrs

struct _Attrs
  attributes: List<_RawAttribute>
  self_closing: bool
  end_pos: int
end

A scanned attribute list: the attributes, whether the tag was self-closing, and the position just past the closing >.

scanname

def _scan_name(input: bytes, pos: int) -> Result<_Tok, XmlError>

Scan an XML Name at pos (raw qname: : is an ordinary name char at this layer).

BytesBuilder

BytesBuilder, or bytes.BytesBuilder, is a runtime-managed native resource handle (HANKI.md §4): live native state the per-actor memory manager owns, released when the last handle drops. It is single-owner - never copied, moved across a send - and has no fields of its own, so its methods are its whole surface. A handle is minted by an API that opens one; it is never constructed.

decodereference!

def _decode_reference!(input: bytes, pos: int, out: BytesBuilder) -> Result<int, XmlError>

Decode one reference at pos (which points at &) into out, returning the position just past the closing ;. Only the five predefined named references and numeric character references decode; any other name is UndefinedEntity at the &.

_predefined

def _predefined(name: string) -> Option<u8>

decodechar_reference!

def _decode_char_reference!(input: bytes, pos: int, out: BytesBuilder) -> Result<int, XmlError>

&#123; / &#x1F;, with pos at the &. The decoded code point must be an XML Char.

digitvalue

def _digit_value(b: u8, hex: bool) -> Option<i32>

pushutf8!

def _push_utf8!(out: BytesBuilder, cp: i32) -> ()

Encode a code point as UTF-8 into out (mirrors json._push_utf8!).

_u8

def _u8(n: i32) -> u8

checksource_char

def _check_source_char(input: bytes, p: int, b: u8) -> Option<XmlError>

Reject an XML-forbidden character in raw source at p. The input is valid UTF-8, and only two classes are expressible and forbidden: control bytes below #x20 (other than tab/LF/CR) and the BMP non-characters #xFFFE/#xFFFF.

scantext

def _scan_text(input: bytes, pos: int) -> Result<_Tok, XmlError>

Scan character data from pos up to the next < (or end of input, which the caller judges), decoding references and normalizing line ends (\r\n/\r\n, §2.11). A literal ]]> in character data is forbidden.

scanattr_value

def _scan_attr_value(input: bytes, pos: int, quote: u8) -> Result<_Tok, XmlError>

Scan an attribute value from just past its opening quote to the matching close quote. Raw < is forbidden; references decode; each literal whitespace character normalizes to a space (§3.3.3; a \r\n pair first becomes one \n per §2.11, hence one space).

finishtok!

def _finish_tok!(input: bytes, out: BytesBuilder, at: int, end_pos: int) -> Result<_Tok, XmlError>

_next

def _next(rd: _RawReader) -> _RawStep

Advance the reader by one event. The core dispatch: a pending self-closing end first, then end-of-input bookkeeping, then markup or character data at the cursor.

_advance

def _advance(rd: _RawReader) -> _RawStep

advancein_root

def _advance_in_root(rd: _RawReader) -> _RawStep

Inside the root element: markup or character data.

advancemisc

def _advance_misc(rd: _RawReader) -> _RawStep

Outside the root (prolog and epilog): whitespace is insignificant Misc; anything textual is an error; markup dispatches.

_markup

def _markup(rd: _RawReader) -> _RawStep

Dispatch at a <.

hasliteral_at?

def _has_literal_at?(input: bytes, pos: int, lit: string) -> bool

opentag

def _open_tag(rd: _RawReader) -> _RawStep

<name attr="v" …> or <name …/>.

scanattrs

def _scan_attrs(input: bytes, pos: int) -> Result<_Attrs, XmlError>

The attribute list of an open tag, from just past the element name to just past the closing >; enforces per-element attribute-name uniqueness.

attrvalue_of

def _attr_value_of(input: bytes, pos: int) -> Result<_Tok, XmlError>

The = "value" half of an attribute, whitespace allowed around =.

closetag

def _close_tag(rd: _RawReader) -> _RawStep

</name>, which must match the innermost open element.

_comment

def _comment(rd: _RawReader) -> _RawStep

<!-- … -->. Per XML 1.0 §2.5, -- must not appear inside a comment.

_cdata

def _cdata(rd: _RawReader) -> _RawStep

<![CDATA[ … ]]>, only inside the root element; contents verbatim.

_doctype

def _doctype(rd: _RawReader) -> _RawStep

<!DOCTYPE …>, skipped inert, at most once, prolog only. The skip bracket-matches an internal subset ([]) and steps over quoted literals so a bracket or > inside quotes cannot end it early; nothing is interpreted.

doctypeskip

def _doctype_skip(rd: _RawReader, from: int) -> _RawStep

skipquoted

def _skip_quoted(input: bytes, pos: int, quote: u8) -> int

Past a quoted literal (pos at the opening quote); an unterminated quote runs to end of input, which the caller reports as EOF.

pior_declaration

def _pi_or_declaration(rd: _RawReader) -> _RawStep

<?…?>: the <?xml …?> declaration when (and only when) it sits at the very start of the content; a reserved-target error for any other case-variant of xml; otherwise a processing instruction.

isxml_target?

def _is_xml_target?(t: string) -> bool

Any case-variant of the reserved target xml (xml, XML, Xml, …).

_lower

def _lower(b: u8) -> u8

pibody

def _pi_body(rd: _RawReader, target: string, after_target: int) -> _RawStep

The body of a processing instruction after its target: optional whitespace-separated content up to ?>, verbatim.

_declaration

def _declaration(rd: _RawReader, after_target: int) -> _RawStep

<?xml version="1.x" encoding="…"? standalone="yes|no"? ?>: the pseudo-attributes in that fixed order, version required. Any declared encoding other than UTF-8 (case-insensitive) is UnsupportedEncoding.

declarationtail

def _declaration_tail(rd: _RawReader, version: string, enc_opt: Option<_Tok>, from: int) -> _RawStep

declattr

def _decl_attr(input: bytes, pos: int, name: string) -> Result<Option<_Tok>, XmlError>

One optional name="value" pseudo-attribute of the declaration: Some with the value and end position when name is next, None (position unchanged) when it is not.

declvalue

def _decl_value(input: bytes, quote_at: int, quote: u8) -> Result<Option<_Tok>, XmlError>

isversion_10?

def _is_version_10?(v: string) -> bool

1.0, 1.1 and onward: any 1.x per the §2.8 forward-compatibility note, processed as 1.0.

isutf8_name?

def _is_utf8_name?(e: string) -> bool

xmluri

def _xml_uri() -> string

The reserved xml prefix namespace (always bound, never rebindable to anything else).

xmlnsuri

def _xmlns_uri() -> string

The reserved namespace of xmlns itself: never usable as an element or attribute namespace, never bindable.

XmlName

struct XmlName
  uri: string
  local: string
  prefix: string
end

An expanded name: the namespace uri (empty = no namespace), the local part, and the original prefix (kept for diagnostics and round-trip fidelity; two names are the same when uri and local agree, whatever their prefixes).

XmlAttribute

struct XmlAttribute
  name: XmlName
  value: string
end

One attribute of an element-start event, in written order, its name resolved. An unprefixed attribute has no namespace (uri empty) even under a default xmlns=… declaration, which is the spec's asymmetry: default namespaces apply to elements only.

XmlEvent

type XmlEvent
  Declaration(string, Option<string>, Option<bool>)
  ElementStart(XmlName, List<XmlAttribute>)
  ElementEnd(XmlName)
  Text(string)
  CData(string)
  Comment(string)
  ProcessingInstruction(string, string)
end

One event of the namespace-aware pull stream, the public face. Element and attribute names arrive resolved as [XmlName]; xmlns/xmlns:p declarations are scope and no data; the reader consumes them and never surfaces them as attributes. The other variants mirror the raw layer: a self-closing tag yields ElementStart then ElementEnd as two pulls, and a DOCTYPE yields no event.

_NsBinding

struct _NsBinding
  prefix: string
  uri: string
end

One in-scope prefix binding; the empty prefix is the default namespace.

XmlReader

opaque XmlReader
  raw: _RawReader
  scopes: List<List<_NsBinding>>
end

The namespace-aware pull reader: the raw well-formedness reader plus the scoped binding stack (one entry per open element, innermost last). A plain value like the raw reader: each pull returns the successor.

XmlStep

type XmlStep
  Next(XmlReader, XmlEvent)
  Done
  Fail(XmlError)
end

The result of one pull of the public reader.

reader

def reader(input: string) -> XmlReader

A reader over a complete XML document string.

n = match next(reader("<a/>"))
  Next(_, ElementStart(name, _)) -> name.local
  _ -> "?"
end
n => "a"

readerwithmax_depth

def reader_with_max_depth(input: string, max_depth: int) -> XmlReader

reader with a custom element-nesting limit in place of the default (see the raw layer's _default_max_depth).

k = match next(reader_with_max_depth("<a><b/></a>", 1))
  Next(rd, _) -> match next(rd)
    Fail(e) -> _show_kind(e.kind) == "deep"
    _ -> false
  end
  _ -> false
end
k => true

reader_bytes

def reader_bytes(input: bytes) -> Result<XmlReader, XmlError>

A reader over raw bytes: validates UTF-8 (stripping a byte-order mark) before reading.

ok = match reader_bytes("<a/>".to_bytes())
  Ok(_) -> true
  Err(_) -> false
end
ok => true

next

def next(rd: XmlReader) -> XmlStep

Advance the reader by one namespace-resolved event.

u = match next(reader("<a xmlns=\"urn:x\"/>"))
  Next(_, ElementStart(name, _)) -> name.uri
  _ -> "?"
end
u => "urn:x"

resolveevent

def _resolve_event(rd: XmlReader, raw2: _RawReader, ev: _RawEvent, at: int) -> XmlStep

_step

def _step(raw2: _RawReader, scopes: List<List<_NsBinding>>, ev: XmlEvent) -> XmlStep

resolvestart

def _resolve_start(rd: XmlReader, raw2: _RawReader, name: string, attributes: List<_RawAttribute>, at: int) -> XmlStep

resolveend

def _resolve_end(rd: XmlReader, raw2: _RawReader, name: string, at: int) -> XmlStep

_QName

struct _QName
  prefix: string
  local: string
end

The prefix/local split of a raw qname. Namespaces in XML restrict the raw layer's colon-tolerant names: at most one colon, neither side empty.

splitqname

def _split_qname(name: string, input: bytes, at: int) -> Result<_QName, XmlError>

_lookup

def _lookup(scopes: List<List<_NsBinding>>, prefix: string) -> Option<string>

The URI bound to prefix, innermost scope first. None when unbound. A default-namespace binding is the empty prefix; its empty-URI form (xmlns="") un-binds, which reaches the caller as Some("").

lookupin

def _lookup_in(scope: List<_NsBinding>, prefix: string) -> Option<string>

resolvename

def _resolve_name(scopes: List<List<_NsBinding>>, name: string, is_attribute: bool, input: bytes, at: int) -> Result<XmlName, XmlError>

Resolve one raw qname to an [XmlName]. is_attribute selects the spec's asymmetry: an unprefixed attribute has NO namespace, while an unprefixed element takes the in-scope default.

collectbindings

def _collect_bindings(attributes: List<_RawAttribute>, input: bytes, at: int) -> Result<List<_NsBinding>, XmlError>

The namespace declarations of one element-start tag, validated: the reserved xml prefix may only restate its fixed URI, xmlns is never declarable, no other prefix may take a reserved URI, and a non-default prefix cannot be un-bound (xmlns:p="" - Namespaces 1.0).

checkdefault_binding

def _check_default_binding(uri: string, input: bytes, at: int) -> Option<XmlError>

checkprefix_binding

def _check_prefix_binding(prefix: string, uri: string, input: bytes, at: int) -> Option<XmlError>

resolveattributes

def _resolve_attributes(scopes: List<List<_NsBinding>>, attributes: List<_RawAttribute>, input: bytes, at: int) -> Result<List<XmlAttribute>, XmlError>

The ordinary (non-declaration) attributes of a start tag, resolved, with the expanded-name uniqueness check: two attributes may not share the same (uri, local) even under different prefixes.

XmlDeclaration

struct XmlDeclaration
  version: string
  encoding: Option<string>
  standalone: Option<bool>
end

The <?xml …?> declaration's parts, as parsed.

XmlNamespaceDeclaration

struct XmlNamespaceDeclaration
  prefix: string
  uri: string
end

One namespace declaration on an element (xmlns=… has the empty prefix). Recorded on the element so a writer can re-emit the document faithfully; resolution itself already happened in the reader.

XmlNode

type XmlNode
  Element(XmlElement)
  Text(string)
  Comment(string)
  ProcessingInstruction(string, string)
end

One node of an element's content, in document order. CDATA sections merge into Text at this level (and adjacent character data coalesces into one node); the event layer preserves the distinction for consumers that care.

XmlElement

struct XmlElement
  name: XmlName
  attributes: List<XmlAttribute>
  namespace_declarations: List<XmlNamespaceDeclaration>
  children: List<XmlNode>
end

One element: resolved name, attributes in document order, the namespace declarations written on this tag, and child nodes. A plain immutable value: construct it directly to build a document for writing. Navigation is children-only by design (no parent pointers: plain values cannot cycle; walk downward, as with ElementTree).

XmlDocument

struct XmlDocument
  declaration: Option<XmlDeclaration>
  before_root: List<XmlNode>
  root: XmlElement
  after_root: List<XmlNode>
end

A parsed document: the optional declaration, any prolog comments and processing instructions, the single root element, and any epilog misc.

_Frame

struct _Frame
  name: XmlName
  attributes: List<XmlAttribute>
  namespace_declarations: List<XmlNamespaceDeclaration>
  children: List<XmlNode>
end

A partially-built element while its subtree is still open.

parse

def parse(input: string) -> Result<XmlDocument, XmlError>

Parse one complete XML document into its value tree.

atom = "<feed xmlns=\"http://www.w3.org/2005/Atom\"><entry><title>One</title><link href=\"/a\"/></entry><entry><title>Two</title><link href=\"/b\"/></entry></feed>"
d = parse(atom).unwrap_or(_empty_document())
ns = "http://www.w3.org/2005/Atom"
entries = d.root.elements_ns(ns, "entry")
titles = entries.map(|e| _title_text(e, ns))
", ".join(titles) => "One, Two"
hrefs = entries.map(|e| _link_href(e, ns))
" ".join(hrefs) => "/a /b"

parse_bytes

def parse_bytes(input: bytes) -> Result<XmlDocument, XmlError>

parse over raw bytes (UTF-8 validated, byte-order mark stripped).

d = parse_bytes("<r/>".to_bytes()).unwrap_or(_empty_document())
d.root.name.local => "r"

titletext

def _title_text(entry: XmlElement, ns: string) -> string
def _link_href(entry: XmlElement, ns: string) -> string

emptydocument

def _empty_document() -> XmlDocument

_build

def _build(start: XmlReader) -> Result<XmlDocument, XmlError>

Drain the reader into a tree. Iterative: the open-element chain is a list of frames, which leaves document depth off the call stack.

internaltree_error

def _internal_tree_error(rd: XmlReader) -> XmlError

The reader guarantees event balance, which leaves these paths unreachable; reported as a positioned Syntax error and never a crash if they ever fire.

_PopStep

struct _PopStep
  stack: List<_Frame>
  finished: Option<XmlElement>
end

The result of closing one element: the remaining stack (with the element appended to its parent), or the finished root when the stack emptied.

popelement

def _pop_element(stack: List<_Frame>) -> Option<_PopStep>

appendtext

def _append_text(stack: List<_Frame>, t: string) -> List<_Frame>

Append character data to the innermost open element, coalescing with a preceding Text child (this is where CDATA merges into Text).

appendchild

def _append_child(stack: List<_Frame>, node: XmlNode) -> Option<List<_Frame>>

Append a non-element child to the innermost open element; None when no element is open (the node belongs to the prolog or epilog).

_declared

def _declared(rd: XmlReader) -> List<XmlNamespaceDeclaration>

The declarations of the element just started: the innermost scope the reader pushed for it.

impl XmlElement

elements

prop elements(self) -> List<XmlElement>

The child elements, in document order (text, comments, and processing instructions skipped).

d = parse("<a>x<b/>y<c/></a>").unwrap_or(_empty_document())
d.root.elements.length => 2

elements_named

def elements_named(self, local: string) -> List<XmlElement>

The child elements whose local name is local, in any namespace. Use elements_ns to also pin the namespace.

d = parse("<a><b/><c/><b/></a>").unwrap_or(_empty_document())
d.root.elements_named("b").length => 2

elements_ns

def elements_ns(self, uri: string, local: string) -> List<XmlElement>

The child elements with the expanded name (uri, local).

d = parse("<a xmlns:p=\"urn:x\"><p:b/><b/></a>").unwrap_or(_empty_document())
d.root.elements_ns("urn:x", "b").length => 1

first

def first(self, local: string) -> Option<XmlElement>

The first child element with local name local, in any namespace.

d = parse("<a><b n=\"1\"/><b n=\"2\"/></a>").unwrap_or(_empty_document())
k = match d.root.first("b")
  Some(e) -> e.attribute("n").unwrap_or("?")
  None -> "?"
end
k => "1"

first_ns

def first_ns(self, uri: string, local: string) -> Option<XmlElement>

The first child element with the expanded name (uri, local).

d = parse("<a xmlns:p=\"urn:x\"><b/><p:b/></a>").unwrap_or(_empty_document())
k = match d.root.first_ns("urn:x", "b")
  Some(e) -> e.name.prefix
  None -> "?"
end
k => "p"

attribute

def attribute(self, name: string) -> Option<string>

The no-namespace attribute named name (an unprefixed attribute has no namespace even under a default declaration). Use attribute_ns for a namespaced attribute.

d = parse("<a xmlns=\"urn:x\" k=\"v\"/>").unwrap_or(_empty_document())
d.root.attribute("k").unwrap_or("?") => "v"
d.root.attribute("missing").unwrap_or("?") => "?"

attribute_ns

def attribute_ns(self, uri: string, local: string) -> Option<string>

The attribute with the expanded name (uri, local).

d = parse("<a xmlns:p=\"urn:x\" p:k=\"v\"/>").unwrap_or(_empty_document())
d.root.attribute_ns("urn:x", "k").unwrap_or("?") => "v"

text

prop text(self) -> string

All character data under this element, depth-first in document order - the "text content" of the subtree (lxml's text_content semantics, not ElementTree's leading-text-only .text).

d = parse("<a>Hello <b>brave</b> world</a>").unwrap_or(_empty_document())
d.root.text => "Hello brave world"

textfold

def _text_fold(acc: string, node: XmlNode) -> string

XmlWriter

struct XmlWriter
  parts: List<string>
  stack: List<string>
  scopes: List<List<_NsBinding>>
  pending: Option<string>
  pending_attributes: List<string>
  seen_root: bool
  root_closed: bool
  started: bool
  error: Option<XmlError>
end

A streaming XML writer, a plain immutable value: every operation returns the successor writer, which makes generation a chain of rebinds. Errors are sticky: an invalid operation poisons the writer (first error wins) and finish reports it, which leaves a generation chain free of per-step matching. Output is well-formed by design: content is escaped for its context, names are validated, prefixes must be declared in scope, and tag balance is enforced.

writer

def writer() -> XmlWriter

A fresh writer.

w = writer().start("a").text("hi").close()
w.finish().unwrap_or("?") => "<a>hi</a>"

impl XmlWriter

declaration

def declaration(self) -> XmlWriter

Emit the <?xml version="1.0" encoding="UTF-8"?> declaration. Must be the first operation. @transform with finish and close: despite the attribute-shaped name this writes output, and a bare-dot read would lie.

w = writer().declaration().start("a").close()
w.finish().unwrap_or("?") => "<?xml version=\"1.0\" encoding=\"UTF-8\"?><a/>"

start

def start(self, name: string) -> XmlWriter

Open an element. Attributes and namespace declarations may follow until the next content operation or end closes the tag; <name/> is emitted when end follows immediately.

w = writer().start("a").start("b").close().close()
w.finish().unwrap_or("?") => "<a><b/></a>"

attribute

def attribute(self, name: string, value: string) -> XmlWriter

Write one attribute on the open start tag (double-quoted, escaped).

w = writer().start("a").attribute("k", "x<\"y\"&z").close()
w.finish().unwrap_or("?") => "<a k=\"x&lt;&quot;y&quot;&amp;z\"/>"

namespace

def namespace(self, prefix: string, uri: string) -> XmlWriter

Declare a namespace on the open start tag (prefix empty = the default namespace). The binding scopes over this element and its content; reserved-name rules are enforced as in the reader.

w = writer().start("p:a").namespace("p", "urn:x").close()
w.finish().unwrap_or("?") => "<p:a xmlns:p=\"urn:x\"/>"

text

def text(self, content: string) -> XmlWriter

Write character data (escaped for text context).

w = writer().start("a").text("1 < 2 && x]]>y").close()
w.finish().unwrap_or("?") => "<a>1 &lt; 2 &amp;&amp; x]]&gt;y</a>"

cdata

def cdata(self, content: string) -> XmlWriter

Write a CDATA section. A literal ]]> inside content cannot appear in one section, and the content is therefore split across sections at each occurrence; the reader merges them back into one text run.

w = writer().start("a").cdata("x]]>y").close()
w.finish().unwrap_or("?") => "<a><![CDATA[x]]]]><![CDATA[>y]]></a>"

comment

def comment(self, content: string) -> XmlWriter

Write a comment. -- cannot be escaped inside a comment, and content containing it (or ending with -) is a typed error.

w = writer().start("a").comment(" note ").close()
w.finish().unwrap_or("?") => "<a><!-- note --></a>"

processing_instruction

def processing_instruction(self, target: string, content: string) -> XmlWriter

Write a processing instruction. ?> cannot be escaped in PI content, so content containing it is a typed error.

w = writer().start("a").processing_instruction("ping", "pong").close()
w.finish().unwrap_or("?") => "<a><?ping pong?></a>"

close

def close(self) -> XmlWriter

Close the innermost open element (<name/> when nothing was written inside it).

writer().start("a").close().finish().unwrap_or("?") => "<a/>"

finish

def finish(self) -> Result<string, XmlError>

The finished document. The first recorded error, an unclosed element, or a missing root reports here.

k = match writer().start("a").finish()
  Ok(_) -> "?"
  Err(e) -> e.message
end
k => "`finish` with `<a>` still open"

_emit

def _emit(self, piece: string) -> XmlWriter

-- internal steps -----------------------------------------------------

_poison

def _poison(self, kind: XmlErrorKind, message: string) -> XmlWriter

closepending

def _close_pending(self) -> XmlWriter

Close a pending start tag with > after validating its prefixes.

validatepending

def _validate_pending(self) -> XmlWriter

Validate the pending tag's element and attribute prefixes against the in-scope declarations (including this element's own).

requirein_root

def _require_in_root(self, what: string) -> XmlWriter

Text or CDATA is only legal inside the root element.

writererror

def _writer_error(w: XmlWriter, kind: XmlErrorKind, message: string) -> XmlError

validqname?

def _valid_qname?(name: string) -> bool

A syntactically valid QName for writer input: what the reader would accept, restricted to at most one colon with non-empty sides.

prefixerror

def _prefix_error(scopes: List<List<_NsBinding>>, name: string) -> Option<XmlError>

The prefix-resolution error for a written qname, if any (None = fine).

escapetext

def _escape_text(content: string) -> string

escapeattribute

def _escape_attribute(value: string) -> string

splitcdata

def _split_cdata(content: string) -> string

render

def render(document: XmlDocument) -> Result<string, XmlError>

Render a document compactly. Structural round-trip: parse of the output reproduces the tree (modulo the documented normalizations: CDATA arrives back as Text, attribute quoting is always double).

d = parse("<a xmlns:p=\"urn:x\"><p:b k=\"v\">hi</p:b></a>").unwrap_or(_empty_document())
render(d).unwrap_or("?") => "<a xmlns:p=\"urn:x\"><p:b k=\"v\">hi</p:b></a>"

render_pretty

def render_pretty(document: XmlDocument, indent: string) -> Result<string, XmlError>

Render with pretty-printing: children of element-only content go one per line at indent per depth; any element with character data among its children leaves that content untouched (whitespace is data there; mixed content is never reformatted).

d = parse("<a><b>x</b><c/></a>").unwrap_or(_empty_document())
render_pretty(d, "  ").unwrap_or("?") => "<a>\n  <b>x</b>\n  <c/>\n</a>"

renderwith

def _render_with(document: XmlDocument, indent: Option<string>) -> Result<string, XmlError>

rendermisc

def _render_misc(w: XmlWriter, nodes: List<XmlNode>) -> XmlWriter

rendermisc_node

def _render_misc_node(w: XmlWriter, node: XmlNode) -> XmlWriter

renderelement

def _render_element(w: XmlWriter, root: XmlElement, indent: Option<string>, depth: int) -> XmlWriter

Render one element subtree iteratively (an explicit work list, which leaves a hand-built tree of any depth cannot grow the call stack). Each item is either an element to open or a pending close.

_RenderStep

type _RenderStep
  RenderOpen(XmlElement, int)
  RenderClose(int)
  RenderText(string)
  RenderComment(string, int)
  RenderPi(string, string, int)
end

One unit of rendering work. Depth -1 marks compact context (inside mixed content, or compact mode): no indentation is emitted for it.

openstep

def _open_step(el: XmlElement, depth: int) -> _RenderStep

startelement

def _start_element(w: XmlWriter, el: XmlElement) -> XmlWriter

qnameof

def _qname_of(n: XmlName) -> string

iselement_only?

def _is_element_only?(el: XmlElement) -> bool

emitopen_indent

def _emit_open_indent(w: XmlWriter, depth: int, indent: Option<string>) -> XmlWriter

Emit a newline + indent-times-depth before a pretty child; depth -1 (compact) emits nothing. The writer's pending tag is closed first so the indentation lands inside the parent's content.

emitclose_indent

def _emit_close_indent(w: XmlWriter, depth: int, indent: Option<string>) -> XmlWriter

XmlShapeError

type XmlShapeError
  MissingElement(string)
  MissingAttribute(string)
  UnexpectedShape(string, string)
  BadScalar(string, string)
end

Why a typed decode failed, carrying the element path it failed at as a cheap feed/entry/title-style name chain (attributes as @name). The path an accessor reports starts at the element it was called on; wrap a nested decode with [within] to extend it upward.

impl Display<XmlShapeError>

to_string

def to_string(self) -> string

Renders as path: what went wrong, e.g. feed/entry/title: missing element.

@no-doctest: structured decode error; rendered forms pinned by the decode tests

within

def within<T>(parent: string, result: Result<T, XmlShapeError>) -> Result<T, XmlShapeError>

Extend a nested decode error's path with a parent segment: an error at title inside the decode of an entry child becomes entry/title.

e = within("entry", Err(MissingElement("title"))).map(|x: i32| x)
k = match e
  Err(MissingElement(p)) -> p
  _ -> "?"
end
k => "entry/title"

_prefixed

def _prefixed(parent: string, e: XmlShapeError) -> XmlShapeError

FromXml

trait FromXml

Decode a value of Self from one element. The v1 mapping convention - the convention @derive(FromXml) will mechanize, is: a record field decodes from the child element of the same (local) name; an attribute is reached explicitly through the attribute accessors. Scalar impls decode from the element's own text content, trimmed.

from_xml

def from_xml(element: XmlElement) -> Result<Self, XmlShapeError>

Decode a Self from element, or report how it did not fit.

d = parse("<n>7</n>").unwrap_or(_empty_document())
int.from_xml(d.root).unwrap_or(0) => 7

impl FromXml<string>

from_xml

def from_xml(element: XmlElement) -> Result<string, XmlShapeError>

The element's text content, trimmed (scalar whitespace convention).

d = parse("<a>  hi  </a>").unwrap_or(_empty_document())
string.from_xml(d.root).unwrap_or("?") => "hi"

impl FromXml<bool>

from_xml

def from_xml(element: XmlElement) -> Result<bool, XmlShapeError>

true or false, trimmed. Anything else is a BadScalar naming the element and the text that did not fit.

d = parse("<a>true</a>").unwrap_or(_empty_document())
bool.from_xml(d.root).unwrap_or(false) => true
bad = parse("<a>yes</a>").unwrap_or(_empty_document())
bool.from_xml(bad.root).unwrap_or(false) => false

impl FromXml<int>

from_xml

def from_xml(element: XmlElement) -> Result<int, XmlShapeError>

The text content as an arbitrary-precision integer, trimmed, which lets a value past every fixed width decode without loss.

d = parse("<a>170141183460469231731687303715884105728</a>").unwrap_or(_empty_document())
int.from_xml(d.root).unwrap_or(0) => 170141183460469231731687303715884105728

impl FromXml<i32>

from_xml

def from_xml(element: XmlElement) -> Result<i32, XmlShapeError>

The text content as an i32, trimmed. Out of range is a BadScalar and never a wrap, the text having said something this width cannot hold.

d = parse("<a>2147483647</a>").unwrap_or(_empty_document())
i32.from_xml(d.root).unwrap_or(0i32) => 2147483647i32

impl FromXml<i64>

from_xml

def from_xml(element: XmlElement) -> Result<i64, XmlShapeError>

The text content as an i64, trimmed. Out of range is a BadScalar and never a wrap, the text having said something this width cannot hold.

d = parse("<a>9223372036854775807</a>").unwrap_or(_empty_document())
i64.from_xml(d.root).unwrap_or(0i64) => 9223372036854775807i64

impl FromXml<f64>

from_xml

def from_xml(element: XmlElement) -> Result<f64, XmlShapeError>

The text content as an f64, trimmed.

d = parse("<a> 1.5 </a>").unwrap_or(_empty_document())
f64.from_xml(d.root).unwrap_or(0.0f64) => 1.5f64

impl XmlElement

require_attribute

def require_attribute(self, name: string) -> Result<string, XmlShapeError>

The value of the no-namespace attribute name, or a MissingAttribute whose path is element@name. (attribute is the optional form.)

d = parse("<a k=\"v\"/>").unwrap_or(_empty_document())
d.root.require_attribute("k").unwrap_or("?") => "v"
e = match d.root.require_attribute("missing")
  Err(MissingAttribute(p)) -> p
  _ -> "?"
end
e => "a@missing"

require_child

def require_child(self, name: string) -> Result<XmlElement, XmlShapeError>

The first child element with local name name, or a MissingElement whose path is element/name. (first is the optional form; repetition reads through elements_named.)

d = parse("<a><b/></a>").unwrap_or(_empty_document())
k = match d.root.require_child("b")
  Ok(b) -> b.name.local
  Err(_) -> "?"
end
k => "b"
e = match d.root.require_child("c")
  Err(MissingElement(p)) -> p
  _ -> "?"
end
e => "a/c"

child_text

def child_text(self, name: string) -> Result<string, XmlShapeError>

The trimmed text content of the required child name - the everyday scalar-field read.

d = parse("<a><t> hi </t></a>").unwrap_or(_empty_document())
d.root.child_text("t").unwrap_or("?") => "hi"

decode_child

def decode_child<T: FromXml>(self, name: string) -> Result<T, XmlShapeError>

Decode the required child name via T.from_xml - the derived-field read (@derive(FromXml) generates this call): the child's own decode errors extend with this element's name.

d = parse("<a><n>7</n></a>").unwrap_or(_empty_document())
n: Result<int, XmlShapeError> = d.root.decode_child("n")
n.unwrap_or(0) => 7
e: Result<int, XmlShapeError> = d.root.decode_child("m")
k = match e
  Err(x) -> x.to_string()
  Ok(_) -> "?"
end
k => "a/m: missing element"

decodeoptionalchild

def decode_optional_child<T: FromXml>(self, name: string) -> Result<Option<T>, XmlShapeError>

Decode the child name when present (Ok(None) when absent) - the Option<T> field read of @derive(FromXml).

d = parse("<a><n>7</n></a>").unwrap_or(_empty_document())
n: Result<Option<int>, XmlShapeError> = d.root.decode_optional_child("n")
n.unwrap_or(None).unwrap_or(0) => 7
m: Result<Option<int>, XmlShapeError> = d.root.decode_optional_child("m")
k = match m
  Ok(None) -> "absent"
  _ -> "?"
end
k => "absent"

decode_children

def decode_children<T: FromXml>(self, name: string) -> Result<List<T>, XmlShapeError>

Decode every child named name, in document order (possibly empty) - the List<T> field read of @derive(FromXml). The first failing child short-circuits.

d = parse("<a><n>1</n><n>2</n></a>").unwrap_or(_empty_document())
ns: Result<List<int>, XmlShapeError> = d.root.decode_children("n")
ns.unwrap_or([]).length => 2

_AtomEntry

struct _AtomEntry
  title: string
  id: string
  updated: string
end

impl FromXml<_AtomEntry>

from_xml

def from_xml(element: XmlElement) -> Result<_AtomEntry, XmlShapeError>

Field-by-field child-element mapping, the derive's target form. @no-doctest: _AtomEntry is private to this module; there is no call a reader could write

_events

def _events(input: string) -> Result<List<_RawEvent>, XmlError>

eventsdepth

def _events_depth(input: string, max_depth: int) -> Result<List<_RawEvent>, XmlError>

showraw_events

def _show_raw_events(evs: List<_RawEvent>) -> string

showraw_event

def _show_raw_event(ev: _RawEvent) -> string

showstandalone

def _show_standalone(sa: Option<bool>) -> string

showraw_attributes

def _show_raw_attributes(attrs: List<_RawAttribute>) -> string

showerror

def _show_error(e: XmlError) -> string

showkind

def _show_kind(k: XmlErrorKind) -> string

_golden

def _golden(input: string) -> string

nsevents

def _ns_events(input: string) -> Result<List<XmlEvent>, XmlError>

nsgolden

def _ns_golden(input: string) -> string

showname

def _show_name(n: XmlName) -> string

showevent

def _show_event(ev: XmlEvent) -> string

showattributes

def _show_attributes(attributes: List<XmlAttribute>) -> string

shownode

def _show_node(n: XmlNode) -> string

showelement

def _show_element(e: XmlElement) -> string

treegolden

def _tree_golden(input: string) -> string

_Lcg

struct _Lcg
  value: int
end

lcgnext

def _lcg_next(g: _Lcg) -> _Lcg

lcgpick

def _lcg_pick(g: _Lcg, n: int) -> int

_GenOut

struct _GenOut
  g: _Lcg
  el: XmlElement
end

genname

def _gen_name(g: _Lcg) -> string

gentext

def _gen_text(g: _Lcg) -> string

genelement

def _gen_element(g0: _Lcg, depth: int) -> _GenOut

_normalized

def _normalized(el: XmlElement) -> XmlElement

The tree as parse will see it: adjacent generated text runs coalesce.

countstarts

def _count_starts(events: List<XmlEvent>) -> int

countelements

def _count_elements(el: XmlElement) -> int

eventtext

def _event_text(events: List<XmlEvent>) -> string

eventtext_fold

def _event_text_fold(acc: List<string>, e: XmlEvent) -> List<string>

readerbytes_drain

def _reader_bytes_drain(b: bytes) -> Result<int, XmlError>