# 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; `&lt;` 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 (`&lt; &gt; &amp; &apos; &quot;`) and numeric (`&#65;`, `&#x41;`) 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>&lt;tag&gt;</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 &lt; b &amp; c"
[xml/unescape "a &lt; 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