@retorquere/parse-xml
v1.0.0
Published
A fast, safe, compliant XML parser for Node.js and browsers.
Downloads
1
Readme
parse-xml
A fast, safe, compliant XML parser for Node.js and browsers.
Contents
Installation
npm install @rgrove/parse-xml
Features
Returns an object tree representing an XML document.
Works great in browsers. Around 6KB minified and gzipped (depending on how you package it).
Provides helpful, detailed error messages with context when a document is not well-formed.
Mostly conforms to XML 1.0 (Fifth Edition) as a non-validating parser (see below for details).
Passes all relevant tests in the XML Conformance Test Suite.
It's fast.
Not Features
This parser is not a complete implementation of the XML specification because parts of the spec aren't very useful or aren't safe when the XML being parsed comes from an untrusted source. However, those parts of XML that are implemented behave as defined in the spec.
The following XML features are ignored by the parser and are not exposed in the document tree:
- XML declarations
- Document type definitions
- Processing instructions
In addition, the only supported character encoding is UTF-8.
Examples
Basic Usage
const parseXml = require('@rgrove/parse-xml');
parseXml('<kittens fuzzy="yes">I like fuzzy kittens.</kittens>');
Output
{
type: "document",
children: [
{
type: "element",
name: "kittens",
attributes: {
fuzzy: "yes"
},
children: [
{
type: "text",
text: "I like fuzzy kittens."
}
]
}
]
}
Friendly Errors
When something goes wrong, parse-xml throws an error that tells you exactly what happened and shows you where the problem is so you can fix it.
parseXml('<foo><bar>baz</foo>');
Output
Error: Missing end tag for element bar (line 1, column 14)
<foo><bar>baz</foo>
^
In addition to a helpful message, error objects have the following properties:
column Number
Column where the error occurred (1-based).
excerpt String
Excerpt from the input string that contains the problem.
line Number
Line where the error occurred (1-based).
pos Number
Character position where the error occurred relative to the beginning of the input (0-based).
API
parseXml(xml: string, options?: object) => object
Parses an XML document and returns an object tree.
Options
The following options may be provided as properties of the options
argument:
ignoreUndefinedEntities Boolean (default:
false
)When
true
, an undefined named entity like&bogus;
will be left as is instead of causing a parse error.preserveCdata Boolean (default:
false
)When
true
, CDATA sections will be preserved in the document tree as nodes of typecdata
. Otherwise CDATA sections will be represented as nodes of typetext
.preserveComments Boolean (default:
false
)When
true
, comments will be preserved in the document tree as nodes of typecomment
. Otherwise comments will not be included in the document tree.
Nodes
An XML document is parsed into a tree of node objects. Each node has the following common properties:
parent Object?
Reference to this node's parent node, or
null
if this node is thedocument
node (which has no parent).type String
Node type.
Each node also has a toJSON()
method that returns a serializable
representation of the node without the parent
property (in order to avoid
circular references). This means you can safely pass any node to
JSON.stringify()
to serialize it and its children as JSON.
cdata
A CDATA section. Only emitted when the preserveCdata
option is true
(by
default, CDATA sections become text
nodes).
Properties
text String
Unescaped text content of the CDATA section.
Example
<![CDATA[kittens are fuzzy & cute]]>
{
type: "cdata",
text: "kittens are fuzzy & cute",
parent: { ... }
}
comment
A comment. Only emitted when the preserveComments
option is true
.
Properties
content String
Comment text.
Example
<!-- I'm a comment! -->
{
type: "comment",
content: "I'm a comment!",
parent: { ... }
}
document
The top-level node of an XML document.
Properties
children Object[]
Array of child nodes.
Example
<root />
{
type: "document",
children: [
{
type: "element",
name: "root",
attributes: {},
children: [],
parent: { ... }
}
],
parent: null
}
element
An element.
Note that since parse-xml doesn't implement XML Namespaces, no special treatment is given to namespace prefixes in element and attribute names.
In other words, <foo:bar foo:baz="quux" />
will result in the element name
"foo:bar" and the attribute name "foo:baz".
Properties
attributes Object
Hash of attribute names to values.
Attribute names in this object are always in alphabetical order regardless of their order in the document, and values are normalized and unescaped. Values are always strings.
children Object[]
Array of child nodes.
name String
Name of the element as given in the start and/or end tags.
preserveWhitespace Boolean?
This property will be set to
true
if the specialxml:space
attribute on this element or on the closest parent with anxml:space
attribute has the value "preserve". This indicates that whitespace in the text content of this element should be preserved rather than normalized.If neither this element nor any of its ancestors has an
xml:space
attribute set to "preserve", or if the closestxml:space
attribute is set to "default", this property will not be defined.
Example
<kittens description="fuzzy & cute">I <3 kittens</kittens>
{
type: "element",
name: "kittens",
attributes: {
description: "fuzzy & cute"
},
children: [
{
type: "text",
text: "I <3 kittens",
parent: { ... }
}
],
parent: { ... }
}
text
Text content inside an element.
Properties
text String
Unescaped text content.
Example
kittens are fuzzy & cute
{
type: "text"
text: "kittens are fuzzy & cute",
parent: { ... }
}
Why another XML parser?
There are many XML parsers for Node, and some of them are good. However, most of them suffer from one or more of the following shortcomings:
Native dependencies.
Loose, non-standard, "works for me" parsing behavior that can lead to unexpected or even unsafe results when given input the author didn't anticipate.
Kitchen sink APIs that tightly couple a parser with DOM manipulation functions, a stringifier, or other tooling that isn't directly related to parsing.
Stream-based parsing. This is great in the rare case that you need to parse truly enormous documents, but can be a pain to work with when all you want is an object tree.
Poor error handling.
Too big or too Node-specific to work well in browsers.
parse-xml's goal is to be a small, fast, safe, reasonably compliant, non-streaming, non-validating, browser-friendly parser, because I think this is an under-served niche.
I think parse-xml demonstrates that it's not necessary to jettison the spec entirely or to write complex code in order to implement a small, fast XML parser.
Also, it was fun.
Benchmark
Here's how parse-xml stacks up against two comparable libraries, libxmljs (which is based on the native libxml library) and xmldoc (which is based on sax-js).
Node.js v8.0.0 / Darwin x64
Intel(R) Core(TM) i7-6920HQ CPU @ 2.90GHz
Small document (291 bytes)
31,247 op/s » libxmljs (native)
58,804 op/s » parse-xml
33,669 op/s » xmldoc (sax-js)
Medium document (72081 bytes)
661 op/s » libxmljs (native)
408 op/s » parse-xml
211 op/s » xmldoc (sax-js)
Large document (1162464 bytes)
64 op/s » libxmljs (native)
24 op/s » parse-xml
20 op/s » xmldoc (sax-js)
Suites: 3
Benches: 9
Elapsed: 16,828.19 ms
To run this benchmark yourself, clone this repo and run:
npm i && npm run benchmark