# Library

> ReviseDocument: open, read, edit, review, and export documents from Node.

```bash
npm i revise
```

```ts
import { writeFile } from "node:fs/promises";
import { ReviseDocument } from "revise";

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();
```

The `revise` CLI is built on this API. 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](https://revise.io/developer/docs/revise/collaboration). 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

```ts
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:

```ts
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](https://revise.io/developer/docs/editor-sdk/api/agent-tools); `revise` 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

```ts
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: number
```

A 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 throws `section_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

```ts
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`. 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 `@reviseio/editor`, 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

```ts
import { compare, convert } from "revise";

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"); // Uint8Array
```

## The raw package

For what the tools don't cover, read and patch the `.docx` parts directly:

```ts
import { readFile } from "node:fs/promises";
import { catPart, listParts, patchPackage } from "revise";

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.replacements
```

## Errors

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 }`.
