Skip to content
Apertura
Guides

Extraction

The same parse the viewer draws from, written down instead of rendered: a typed tree for a program, Markdown for a language model, and plain text for an index.

@apertura/extract takes the model the parsers already produce — the one that mirrors the file format and knows nothing about CSS or the DOM — and emits it. It is not a second implementation to keep in agreement with the first, which is why the Markdown and the blocks can never disagree about what the document said.

It runs anywhere JavaScript does. No Python, no WASM, no native binary and no dependencies, so the same call works in a Node pipeline, in a worker, on an edge runtime and in a browser tab on a file that never leaves the machine — which is what the extraction lab is doing while you read this.

The three shapes

import { extract } from '@apertura/extract';

const doc = await extract(bytes);

for (const block of doc.walk()) {
  block.type;     // 'heading' | 'paragraph' | 'table' | 'chart' | …
  block.text;     // the flattened text of the block
  block.locator;  // where it came from, as an address
}

What it does that reading the file literally does not

  • A chart comes out as its numbers. The values are in the file — that is how a chart survives being opened away from its workbook — so a quarterly report’s headline figures are in the text rather than behind a picture.
  • A merged cell is filled. Excel stores the value once, in the top-left of the merge. Read naively, a header row is one word followed by nine empty columns.
  • A number is what the sheet shows. The cell holds 45306 and the sheet says 15 January 2024; every value goes through its number format.
  • SmartArt comes out as its nesting, read from the shapes the application laid out — which is where the words are.
  • Footnotes, comments and speaker notes are annotations, not inline text. Putting them in the flow would corrupt both the reading order and every offset after them.

Handlers

Handlers run over the finished tree rather than inside the parse, so the parse stays fast and everything slow is optional, cancellable and rate-limited. This is where OCR, a vision model or a LaTeX converter goes.

ocr.ts
await extract(bytes, {
  concurrency: 4,
  onImage: async (image, { readMedia, breadcrumbs }) => ({
    alt: await describe(await readMedia(image.source!.partName), breadcrumbs),
  }),
});

On the command line

apertura
npx @apertura/node md report.docx
npx @apertura/node chunks *.docx --max-tokens 512 > chunks.jsonl
Every output carries its addresses
A range of any output can be turned back into a place in the document, and back again. That is what lets a passage found by a retrieval system be shown rather than quoted — see anchors.