ts-docx
v0.1.0
Published
Read, edit, and save existing Word (.docx) files in Node — a TypeScript port of python-docx.
Maintainers
Readme
ts-docx
Read, edit, and save existing Word (.docx) files in Node — a TypeScript port of python-docx.
Status: scaffold only — this folder holds this README and nothing else. The port is built in a dedicated session off this brief. Standalone and FlowDot-agnostic; the flowdot-documents engine (separate project) wraps it. This is the docx sibling of ts-pptx — same architecture, same fidelity guarantees, same conventions. Where this brief is thin, ts-pptx's shipped code is the reference (its src/xml/ lossless DOM, src/opc/ package layer, and fidelity model port directly).
The JS ecosystem has no python-docx equivalent for editing: mammoth only reads (→HTML/text), the docx package only creates. ts-docx opens an existing document, lets you read and edit its paragraphs/runs/tables/sections/styles, and saves it back — in pure TypeScript, with jszip as the only runtime dependency. Runs anywhere Node ≥ 18 runs (including the Electron main process); no native modules, no DOM.
- Node ≥ 18 · ESM · MIT · one runtime dep (
jszip) - Ergonomics mirror python-docx (
snake_case→camelCase), so its docs and examples translate directly.
Why this exists
python-docx (MIT) is the proven design for programmatic Word editing. Port its layered architecture, not just its feature list. FlowDot's document engine needs full docx read/edit/save in pure TS so it runs identically in the Electron native app, the CLI, and the published MCP server — and so agents and users can edit real Word documents without a Python dependency or a bespoke local MCP server.
What to port (python-docx's layered architecture)
Mirror ts-pptx's layering exactly:
src/xml/— lossless XML parser / DOM / serializer. Reuse ts-pptx's (byte-span preserving; unknown elements survive verbatim). This is the backbone of the fidelity guarantee.src/opc/— OPC package: zip open/save viajszip,[Content_Types].xml, relationships, part loading/serialization. Near-identical to ts-pptx'ssrc/opc/.src/oxml/— typed wrappers over WordprocessingML elements (w:document,w:body,w:p,w:r,w:rPr,w:pPr,w:tbl,w:sectPr, …).- Public API (
src/{text,table,section,styles,shape,parts}/), mirroring python-docx:
import { Document, Pt, Inches } from "ts-docx";
const doc = await Document.open("report.docx"); // path, Uint8Array, or ArrayBuffer
for (const p of doc.paragraphs) {
console.log(p.style?.name, p.text);
for (const run of p.runs) { /* run.text, run.bold, run.font.* */ }
}
// the formatting-preserving edit — change wording, keep the run's look (rPr untouched)
doc.paragraphs[3].runs[0].text = "revised sentence.";
doc.addHeading("Appendix", 1);
const p = doc.addParagraph("Body text ", "Normal");
p.addRun("bold bit").bold = true;
const table = doc.addTable(2, 3);
table.cell(0, 0).text = "Header";
await doc.save("report-edited.docx"); // or: const bytes = await doc.toBuffer();Everything except open, save, toBuffer is synchronous (only those touch the zip container). The object model — paragraphs, runs, tables, sections — is all sync.
API surface to cover (python-docx parity + the additions below)
- Document:
Document.open(),save,toBuffer,paragraphs,tables,sections,styles,inlineShapes,coreProperties,addParagraph,addHeading,addPageBreak,addPicture,addTable,addSection. - Paragraph:
text(get/set),runs,style,alignment(WD_ALIGN_PARAGRAPH),paragraphFormat(indents, spacing, keepWithNext, pageBreakBefore, tabs),addRun,insertParagraphBefore,clear. - Run:
text(the formatting-preserving setter —rPruntouched, matching ts-pptx'srun.text),bold/italic/underline,font(name, size, color.rgb / color.themeColor, highlightColor, strike, subscript/superscript, allCaps, smallCaps),styleId,addBreak,addTab,addPicture. - Tables:
table.rows,columns,cell(r,c),rows[i].cells,cell.text,cell.paragraphs,cell.merge(other),addRow,addColumn, table style, alignment, autofit, column widths, cell vertical alignment/margins; read existing merged cells (cell.isMergeOrigin/ span info). - Sections:
pageWidth/pageHeight,leftMargin/right/top/bottom/gutter/header/footer,orientation,startType; headers & footers (section.header,section.footer,.isLinkedToPrevious, first-page/even-page variants) with their own paragraphs. - Styles:
doc.stylescollection — read/apply paragraph, character, table, list styles; style inheritance (basedOn). - Numbering / lists: apply list styles; read list level/numId (full numbering authoring can be a later milestone — flag, don't silently drop).
- Images:
addPicture(bytes|path, width?, height?)(inline), read inline shapes, replace image bytes keeping the shape (mirror ts-pptxpicture.replaceImage). - Core properties: title/author/subject/keywords/comments/category/created/modified/lastModifiedBy/revision.
Additions beyond python-docx (high-value, python-docx lacks these)
- Run-aware find & replace —
doc.replaceText(search, replace, { all? })that correctly handles text split acrossw:rruns (Word constantly splits mid-word on rsid/spellcheck boundaries). This is the #1 real docx-editing operation and the exact thing naive raw-XML regex replace (e.g. the old document-mcp-server) gets wrong. Extract run text into a logical string with an index→run map, match on the logical string, splice runs at boundaries, inherit the matched run'srPr. Ship it with a tests corpus of run-split fixtures. - Tracked changes & comments (display + preserve on round-trip at minimum; authoring is a flagged later milestone). Preservation is free from the fidelity model; authoring
w:ins/w:del/comments is additive. - Reads never mutate (see below).
Units
EMU throughout for lengths, as branded number (mirror ts-pptx). Word also uses twips (page/margins) and half-points (font size) internally — expose the same unit constructors (Pt, Inches, Cm, Mm, Emu, Twips) and a Length converter; the wrappers handle the internal unit per property so callers always work in real units.
The fidelity model (identical to ts-pptx — non-negotiable)
- Open → save with no edits is loss-free. Every untouched part is written back byte-identical (decompressed entry bytes; the zip container is re-deflated). Unknown/vendor XML survives untouched. Zip entries unreachable from the relationship graph are carried through verbatim (python-docx drops some of these — ts-docx must not).
- An edit touches only what it must. Change one run's text and only that run's
w:tchanges. - Reads never mutate. Accessing
font.color,paragraphFormat, etc. to read never writes XML (a deliberate divergence from python-docx, whose accessors can rewrite on read — required for the loss-free guarantee).
Constraints
- Pure TypeScript. No native modules. Node ≥ 18 (Electron 28 main floor). Runs in Node and the Electron main process; no DOM.
- Dependencies pass the FlowDot sanity test: frictionless in the native app, no security exposure, "should we build it ourselves?" answered honestly. Keep the tree at
jsziponly (build thin XML handling — ported from ts-pptx — rather than importing a framework). - License: MIT (© FlowDot LLC). All deps permissive. python-docx's MIT copyright/permission notice reproduced in
LICENSEunder "Third-party notices," as its license requires. .docxis the open ECMA-376 (Office Open XML) standard.
Testing
- Fixture corpus of real Word-authored documents in
tests/assets/(round-trip oracle) andfixtures/(authored corpus): headings, styles, tables (incl. merged + nested), sections with headers/footers, inline images, lists, tracked changes, comments, run-split paragraphs. - Backbone test: round-trip every XML entry of every doc through
parse → serialize, assert byte-equality. - API unit tests per surface; a dedicated run-split find-&-replace suite.
scripts/make-mN-gate.mjsper milestone →out/mN-*.docxfor manual review in Word.- Vitest;
tsc --strictclean; publish infra mirrors ts-pptx / ts-pdf-edit (eslint,.npmignore,verify-pack, CI publish with--provenance).
Reference
- python-docx source + docs (MIT) — the architecture and API being ported.
- ts-pptx shipped source — the XML/OPC layers and fidelity model to reuse verbatim.
- ECMA-376 (WordprocessingML) for the XML specifics.
- Consumer:
flowdot-documents; keep this library FlowDot-agnostic.
