Architecture
How the editor renders DOCX with Word fidelity: one canonical OOXML tree, a DOM-free layout pass, and painted pages that are themselves the editable surface.
There is one document model and one pipeline:
bytes → bounded OPC/XML read → canonical OOXML tree → layout → painted pages → serializeWhat you see painted and what an edit mutates are the same tree, so there is no separate view model to synchronize.
Architecture boundaries
| Boundary | Owns | Must not own |
|---|---|---|
contracts | Public engine types and interfaces | Runtime implementation or state |
store | Canonical trees, package reads and writes, indexes, and transactions | DOM or ProseMirror state |
layout | DOM-free pagination and interaction geometry | Browser layout or authored state |
output | Painted pages from layout records | Document mutation |
binding | The ProseMirror projection and conversion of edits into tree operations | Canonical document authority |
automation | Transport-neutral host operations for browser and server automation | DOM or editor chrome |
editor | The public editor facade, surface input, caret, and chrome registry | A second document model |
react and vue | Providers, hooks or composables, and framework chrome | Editing state |
The package boundary keeps one copy of @docx-editor.dev/core in an
application. Both adapters use it as a peer dependency.
The canonical tree
Reading a .docx produces one tree per XML part, up to a bounded part count. Non-XML parts stay as bytes and are never parsed. Nodes are typed where layout needs them (the paragraph, run, and table vocabulary) and generic everywhere else, preserving the element verbatim.
Two consequences:
- Content the engine does not model is carried, not dropped. A known element in an invalid position demotes to generic rather than erroring.
- Unknown content never blocks editing. A document full of extensions the engine does not recognize opens, accepts edits, and saves.
Every write is a transaction over the tree. Content edits are addressed by node id and character offset; package-level operations such as creating a header or setting section properties are addressed by section index. That is the only write path. There is no second way to mutate a document, so undo, the editing API, and every command use the same write path.
Layout
The layout pass is DOM-free. It takes the tree and a text measurer and returns positioned pages, working in the document's own units (twips, half-points, EMUs) rather than approximating from browser layout.
Because it is not constrained by what contenteditable can express, it lays text out according to Word layout rules:
- Pagination is computed, not approximated: lines are measured, blocks split where Word splits them, and tables fragment across page boundaries with Word-compatible border treatment at the split.
- Paragraph fidelity resolves through the real style cascade:
w:spacingline rules, first-line and hanging indents,w:contextualSpacing, paragraph borders, tab stops and leaders, list markers fromnumbering.xml, and table styles through theirbasedOnchain gated byw:tblLook. - Headers and footers lay out once per variant and attach per page. Editing one uses the same edit path as body content.
The pass is incremental: per-block cache keys and flow checkpoints mean a keystroke re-lays out what changed, and a pass with no changes returns the previous pages by identity.
Paint and interaction
The painted pages are contenteditable, but the DOM is a non-authoritative rendering. Browser mutations are prevented and re-expressed as tree operations, so the browser cannot insert markup inside your document.
The editor also paints its own caret, from layout geometry rather than from the DOM, which is why an empty paragraph gets a caret. On failure it falls back: range selection, IME composition, or a position it cannot place all restore the native caret instead of showing no caret.
Selection maps through paragraph identity and offsets. It does not use DOM traversal.
| Page furniture | Layout behavior | Interaction behavior |
|---|---|---|
| Headers and footers | Lay out by section and page variant | Use scoped editing controls |
| Page numbers and other furniture | Attach to each painted page | Stay non-editable and outside selection |
| Body content | Flows around reserved furniture bands | Uses the normal caret and selection model |
Saving
Every parsed XML part is re-emitted from the canonical tree with structural fidelity, including custom XML and unknown extensions. Package payloads such as embedded fonts, media, and VBA binaries pass through untouched.
That is what "structural fidelity" means here: unsupported XML and package payloads survive editing and save without loss. Two oracles gate it in CI: a canonical fingerprint over the tree, and a save-and-reopen semantic digest.
Adapters
@docx-editor.dev/core contains the engine and no framework code. An adapter
mounts the engine and provides framework-specific state access and chrome.
@docx-editor.dev/react and @docx-editor.dev/vue hold no editing state.
Packaged and custom controls consume the same public engine contract.
Next steps
- Core package overview: entry points and the
Editorcontract - Word fidelity: what the fidelity claim covers, and its limits
- Composition: building on the adapter
@docx-editor.dev/core
The framework-agnostic engine: OOXML read and write, the canonical document tree, layout, paint, and the Editor contract that adapters render.
@docx-editor.dev/i18n
Locale data for the editor chrome. Ships ten languages: English, Polish, German, French, Portuguese, Hebrew, Hindi, Indonesian, Turkish, and Chinese.