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

@onodocs/sdk

v0.5.6

Published

OnoDocs Word document analysis, text updates and rendering for browsers and Node.js.

Readme

OnoDocs SDK

Try the proposal studio for a runnable business-data, Word-template, editing and DOCX/PDF export workflow.

See document modes for viewing, editing, reviewing and form filling in one session.

For form authoring, controlled answer regions and completed DOCX plus structured answers, see document forms.

For application events, autosave, recovery, framework lifecycle and self-hosted HTTP, see application integration.

@onodocs/sdk processes Word documents. Its default entry performs analysis in Node or a browser; /browser adds layout and immutable page publication using browser font services; /server runs that processing in headless Chromium. @onodocs/canvas paints prepared pages and provides the viewer, selection and DOM attachments. The Canvas package runs independently of the parser, semantic engine and layout engine. @onodocs/editor provides the ready-made editor and its UI workflows through public SDK and Canvas APIs. See the editor package and migration guide.

Supported Word inputs are DOCX, DOCM, DOTX and DOTM. Macro-enabled files are rendered without executing VBA. Older binary DOC/DOT files and RTF are not supported.

Install with npm install @onodocs/sdk @onodocs/canvas. Use matching versions of both packages. A backend performing data operations can install only the SDK; a frontend receiving prepared pages can install only Canvas. Both packages include TypeScript declarations and this guide. Archives are also available from the public releases. Server rendering requires npm install @onodocs/sdk playwright-core and an installed Chromium-family browser. Playwright is an optional peer dependency, so browser and data-only installs do not install it. The server adapter includes the Canvas runtime it uses for image and PDF export.

Without a key, the SDK runs in free, non-production evaluation mode with full features and a watermark on rendered output. Pass { licenseKey } to openDocument for a 30-day trial or commercial entitlement; this works in all three entry points. Verification is entirely offline. Commercial keys permit covered releases indefinitely, with renewal for later releases and support. Browser verification requires HTTPS or a secure local context. See the included LICENSING.md and LICENSE for details.

Optional shared document collaboration adds multi-user editing and review with customer authentication, atomic persistence and recoverable local drafts. Ordinary browser sessions remain independent.

Inspect and query

import { openDocument } from "@onodocs/sdk";

const doc = await openDocument(bytes);
const customer = doc.query.contentControls().where({ tag: "customer-name" }).one();
const cells = doc.query.tables().cells().all();
const matches = doc.query.findText("Invoice total").all();
const headers = doc.query.stories().where({ story: "header" }).paragraphs().all();

TypeScript completion shows valid paths and filters. Select a kind, narrow with where, then choose how many results you expect:

| Method | Result | | --- | --- | | one() | One match; throws for zero or multiple matches | | optional() | One match or undefined; throws for multiple matches | | first() / at(index) | An explicitly chosen match or undefined | | all() / iteration | Every match in document traversal order | | count() | Number of matches | | map(project) | Project matches into application data |

Selectors (paragraphs, runs, tables, contentControls, bookmarks, stories) search descendants. rows selects a table's own rows; cells selects a table or row's own cells. Nested tables are selected separately with tables(). Typed element collections also allow table.rows[0].cells[1]. children() and descendants() provide generic traversal. Scope an existing element with doc.query.within(element). Nested or overlapping selections never duplicate the same element. A story represents an authored story; a reused header remains one semantic story with several physical occurrences.

where({ text: "Total" }) matches exactly and case sensitively. Use where({ text: { contains: "Total" } }), startsWith, or endsWith for other matches. Multiple fields and chained filters combine with AND. Paragraphs support style; content controls support tag and title; bookmarks support name; stories support story (body, header, footer, footnote, endnote, textbox). Unknown keys fail instead of silently returning unexpected content. Use .filter(element => ...) for application predicates.

doc.query.get(id) resolves an element in the current snapshot. Every element has a parent; doc.query.within(element).closest("cell") finds its containing cell, including the input itself if it is a cell. Negative query indices count from the end.

findText("...") or findText(/pattern/i) searches contiguous text in selected paragraphs or runs, including text split across runs. It returns a paragraph and a half-open [start, end) range in JavaScript UTF-16 offsets. It does not join separate paragraphs. Regular expressions search each selected contiguous interval, preserve the caller's lastIndex, and omit zero-length matches. Queries preserve the selected revision view. Semantic text includes authored content; generated fields and physical appearances can differ after layout. Rasterized picture/chart text has no text query model.

Bookmarks expose their published start target through bookmark.run; they do not represent an enclosing bookmark text range. Symbols without a Unicode text representation use the object replacement character in query text.

Extract content for search and automation

extractDocument returns JSON-safe content and Markdown with links back to the source document. It is available from the default SDK entry and /browser and makes no network requests. Applications can pass its output to their own search or AI service.

import { extractDocument } from "@onodocs/sdk";

const extraction = extractDocument(doc, { stories: ["body"] });
const json = JSON.stringify(extraction.content);
const markdown = extraction.markdown;

The content tree retains stories, paragraphs, runs, tables, cells, content controls, hyperlink metadata and source references. Tables preserve authored header and merge information. Markdown uses pipe tables for simple tables and ordered cell passages for nested or merged tables. JSON preserves the full table structure. Paragraph headings use selected style IDs Heading1 through Heading9 by default; pass headingStyles: { ReportTitle: 1, SectionTitle: 2 } to replace that mapping. This does not infer headings from visual formatting. Omit stories to include all supported stories. Text has the same semantic and cached-field behavior as queries.

Each extracted element has a source string. In a browser document, resolve it into the existing geometry API to scroll to or highlight the original passage:

const anchor = extraction.resolve(sourceFromSearchResult);
const fragments = doc.geometry.fragments(anchor);

For partial passages, extraction.reference(paragraphSource, start, end) creates a source reference with UTF-16 text offsets. To apply a reviewed automation result, use the existing edit commands without a selection field:

await extraction.edit(sourceFromReviewedResult, { kind: "replace", text: replacement });
const bytes = await doc.save();

Editing accepts paragraph or text-range references and has the same supported-content restrictions as doc.edit. Formatting and authoring commands work through the same method. extraction.selection(source) returns a logical selection for application controls; editor.select(selection) followed by editor.execute(command) preserves the editor's undo history.

References belong to one extraction snapshot. After a committed change or disposal, extraction.stale is true and reference operations throw StaleDocumentReferenceError. Re-extract before applying another result. The check also runs inside the edit queue, so a concurrent edit cannot redirect a delayed automation result to shifted text. Failed changes and no-op text updates preserve references. Saving alone preserves them; reopening requires a new extraction. Source links are not persistent document identities, and content remains readable after its references become stale.

The report review example combines extraction, citations, reviewed edits and Word/PDF export. Its local sample reviewer can be replaced with the host's AI service.

Design and generate Word templates

Use createTemplateEditor from @onodocs/editor for visual field authoring and preview. openTemplate and templateTag from the default SDK and browser entries support fields, repeated content, conditions, images, formatted values, reusable sections and batch generation. See Word templates and generation for the binding model, authoring commands and output workflow.

Replace text and fill templates

const customer = doc.query.contentControls().where({ tag: "customer" }).one();
await doc.update({ target: customer, text: "Willow Design" });

update accepts one update or a batch. Targets are runs, paragraphs, cells, content controls, their IDs, or search ranges. Element targets must contain ordinary text in one paragraph. Replacement text inherits the first selected run’s formatting; other selected runs become empty. Tabs and newlines are supported. Batches reject overlapping or missing targets, generated fields, and multi-paragraph replacements without changing the document.

Use search ranges for partial replacements while keeping surrounding text and formatting:

await doc.update(doc.query.findText("{{customer}}").map(target => ({ target, text: "Willow Design" })));

Batch range offsets refer to the original paragraph text. Empty ranges insert text; offsets must not split an emoji or other surrogate pair. Unchanged updates preserve existing formatting, snapshots, and attachments.

Browser updates rebuild layout and refresh mounted views. Query, semantic, layout, page and geometry values obtained earlier remain snapshots; read them again after updating. Application attachments are removed during refresh and can be reattached using fresh geometry. Updates run in submission order and accept { signal } for cancellation. The original source is unchanged.

Node inspection exposes the same update method. For server rendering, inspect the input with the default entry point to select elements or ranges, then pass those updates to the server document before renderPage, pdf, or save. Exports reflect the updated text.

Save an edited Word document

save() returns DOCX bytes in Node, the browser, and server rendering. It refreshes supported fields, including after text edits or template generation. Use save({ fields: "preserve" }) to retain cached field results; an unedited document then returns its original bytes. Text edits preserve surrounding run formatting and unedited package content. Saving and updating run in submission order. Both accept { signal }; cancellation leaves the committed document available. Call dispose() when finished, including for Node documents, to release the preserved source package.

await doc.fieldStatus() reports which fields can be updated, which require layout, and which are locked, static or unsupported. Node inspection updates supported sequences, text bookmark references and document properties. Browser and server documents also calculate page numbers, page references and supported tables of contents. Unsupported fields keep their cached values. Saving reports these through onFieldStatus, or a console warning when no callback is provided. Signed or protected packages remain unchanged when their fields cannot be written. save({ fields: "require-current" }) throws DocumentFieldUpdateError if a field cannot be refreshed. PDF export accepts the same field policy and diagnostic callback.

import { readFile, writeFile } from "node:fs/promises";
import { openDocument } from "@onodocs/sdk";

const doc = await openDocument(await readFile("template.docx"));
try {
  await doc.update({
    target: doc.query.contentControls().where({ tag: "customer" }).one(),
    text: "Willow Design"
  });
  await writeFile("completed.docx", await doc.save());
} finally {
  doc.dispose();
}

In a browser, create a Blob from the returned bytes with type application/vnd.openxmlformats-officedocument.wordprocessingml.document. In a combined deployment, serve await doc.save() from an application-owned download endpoint with that content type and a .docx filename. The backend processing document owns saving; a Canvas-only frontend does not contain the original Word package.

Runnable examples live in onodocs/onodocs. The command-line template example fills a tagged content control; its setup instructions live alongside the source. Examples are not included in npm packages.

The initial editing profile covers ordinary text and unbound text or rich-text content controls within one paragraph, including supported tables, headers, footers and notes. Edits inside bound or content-locked controls, temporary or specialized controls, fields, revisions, alternate-content branches, and source runs with generated or unsupported content are rejected before publication. Plain-text controls that disallow multiple lines reject newlines. Filling an unbound text control clears its placeholder-display flag. Edits elsewhere preserve these constructs. DocumentSaveError.reason identifies source-writing restrictions; existing semantic target errors remain RangeError.

Signed packages, enforced document protection, macro-enabled documents and templates retain their editing restrictions. Ordinary text updates reject documents with change tracking enabled; use the review operations below to author tracked replacements and formatting. Saving refreshes supported fields under the selected field policy. Original authored formatting is preserved independently of rendering compatibility decisions.

Evaluation exports contain the full Word document without adding a Word watermark. The existing non-production evaluation terms apply; rendered evaluation output continues to include its watermark.

Edit formatting and paragraphs

edit() accepts a logical selection with paragraph IDs and UTF-16 offsets. It returns the selection in the resulting document. Use fresh queries after editing because structural operations rebuild the document and its IDs. The source getter then describes the new committed package.

const paragraph = doc.query.paragraphs().first();
let selection = {
  start: { paragraphId: paragraph.id, offset: 0 },
  end: { paragraphId: paragraph.id, offset: paragraph.text.length }
};
selection = await doc.edit({ kind: "format", selection, formatting: { bold: true, fontSize: 18 } });
selection = await doc.edit({ kind: "replace", selection, text: "First paragraph\nSecond paragraph" });
await doc.edit({ kind: "align", selection, alignment: "center" });
const bytes = await doc.save();

Character formatting supports bold, italic, underline, strike, fontSize in points, fontFamily, and color as #RRGGBB. Values change direct formatting on the selected text while preserving the other authored properties. run.formatting exposes the common effective character properties for controls; paragraph.alignment exposes left, center, right or justified alignment. These convenience properties describe ordinary Latin text; they do not replace the full semantic typography model for script-specific inspection.

A replacement with a collapsed selection inserts text. Newlines split paragraphs; set paragraphBreaks: false for line breaks within a paragraph. Replacing a selection across adjacent paragraphs joins their surviving prefix and suffix. Empty text deletes the selection. New text inherits the starting run's formatting unless the operation includes formatting. Split paragraphs inherit the starting paragraph's properties. The first paragraph's properties win when joining.

Rich editing supports ordinary text paragraphs, including empty paragraphs and paragraphs in table cells and secondary stories. Text replacement within one paragraph and selected character formatting preserve surrounding fields, comments, bookmarks, drawings and unbound controls. Selected generated fields, locked or bound controls and tracked runs reject atomically. Structural operations such as splitting or joining still reject paragraphs containing these structures. Splitting or joining section boundaries and joining across cells or stories also reject. Such content remains preserved elsewhere in the document.

The Word interoperability evidence covers reviewed agreements, completed forms and generated invoices through actual Word edit/save/reopen journeys. It does not imply compatibility with every Word feature. For paragraph styles without primary names, rendering follows LibreOffice recovery and can omit direct paragraph formatting that Word retains; saved unedited source preserves those properties.

Node and browser documents expose edit; server documents forward the same operation. Parsing, semantic construction and browser layout finish before publication, so cancellation or failure leaves the previous document usable. This first implementation rebuilds the document for structural edits. Browser resources stay with the document lifetime, and the sample keeps undo history as document snapshots. Large documents need further editing-performance work before this can serve as a general-purpose editor.

Embed the Word editor

import { createEditor } from "@onodocs/editor";

const editor = createEditor({ container: document.querySelector("#editor") });
await editor.newDocument();
const bytes = await editor.save();
editor.dispose();

The component includes file opening, a new-document command, formatting, imported paragraph styles, lists, regular tables, paragraph images, links, page settings, default headers and footers, find/replace, clipboard operations, undo/redo and Word/PDF downloads. It runs in the browser. The standalone editor example lives in the public repository.

The Open Word picker accepts DOCX, DOCM, DOTX and DOTM. Opening another document cancels a pending open; its promise rejects with AbortError. A failed open leaves the current document available. Editing remains subject to the supported writer formats.

Use toolbar to choose which controls are available in the ribbon, or pass false to supply your own controls. The editor groups controls into File, Home and Insert tabs, with Page setup in Home and Table and Picture tabs for the selected content. Available names are open, new, save, pdf, undo, redo, bold, italic, underline, strike, font, size, color, alignment, style, list, table, tableTools, link, image, imageTools, page, header, footer, find and replace. fonts supplies font-family choices; styles supplies { id, label } choices using existing Word style IDs. onChange receives the editor after committed changes; onError reports queued operation failures. Use document for browser SDK options such as licensing and font sources, and readOnly: true to disable authoring.

commands adds application actions to the Home tab. Set a command's ribbon metadata to place it in a named tab and group. Include its ID when restricting toolbar to a list of controls.

const editor = createEditor({
  container: document.querySelector("#editor"),
  commands: [{
    id: "save-to-project",
    label: "Save to project",
    ribbon: { tab: "File", group: "Project" },
    execute: async editor => saveToProject(await editor.save()),
  }],
});

The editor supports native text selection and IME input. F6 switches between the document and ribbon, Escape focuses the ribbon, and Shift+Tab leaves the editing area. Arrow keys move between ribbon tabs. On touch screens, tap to place the caret, hold to select a word, and drag the selection endpoints. Narrow ribbons scroll horizontally. Mounted pages expose text and headings to browser accessibility tools, including pages whose canvases have been evicted.

Each instance has isolated styles and history. Customize --onodocs-color, --onodocs-background, --onodocs-toolbar-background, --onodocs-accent and --onodocs-selection on the host container. The toolbar, pages and status shadow parts are available for host styling. Call dispose() when removing an editor.

For application controls, use editor.execute(command) without a selection field, editor.select(selection), editor.find(text), editor.replaceAll(text, replacement), editor.undo(), editor.redo(), editor.save() and editor.pdf(). The current browser document and logical selection are available as editor.document and editor.selection. Direct document mutations bypass component history; use execute for edits that need undo.

Selection normally scrolls its endpoint into view. Pass false as the second argument to editor.select(selection, false) or view.setSelection(selection, false) to update selection without scrolling. Form inputs use this to highlight the linked document region while keeping the active input in place.

The additional SDK doc.edit commands use the same selection contract:

| Kind | Arguments | Behavior | | --- | --- | --- | | style | style: string \| null | Apply a Word paragraph style ID or remove the direct style | | link | target: string \| null | Link selected text to HTTP, HTTPS, mailto or #bookmark; null removes links | | list | list: "bullet" \| "decimal" \| null, optional level 0-8 | Set paragraph numbering or remove it | | insertTable | rows, columns | Insert a regular table after the selected paragraph | | table | action | insertRow, deleteRow, insertColumn, deleteColumn or delete | | image | bytes, mediaType, width, height, optional description | Insert a PNG/JPEG picture paragraph after the selected paragraph; dimensions are points | | page | optional width, height, margins: { top, right, bottom, left } | Change the selected section using point dimensions | | header, footer | text | Set default story text for the selected section |

The authoring profile supports regular unmerged tables and paragraph-image insertion. Select inline images with doc.query.images() and use imageProperties with imageId, optional point dimensions width/height, or remove: true. The component offers these commands through Image properties after clicking a picture. Floating-image manipulation and merged-table restructuring are not included. Configure review to author tracked text replacements and formatting. Clipboard copy, cut and paste preserve common text formatting, links, paragraph alignment and nested bullet/decimal lists. Tables and image alternative text paste as text, with a conversion message. External stylesheets and custom list formats are not imported. Plain-text clipboard content remains supported. History uses full document snapshots. Structural edits reimport and lay out the candidate document; ordinary typing reuses unchanged semantic data and font measurements. Run npm run validate:editor for the component and standalone browser journeys.

Render and attach ordinary DOM

Mounted views include read-only text selection and plain-text copying. Drag or Shift-click to select text, extend with Shift and the arrow/Home/End keys, select all with Ctrl/Command+A, and copy with Ctrl/Command+C or the Copy context menu. Selection spans pages and survives scrolling and zoom, including pages whose canvases have been released. Document updates clear the previous selection. Attached inputs retain their normal editing and clipboard behavior.

import { openDocument } from "@onodocs/sdk/browser";
import { createDocument } from "@onodocs/canvas";

const doc = await openDocument(file);
const canvasDocument = createDocument(doc);
const view = canvasDocument.mount(container);
const field = doc.query.contentControls().where({ tag: "customer-name" }).one();
const input = document.createElement("input");
input.name = "customerName";
const attachment = view.attach(input, { anchor: doc.geometry.fragments(field)[0] });

The example requires a tagged control that produces one visible fragment. Call doc.geometry.fragments(field) for content spanning lines, pages, or repeated stories, and attach to an explicitly chosen fragment. Fragments retain transforms and clipping. Table/cell anchors use their final physical rectangles. offset and size optionally adjust attachment placement in page units. Use interactive: false for decorative overlays so clicks reach the document underneath. Values, validation, submission, focus, and accessibility labels belong to the application. Attaching an element moves it into the view. Detachment removes it and restores its original inline style. Reattaching the same HTML element disposes its previous attachment; old handles become inert. An attachment exposes its disposed state.

Pages are anchors too. Placement avoids manual rectangle arithmetic:

view.attach(checkbox, { anchor: doc.geometry.fragments(doc.query.findText("Client approval:").one())[0], placement: "outside-right", gap: 80, size: { width: 420, height: 420 } });
view.attach(submitButton, { anchor: doc.pages[0], placement: "inside-bottom-right", inset: 480, size: { width: 2400, height: 600 } });

Inside placements use inside-{top|center|bottom}-{left|center|right} with optional inset. Outside placements use outside-{top|right|bottom|left} with optional gap and center the element along the selected edge. The default is overlay. All sizes, spacing and offsets use twips (1/1440 inch). Without a size, the element fills the anchor, reduced by the inset for inside placement. Offsets apply after placement. Content anchors retain their transforms and clips; page anchors use the full page. Attachments do not reflow text, so reserve room in the document.

Adjacent styled runs on one line share a fragment. Fragment bounds enclose the unclipped geometry; the supplied clips determine which portions are visible.

view.setZoom("fit-width") tracks container width. Numeric zoom uses 96 CSS pixels per inch at 1. view.toClient(pageIndex, point) and view.fromClient({ x: event.clientX, y: event.clientY }) translate coordinates. view.hitTest(clientPoint) returns { elementId, fragment, caret? }. Resolve elementId with doc.query.get(elementId) when the processing SDK is available. Text hits resolve to a run or paragraph; table whitespace resolves to a cell or table. caret contains { paragraphId, offset, point }, with the same UTF-16 offset convention as search ranges. Generated field text omits a caret when no source position can be represented. Raster drawings without a semantic hit identity and blank page space return no element.

view.scrollTo(anchor, { block: "center" }) reveals a page or a physical fragment. Resolve elements and text ranges through doc.geometry.fragments(anchor) first, then choose the occurrence to reveal.

A run's optional link is { kind: "external", target, tooltip } or { kind: "bookmark", target }. Use bookmark queries and scrollTo for internal links. The host application decides whether and how to open external destinations.

All page and query indices are zero based. Page geometry uses twips: 1440 units per inch. await canvasDocument.pages[index].render(canvas, { dpi: 144 }) prepares the page resources and paints the canvas; await doc.pages[index].load(signal) returns the immutable page commands and their resources. Containers should have a usable width. Multiple views can share one document.

Call attachment.dispose(), view.dispose(), canvasDocument.dispose(), or doc.dispose() when done. Canvas disposal removes its views and releases its font registrations. Processing disposal also disposes subscribed Canvas documents and releases processing resources. Models remain inspectable; further painting is rejected. Pass { signal } when opening. Opening copies byte inputs; a File/Blob is read once by either the browser or analysis entry point. Fetch URLs in host code with the authentication policy your application needs, then pass bytes.

Show pages while processing

Progress events expose a document source that the Canvas package can observe. Create the view once. Its subscription follows provisional page replacements and the completed layout.

import { openDocument } from "@onodocs/sdk/browser";
import { createDocument, type CanvasDocument } from "@onodocs/canvas";

let canvasDocument: CanvasDocument | undefined;
const doc = await openDocument(bytes, {
  signal,
  onProgress(progress) {
    canvasDocument ??= createDocument(progress.document, { container });
    status.textContent = progress.stage;
  },
});

Cancelling or failing processing disposes the source and its subscribed view. After success, dispose doc when the application closes the document. A Canvas document can be disposed earlier without closing the processor.

Render on the server

import { readFile, writeFile } from "node:fs/promises";
import { createRenderer } from "@onodocs/sdk/server";

const renderer = await createRenderer({ channel: "chrome" });
try {
  const doc = await renderer.openDocument(await readFile("invoice.docx"));
  try {
    await writeFile("invoice.png", await doc.renderPage(0, { dpi: 144 }));
    await writeFile("invoice.pdf", await doc.pdf({ dpi: 150 }));
  } finally { await doc.dispose(); }
} finally { await renderer.dispose(); }

Install Chromium/Chrome separately; use executablePath for a deployment-managed binary or channel: "chrome"/"msedge" for an installed browser. The renderer reuses one browser, with a separate context per document. PNG and JPEG output use the browser engine's Canvas path. PDF preserves document page sizes, including mixed sizes, and includes searchable/selectable Unicode text and external and internal links. Supported solid graphics remain vector content. Native text and complex effects use cropped lossless browser-rendered layers at the requested DPI. The shared browser/server exporter includes tagged headings, lists, tables with merged-cell spans, authored figure descriptions and links. PDF/UA and PDF/A conformance are not claimed.

Contexts make no outbound network requests. Supply fonts as data URLs through fonts, or install them on the rendering host. Browser consumers can also supply font URLs. Embedded document fonts use the engine's existing resource path. Use identical browser versions and fonts when reproducible output matters.

Opening and output methods accept an AbortSignal. Cancelling an active operation closes that document context, except for save() and pdf(), which leave the document usable for retry. If the context closes, open a new document to retry. Cancelling an operation still waiting in the queue leaves the active operation and document intact. Both server documents and renderers expose disposed. Disposing the renderer closes all remaining documents. A renderer can open multiple documents concurrently; output requests on one document are serialized. Applications control scheduling and process isolation.

Process on the backend and render in the frontend

The complete backend viewer sample includes a Node.js server, working endpoints, a browser frontend and an authored DOCX. Install Node.js 22.18 or newer and Google Chrome, then run:

git clone https://github.com/onodocs/onodocs.git
cd onodocs/examples/backend-viewer
npm install
npm start

Open http://127.0.0.1:5175. Use npm start -- /path/to/document.docx to open another server-side file. Stop the server with Ctrl+C. The website guide includes the complete server and frontend source.

The server's manifest({ format: "json" }) and page(index, { format: "json" }) methods return response bodies ready to send with Content-Type: application/json. For a document at /document, serve its manifest there and individual pages at /document/pages/:index. The original methods without a format option still return typed objects for in-process use.

import { openDocument } from "@onodocs/canvas";

const doc = await openDocument("/document", {
  container: document.querySelector("#pages"),
  viewOptions: { zoom: "fit-width" }
});
await doc.view.whenRendered();

Canvas handles loading and decoding. It requests page content as needed. signal cancels requests; request accepts fetch options such as headers and credentials for both the manifest and page requests. Same-origin cookies work by default. Requests default to no-store, and non-successful HTTP responses reject with the status code. Dispose the Canvas document when closing or replacing the view.

The application owns the endpoints and document authorization. Keep server documents open while their endpoints are in use, then dispose them. For updates, publish each completed snapshot at its own URL and reopen the frontend view. Use matching SDK and Canvas releases.

Selection, copying, hit testing, zoom and attachments use the published geometry. Queries and updates stay on the backend. The frontend receives the visible text and resources needed to display the document. Supplied and embedded fonts travel with the pages; fonts resolved from the server's installed fonts must also be available on the client.

Errors and application data

Opening, updating, and exporting can reject. Handle errors at your application boundary and dispose browser/server resources in finally blocks. The analysis and browser entries export PackageOperationError, SemanticOperationError, and QueryCardinalityError; cancellation uses the signal's reason.

Query elements are readonly in-memory models with parent references, source metadata, and bigint values. Project the fields your application needs instead of serializing the entire model:

const paragraphs = doc.query.stories().where({ story: "body" }).paragraphs()
  .map(({ id, text, style }) => ({ id, text, style }));
const json = JSON.stringify(paragraphs);

The same projection supports AI context, search indexing, and backend messages without coupling your data format to engine internals.

Low-level stages

doc.source is the typed source model and doc.semantic contains completed document meaning. Element source locations refer into doc.source.sourceFacts and provenance. These identifiers belong to that loaded document, not arbitrary later revisions. The models describe supported DOCX content; they do not preserve every XML detail for round-trip writing.

The main SDK also exports the core stage APIs: parseDocumentSource, createSemanticDocument, parseDocumentForRendering, font preparation, createLayoutDocument, and createPageDisplayList. Low-level materializePageDisplayListToCanvas is exported by @onodocs/canvas. Advanced consumers can stop at a stage or replay paint through their own adapter. Use canonical readonly contracts; do not mutate stage outputs. Layout remains the authority for all physical geometry.

Extract a table as text

const data = doc.query.tables()
  .where({ headers: ["Deliverable", "Owner", "Due"] })
  .one().textRows;

textRows includes the first row and keeps cells grouped by their own table rows. The header filter matches the first row exactly, in order and case sensitively; it does not infer header formatting. Nested tables remain text inside their containing cells. Merged cells retain authored topology; the result is not padded into a rectangular grid. Use data.slice(1) to omit the header. The readonly matrix is a snapshot; query again after an update.

Large documents

Use the progress subscription shown above to display early pages while layout continues. Early pages are provisional: page counts, fields, notes and placement can change as formatting settles. Opening resolves with the complete document. Abort the loading signal to dispose the source and its subscribed views.

Mounted views paint the visible neighborhood and release distant canvas pixels. Zoom and scrolling render pages on demand. Decoded images are acquired only for a page render and released afterward. Parsing and document layout still require memory proportional to document content; virtualization does not make the document model constant-memory. Auto-fit tables and document-wide fields can require later content before their output settles.

Await page renders before reading or exporting the canvas:

await canvasDocument.pages[0].render(canvas, { dpi: 144, signal: controller.signal });
const png = canvas.toDataURL("image/png");

For documents that omit font declarations, browser and server opening options accept defaultFont and defaultFontSize (points). They default to Calibri and 11pt; authored font choices and sizes take precedence.

Review and saved versions

Node and browser documents expose the same review operations. Supply your application's user identity and timestamp for authored comments and changes:

const identity = { author: currentUser.name, initials: currentUser.initials, date: new Date().toISOString() };
const paragraph = doc.query.paragraphs().first();
const selection = { start: { paragraphId: paragraph.id, offset: 0 }, end: { paragraphId: paragraph.id, offset: 6 } };
await doc.review({ kind: "comment", selection, text: "Please verify this wording.", identity });
const thread = (await doc.readReview()).comments.at(-1);
await doc.review({ kind: "reply", id: thread.id, text: "Confirmed.", identity });
await doc.review({ kind: "resolveComment", id: thread.id, resolved: true });
await doc.review({ kind: "trackedReplace", selection: thread.selection, text: "Revised", identity });
await doc.review({ kind: "revision", accept: true });
const bytes = await doc.save();

Open with revisionView: "accepted" when editing current text. readReview() includes hidden deleted changes, which may have no visible selection. Pass revision ids to accept or reject individual changes; omit them for all changes. trackedFormat records selected run formatting with a rejectable property snapshot. trackChanges enables or disables Word's tracking setting. Review mutations support signal and ifUnchangedSince like ordinary edits. Text replacement stays within a paragraph; comments can span paragraphs in one story. Protected documents retain their write restrictions.

Enable the editor panel with review: { identity: () => identity, history }. The optional history object implements list(signal), load(id, signal) and save(version, bytes, signal). Your application stores the complete Word bytes and version metadata. The SDK adds no persistence service or user account system. Restoring a version is undoable and raises the editor's change event, so application autosave can persist it. compareDocuments(savedBytes, doc) returns paragraph text and formatting differences without modifying the document.

Browser and server opening options accept commentDisplay: "hidden" to hide comment indicators in rendered pages and PDF exports. Comments remain available through readReview() and in saved Word files. The editor uses this display by default and shows comments in its Review pane. Set document: { commentDisplay: "markers" } to display indicators in the editor; SDK rendering defaults to markers.

Searchable PDF in the browser

import { openDocument } from "@onodocs/sdk/browser";

const doc = await openDocument(await file.arrayBuffer());
try {
  const bytes = await doc.pdf({ dpi: 150, title: "Quarterly report", language: "en-US" });
  const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
  const link = document.createElement("a");
  link.href = url;
  link.download = "document.pdf";
  link.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
} finally {
  doc.dispose();
}

PDF export runs locally and uses the same implementation as server doc.pdf(). It waits for earlier edits to finish. Pass signal to cancel an export without closing the document. The PDF writer loads on demand; deploy the browser entry's generated chunks alongside it. Text extraction order for bidirectional paragraphs and selection across complex clusters depend on the PDF reader. Solid graphics use vector PDF paths where supported. Native text and complex effects retain browser rendering in cropped lossless image layers, including evaluation marks. Higher DPI increases the sharpness, file size and temporary memory use of those layers; compressed output remains in memory until export finishes.

The PDF includes tagged headings, paragraphs, nested lists, tables, header cells, merged-cell row/column spans, authored figure descriptions and links, plus heading and bookmark navigation. Set title and language for reader metadata. DrawingML descriptions use wp:docPr/@descr, falling back to title; VML uses authored alt text. Undescribed graphics remain artifacts. Reader support varies. Firefox PDF.js exposes named images, list nesting and table spans in its accessibility tree. This does not claim PDF/UA conformance or validation with every screen reader.

One named-application license covers SDK and Canvas, including the application's backend and self-hosted collaboration. Finished editors for end users are included. Editor source is separately licensed under MIT. See application scope, output rights and redistribution terms.