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

@okrapdf/pdfdom

v0.1.0

Published

jQuery for PDFs - query document entities with CSS-like selectors

Readme

pdfquery

jQuery for PDFs. CSS selectors and vision models on any PDF.

npm install pdfquery @okrapdf/pdfdom-plugins

Headless PDF DOM runtime

The local runtime takes a source plus an explicit parser schedule and writes boring DOM-compatible markup. That keeps the first milestone compatible with existing tools instead of inventing a query shell.

pdfdom create ./invoice.pdf --parser text-layer -o html | htmlq --text 'page'
pdfdom create ./invoice.pdf --parser text-layer -o html | htmlq --attribute data-page 'page'
pdfdom create ./invoice.pdf --parser text-layer -o html | xidel -s - -e "css('page')"
import { openPdfDomRuntime, textLayerNodeParser } from '@okrapdf/pdfdom';

const runtime = openPdfDomRuntime({
  source: { text: 'Invoice total: $42' },
  parsers: [textLayerNodeParser],
});
await runtime.ready;

console.log(runtime.serialize());
// <document ...>Invoice total: $42</document>

Parsers are node parsers: they declare what input they need, what capabilities they provide, and any host/network requirements. Generic parsers can stay fully local; specialized parsers can be explicit. For example, an arxiv-paper parser can declare network: { mode: 'required', allowedHosts: ['arxiv.org'] }, read source.metadata.sourceUrl, fetch the arXiv PDF/HTML variants, and still mount the result as the same <document><article>...</article></document> tree.

The runtime owns scheduling, parser lifecycle, DOM mounting, events, snapshots, and serialization. The DOM is the in-memory runtime object; -o html materializes it as streamable markup for tools like htmlq, xidel, Cheerio, jsdom, browser querySelectorAll, and LLM CSS selectors. Use -o json when you want the runtime snapshot/object instead.


Tree-first query (no session)

The document tree is the source of truth. pdfquery is the query/mutation lens over that tree.

import pdfquery from '@okrapdf/pdfdom';

const $ = pdfquery(tree, {
  // Optional: persist patches to your durable tree
  onPatches: (patches) => fetch('/parties/document/my-room/tree', {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ patches, reason: 'ui-mutation' }),
  }),
});

Selectors, text search, and aggregation work instantly:

$('table').count();                        // 12 tables found locally
$('ocr').contains('revenue').texts();      // text search across all pages
$('[confidence>0.9]').count();             // filter by OCR confidence
$('*').onPage(1).countByType();            // Map { ocr: 45, table: 2, heading: 3 }

Need rich markdown for a specific page? .markdown() delegates to your tree dispatch:

const md = await $('table').onPage(6).markdown();

Need visual understanding? .vlm() delegates to your tree dispatch:

await $('table').onPage(6).vlm('what are the column headers?');
await $('figure').eq(0).css({ margin: 20 }).vlm('describe this chart');
await $('page:first').vlm('summarize this page in 2 sentences');

One query interface over a canonical tree.


PDF Browser runtime

PdfRuntime is the live layer above the tree. It accepts parser/facet candidates, reconciles the current visible winner per node, emits MutationObserver-style records, and exposes snapshot/replay plus a surface binding for viewers.

import { PdfMutationObserver, openPdfRuntime } from '@okrapdf/pdfdom';

const runtime = openPdfRuntime({
  documentId: 'doc_123',
  pages: [{ pageNumber: 1, width: 1000, height: 1200 }],
});
await runtime.ready;

const observer = new PdfMutationObserver((records) => {
  for (const record of records) {
    if (record.type === 'text' || record.type === 'bbox') {
      // repaint only the affected node/region
    }
  }
});
observer.observe(runtime, {
  childList: true,
  characterData: true,
  bbox: true,
  class: true,
  facetWinners: true,
});

await runtime.applyFacetBatch({
  facet: 'text',
  source: 'native-text',
  candidates: [{
    id: 'native-1',
    nodeId: 'p1',
    facet: 'text',
    source: 'native-text',
    pageNumber: 1,
    nodeType: 'paragraph',
    confidence: 0.5,
    stage: 'draft',
    text: 'Revneue',
    bbox: { x: 0.1, y: 0.1, width: 0.4, height: 0.03 },
  }],
});

const surface = runtime.surface();
const unsubscribe = surface.subscribe((snapshot) => {
  renderPdfSurface(snapshot);
});

runtime.snapshot() captures document state, facet candidates, winners, provenance, invalidated paint regions, and the replay event log. Use replayPdfRuntimeSnapshot() or replayPdfRuntimeEvents() for reconnects and debugging.


Ingestion (separate concern)

| Plugin | What it does | API key | |--------|-------------|---------| | pymupdf | Local text + table extraction, page rasterization | No | | llamaParse | LlamaIndex Cloud extraction (eager or defer: true) | LLAMAINDEX_API_KEY | | vlmOpenRouter | .vlm() on any element via OpenRouter | OPENROUTER_API_KEY | | vlmBboxDetect | VLM visual entity detection (tables/figures the OCR missed) | Uses vlmOpenRouter | | doclingServe | IBM Docling (self-hosted) | No | | googleOcr | Google Document AI | GCP credentials |

Use your preferred ingestion pipeline to populate the tree (OCR, layout, VLM, etc). pdfquery itself does not own or store state.

Quick start (no API key)

npx tsx examples/basic.ts
import { loadFixture } from '@okrapdf/pdfdom';

const $ = loadFixture('financial-report');

$('table').count();            // 8
$('table').texts();            // markdown content of each table
$('[confidence>0.95]').count(); // 81
$('*').countByType();          // Map { table: 8, header: 15, ... }

License

MIT