xml - XML encode / decode
Enable with use xml;. Parses and serializes XML over an opaque xml.Value, designed exactly like json and toml: decode returns a handle you walk with accessors, and a small build surface constructs a tree to encode. Hand-rolled (no encoding/xml, which is reflect-bound), so it runs on both binaries.
Unlike JSON's map/list/scalar tree, XML is a tree of elements - each with a tag name, ordered attributes, and ordered children that mix elements and text. The accessors reflect that shape, and navigation uses a small XPath-style path dialect in place of JSON Pointer.
use io;
use xml;
def root as xml.Value init xml.decode(
"<library><book id=\"1\"><title>Go</title></book></library>");
io.printf("%s\n", xml.tag($root)); # library
io.printf("%s\n", xml.attr(xml.get($root, "book"), "id")); # 1
io.printf("%s\n", xml.text(xml.get($root, "book/title"))); # GoDecode / encode
| Call | Returns | Notes |
|---|---|---|
xml.decode(s) | xml.Value | Parse a document into the root element. Errors (with line / column) on malformed XML. |
xml.encode(v) | string | Serialize compactly. |
xml.encodePretty(v) | string | Serialize with 2-space indentation. |
decode handles elements, attributes, text, <![CDATA[...]]> (as text), comments, processing instructions, an XML declaration, and a DOCTYPE (the last four are skipped, not kept). The five predefined entities (< > & ' ") and numeric character references (A, A) are decoded; an unknown entity is an error. Namespace prefixes are kept verbatim in the name (ns:tag), and xmlns declarations are ordinary attributes.
encodePretty indents element-only content, but an element that contains any text child is emitted inline so its character data stays byte-exact.
Reading
Every accessor takes an xml.Value. xml.get / xml.findAll / xml.has take a path as a second argument (see below).
| Call | Returns | Notes |
|---|---|---|
xml.typeOf(node) | string | "element" or "text". |
xml.tag(node) | string | The element's tag name (with prefix, if any). |
xml.text(node) | string | The concatenated character data of the element's direct text children (or a text node's own string). |
xml.attr(node, name) | string | An attribute's value; errors if the attribute is absent. |
xml.hasAttr(node, name) | bool | Whether the attribute is present. |
xml.attrs(node) | list of string | The attribute names, in document order. |
xml.children(node) | list of xml.Value | The element children (text children are excluded). |
xml.get(node, path) | xml.Value | The first element matching the path; errors if none. |
xml.findAll(node, path) | list of xml.Value | Every element matching the path (empty if none). |
xml.has(node, path) | bool | Whether the path matches at least one element. |
Path dialect
A path is /-separated steps, evaluated against the element children of the node (a leading / is optional; "" addresses the node itself):
| Step | Matches |
|---|---|
name | every element child with that tag. |
name[k] | the 1-based k-th element child with that tag. |
* | every element child, whatever its tag. |
xml.findAll($root, "book") returns all book children; xml.get($root, "book[2]/title") drills to the second book's title. Attributes and text are read with xml.attr / xml.text after navigating, not addressed inside the path. A malformed path is an error; a well-formed path that matches nothing is an empty findAll (and a false has, and an error from get).
Building
The build surface is non-mutating - each call returns a fresh xml.Value, the same $v = xml.set(...) idiom as json / toml:
| Call | Returns | Notes |
|---|---|---|
xml.element(name) | xml.Value | A new empty element. |
xml.setAttr(node, name, value) | xml.Value | The element with the attribute added or updated. |
xml.setText(node, s) | xml.Value | The element with its children replaced by one text node. |
xml.append(parent, child) | xml.Value | The parent with child (an xml.Value element) appended. |
use xml;
def note as xml.Value init xml.element("note");
$note = xml.setAttr($note, "type", "info");
$note = xml.setText($note, "hi & bye");
def page as xml.Value init xml.append(xml.element("page"), $note);
# <page><note type="info">hi & bye</note></page>Notes and limits
convert.typeOfreports"object";convert.objectTypereports"xml.Value". A bare$vat the REPL (or%v) shows the compact XML.- Comments, PIs, the prolog, and the DOCTYPE are dropped on decode, so a decode / encode round-trip is faithful for the element tree but not for those. The one root element is required (XML has exactly one).
- Namespaces are lexical: prefixes are kept as written, not resolved to their
xmlnsURIs. Full namespace resolution and richer XPath (predicates,@attr/text()path steps) are future extensions. The practical consequence for real-world KML / GML / GPX, which use prefixes: a name must be matched verbatim, including the prefix.xml.get($root, "Document")silently matches nothing on a<kml:Document>(use"kml:Document"), andxml.attr($el, "lat")raises on agpx:latattribute (usexml.attr($el, "gpx:lat")). To accept either form, match on the local name yourself - split each tag / attribute name on":"and compare the last part:
use strings;
# local name of "kml:Document" or "Document" -> "Document"
func localName(name as string) {
def i as int init strings.indexOf($name, ":");
if ($i < 0) { return $name; }
return strings.substring($name, $i + 1, len($name));
}- Element nesting is capped on decode, encode and
xml.append: a deeper document (or anappendthat would build one) raises a catchable error rather than overflowing the stack. The limit is shared by every parser in the toolchain and is set per binary: 1000 levels on the defaultjennifer(Go's growable stack), 64 onjennifer-tiny(its fixed 4 MB stack). No entity is expanded beyond the five predefined names and numeric references, so there is no entity-expansion ("billion laughs") blow-up, and nothing is fetched externally - an unknown or external entity is simply an error. - Node count is capped on decode: a document with more than about two million elements, attributes and text runs (a quarter million on
jennifer-tiny) raises a catchable error rather than growing without bound. - Memory: a decoded
xml.Valueis a compact element tree (name, ordered attributes, ordered children); names, and text and attribute values with no entity to decode, share the input string. A document takes from about its text size (text-heavy markup) up to roughly 15 times it (many small elements) in memory. Copies of a handle, and the sub-treesget/findAll/childrenhand out, share the tree instead of copying it. A sub-tree handle therefore keeps the whole document's
text in memory, while tag / text / attr / attrs return copies: to keep a few values of a large document, read them out rather than holding a node.
See also
- json.md / toml.md - the sibling opaque-handle formats with the same read / write shape.
- cheatsheet.md - every builtin at a glance.