posthtml-parser
posthtml-parser synchronously converts an HTML or XML string into PostHTML's compact tree format. The root is an array; text, comments, and recognized directives are strings, while elements are objects with tag, optional attrs, optional content, and optional location. It wraps htmlparser2 and preserves tag case, attribute case, and entities by default. This is the parser layer for PostHTML transforms, not a DOM, selector engine, validator, sanitizer, template compiler, or renderer.
posthtml-parser is the right narrow tool when PostHTML tree compatibility is the requirement. For general HTML inspection use a queryable DOM parser, and for standards-focused parsing plus serialization use parse5.
Use it if
- You are writing a PostHTML plugin or another transform that already consumes the PostHTML tree shape
- You want a small synchronous parser with TypeScript types and direct access to htmlparser2 parsing options
- Your markup includes custom tags, PHP-like processing directives, XML, CDATA, or valueless attributes that need explicit parser controls
- You need optional line and column locations on element nodes for diagnostics or source-aware transforms
- You need browser-DOM methods, CSS selectors, parent links, or mutation helpers: the tree is plain arrays and objects, and open issues #81 and pull request #82 track the still-missing parent property
- You need HTML-standard tree construction and matching serialization: this package delegates parsing to htmlparser2 and emits the PostHTML shape, while parse5 is the better fit for specification-oriented parsing and serialization
- You need streaming over very large documents: the public parser accepts one string, writes it into an internal htmlparser2 parser, ends it immediately, and returns the entire tree synchronously
- You need source spans for every token: sourceLocations is attached only to element objects in the type definitions and source; text, comments, and directives remain strings without locations
- You pass Buffers directly or require Node older than 16: the package declares a string input and Node 16 minimum, and open issue #73 requests Buffer detection and conversion that the current API does not perform
Setup reality
Install with npm install posthtml-parser. Version 0.12.1 requires Node 16 or newer and has one runtime dependency, htmlparser2 9.x. There are no peers, native extensions, credentials, config files, or background services. The published build is CommonJS even though the README uses ESM syntax. Both import { parser } from 'posthtml-parser' through Node interop and const { parser } = require('posthtml-parser') are valid; there is no default export, a change recorded in the 0.10.0 changelog. TypeScript declarations ship through dist/index.d.ts and include the parser options and tree node types. Input must be a string, so convert fs.readFile buffers with toString('utf8'). Parsing is synchronous and returns a complete array, which is simple in build tools but can block the process on large files. Defaults are deliberately conservative: tag and attribute names keep their case, entities stay encoded, XML mode is off, and valueless attributes become empty strings. Comments and recognized processing directives are stored as literal strings, not distinct typed nodes. Empty attrs and content keys may be omitted, so walkers must check whether a node is a string or object and whether content is an array. Enabling sourceLocations adds one-based line and column data to element nodes, but implied HTML closures and absent locations on text mean it is not a token map. xmlMode changes void-tag, script/style, CDATA, and self-closing behavior, so it is not just a formatting switch. Custom server-template syntax may need directives with exact start and end markers. Parsing alone does not turn a changed tree back into markup; install posthtml-render or use the complete posthtml processor for that half. Finally, decodeEntities changes actual text and attribute values, and lowerCaseAttributeNames has a documented speed cost, so make those choices based on the consumer rather than enabling every option at once.
Patterns
Parse HTML into a PostHTML treeparse-html-string
import { parser } from 'posthtml-parser';
const tree = parser('<main><h1>Hello</h1></main>');
console.dir(tree, { depth: null });parser is synchronous and returns an array whose entries are text strings or element objects.
Load the named export from CommonJSrequire-commonjs
const { parser } = require('posthtml-parser');
const tree = parser('<p>CommonJS</p>');Version 0.10.0 removed the default parser export; destructure the named parser export.
Parse an HTML fileparse-file
import { readFile } from 'node:fs/promises';
import { parser } from 'posthtml-parser';
const html = await readFile('input.html', 'utf8');
const tree = parser(html);Pass an encoding to readFile or call buffer.toString('utf8'); the declared parser input is a string, not a Buffer.
Walk every element recursivelywalk-tree
function walk(nodes, visit) {
for (const node of nodes) {
if (typeof node !== 'object' || node === null) continue;
visit(node);
if (Array.isArray(node.content)) walk(node.content, visit);
}
}
walk(tree, (node) => console.log(node.tag));Text, comments, and directives are strings, and content may be absent; the tree has no built-in walk method or parent pointer.
Collect link targetscollect-links
const links = [];
walk(tree, (node) => {
if (node.tag === 'a' && typeof node.attrs?.href === 'string') {
links.push(node.attrs.href);
}
});Attribute values can also be numbers or booleans in the exported type, so narrow the value before string operations.
Change attributes in the parsed treeedit-attributes
walk(tree, (node) => {
if (node.tag === 'a') {
node.attrs = { ...node.attrs, rel: 'noopener noreferrer' };
}
});Mutation changes the in-memory tree only; use posthtml-render or the full posthtml package to produce HTML afterward.
Decode character entitiesdecode-entities
const tree = parser('<p title="Tom & Ada">A < B</p>', {
decodeEntities: true,
});decodeEntities defaults to false, so enabling it changes the stored text and attribute values rather than only parser metadata.
Lowercase tag and attribute namesnormalize-name-case
const tree = parser('<My-Card DATA-ID="7"></My-Card>', {
lowerCaseTags: true,
lowerCaseAttributeNames: true,
});Both options default to false, lowerCaseTags does not apply in XML mode, and the README warns that lowercasing attribute names has a noticeable speed cost.
Parse XML-style markupparse-xml
const tree = parser('<feed><entry id="1" /></feed>', {
xmlMode: true,
});XML mode affects case, self-closing elements, CDATA, void elements, and script/style handling; it is a parsing-semantics choice.
Attach line and column locationstrack-source-locations
const tree = parser('<section>
<h2>Title</h2>
</section>', {
sourceLocations: true,
});
console.log(tree[0].location);Locations are one-based and exist on element objects only; text, comment, and directive strings receive no span.
Preserve PHP processing directivesrecognize-directives
const tree = parser('<?php echo "Hello"; ?><p>Hi</p>', {
directives: [
{ name: '?php', start: '<', end: '>' },
],
});A recognized directive is stored as its literal source string; use a RegExp name when several processing-instruction forms share markers.
Distinguish a valueless attributepreserve-valueless-attrs
const tree = parser('<input disabled value="">', {
recognizeNoValueAttribute: true,
});
// attrs.disabled is true; attrs.value is ''Without this option, a valueless attribute is stored as an empty string, making it indistinguishable from an explicitly empty value.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| htmlparser2 | npm | Choose it for lower-level callbacks, streaming input, or its DOM handler without converting to PostHTML's tree shape |
| parse5 | npm | Choose it when HTML-standard parsing, error locations, and a paired serializer matter more than PostHTML compatibility |
| node-html-parser | npm | Choose it when you want a simplified DOM with query selectors and element methods for direct scraping or edits |
| posthtml | npm | Choose the full processor when you want parsing, plugin execution, tree helpers, and rendering in one pipeline |