npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

xmlsax-typescript

v1.0.0

Published

One-pass, streaming (SAX-style) XML parser for TypeScript (Node.js & browsers)

Downloads

1,546

Readme

xmlsax-typescript

TypeScript Node.js zero dependencies Coverage Status

One-pass, streaming (SAX-style) XML parser for TypeScript — works in Node.js and browsers.

Highlights

  • Streaming — feed chunks of XML as they arrive; no need to buffer the whole document
  • Lightweight — zero runtime dependencies, tree-shakeable ESM + CJS
  • Type-safe — written in TypeScript with full type exports
  • Namespace-aware — resolves prefixes, URIs, and local names out of the box
  • Two-way — parse XML to a tree (parseXmlString) or serialize a tree back to XML (serializeXml)
  • Design-by-contract — invariant checks in development, stripped in production

Install

npm install xmlsax-typescript

API Selection Guide

  • Use parseXmlString(xml) when you already have the full XML string and need an XmlNode tree.
  • Use tokenizeXml(xml) for one-shot SAX tokenization of a full string.
  • Use tokenizeXmlAsync(chunks) for async chunk sources.
  • Use XmlSaxParser directly when you need fine-grained feed/close control.
  • Use buildObject(root) to project a parsed tree to a plain object.
  • Use ObjectBuilder to build objects while streaming tokens without materializing a tree.
  • Use objectToXml(obj, options) when converting plain objects directly back to XML.

Quick start

Token streaming (sync)

import { XmlSaxParser, dispatchToken } from "xmlsax-typescript";

const parser = new XmlSaxParser();

for (const token of parser.feed("<root>")) {
  dispatchToken(token, {
    openTag: (openTagToken) => {
      console.log("open", openTagToken.tag.name);
    }
  });
}
for (const token of parser.feed("Hello</root>")) {
  dispatchToken(token, {
    text: (textToken) => {
      console.log("text", textToken.text);
    },
    closeTag: (closeTagToken) => {
      console.log("close", closeTagToken.tag.name);
    }
  });
}
parser.close();

Token streaming (async)

import { OpenTagToken, tokenizeXmlAsync } from "xmlsax-typescript";

async function* chunks(): AsyncGenerator<string> {
  yield "<root><item>1</item>";
  yield "<item>2</item></root>";
}

for await (const token of tokenizeXmlAsync(chunks())) {
  if (token instanceof OpenTagToken) {
    console.log(token.depth, token.path.join("/"));
  }
}

Parse to tree

import { parseXmlString } from "xmlsax-typescript";

const root = parseXmlString("<root><a>1</a><b/></root>");
console.log(root.name); // "root"

Project to plain objects

import { buildObject, parseXmlString } from "xmlsax-typescript";

const root = parseXmlString("<root id='1'><item>1</item><item>2</item></root>");
const obj = buildObject(root);
// { "@_id": "1", item: ["1", "2"] }

Streaming object builder

import { ObjectBuilder, XmlSaxParser, dispatchToken } from "xmlsax-typescript";

const builder = new ObjectBuilder();
const parser = new XmlSaxParser();

const consume = (token: Parameters<typeof dispatchToken>[0]): void => {
  dispatchToken(token, {
    openTag: (openTagToken) => builder.onOpenTag(openTagToken.tag),
    text: (textToken) => builder.onText(textToken.text),
    cdata: (cdataToken) => builder.onCdata(cdataToken.text),
    closeTag: () => builder.onCloseTag()
  });
};

for (const token of parser.feed("<root><item>1</item>")) consume(token);
for (const token of parser.feed("<item>2</item></root>")) consume(token);
for (const token of parser.close()) consume(token);

const obj = builder.getResult();
// { item: ["1", "2"] }

Object to XML

import { objectToXml } from "xmlsax-typescript";

const xml = objectToXml({
  root: {
    "@_id": "1",
    item: ["1", "2"],
  }
});

// <root id="1"><item>1</item><item>2</item></root>
import { buildObject, objectToXml, parseXmlString } from "xmlsax-typescript";

const root = parseXmlString("<root id='1'><item>1</item></root>");
const obj = buildObject(root);
const xml = objectToXml(obj, { rootName: "root" });

// <root id="1"><item>1</item></root>

Serialize to XML

import { serializeXml } from "xmlsax-typescript";

const xml = serializeXml(
  {
    name: "root",
    attributes: { id: "1" },
    children: ["Hello", { name: "child", children: ["World"] }],
  },
  { pretty: true, xmlDeclaration: true },
);
// <?xml version="1.0" encoding="UTF-8"?>
// <root id="1">
//   Hello
//   <child>World</child>
// </root>

API

XmlSaxParser

new XmlSaxParser(options?: ParserOptions)

| Method. | Description | | --------------------- | -------------------------------------------------------------------------------------------- | | feed(chunk) | Feed one XML chunk and return parsed tokens for that chunk | | close() | Finalize parsing, validate state, and return remaining tokens plus EndToken | | drainTokens() | Return and clear buffered tokens (usually empty if you consume feed/close return values) | | [Symbol.iterator]() | Iterate currently buffered tokens | | iterateChunks(src) | Async iterator over an Iterable<string> or AsyncIterable<string> chunk source |

Example for iterateChunks(src):

import { OpenTagToken, XmlSaxParser } from "xmlsax-typescript";

async function* chunks(): AsyncGenerator<string> {
  yield "<root><item>1</item>";
  yield "<item>2</item></root>";
}

const parser = new XmlSaxParser();
for await (const token of parser.iterateChunks(chunks())) {
  if (token instanceof OpenTagToken) {
    console.log(token.tag.name);
  }
}

ParserOptions

| Option | Type | Default | Description | | ---------------------------- | --------- | ------- | ---------------------------------------------- | | xmlns | boolean | true | Enable namespace resolution | | includeNamespaceAttributes | boolean | false | Include xmlns:* attributes in tag output | | allowDoctype | boolean | true | Allow <!DOCTYPE …> declarations | | coalesceText | boolean | true | Merge adjacent text tokens into a single token | | trackPosition | boolean | true | Track line/column; disable for faster parsing |

By default (coalesceText: true), adjacent text chunks are merged and emitted as one TextToken per structural boundary. Set coalesceText: false to keep chunk-level text tokenization.

trackPosition controls line/column tracking for parser errors. When set to false, parsing is faster and XmlSaxError still reports offset, while line and column are set to 0.

Token payload note: with xmlns: false, OpenTagToken and CloseTagToken use plain-mode tag shapes aligned with saxes performance semantics.

  • OpenTagToken.tag.attributes values are strings (not XmlAttribute objects)
  • OpenTagToken.tag and CloseTagToken.tag omit prefix, local, and uri
  • With xmlns: true, full namespace metadata remains present

Tokens

Token classes:

  • OpenTagToken
  • CloseTagToken
  • TextToken
  • CdataToken
  • CommentToken
  • ProcessingInstructionToken
  • DoctypeToken
  • EndToken

All token classes derive from XmlToken and include:

  • kind
  • position ({ offset, line, column } when trackPosition is enabled)

OpenTagToken and CloseTagToken also include:

  • depth
  • path

dispatchToken(token, handlers)

Typed utility to route any token to kind-specific callbacks without instanceof chains.

import { dispatchToken } from "xmlsax-typescript";

dispatchToken(token, {
  openTag: (openTagToken) => console.log(openTagToken.tag.name),
  text: (textToken) => console.log(textToken.text),
  otherwise: (unknownToken) => console.log(unknownToken.kind)
});

isBuildToken(token)

Type guard for structural tokens accepted by TreeBuilder.consume and ObjectBuilder.consume (open-tag, text, cdata, close-tag).

tokenizeXml(xml, options?)

Convenience helper for one-shot tokenization of a complete XML string.

tokenizeXmlAsync(chunks, options?)

Convenience async generator for iterating tokens from an Iterable<string> or AsyncIterable<string> source.

parseXmlString(xml, options?)

Convenience function that parses a complete XML string into an XmlNode tree using XmlSaxParser + TreeBuilder internally.

TreeBuilder

Low-level tree builder. Consume parser tokens via consume(token) and call getRoot() to retrieve the resulting XmlNode.

buildObject(root, options?)

Projects an XmlNode tree into a plain object. Attributes are prefixed (default @_), text is stored under #text, repeated elements are arrays, and elements with only text return the text directly.

ObjectBuilder

Streaming builder that produces the same object shape as buildObject without building a full XmlNode tree. Consume parser tokens via consume(token).

Methods:

| Method | Description | | --- | --- | | consume(...) | Consume token-based events (OpenTag, Text, Cdata, CloseTag) | | onOpenTag(...) | Handle an open tag event directly | | onText(...) | Handle a text event directly | | onCdata(...) | Handle a CDATA event directly | | onCloseTag() | Handle a close-tag event directly | | getResult() | Get the built object | | getRootName() | Get the root element name |

ObjectBuilderOptions

| Option | Type | Default | Description | | ------------------ | ------------------------------------------------------------ | --------- | ---------------------------------------------- | | attributePrefix | string | "@_" | Prefix for attribute keys | | textKey | string | "#text" | Key used for text nodes | | stripNamespaces | boolean | false | Strip namespace prefixes from names | | arrayElements | Set\<string\> \| (name: string, path: string[]) => boolean | — | Force specific elements to always be arrays | | coalesceText | boolean | true | Merge adjacent text nodes into a single string |

buildXmlNode(obj, options?)

Converts a plain object into an XmlNode tree using the same attribute/text conventions as buildObject.

objectToXml(obj, options?)

Builds an XmlNode with buildXmlNode and serializes it with serializeXml.

stripNamespace(name)

Removes namespace prefix from a qualified name.

stripNamespace("p:item"); // "item"
stripNamespace("item"); // "item"

resolveName(value)

Normalizes either a qualified string or a tag-like object into a unified name representation.

resolveName("p:item");
// { name: "p:item", localName: "item", prefix: "p", uri: "" }

resolveName({ name: "p:item", prefix: "p", local: "item", uri: "urn:p" });
// { name: "p:item", localName: "item", prefix: "p", uri: "urn:p" }

XmlBuilderOptions

| Option | Type | Default | Description | | ------------------ | ------------------------------------------------------------ | --------- | ---------------------------------------------- | | attributePrefix | string | "@_" | Prefix for attribute keys | | textKey | string | "#text" | Key used for text nodes | | stripNamespaces | boolean | false | Strip namespace prefixes from names | | arrayElements | Set\<string\> \| (name: string, path: string[]) => boolean | — | Force specific elements to always be arrays | | rootName | string | — | Root element name when object has multiple keys|

serializeXml(node, options?)

Serializes an XmlNode back to an XML string.

SerializeOptions

| Option | Type | Default | Description | | ----------------- | --------- | -------- | ---------------------------------------- | | xmlDeclaration | boolean | false | Prepend <?xml …?> declaration | | pretty | boolean | false | Enable indented output | | indent | string | " " | Indentation string (when pretty) | | newline | string | "\n" | Newline string (when pretty) |

XmlSaxError

Custom error class thrown on parse errors. Includes offset, line, and column properties for precise error location.

XmlSaxBuildError

Structured build-time error thrown by TreeBuilder, ObjectBuilder, and XML object-to-node conversion helpers when consumed in an invalid state.

XmlSaxInvariantError

Debug-only invariant error thrown when an internal design-by-contract check fails.

XmlSaxLibraryError

Base class for non-parse library errors (XmlSaxBuildError, XmlSaxInvariantError).

Error Handling

Typical error handling splits into parser errors and build/usage errors:

import {
  parseXmlString,
  XmlSaxBuildError,
  XmlSaxError,
  XmlSaxLibraryError
} from "xmlsax-typescript";

try {
  parseXmlString("<root><broken></root>");
} catch (error) {
  if (error instanceof XmlSaxError) {
    console.error(`Parse error at ${error.line}:${error.column} (offset ${error.offset})`);
  } else if (error instanceof XmlSaxBuildError) {
    console.error(`Builder usage error (${error.phase})`);
  } else if (error instanceof XmlSaxLibraryError) {
    console.error(`Library error code: ${error.code}`);
  } else {
    throw error;
  }
}

Compatibility and Security Notes

  • Runtime support: Node.js 20+ and modern browsers.
  • XML input is parsed from JavaScript strings.
  • For untrusted XML input, disable doctype parsing:
import { parseXmlString } from "xmlsax-typescript";

const root = parseXmlString(inputXml, { allowDoctype: false });
  • Keep xmlns: true when namespace correctness matters.

Performance Tuning

  • Set trackPosition: false for faster parse throughput if line/column reporting is not required.
  • Keep coalesceText: true when you want fewer text-token allocations.
  • Use streaming APIs (XmlSaxParser, tokenizeXmlAsync) for large inputs.
  • Run npm run bench:parser to compare common parser modes on your machine.

Exported types

XmlTokenKind · XmlAnyToken · XmlBuildToken · XmlTokenHandlers · OpenTag · CloseTag · XmlAttribute · ProcessingInstruction · Doctype · XmlNode · XmlChild · XmlPosition · XmlChunkIterable · ParserOptions · SerializeOptions · ObjectBuilderOptions · ArrayElementSelector · XmlObjectMap · XmlObjectValue · XmlBuilderOptions · XmlInputObject · XmlInputValue · ObjectToXmlOptions

Features

  • Namespace resolution (xmlns)
  • CDATA sections
  • Entity decoding (named + numeric)
  • Processing instructions
  • DOCTYPE handling (parse + emit)
  • Comments
  • Precise error positions (line, column, offset)
  • Pretty-print serialization with XML declaration

Design-by-contract

Internal invariants are checked during development. Set NODE_ENV=production to strip them from production bundles — no runtime overhead.

Development

npm install           # install dependencies
npm run build         # build ESM + CJS with tsup
npm test              # run tests with vitest
npm run test:coverage # run tests with coverage report
npm run test:watch    # run tests in watch mode
npm run bench:parser  # run parser throughput benchmark scenarios
npm run lint          # eslint + tsc type check
npm run typecheck     # run TypeScript type-check only
npm run lint:fix      # auto-fix lint issues
npm run verify        # lint + tests + build + package dry-run