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

domq-wasm

v0.5.0

Published

Tiny, forgiving HTML parser for JavaScript (WebAssembly): CSS selectors, readable text, Markdown and JSON from messy HTML. Zero JS dependencies.

Readme

domq for JavaScript

Messy HTML in. Clean tree, text, Markdown, or JSON out. A forgiving HTML parser with CSS selectors for JavaScript: browsers, Node, Deno, Bun and edge workers. It is the Rust crate domq compiled to one ~41 KB (gzipped) WebAssembly module, with zero JS dependencies.

Try it in the playground: paste any HTML and query it live.

import { select, toMarkdown } from "domq-wasm";

const html = `<ul id=links><li><a href="/a">One</a><li><a href="/b">Two</a></ul>`;

select(html, "#links a[href]").map((a) => a.attrs.href); // ["/a", "/b"]
toMarkdown(html); // "- [One](/a)\n- [Two](/b)"
  • Never throws on bad HTML. Unclosed tags, mis-nesting and stray end tags are repaired, and errors() tells you where.
  • Real CSS selectors. Combinators, attribute operators, :nth-child(), :not(), :is(), :has(), plus jQuery-style :contains("text").
  • Content out. Readable text (no scripts or styles), CommonMark/GFM Markdown for LLM prompts and RAG, a JSON-ready tree, or repaired HTML.
  • Small. One .wasm (~41 KB gzipped) plus ~4 KB of generated glue JS.

It is not a spec-compliant WHATWG parser (no implicit <html>/<body>, no adoption agency) and not a sanitizer. Use DOMPurify for untrusted HTML that you render.

Install

npm install domq-wasm

Use it

The package ships one ES module build. How it loads the .wasm depends on the runtime:

| Runtime | Setup | Status | |---|---|---| | Node.js 22+ (supported LTS lines) | nothing: import { select } from "domq-wasm" | tested in CI on the current LTS | | Browsers, bundlers (Vite, webpack 5, …) | await init() once | tested (the playground uses this build) | | Bun | nothing (uses the Node entry point) | expected to work, not tested | | Deno | nothing with npm:domq-wasm (Node entry point) | expected to work, not tested | | Cloudflare Workers | initSync({ module }) with the imported .wasm | expected to work, not tested |

Node.js

Import and call. The Node entry point loads the module synchronously at import time, so there is no init() to await (calling it anyway is harmless).

import { select, toText } from "domq-wasm";

const res = await fetch("https://news.ycombinator.com");
const html = await res.text();
for (const link of select(html, ".titleline > a")) {
  console.log(link.text, link.attrs.href);
}

From CommonJS, require("domq-wasm") works on Node versions that can require() ES modules (20.19+ and 22.12+). On older versions, use const { select } = await import("domq-wasm").

Browsers and bundlers

Call the default export once before anything else. It fetches domq_wasm_bg.wasm from next to the JS file, which bundlers such as Vite and webpack 5 handle as an asset.

import init, { select, toMarkdown } from "domq-wasm";

await init();
const md = toMarkdown(document.documentElement.outerHTML, "article");

Without a bundler, import it from a CDN:

<script type="module">
  import init, { toText } from "https://cdn.jsdelivr.net/npm/[email protected]/domq_wasm.js";
  await init();
  console.log(toText("<p>Hello <b>world</b>"));
</script>

Deno and Bun

import { select } from "npm:[email protected]"; // Deno; in Bun: "domq-wasm"

Both resolve the package's Node entry point, which reads the .wasm with node:fs. Deno may ask for read permission (--allow-read).

Cloudflare Workers

Wrangler imports .wasm files as compiled WebAssembly.Modules; hand it to initSync:

import { initSync, toMarkdown } from "domq-wasm";
import wasm from "domq-wasm/domq_wasm_bg.wasm";

initSync({ module: wasm });

export default {
  async fetch(request) {
    const page = await fetch("https://blog.rust-lang.org");
    return new Response(toMarkdown(await page.text(), "main"));
  },
};

API

Every function takes the HTML as a string. Functions that take a selector throw a SelectorError if it is invalid; nothing else throws.

| Function | Returns | |---|---| | parse(html) | DomDocument: the tree as plain objects | | parseJson(html) | string: the same tree as JSON text | | select(html, selector) | Match[]: every matching element, in document order | | selectFirst(html, selector) | Match \| undefined: the first match | | toText(html, selector?) | string: readable text of the document, or of every match separated by a blank line | | toMarkdown(html, selector?) | string: Markdown of the document, or of every match separated by a blank line | | toHtml(html) | string: the repaired HTML (every element closed and properly nested) | | errors(html) | ParseError[]: what the parser had to repair, and where | | init(input?), initSync({ module }) | load the .wasm (browsers and Workers only; see above) |

Parse once, query many times

Each function above parses the HTML again. To run several queries on one document, parse it once:

import { Document } from "domq-wasm";

const doc = new Document(html); // or Document.parse(html)
const title = doc.selectFirst("h1")?.text;
const prices = doc.select("tr:has(td.in-stock) .price").map((m) => m.text);
const body = doc.toMarkdown("article");
doc.free(); // release its wasm memory now instead of waiting for the GC

Document has the same operations as methods: select(selector), selectFirst(selector), toText(selector?), toMarkdown(selector?), toHtml(), tree(), errors() and free(). toJSON() returns the same as tree(), so JSON.stringify(doc) gives the JSON tree. Where explicit resource management is available, using doc = new Document(html) frees it automatically.

If you also use the DOM's Document type in the same file, rename the import: import { Document as HtmlDocument } from "domq-wasm".

Types

The package includes TypeScript declarations; none of the API is typed any.

interface Match {
  tag: string;                   // lower-cased tag name
  attrs: Record<string, string>; // attributes; boolean attributes are ""
  text: string;                  // readable text, as toText() gives it
  html: string;                  // outer HTML, re-serialized
  nodeIndex: number;             // position in a depth-first walk of tree()
}

interface DomDocument {
  type: "document";
  errors: number;                // how many repairs (0 = clean input)
  children: DomNode[];
}

type DomNode =
  | { type: "element"; tag: string; attrs: Record<string, string>; children: DomNode[] }
  | { type: "text"; value: string }
  | { type: "comment"; value: string }
  | { type: "doctype"; value: string };

interface ParseError {
  position: number;              // index into the html string
  message: string;               // e.g. "element implicitly closed"
}

interface SelectorError extends Error {
  name: "SelectorError";
  position: number;              // index into the selector string
}

Invalid selectors

try {
  select(html, "a:hover");
} catch (e) {
  e.name;     // "SelectorError"
  e.message;  // "unsupported pseudo-class ':hover' at byte 1"
  e.position; // 1
}

Unsupported selectors are errors rather than silently matching nothing: pseudo-elements, :hover and other interactive states, namespaces, and :has() inside :has(). The full grammar is in the Selector docs.

Size

| File | Raw | Gzipped | |---|---:|---:| | domq_wasm_bg.wasm | ~88 KB | ~41 KB | | domq_wasm.js (glue) | ~22 KB | ~4 KB |

The module contains the parser, the selector engine, and the text, Markdown, JSON and HTML serializers. CI fails if the gzipped .wasm grows past 45 KiB.

Upgrading from 0.1 or 0.2

0.3 is a new API. parse() now returns objects, so drop the JSON.parse, or call parseJson() for the string. parseErrorCount(html) is now errors(html).length. query(html, selector) is now select(html, selector), which returns Match objects instead of a JSON string of subtrees.

Building from source

rustup target add wasm32-unknown-unknown
cargo install wasm-pack

cd wasm
wasm-pack build --target web --release   # -> pkg/
node npm/finish.mjs                      # add the Node entry point + "exports"
wasm-pack test --node                    # Rust-side tests of the bindings
sh npm/smoke-test.sh                     # pack pkg/ and run tests/node.mjs against it

For the playground: wasm-pack build --target web --release --no-pack --out-dir ../playground/pkg, then serve playground/ over HTTP (for example python3 -m http.server -d ../playground).

License

MIT