Library
ReviseDocument: open, read, edit, review, and export documents from Node.
Programmatic access uses revise/server from the revise package and follows the SDK license.
npm i revisepnpm add reviseyarn add reviseimport { writeFile } from "node:fs/promises";
import { ReviseDocument } from "revise/server";
const doc = await ReviseDocument.open("contract.docx", { author: "Legal bot" });
await doc.replaceAll("Provider", "Northstar"); // tracked changes
await doc.reject({ section: "8" });
const { bytes, fidelity } = await doc.export({ format: "docx" });
await writeFile("contract-out.docx", bytes);
doc.dispose();ESM only; Node 20 or newer.
Opening a document#
ReviseDocument.open(source, options?) | A file path, or { bytes, filename }: .docx, .odt, .rtf, .md, .html, or .txt. A Word file keeps its original package, so untouched content exports byte for byte. |
ReviseDocument.create({ html?, markdown?, title? }, options?) | A new document from the HTML the tools read and write, or from Markdown |
ReviseDocument.restore(snapshot, options?) | From doc.snapshot(), or a bare Y.js update |
ReviseDocument.fromYDoc(ydoc, options?) | Wrap a Y.Doc you own, such as a collaboration room. The Y.Doc stays yours. |
Options:
| Option | |
|---|---|
author | Shown as the author of tracked changes and comments |
mode | "suggesting" records edits as tracked changes (the default for Word files); "editing" applies them directly (the default for plain text) |
documentId | A stable id, for tool sessions and collaboration |
images | How images inserted by URL are fetched. By default only public http(s) hosts are reachable. |
A document runs its operations one at a time, in the order you call them, so
concurrent calls are safe. Call dispose() when you're done.
Reading#
const status = await doc.status(); // blocks, words, pendingSuggestions, fidelity
const outline = await doc.outline(); // sections with block ranges and word counts
const summary = await doc.summary(); // page setup, headers and footers, headings, tables
const hits = doc.grep("indemnif", { ignoreCase: true }); // [{ id, blockIndex, count, text, matches }]Pending tracked changes read in a view: "tracked" (the default for
grep) finds text on either side of a change, writes the block as
"Fees are $[-100-]{+120+} per hour.", and labels each match "deleted",
"inserted", or null; "accepted" and "original" read the text as if every
change were accepted or rejected. The read tools return the accepted view;
doc.viewHtml(html, "tracked") marks their HTML with <ins>/<del> (each
with data-change-id and data-author) and "original" shows the text before
the changes.
status().fidelity says how faithfully the document will export back to its
original Word file.
Editing#
replaceAll(find, replace, options?) replaces every occurrence: in the
body, tables, and lists, in page headers and footers, and in footnotes and
endnotes. Curly and straight quotes, no-break and plain spaces, and
no-break and plain hyphens match each other, so '"Provider"' finds
“Provider”; a straight quote in the replacement then takes the document's
curly style. Options: ignoreCase, exact (characters must match exactly),
blocks (only these block ids). It returns replaced (occurrences),
blocks (ids of the blocks that contained the text), regions
(occurrences per part: body, header, footer, footnote, endnote),
changeIds (tracked changes created), failures (each: blockId, code,
message), and nearMisses: text it left alone that ignoreCase or
dropping exact would have matched (each: text, count, reason).
replaceText is its 3.0 name and still works. For everything else, run the
editing tools:
const result = await doc.tools.call("replace", {
id: hits[0].id,
replacements: [{ find: "thirty", replace: "sixty" }],
});
if (!result.ok) console.error(result.error.code, result.error.message);doc.tools.definitions() | Every tool's name, description, and JSON input schema |
doc.tools.call(name, input, { directMode? }) | Run a tool with typed input. Literal names infer the input and result types. |
doc.tools.run(name, input, { direct? }) | Run a tool by name with untyped input, as a model sends it |
Neither throws for a bad edit: both return { ok: true, value } or
{ ok: false, error }. A mutation's value.suggestionIds lists the tracked
changes it created. The tools are described in the tool
reference; revise/server runs the server subset (no
get_selection or view_image, no document_id).
await doc.setMode("editing") switches to direct edits; "suggesting"
switches back.
Review#
const changes = await doc.changes(); // [{ id, type, author, section, deleted, inserted }]
const inSection8 = await doc.changes({ section: "8" });
const preview = await doc.preview({ exceptSection: "Fees" }); // what a selection would settle
const accepted = await doc.accept({ ids: [changes[0].id] });
const rejected = await doc.reject({ all: true });
// { resolved, missing, unresolved }
const diff = await doc.diff(); // text: "~ a [-old-]{+new+} b", changed: numberA selection is exactly one of ids, all, or filters, which combine:
section: a heading and its subsections, by number ("8","8.1", Word's automatic numbering included) or title. A title is matched in full, or by text in only one heading; otherwise it throwssection_ambiguous.exceptSection: everything outside one.blocks: block ids and{ from, to }ranges in document order. Ids of list items, table cells, and blocks inside them work.
A section or block that doesn't exist throws (section_not_found,
block_not_found) rather than selecting nothing.
Output#
const docx = await doc.export({ format: "docx" }); // review: keeps tracked changes
const clean = await doc.export({ format: "docx", view: "accepted" }); // or mode: "final"
const before = await doc.export({ format: "docx", view: "original" }); // every change rejected
const pdf = await doc.export({ format: "pdf" });Formats: docx, pdf, markdown, html, txt. The result:
| Field | |
|---|---|
bytes | The file |
mode | "review" or "final", as written (only .docx holds tracked changes; other formats are always final) |
view | "tracked", "accepted", or "original", as written |
pendingTrackedChanges | Tracked changes still in the output |
fidelity | For .docx: method: "graft" carried untouched content over from the original byte for byte (exact when nothing changed); "regenerated" wrote the file fresh, with fallbackCode saying why |
warnings | Anything the output can't hold |
doc.renderPages({ startPage, pageCount, view }) draws pages as one PNG,
laid out the way the editor lays them out. It needs a canvas package:
npm i @napi-rs/canvas (or canvas).
State#
doc.snapshot() | The whole document, versioned, including the original Word package: for storing and restoring with revise/server. It is not a Y.js update. |
ReviseDocument.restore(snapshot) | Back from a snapshot |
doc.version() | An opaque value that changes whenever the document does |
doc.reload(bytes, filename) | Re-read a file that changed elsewhere. Block ids carry over only where identity is provable; changedIds lists kept ids whose content changed. |
doc.ydoc | The live Y.Doc. To hand the document to the Revise Editor SDK (revise), send Y.encodeStateAsUpdate(doc.ydoc) and apply it with Y.applyUpdate on the editor's side (or share the Y.Doc over your collaboration provider). |
doc.resetToolContext() | Forget what an agent has read (after compacting its transcript) |
Comparing and converting#
import { compare, convert } from "revise/server";
const redline = await compare("v1.docx", "v2.docx");
// redline.document: the revised version with every difference tracked
// redline.verified: { acceptAllGivesRevised, rejectAllGivesOriginal }
const { bytes } = await redline.document.export({ format: "docx" });
const markdown = await convert("brief.docx", "markdown"); // Uint8ArrayThe raw package#
For what the tools don't cover, read and patch the .docx parts directly:
import { readFile } from "node:fs/promises";
import { catPart, listParts, patchPackage } from "revise/server";
const bytes = new Uint8Array(await readFile("contract.docx"));
const parts = await listParts(bytes); // [{ path, bytes }]
const styles = await catPart(bytes, "word/styles.xml", 'w:styleId="Heading2"');
const patched = await patchPackage(
bytes,
{ part: "word/settings.xml", find: "<w:zoom w:percent=\"100\"/>", replace: "<w:zoom w:percent=\"120\"/>" },
(path) => "",
);
// patched.bytes, patched.replacementsErrors#
Expected failures (a missing file, an unsupported format, a malformed patch)
throw ReviseError, with a stable machine-readable code. Tool calls never
throw for a bad edit; they return { ok: false, error }.