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>
{ / , 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
linkhref
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<"y"&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 < 2 && x]]>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>