Skip to content
Jennifer Programming Language

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.

jennifer
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"))); # Go

Decode / encode

CallReturnsNotes
xml.decode(s)xml.ValueParse a document into the root element. Errors (with line / column) on malformed XML.
xml.encode(v)stringSerialize compactly.
xml.encodePretty(v)stringSerialize 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 (&lt; &gt; &amp; &apos; &quot;) and numeric character references (&#65;, &#x41;) 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).

CallReturnsNotes
xml.typeOf(node)string"element" or "text".
xml.tag(node)stringThe element's tag name (with prefix, if any).
xml.text(node)stringThe concatenated character data of the element's direct text children (or a text node's own string).
xml.attr(node, name)stringAn attribute's value; errors if the attribute is absent.
xml.hasAttr(node, name)boolWhether the attribute is present.
xml.attrs(node)list of stringThe attribute names, in document order.
xml.children(node)list of xml.ValueThe element children (text children are excluded).
xml.get(node, path)xml.ValueThe first element matching the path; errors if none.
xml.findAll(node, path)list of xml.ValueEvery element matching the path (empty if none).
xml.has(node, path)boolWhether 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):

StepMatches
nameevery 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:

CallReturnsNotes
xml.element(name)xml.ValueA new empty element.
xml.setAttr(node, name, value)xml.ValueThe element with the attribute added or updated.
xml.setText(node, s)xml.ValueThe element with its children replaced by one text node.
xml.append(parent, child)xml.ValueThe parent with child (an xml.Value element) appended.
jennifer
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 &amp; bye</note></page>

Notes and limits

  • convert.typeOf reports "object"; convert.objectType reports "xml.Value". A bare $v at 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 xmlns URIs. 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"), and xml.attr($el, "lat") raises on a gpx:lat attribute (use xml.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:
jennifer
  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 an append that 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 default jennifer (Go's growable stack), 64 on jennifer-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.Value is 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-trees get / findAll / children hand 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