Host-independent OOXML reconciliation engine for .docx manipulation with track changes (redlines).
Converts AI-generated or programmatic text/markdown edits into valid Office Open XML (OOXML) with w:ins/w:del revision markup that Microsoft Word renders as native tracked changes.
- Text reconciliation with word-level diffing and native-looking redlines
- Formatting updates (bold, italic, underline, strikethrough) via surgical
w:rPrChange - Lists: generate and edit real Word lists (
w:numPr) from markdown - Tables: virtual-grid diffing for cell-level edits with merge safety
- Comments: inject OOXML comments anchored to text ranges
- Revision management: detect existing revisions, consume move revisions, and accept/reject tracked changes by author or for all authors
- Comment management: delete comments by author or for all authors
- Highlights: apply highlight colors to runs
- Markdown and OOXML conversion in both directions
- Status/error result fields for parse, targeting, and existing-revision failures
- Package plumbing helpers for numbering.xml, comments.xml, content types, and relationships
- Zero host dependencies: works in Node.js, browsers, Deno, and similar JS runtimes with DOM parsing support
- TypeScript declarations included via
index.d.ts
npm install @ansonlai/docx-redline-js<script type="module">
import { applyRedlineToOxml } from 'https://esm.sh/@ansonlai/docx-redline-js';
</script>Or use the pre-bundled file (no import map needed, diff-match-patch is inlined):
<script type="module">
import { applyRedlineToOxml } from 'https://cdn.jsdelivr.net/npm/@ansonlai/docx-redline-js/dist/docx-redline-js.esm.min.js';
</script>git clone https://github.com/AnsonLai/docx-redline-js.gitimport { applyRedlineToOxml } from './docx-redline-js/index.js';import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
import {
configureXmlProvider,
setDefaultAuthor,
applyRedlineToOxml
} from '@ansonlai/docx-redline-js';
configureXmlProvider({ DOMParser, XMLSerializer });
setDefaultAuthor('My App');
const result = await applyRedlineToOxml(
paragraphOoxml,
'Original sentence.',
'Updated sentence.',
{ generateRedlines: true, author: 'Editor' }
);
console.log(result.hasChanges);
console.log(result.oxml);import {
setDefaultAuthor,
applyRedlineToOxml
} from '@ansonlai/docx-redline-js';
setDefaultAuthor('Browser Editor');
const result = await applyRedlineToOxml(oxml, original, modified, {
generateRedlines: true
});| Function | Purpose |
|---|---|
configureXmlProvider({ DOMParser, XMLSerializer }) |
Inject XML parser. Required in Node.js; browsers usually provide native support. |
configureLogger({ log, warn, error }) |
Replace default console logger. |
setDefaultAuthor(name) |
Set fallback track-change author (default: 'Author'). |
setPlatform(label) |
Set platform label for diagnostics (default: 'Unknown'). |
| Function | Purpose |
|---|---|
applyRedlineToOxml(oxml, original, modified, options) |
Core engine entry point for text/markdown reconciliation with optional redlines. |
applyRedlineToOxmlWithListFallback(oxml, original, modified, options) |
Core engine with automatic single-line list structural fallback. |
reconcileMarkdownTableOoxml(oxml, original, markdownTable, options) |
Table-specific reconciliation helper. |
Common applyRedlineToOxml options:
| Option | Purpose |
|---|---|
generateRedlines |
When true, emit Word-native tracked changes; when false, apply clean text changes. |
author |
Track-change author used for generated revisions. |
existingRevisions |
Policy for source OOXML that already contains tracked changes: 'reject-input' (default) returns status: 'error' with code EXISTING_REVISIONS; 'accept-all-first' accepts existing revisions before applying the new edit. |
removeFormatting |
When true and the text is unchanged with no Markdown hints, explicitly remove existing bold/italic/underline/strikethrough formatting. Defaults to false. |
Common result fields:
| Field | Purpose |
|---|---|
status |
Optional non-breaking status: 'ok', 'no-op', or 'error'. |
error |
Present when status === 'error'; includes a stable code such as PARSE_ERROR, TARGET_NOT_FOUND, or EXISTING_REVISIONS. |
| Function | Purpose |
|---|---|
ReconciliationPipeline |
Direct pipeline access (ingest, diff, patch, serialize). |
ingestWordOoxmlToPlainText(oxml) |
Extract plain text from OOXML. |
ingestWordOoxmlToMarkdown(oxml) |
Convert OOXML to markdown. |
ingestOoxml(oxml) |
Flatten OOXML into an internal run model with offsets. |
preprocessMarkdown(text) |
Normalize markdown and extract format hints. |
containsTrackedChanges(xmlDoc) |
Detect w:ins, w:del, move revisions, property changes, and paragraph-mark revision markup in a parsed OOXML document/fragment. |
validateRedlineOoxml(oxml) |
Validate generated redline OOXML against the package's structural invariants (no nested revisions, w:delText inside w:del, complete metadata, unique revision ids, preserved boundary whitespace). Returns { valid, issues }; run it before writing output into a package. |
| Function | Purpose |
|---|---|
injectCommentsIntoOoxml(oxml, comments, options) |
Add comments anchored to text ranges. |
acceptTrackedChangesInOoxml(oxml, { author?, allAuthors? }) |
Accept w:ins / w:del / w:moveFrom / w:moveTo / *PrChange revisions for one author or all authors. |
rejectTrackedChangesInOoxml(oxml, { author?, allAuthors? }) |
Reject w:ins / w:del / w:moveFrom / w:moveTo / *PrChange revisions for one author or all authors. |
deleteCommentsByAuthorInOoxml(oxml, { author?, allAuthors? }) |
Delete comments and matching anchors/references for one author or all authors. |
generateTableOoxml(headers, rows, options) |
Generate a w:tbl from tabular data. |
createDynamicNumberingIdState(numberingXml) |
Allocate numbering IDs without collisions. |
ensureNumberingArtifactsInZip(zip, numberingXml) |
Merge numbering artifacts into a .docx package. |
ensureCommentsArtifactsInZip(zip, commentsXml) |
Merge comments artifacts into a .docx package. |
validateDocxPackage(zip) |
Validate .docx structural consistency. |
For advanced usage, import specific submodules:
import {
applyOperationToDocumentXml,
applyOperationsToDocumentXml,
orderOperationsForStableTargets
} from '@ansonlai/docx-redline-js/services/standalone-operation-runner.js';
import { getParagraphText } from '@ansonlai/docx-redline-js/core/paragraph-targeting.js';Use applyOperationsToDocumentXml(...) for mixed batches. It stably runs comments before text-changing operations so replacements cannot invalidate their original anchors. Other operation types retain their relative order. Batch results retain each operation's original 1-based index and expose the actual executionOrder.
Different APIs return different OOXML shapes. Use this as a packaging safety check.
| API | Typical input scope | Output field | Possible root/output shape | Safe to write directly into word/document.xml |
|---|---|---|---|---|
applyRedlineToOxml(...) |
Paragraph, range, or table-scope OOXML | result.oxml |
Fragment, <w:document>, or package payload (<pkg:package>) |
No. Inspect first. |
applyRedlineToOxmlWithListFallback(...) |
Paragraph or range-scope OOXML | result.oxml |
Fragment, <w:document>, or package payload (<pkg:package>) |
No. Inspect first. |
reconcileMarkdownTableOoxml(...) |
Table or paragraph-scope OOXML | result.oxml |
Same shapes as applyRedlineToOxml(...) for the supplied scope |
No. Inspect first. |
applyOperationToDocumentXml(...) |
Full word/document.xml string |
result.documentXml |
<w:document> |
Yes. This is the document-safe helper. |
applyOperationsToDocumentXml(...) |
Full word/document.xml plus an operation batch |
result.documentXml |
<w:document> |
Yes. Comments are applied before text-changing operations. |
extractReplacementNodesFromOoxml(...) |
Any OOXML payload | { replacementNodes, numberingXml, sourceType } |
Normalized to fragment, document, or package |
Yes. Use this when consuming result.oxml. |
- Do use
applyOperationToDocumentXml(...).documentXmlwhen your intent is to replaceword/document.xml. - Do use
applyOperationsToDocumentXml(...)rather than an unsorted loop for batches containing comments and replacements that target the same original paragraph. - Redline application now strips non-visible field scaffolding (
w:fldChar,w:instrText) and proofing markers (w:proofErr) from the matched target paragraph before diffing, while preserving the visible field result text. This avoids a class of Word-open failures caused by tracked changes spanning hidden field instruction runs. - Hyperlinks, bookmarks, comment range markers, tabs/breaks, and footnote/endnote references are treated as structural OOXML that should survive adjacent redline edits instead of being orphaned or wrapped in deletions.
- Do use
extractReplacementNodesFromOoxml(...)when you are consumingresult.oxmlfrom paragraph/range/table APIs. - Do merge numbering/comments artifacts with
ensureNumberingArtifactsInZip(...)andensureCommentsArtifactsInZip(...)when those parts are present. - Don't write payloads that start with
<pkg:packagedirectly intoword/document.xml. - Don't assume every
result.oxmlpayload is a raw paragraph fragment.
This package operates on OOXML strings (XML parts inside .docx zip archives), not raw .docx binaries.
Typical flow:
- Extract the
.docxzip (for example with JSZip, fflate, or similar) - Read
word/document.xml - Apply reconciliation APIs to XML strings
- Merge numbering/comments artifacts when needed
- Write the archive back to a
.docxfile
import JSZip from 'jszip';
import {
applyRedlineToOxml,
extractReplacementNodesFromOoxml,
ensureNumberingArtifactsInZip,
validateDocxPackage
} from '@ansonlai/docx-redline-js';
import { applyOperationToDocumentXml } from '@ansonlai/docx-redline-js/services/standalone-operation-runner.js';
const zip = await JSZip.loadAsync(docxBuffer);
const documentXml = await zip.file('word/document.xml').async('string');
const opResult = await applyOperationToDocumentXml(
documentXml,
{ type: 'redline', target: 'old text', modified: 'new text' },
'Editor'
);
// applyOperationToDocumentXml(...) returns a full w:document payload.
zip.file('word/document.xml', opResult.documentXml);
const fragmentResult = await applyRedlineToOxml(
paragraphOoxml,
'Item text',
'1. Item text',
{ generateRedlines: true, author: 'Editor' }
);
const normalized = extractReplacementNodesFromOoxml(fragmentResult.oxml);
// If sourceType === 'package', merge extracted content/artifacts instead of
// writing the raw pkg:package payload into word/document.xml.
if (normalized.numberingXml) {
await ensureNumberingArtifactsInZip(zip, normalized.numberingXml);
}
await validateDocxPackage(zip);
const output = await zip.generateAsync({ type: 'nodebuffer' });Run the automated package checks:
npm test
npm run test:isolation
npm run check:typesFor release-time fixture export:
node scripts/export-validation-fixtures.mjsOn Windows with desktop Word installed, you can smoke-test a completed .docx:
npm run smoke:word -- path/to/file.docxTo validate against Word as an independent oracle (Word itself accepts and rejects the generated revisions and the resulting text is compared to the expected outcomes):
node scripts/export-validation-fixtures.mjs
npm run smoke:word:diffA nightly GitHub Actions workflow additionally validates generated fixtures
against the ECMA-376 transitional schemas (xmllint), opens them with
LibreOffice, and runs an extended fuzz sweep of the accept/reject round-trip
invariant with a fresh seed. See docs/VALIDATION.md.
See ARCHITECTURE.md for module layout, data flow, and contributor guidance.
See AGENTS.md for a concise reference for AI coding agents.
See docs/VALIDATION.md for release-time validation steps.