# XML
`xml/` parses and builds XML documents. The functions are native
stdlib — compiled into every runtime, available in every host without an
import — and are the XML counterpart to Data Formats
(`json/`, `yaml/`).
The parser is a non-validating XML 1.0 processor: it enforces
well-formedness, expands the predefined and numeric character
references, and expands entity declarations from the internal DTD
subset. It never reads external DTDs or external entities, so there
is no XXE surface. Namespace prefixes are kept verbatim
(`"soap:Body"`); `xmlns` declarations are ordinary attributes.
## Two Shapes
`xml/parse` and `xml/load` produce one of two value shapes.
`xml/stringify` and `xml/pretty` accept both.
### Compact shape (default)
Keyed by element name. Attributes are `"@name"` keys, the element's own
text is the `"#text"` key, and an element with no attributes and no
child elements collapses to its character data as a plain string —
`""` when empty.
set doc [xml/parse "<order id=\"A1\"><customer>Ada</customer><paid/></order>"] # => %{ :order %{ :@id "A1" :customer "Ada" :paid "" } } set order [get $doc "order"] [get $order "@id"] # => "A1" [get $order "customer"] # => "Ada" [get $order "paid"] # => ""
A child name occurring more than once holds a vector in document order:
set feed [get [xml/parse "<feed><entry>a</entry><entry>b</entry></feed>"] "feed"] [get $feed "entry"] # => @{ "a" "b" }
An element that has both text and child elements keeps the text under
`"#text"` (the runs concatenated, layout whitespace between child
elements dropped):
set p [get [xml/parse "<p>Hello <b>world</b>!</p>"] "p"] [get $p "#text"] # => "Hello !" [get $p "b"] # => "world"
Because Deft map keys are keywords, read `"@id"` / `"#text"` with the
string form shown above (`[get $el "@id"]`); the string is interned to
the keyword automatically. `~` field chains only take bare names, so
`$el~"@id"` does not work.
### Tree shape (`%{ :compact false }` or `:tree`)
Every element becomes `%{ :name :attributes :children }` with both keys
always present. `:children` keeps the content in document order: text
strings, element maps, `%{ :comment "..." }` and
`%{ :target "..." :data "..." }` for processing instructions. This is
the shape to use when order or comments matter (documents rather than
plain data).
set p [xml/parse "<p class=\"lead\">Hello <b>world</b>!<!-- draft --></p>" %{ :compact false }] # => %{ :name "p" # :attributes %{ "class" "lead" } # :children @{ "Hello " # %{ :name "b" :attributes %{} :children @{ "world" } } # "!" # %{ :comment " draft " } } } [get $p :name] # => "p" [get [get $p :attributes] "class"] # => "lead" [nth [get $p :children] 1] # => the <b> element map [get [nth [get $p :children] 3] :comment] # => " draft "
Adjacent text and CDATA sections merge into one string; whitespace-only
runs are kept as written.
## Functions
| Function | Purpose |
|---|---|
| `xml/parse "text" ?opts` | Parse an XML document into a Deft value (compact by default) |
| `xml/load "path" ?opts` | Read an XML file and parse it (encoding-aware; see below) |
| `xml/stringify $value` | Convert an XML value (compact or tree) to a compact element string |
| `xml/pretty $value ?indent` | Indented XML; optional indent string (default two spaces) |
| `xml/escape "text"` | Escape `& < > " '` as entity references |
| `xml/unescape "text"` | Expand predefined and numeric character references |
`?opts` selects the shape: `%{ :compact false }` for tree, `:tree` as
shorthand, or `%{ :compact true }` / `:compact` for the default. Every
function returns `%Err{ :message "..." }` on bad input instead of
throwing; parse errors carry `line N, column M` positions.
set e [xml/parse "<a><b></a>"] if [err? $e] { echo $e~message } # => "xml/parse: line 1, column 11: expected closing tag </b> but found </a>"
## Parsing Semantics
- Text. Character data is returned as written, with line endings
normalized to `\n` and entity/numeric references expanded. `CDATA`
sections contribute their raw characters; `<` contributes `<` as
text, not markup.
- Whitespace. In the compact shape, whitespace-only text runs around
child elements are treated as layout and dropped; non-whitespace runs
are concatenated into `"#text"`. The tree shape keeps every run. In
attribute values, literal tabs/newlines/carriage returns normalize to
spaces (character references do not).
- Entities. The predefined (`< > & ' "`) and
numeric (`A`, `A`) references always expand. General entities
declared in the internal DTD subset
(`<!DOCTYPE a [<!ENTITY greet "hello">]>`) expand with depth and size
limits, so entity-bomb payloads fail cleanly. Replacement text is
expanded as character data — markup inside an entity value is not
re-parsed. An undeclared reference is an error, except when an
external DTD could have declared it (a `SYSTEM`/`PUBLIC` id or a
parameter entity), in which case the reference is kept as written.
A document declaring `standalone="yes"` rejects undeclared references.
- Encodings. `xml/parse` takes already-decoded text (the
declaration's `encoding` is checked for syntax but ignored).
`xml/load` decodes bytes: a UTF-8 BOM or a `UTF-8`/`US-ASCII`
declaration (the default) passes through, a UTF-16 BOM decodes either
byte order, and `ISO-8859-1`/`Latin-1` transcodes. Other encodings
return `%Err`.
- Well-formedness. Mismatched or missing end tags, multiple root
elements, text outside the root, duplicate attributes, `'<'` in
attribute values, `']]>'` in character data, `'--'` in comments, bad
names and invalid XML characters are all rejected, as are pathologically
deep documents.
- Not represented. The `<?xml ?>` declaration, DOCTYPE, comments and
processing instructions do not appear in the compact shape. The tree
shape keeps comments and PIs with the root element, but still drops the
declaration, the DOCTYPE, and anything before/after the root.
## Building and Serializing
`xml/stringify`/`xml/pretty` accept either shape. An element is written
in the tree shape when the map has a `:name` key plus `:attributes` or
`:children`; otherwise it is treated as a compact object with exactly
one key naming the element.
[xml/stringify %{ "order" %{ "@id" "A1" "customer" "Ada" } }]
# => "<order id=\"A1\"><customer>Ada</customer></order>"
[xml/stringify %{ "feed" %{ "entry" @{"a" "b"} } }]
# => "<feed><entry>a</entry><entry>b</entry></feed>"
[xml/stringify %{ :name "p" :attributes %{ "class" "lead" }
:children @{"Hello " %{ :name "b" :children @{"world"} }} }]
# => "<p class=\"lead\">Hello <b>world</b></p>"
Rules:
- In a compact element, `"@name"` keys become attributes, `"#text"`
becomes the text content, and every other key is a child element. A
vector under a child name writes the element once per item. `nil`
writes an empty element (`<paid/>`).
- Scalars become text: strings and keywords as-is, numbers and booleans
via their usual representation.
- `&`, `<`, `>` are escaped in text; `"`, tabs, newlines and carriage
returns also use character references in attribute values.
- Values that cannot be represented produce `%Err` rather than a broken
document: invalid element/attribute names, XML-invalid characters,
`'--'` in a comment, `'?>'` in a processing instruction, nested
arrays/sets, or a compact root without exactly one key.
- The output is the element only — no `<?xml ?>` declaration and no
DOCTYPE. Prepend the prolog yourself when writing a file.
- `xml/pretty` indents element-only content; an element that contains
text stays on one line, so indentation never changes character data.
The compact shape is an unordered map, so pretty-printing it does not
preserve sibling order — use the tree shape when that matters.
set doc [xml/parse "<order id=\"A1\"><customer>Ada</customer><paid/></order>" %{ :compact false }] echo [xml/pretty $doc] # <order id="A1"> # <customer>Ada</customer> # <paid/> # </order>
For any value produced by `xml/parse` in either shape,
`[xml/parse [xml/stringify $value]]` parses back to an equal document
(map key order is not significant).
## Examples
# Read a config document from disk and use its values set cfg [get [xml/load "config.xml"] "config"] echo "environment:" [get $cfg "@env"] "database:" [get [get $cfg "database"] "@host"] # Repeated children come back as a vector — map over them set features [get $cfg "feature"] set names $features |> map {|f| [get $f "@name"] } |> collect # names => @{ "auth" "rateLimit" } # CDATA and entities are plain text after parsing [get [xml/parse "<a><![CDATA[x < y & z]]></a>"] "a"] # => "x < y & z" [get [xml/parse "<a><tag></a>"] "a"] # => "<tag>" # Build a document from Deft data, then pretty-print it set spec %{ "server" %{ "@port" "8080" "route" @{ %{ "@path" "/" "@handler" "home" } %{ "@path" "/health" "@handler" "health" } } } } echo [xml/pretty $spec] # Escape arbitrary text for embedding in markup [xml/escape "a < b & c"] # => "a < b & c" [xml/unescape "a < b"] # => "a < b"
## See Also
- Data Formats — `json/` and `yaml/` counterparts
- Maps — nested `~` access and map operations
- Filesystem — `fs/read` / `fs/write` for document files
- Crypto — hashing or signing serialized documents