htmlparser2-lite
v2.0.3
Published
Fast & forgiving HTML/XML parser for the browser, < 9 KB minified, no dependencies, htmlparser2-compatible
Maintainers
Readme
htmlparser2-lite
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 > 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 & 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: trueuses 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: truedecodes the XML entities and numeric references, which works everywhere.Where there's no DOM (Node, workers), pass a function, e.g. using the
entitiespackage:import { decodeHTML, decodeHTMLAttribute } from "entities"; const dom = parse(html, { decodeEntities: (text, inAttribute) => inAttribute ? decodeHTMLAttribute(text) : decodeHTML(text), });inAttributetells 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©=2stays 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:htmlparser23.x's parser with the same events and optionsparse: builds a DOM compatible withdomhandler2.x (except that in XML mode,<script>and<style>elements have the typetag, like in later versions)serialize:dom-serializerwith a fix for #26- The most useful parts of
domutils - A
createutility function for simple DOM node creation - TypeScript type definitions
Excludes
- Streaming: the whole input is passed to
Parser#end. There are nowrite,parseComplete,reset,pause,resumemethods and no events useful only for streaming:onopentagname,onattribute(useonopentag),oncommentend,onreset,onparserinit,onerror FeedHandler,DomHandler,Tokenizer- The
withDomLvl1option - Some functions from
domutils - Automatic fix-up of mixed-case attribute names when serializing, e.g. of
viewboxin SVG toviewBox. Set thelowerCaseAttributeNamesoption of the parser tofalseto 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, seedecodeEntities),<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. WithrecognizeSelfClosing,<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>bgives 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 bintob, and in XML,<?xintox. - The data of an XML processing instruction includes the final
?(e.g.?xml version="1.0"?), so that it's serialized as it was. startIndexandendIndexare 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
ontextevent 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 atitlehere. 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 < and ", 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"withimport * as htmlparser from "htmlparser2-lite"(or import the functions you use) - Replace
DomHandlerwithparse,getAttribValue(el, name)withel.attribs[name],hasAttrib(el, name)withObject.hasOwn(el.attribs, name),getSiblings(node)withnode.parent.children(for top-level nodes, whoseparentisnull, the array returned byparse) filteris renamed tofilterNodes, so that linters (e.g.eslint-plugin-unicorn) don't mistake it forArray#filter. Itsrecursiveparameter is removed, solimitis the third parameter now: replacefilter(test, nodes, true, limit)withfilterNodes(test, nodes, limit), andfilter(test, nodes, false)with[nodes].flat().filter(test)(nodescan be a node or an array).- Replace
parser.write(a); parser.write(b); parser.end()withparser.end(a + b), andparser.parseComplete(html)withparser.end(html). Handle attributes inonopentaginstead ofonattribute. - See the parsing rules, which have changed
serializeescapes<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 typeroot, pass theirchildreninstead.
Size comparison
The current htmlparser2 on Bundlephobia: https://bundlephobia.com/result?p=htmlparser2
Development
yarn build: buildsdist/htmlparser2-lite.js(UMD) anddist/htmlparser2-lite.mjs(ES module) fromsrc/htmlparser2-lite.js, runs the tests and checks the typesyarn 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 DOMnode fuzz/htmlparser2.js [inputs] [seed]: markup must give the same DOM as withhtmlparser212, except for the differences
