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

htmlparser2-lite

v2.0.3

Published

Fast & forgiving HTML/XML parser for the browser, < 9 KB minified, no dependencies, htmlparser2-compatible

Readme

htmlparser2-lite

npm npm bundle size (minified) npm bundle size (minified + gzip)

Fast & forgiving HTML/XML parser for the browser, < 9 KB minified (4.4 KB gzipped), no dependencies

A compact reimplementation of htmlparser2 and friends: the API of htmlparser2 3.x with the parsing rules of htmlparser2 12. It passes the htmlparser2 3.x and domhandler 2.x test suites, except for the unsupported features listed below and the cases where the parsing rules have changed since.

Usage

import { parse, serialize, findAll } from "htmlparser2-lite";

const dom = parse('<p>Hello <a href="/x">world</a>');
for (const link of findAll("a", dom)) link.attribs.target = "_blank";
serialize(dom); // '<p>Hello <a href="/x" target="_blank">world</a></p>'

Also works with require("htmlparser2-lite"), AMD, and as a <script> that defines the htmlparser global:

<script src="https://unpkg.com/htmlparser2-lite"></script>
<script>
  const dom = htmlparser.parse("<p>Hello</p>");
</script>

Options

parse(markup, options) and new Parser(handler, options):

| Option | Default | Effect | | --- | --- | --- | | xmlMode | false | Parse XML: <script>, <title> etc. contain markup, no elements are void or closed implicitly, /> closes elements, CDATA sections are recognized, <!x> and <?x?> are directives | | lowerCaseTags | true in HTML, false in XML | Lowercase tag names | | lowerCaseAttributeNames | true in HTML, false in XML | Lowercase attribute names | | recognizeCDATA | false (always on in XML) | Parse CDATA sections as CDATA instead of comments (or, in SVG and MathML, text) | | recognizeSelfClosing | false (always on in XML, SVG and MathML) | Close elements written as self-closing (<x/>) | | decodeEntities | false | Decode character references, see below |

Here and below, "in SVG and MathML" means inside <svg> and <math>, except inside <foreignObject>, <desc>, <title>, <mi>, <mo>, <mn>, <ms>, <mtext> and <annotation-xml> there, whose content is HTML again.

parse only:

| Option | Default | Effect | | --- | --- | --- | | normalizeWhitespace | false | Replace each run of whitespace in text with a single space | | withStartIndices | false | Set startIndex of nodes: the index of their first character in the input | | withEndIndices | false | Set endIndex of nodes: the index of their last character (for elements, of their end tag) |

serialize(dom, options):

| Option | Default | Effect | | --- | --- | --- | | xmlMode | false | true: output XML. "foreign": output HTML as inside SVG or MathML (which false switches to inside <svg> and <math>) | | spaceInSelfClosing | false | Write self-closing elements as <x /> instead of <x/> | | decodeEntities | false | For a DOM parsed with decodeEntities: escape &, <, > in text and &, <, " in attribute values (not just < and ") |

The text of the elements whose text is raw in HTML (<script>, <style>, <xmp>, <iframe>, <noembed>, <noframes>, <plaintext>) isn't escaped, except in SVG and MathML. That's in XML output too, unless decodeEntities is set, so that HTML can be written with XML syntax (e.g. parsed with recognizeSelfClosing and serialized with xmlMode: true).

Nodes that are part of a DOM are written as in the whole DOM, e.g. the content of an element inside <svg> as SVG.

So that the output is parsed into the same elements, elements like <foreignObject> aren't self-closing (as when parsing), > is escaped as &gt; in comments where it would end them early (e.g. <![CDATA[-->]]> is a comment in HTML), and an attribute name starting with = is written after a / (not to be the value of the attribute before it).

See the type definitions for details.

Decoding character references

By default, character references like &amp; stay as they are. With the decodeEntities option, they're decoded in text and attribute values (not in comments, CDATA, and in HTML, not in the elements whose text is raw, like <script> and <style>):

  • In browsers, decodeEntities: true uses the browser's own parser, so no table of HTML's more than 2000 named references needs to be bundled. They come out exactly as the browser decodes them in the same markup, quirks included (e.g. Chromium turns &#x; into �).

  • In XML mode, decodeEntities: true decodes the XML entities and numeric references, which works everywhere.

  • Where there's no DOM (Node, workers), pass a function, e.g. using the entities package:

    import { decodeHTML, decodeHTMLAttribute } from "entities";
    
    const dom = parse(html, {
      decodeEntities: (text, inAttribute) =>
        inAttribute ? decodeHTMLAttribute(text) : decodeHTML(text),
    });

    inAttribute tells attribute values from text, because HTML decodes them differently: in attribute values, a named reference without ; followed by = or a letter or digit isn't decoded, so that URLs keep their query parameters (?a=1&copy=2 stays as it is, while in text it's ?a=1©=2).

To serialize a decoded DOM, pass decodeEntities: true to serialize as well. By default, it escapes only < in text and " in attribute values, because text that wasn't decoded is still escaped. With the option, it escapes & and > too (in attribute values, & and <), so that decoded text is escaped again.

Includes

  • Parser: htmlparser2 3.x's parser with the same events and options
  • parse: builds a DOM compatible with domhandler 2.x (except that in XML mode, <script> and <style> elements have the type tag, like in later versions)
  • serialize: dom-serializer with a fix for #26
  • The most useful parts of domutils
  • A create utility function for simple DOM node creation
  • TypeScript type definitions

Excludes

  • Streaming: the whole input is passed to Parser#end. There are no write, parseComplete, reset, pause, resume methods and no events useful only for streaming: onopentagname, onattribute (use onopentag), oncommentend, onreset, onparserinit, onerror
  • FeedHandler, DomHandler, Tokenizer
  • The withDomLvl1 option
  • Some functions from domutils
  • Automatic fix-up of mixed-case attribute names when serializing, e.g. of viewbox in SVG to viewBox. Set the lowerCaseAttributeNames option of the parser to false to retain the casing. (The names of SVG elements get their case when parsing, see the parsing rules.)

Parsing rules

The parsing rules are those of htmlparser2 12, which follows the HTML spec more closely than 3.x did. Compared to 3.x, in HTML:

  • <!--> and <!---> are empty comments, and comments also end with --!>
  • <!…> (except for <!doctype…>), <?…>, and </ followed by anything but a letter or > start comments ending at the next > (e.g. <?xml?> is the comment ?xml?). </> is ignored.
  • Tags start with a letter, so <1> is text
  • The content of <title>, <textarea> (decoded, see decodeEntities), <xmp>, <iframe>, <noembed>, <noframes> and <plaintext> (till the end of the input) is text, like that of <script> and <style>. In SVG and MathML, all of them contain markup, <script> and <style> too. With recognizeSelfClosing, <script/> etc. are empty.
  • <![CDATA[…]]> is text in SVG and MathML
  • Unterminated <![CDATA[ is a comment till the end of the input
  • More elements are closed implicitly (e.g. <p> by <div>, <dt> by <dd>, <td> by <th>, <thead> by <tbody>)
  • A <form> in another one is ignored
  • SVG element names get their case (e.g. clipPath), and <image> outside SVG and MathML is <img>
  • An <svg/> closed by /> doesn't make /> close the elements after it

Differences from htmlparser2 12

  • Implied closes follow the development version of htmlparser2 (unreleased as of 12.0.0): <th> closes an open <td> too, and <thead>, <tbody> and <tfoot> close open table sections, rows and cells (e.g. <table><td>a<th>b gives sibling cells, not a <th> inside the <td>).
  • An unterminated tag at the end of the input, and in XML, an unterminated <!…> or <?…?> there, is dropped. htmlparser2 turns a part of it into text in some cases, e.g. </a b into b, and in XML, <?x into x.
  • The data of an XML processing instruction includes the final ? (e.g. ?xml version="1.0"?), so that it's serialized as it was.
  • startIndex and endIndex are the exact range of each node's markup, and an element closed implicitly by a start tag ends right before that tag. In htmlparser2, the ranges are off in some cases.
  • Text comes in one ontext event up to the next markup, including any < that doesn't start markup. htmlparser2 can split it, e.g. at such a <.
  • With lowerCaseTags: false, the end tag that ends the text of <script>, <style> etc. closes the element whatever the case of their names (e.g. </style> closes <STYLE>). In htmlparser2, the markup after it is then inside the element.
  • In XML, </ followed by / is text. htmlparser2 makes <//x> the comment /x.

Security

htmlparser2-lite isn't a sanitizer. It follows the parsing rules of htmlparser2 12, not the tree construction rules of the HTML spec, so browsers can parse markup that looks harmless in its DOM into other elements, also after serialize. For untrusted HTML that is rendered, use a sanitizer that uses the browser's own parser, e.g. DOMPurify in the browser. For example:

  • <noscript><p title="</noscript><img src=x onerror=alert(1)>"> is a <p> with a title here. With scripting enabled, browsers parse the content of <noscript> as text, which </noscript> ends, so <img> is an element.
  • <svg><p><style><!--</style><img src=x onerror=alert(1)>--> is a comment in <style> here. In browsers, <p> leaves SVG, so the content of <style> is text, which </style> ends, so <img> is an element.

What serialize does ensure is that for a DOM returned by parse and not modified, htmlparser2-lite parses the output into the same elements and attributes, if serialize gets xmlMode: true for a DOM parsed with it and decodeEntities: true for one parsed with decodeEntities, and the output is parsed with the options that the DOM was parsed with (plus recognizeSelfClosing for HTML written with xmlMode: true). Exceptions: with decodeEntities off, < in text and " in attribute values come back as &lt; and &quot;, and in XML, text that ends with < or </ at the end of the input in <script>, <xmp> etc. is written raw before the end tag (<script>< gives <script><</script>, which decodeEntities on for both avoids). A modified DOM can be written into other elements, e.g. </script> in the text of a <script> ends it.

Performance

On real-world pages, it builds a DOM about 1.5× as fast as its predecessor, htmlparser2-20kb (from about 1.1× on some large pages to 3× on feeds), parses events only about 1.7× as fast, and serializes about 1.4× as fast. Markup with many tags and little text, like templates, parses about 1.1× to 1.2× as fast.

Time grows linearly with the size of the input for any markup, so even malicious HTML can't make parsing hang, and elements can be nested arbitrarily deep. The same goes for serialize, remove and create when they get many nodes at once, so pass all the nodes to remove in one call rather than removing them one by one.

Browser support

The code is ES2022, it targets Baseline Widely available browsers and their downstream browsers (browserslist query baseline widely available with downstream).

Migrating from htmlparser2-20kb

  • The ES module has named exports only: replace import htmlparser from "htmlparser2-20kb" with import * as htmlparser from "htmlparser2-lite" (or import the functions you use)
  • Replace DomHandler with parse, getAttribValue(el, name) with el.attribs[name], hasAttrib(el, name) with Object.hasOwn(el.attribs, name), getSiblings(node) with node.parent.children (for top-level nodes, whose parent is null, the array returned by parse)
  • filter is renamed to filterNodes, so that linters (e.g. eslint-plugin-unicorn) don't mistake it for Array#filter. Its recursive parameter is removed, so limit is the third parameter now: replace filter(test, nodes, true, limit) with filterNodes(test, nodes, limit), and filter(test, nodes, false) with [nodes].flat().filter(test) (nodes can be a node or an array).
  • Replace parser.write(a); parser.write(b); parser.end() with parser.end(a + b), and parser.parseComplete(html) with parser.end(html). Handle attributes in onopentag instead of onattribute.
  • See the parsing rules, which have changed
  • serialize escapes < in the text of <noscript>, and of <script>, <style> etc. in SVG and MathML (their content is markup when parsing), and leaves the text of <SCRIPT> etc. raw whatever the case of the name. It doesn't self-close empty <foreignObject>, <desc>, <mi> etc., and escapes > in comments where it would end them early. It ignores nodes of type root, pass their children instead.

Size comparison

The current htmlparser2 on Bundlephobia: https://bundlephobia.com/result?p=htmlparser2

Development

  • yarn build: builds dist/htmlparser2-lite.js (UMD) and dist/htmlparser2-lite.mjs (ES module) from src/htmlparser2-lite.js, runs the tests and checks the types
  • yarn test: runs the tests against the built files, including small seeded versions of the fuzzers (test/round-trip.js, test/htmlparser2.js)
  • yarn fuzz: runs the fuzzers against the built files, which report the smallest inputs that fail:
    • node fuzz/round-trip.js [inputs] [seed]: markup parsed, serialized and parsed again must give the same DOM
    • node fuzz/htmlparser2.js [inputs] [seed]: markup must give the same DOM as with htmlparser2 12, except for the differences