xml
js/format/xml.ts
fino:format/xml - XML 1.0 + Namespaces parser and serializer.
XML is a structured markup format used for documents, feeds, config files, protocols, and interchange with older systems. This module parses XML into a compact document tree, streams SAX-style events from async byte sources, and serializes document trees back to XML text.
The parser enforces XML well-formedness and XML Namespaces rules. It parses
internal DTD entity definitions for expansion, but it is not a validating
DTD processor. External entities are disabled by default to avoid XXE
vulnerabilities. Entity expansion and nesting depth are bounded to reduce
billion-laughs style attacks. Callers that supply resolveExternalEntities
are responsible for their own network, filesystem, and trust boundaries.
Serialization is structural, not text-exact. The serializer emits the
document root, escapes text and attributes, and can add an XML declaration,
but it does not preserve prolog nodes, trailing comments or processing
instructions, original entity spelling, or namespace declaration attributes
consumed during namespace resolution. Use namespaces: false when a caller
needs namespace declaration attributes to remain ordinary attributes.
parseStream() is a convenience SAX-style surface over async byte sources.
It reparses accumulated input until a complete document is available and is
therefore not a true bounded-memory streaming parser for very large XML.
XML 1.0 / Namespaces conformance matrix:
| Area | Status |
|---|---|
| Document/root structure | Enforces exactly one root element and rejects element content after the root. |
| Names and QNames | Validates XML names, namespace QNames, unbound prefixes, and reserved xml/xmlns namespace use. |
| Duplicate attributes | Rejects duplicate raw attributes and duplicate expanded names after namespace resolution. |
| Entity/reference handling | Expands predefined, numeric, and internal DTD entities; rejects undefined, recursive, invalid-character, and external entities unless explicitly resolved. |
| Comments, CDATA, PI, and prolog | Parses comments, CDATA, processing instructions, and supported prolog nodes while enforcing XML character validity and comment/CDATA delimiters. |
| XML declaration | Parses and skips a leading XML declaration; xml processing-instruction targets outside that declaration path are rejected. |
| Serializer normalization | Emits structural XML, escapes text and attributes, and can add a declaration; it does not preserve entity spelling, consumed namespace declarations, or text-exact prolog/trailer nodes. |
parseStream() limits |
Emits SAX-style events after reparsing accumulated input; it is not a bounded-memory streaming parser. |
| XXE/entity/depth safety | External entities are disabled by default, entity expansion is capped, recursive entities are rejected, and element depth is bounded. |
| Intentional non-goals | DTD validation and text-exact prolog round-tripping are outside the supported surface. |
Two output surfaces: - Tree (DOM-lite): parse(input) -> XmlDocument - SAX/streaming: parseStream(src) -> AsyncIterableIterator<XmlEvent>
import { parse, stringify } from 'fino:format/xml';
const doc = parse('<root attr="v"><child>text</child></root>');
doc.root.name; // 'root'
doc.root.children[0].type; // 'element'
const xml = stringify(doc, { xmlDeclaration: true });
import { parseStream } from 'fino:format/xml';
for await (const event of parseStream(byteSource)) {
if (event.type === 'startElement') console.log(event.name);
}
Useful references: - XML 1.0: https://www.w3.org/TR/xml/ - Namespaces in XML: https://www.w3.org/TR/xml-names/ - OWASP XXE guidance: https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing
Classes
class XmlParseError extends ParseError {
Error thrown when XML input is not well-formed or violates parser limits.
The error inherits source location and diagnostic rendering from
ParseError. External entity rejection, entity expansion limits, nesting
depth limits, undefined entities, and mismatched close tags are reported
through this type.
import { XmlParseError, parse } from 'fino:format/xml';
try {
parse('<root>');
} catch (error) {
if (error instanceof XmlParseError) console.error(error.render());
}
Properties
name
Error name reported by XmlParseError instances.
Always the string "XmlParseError", which distinguishes XML errors from
other ParseError subtypes in logs and error.name checks without an
instanceof test.
import { parse } from 'fino:format/xml';
try {
parse('<root>');
} catch (error) {
if ((error as Error).name === 'XmlParseError') console.error('bad XML');
}
Interfaces
interface XmlDocument {
Parsed XML document tree.
A document contains exactly one root element. Processing instructions,
comments, and doctypes before the root are preserved in prolog; trailing
processing instructions and comments are accepted but not retained.
import { parse, type XmlDocument } from 'fino:format/xml';
const document: XmlDocument = parse('<?xml version="1.0"?><root />');
document.root.name;
Properties
type: 'document'
Discriminator for XML document values.
Always the string literal "document".
import { parse } from 'fino:format/xml';
parse('<root />').type;
root: XmlElement
Root element of the document.
XML requires exactly one root element; missing or additional element content throws during parsing.
import { parse } from 'fino:format/xml';
const root = parse('<root><child /></root>').root;
prolog: XmlNode[]
Processing instructions, comments, and doctypes before the root element.
The XML declaration is parsed and skipped rather than added as a prolog processing instruction.
import { parse } from 'fino:format/xml';
const prolog = parse('<!-- note --><root />').prolog;
interface XmlElement {
Element node with namespace metadata, attributes, and child nodes.
With namespace processing enabled, name is the local name, prefix
contains the source prefix when present, and namespace contains the
resolved namespace URI or null.
import { parse, type XmlElement } from 'fino:format/xml';
const element: XmlElement = parse('<x:root xmlns:x="urn:x" />').root;
element.namespace; // 'urn:x'
Properties
type: 'element'
Discriminator for element nodes.
Always the string literal "element".
import { parse } from 'fino:format/xml';
parse('<root />').root.type;
name: string
Element name.
When namespaces are enabled, this is the local name without prefix. When
namespaces: false, this preserves the qualified source name.
import { parse } from 'fino:format/xml';
parse('<x:root xmlns:x="urn:x" />').root.name;
prefix: string | null
Source namespace prefix, or null when absent or namespace processing is disabled.
Prefixes are preserved for serialization, but namespace declaration
attributes themselves are not included in attributes.
import { parse } from 'fino:format/xml';
parse('<x:root xmlns:x="urn:x" />').root.prefix;
namespace: string | null
Resolved namespace URI, or null when none is in scope.
Set namespaces: false to disable namespace resolution entirely.
import { parse } from 'fino:format/xml';
parse('<root xmlns="urn:default" />').root.namespace;
attributes: Record<string, string>
Element attributes as string values.
Attribute entity references are expanded. Namespace declaration attributes are consumed for namespace resolution and omitted from this record.
import { parse } from 'fino:format/xml';
parse('<root id="a" />').root.attributes.id;
children: XmlNode[]
Child nodes in source order.
Text nodes may be omitted when trim removes all text content. Self-closing
elements have an empty child array.
import { parse } from 'fino:format/xml';
parse('<root>text<child /></root>').root.children;
interface XmlText {
Text node.
Entity and character references have already been expanded. When
XmlParseOptions.trim is true, all-whitespace text nodes are omitted.
import { parse, type XmlText } from 'fino:format/xml';
const node = parse('<root>hello</root>').root.children[0] as XmlText;
node.data;
Properties
type: 'text'
Discriminator for text nodes.
Always the string literal "text".
import { parse } from 'fino:format/xml';
parse('<root>hello</root>').root.children[0]?.type;
data: string
Text content after entity expansion.
The string is not HTML-escaped; escape it when embedding in another output format.
import { parse } from 'fino:format/xml';
(parse('<root>&</root>').root.children[0] as { data: string }).data;
interface XmlCData {
CDATA node.
CDATA contents are returned as text data without interpreting markup inside the section. The serializer emits the data inside a CDATA section.
import { parse, type XmlCData } from 'fino:format/xml';
const node = parse('<root><![CDATA[<x>]]></root>').root.children[0] as XmlCData;
node.data;
Properties
type: 'cdata'
Discriminator for CDATA nodes.
Always the string literal "cdata".
import { parse } from 'fino:format/xml';
parse('<root><![CDATA[x]]></root>').root.children[0]?.type;
data: string
Raw CDATA section content.
The closing ]]> delimiter is not included.
import { parse } from 'fino:format/xml';
(parse('<root><![CDATA[x]]></root>').root.children[0] as { data: string }).data;
interface XmlComment {
XML comment node.
Comment text excludes the <!-- and --> delimiters. XML forbids --
inside comments, and such input throws during parsing.
import { parse, type XmlComment } from 'fino:format/xml';
const comment = parse('<!-- note --><root />').prolog[0] as XmlComment;
comment.data;
Properties
type: 'comment'
Discriminator for comment nodes.
Always the string literal "comment".
import { parse } from 'fino:format/xml';
parse('<!-- note --><root />').prolog[0]?.type;
data: string
Comment content without delimiters.
The string is not escaped. Do not insert untrusted comments into another document format without escaping.
import { parse } from 'fino:format/xml';
(parse('<!-- note --><root />').prolog[0] as { data: string }).data;
interface XmlPI {
Processing instruction node.
The XML declaration is parsed but skipped from document prolog; other
processing instructions are retained with their target and data.
import { parse, type XmlPI } from 'fino:format/xml';
const pi = parse('<?xml-stylesheet href="style.css"?><root />').prolog[0] as XmlPI;
pi.target;
Properties
type: 'pi'
Discriminator for processing instruction nodes.
Always the string literal "pi".
import { parse } from 'fino:format/xml';
parse('<?go now?><root />').prolog[0]?.type;
target: string
Processing instruction target.
The target keeps its source spelling. Targets that match xml
case-insensitively are reserved: they are consumed as the XML declaration
at the start of a document and rejected anywhere else.
import { parse } from 'fino:format/xml';
(parse('<?go now?><root />').prolog[0] as { target: string }).target;
data: string
Processing instruction data after the target.
Surrounding whitespace is trimmed by the parser. Empty processing instructions use an empty string.
import { parse } from 'fino:format/xml';
(parse('<?go now?><root />').prolog[0] as { data: string }).data;
interface XmlDoctype {
Doctype declaration node.
Internal entity declarations are parsed for expansion. External entities are rejected unless a resolver is explicitly provided in parse options.
import { parse, type XmlDoctype } from 'fino:format/xml';
const doc = parse('<!DOCTYPE root><root />').prolog[0] as XmlDoctype;
doc.data;
Properties
type: 'doctype'
Discriminator for doctype nodes.
Always the string literal "doctype".
import { parse } from 'fino:format/xml';
parse('<!DOCTYPE root><root />').prolog[0]?.type;
data: string
Doctype declaration content without the <!DOCTYPE wrapper.
Internal subset details are preserved only as parser output text, not as a rich DTD model.
import { parse } from 'fino:format/xml';
(parse('<!DOCTYPE root><root />').prolog[0] as { data: string }).data;
interface XmlParseOptions {
Options controlling XML parsing, namespaces, and entity expansion limits.
Defaults enable namespaces, preserve text whitespace, reject external entities, cap entity expansion at 1,000,000 characters, and cap nesting depth at 500 elements.
import { parse, type XmlParseOptions } from 'fino:format/xml';
const options: XmlParseOptions = { trim: true, maxDepth: 100 };
parse('<root> text </root>', options);
Properties
namespaces?: boolean
Enable XML namespace resolution. Defaults to true.
When disabled, element names preserve their qualified source names and
prefix and namespace are null.
import { parse } from 'fino:format/xml';
parse('<x:r xmlns:x="urn:x" />', { namespaces: false }).root.name;
trim?: boolean
Trim text nodes and omit empty text after trimming. Defaults to false.
CDATA content and comments are not trimmed by this option.
import { parse } from 'fino:format/xml';
parse('<root> text </root>', { trim: true }).root.children[0];
maxEntityExpansion?: number
Maximum expanded character count from XML entity references.
Defaults to 1_000_000. Lower this for untrusted inputs that should fail
quickly on repeated entity expansion.
import { parse } from 'fino:format/xml';
parse('<root>&</root>', { maxEntityExpansion: 10 });
maxDepth?: number
Maximum nested element depth. Defaults to 500.
Deeply nested documents throw once this limit is exceeded.
import { parse } from 'fino:format/xml';
parse('<root><child /></root>', { maxDepth: 10 });
resolveExternalEntities?: ((systemId: string) => string | null) | null
Optional resolver for external DTD entities.
Defaults to null, which rejects external entities. A resolver receives
the entity's system identifier and returns the replacement text, or null
to leave the entity undefined — referencing an undefined entity later
still throws. Supplying a resolver opts into any filesystem, network, and
trust-boundary risks it performs.
import { parse } from 'fino:format/xml';
parse('<!DOCTYPE r [<!ENTITY e SYSTEM "safe">]><r>&e;</r>', {
resolveExternalEntities: (systemId) => systemId === 'safe' ? 'ok' : null,
});
interface XmlStringifyOptions {
Options controlling XML serialization.
Serialization escapes text and attribute values, optionally pretty-prints element children, and emits an XML declaration unless disabled.
import { stringify, parse, type XmlStringifyOptions } from 'fino:format/xml';
const options: XmlStringifyOptions = { indent: ' ', xmlDeclaration: true };
stringify(parse('<root />'), options);
Properties
indent?: string
Indentation string for nested elements. Defaults to "".
An empty string emits compact XML without added newlines between child nodes. A non-empty value inserts newlines around nested children.
import { parse, stringify } from 'fino:format/xml';
stringify(parse('<root><child /></root>'), { indent: ' ' });
xmlDeclaration?: boolean
Whether to emit the XML declaration. Defaults to true.
Set to false when embedding XML fragments or when a caller provides its
own declaration.
import { parse, stringify } from 'fino:format/xml';
stringify(parse('<root />'), { xmlDeclaration: false });
Types
type XmlNode = XmlElement | XmlText | XmlCData | XmlComment | XmlPI | XmlDoctype
Any non-document XML node returned in the tree model.
Use the type discriminator before accessing node-specific fields.
import { parse, type XmlNode } from 'fino:format/xml';
const node: XmlNode | undefined = parse('<root>text</root>').root.children[0];
if (node?.type === 'text') console.log(node.data);
type XmlEvent = {
type: 'startElement';
name: string;
prefix: string | null;
namespace: string | null;
attributes: Record<string, string>;
} | {
type: 'endElement';
name: string;
} | {
type: 'text';
data: string;
} | {
type: 'cdata';
data: string;
} | {
type: 'comment';
data: string;
} | {
type: 'pi';
target: string;
data: string;
}
SAX-style parse event emitted by parseStream().
Events are yielded in document order after the prolog. Start element events contain resolved namespace metadata and attributes; end element events contain only the element name.
import { parseStream, type XmlEvent } from 'fino:format/xml';
async function handle(event: XmlEvent) {
if (event.type === 'text') console.log(event.data);
}
Functions
function parse(input: string | Uint8Array, options: XmlParseOptions = {}): XmlDocument
Parse XML input into a document tree.
Input may be a string or UTF-8 bytes. Well-formedness, namespace resolution, entity expansion limits, and nesting depth are enforced during parsing.
import { parse } from 'fino:format/xml';
const doc = parse('<root><child /></root>');
async function* parseStream(
src: AsyncIterable<Uint8Array>,
options: XmlParseOptions = {
}
): AsyncIterableIterator<XmlEvent>
Parse XML bytes into SAX-style events.
The stream parser reparses accumulated bytes until a complete document is available and yields only newly observed events. Errors thrown after the source is exhausted are real parse errors, while earlier chunk-boundary stalls are retried with more input. Because accumulated bytes are retained and reparsed, this is not a bounded-memory streaming parser for very large documents.
Prolog nodes (XML declaration, comments, doctype, processing instructions
before the root) are consumed but not emitted as events. Entity expansion
limits apply as in parse(), but the maxDepth option is not enforced on
this event path.
import { parseStream } from 'fino:format/xml';
async function* source() {
yield new TextEncoder().encode('<root>');
yield new TextEncoder().encode('<child /></root>');
}
for await (const event of parseStream(source())) {
console.log(event.type);
}
function stringify(doc: XmlDocument, options: XmlStringifyOptions = {}): string
Serialize an XML document tree.
The serializer emits the document root and ignores prolog nodes. Text and
attribute values are escaped, CDATA and comments are emitted as stored, and
an XML declaration is included unless xmlDeclaration: false is set.
Serializer output is normalized: it does not preserve source entity spelling,
trailing document comments or processing instructions, or namespace
declaration attributes removed during namespace resolution.
import { parse, stringify } from 'fino:format/xml';
const xml = stringify(parse('<root attr="&">text</root>'), {
xmlDeclaration: false,
});