@docx-editor.dev/core/contracts/modules
@docx-editor.dev/core/contracts/modules — the EditorModule seam.
How a paid or optional capability contributes to the engine without the free tier importing it: the review module and custom nodes both arrive through here, and their absence leaves the corresponding chrome disabled with the engine's own reason.
NOT a plugin system. The shape is closed: every contribution point is named here, core iterates registered modules at its existing dispatch points, and core never imports a capability package. A capability absent from this file is not extendable from outside — deliberately, so the one-pipeline principle survives the packaging boundary. The free engine behaves identically with an empty registry: documents round-trip losslessly, revisions render in their final-state projection, and the review chrome slots stay disabled with the engine's own reason.
Functions (1)
resolveEditorModulesfunctionSource ↗
Resolve construction-time modules into the registry the instance dispatches over.
declare function resolveEditorModules(modules: readonly EditorModule[] | undefined): EditorModuleRegistry;Interfaces (12)
CommentRecordinterfaceSource ↗
One comment as authored in word/comments.xml.
interface CommentRecord| Member | Type | Summary |
|---|---|---|
| author | string | |
| blocks | readonly OoxmlElement[] | Body paragraphs, as tree nodes, so the surface renders measured text rather than a string. |
| date? | string | |
| id | string | |
| initials? | string | |
| paraId? | string | `w14:paraId` of the first body paragraph — the key thread state is stored under. |
| parentCommentId? | string | `@w16cid:parentId` — the `w:id` of the comment this replies to, when the file names it. |
EditorModuleinterfaceSource ↗
One registered capability module. Registration is construction-time (createDocxEditor({ modules })) and immutable for the instance's lifetime.
interface EditorModule| Member | Type | Summary |
|---|---|---|
| customNodePayloadNamespaces? | readonly string[] | customXml payload namespaces this module OWNS, swept for orphans when a document opens. |
| customNodes? | readonly unknown[] | Custom inline node definitions. Reserved: the definition shape lands with the custom-nodes lane; the registry carries them opaquely until then. |
| id | string | Diagnostic identity (`'review'`, `'custom-nodes'`); not a dispatch key. |
| onCustomNodeDiagnostic? | (diagnostic: unknown) => void | Told when the recognition pass finds something wrong with a node in THIS editor's document. |
| review? | ReviewModuleContribution | Review capability: queue derivation, commands gate, display modes. |
EditorModuleRegistryinterfaceSource ↗
The resolved registry the editor instance holds: at most one review contribution (first registration wins), all custom node definitions in registration order.
interface EditorModuleRegistry| Member | Type | Summary |
|---|---|---|
| customNodeDiagnostics | readonly ((diagnostic: unknown) => void)[] | Every registered diagnostic listener, in registration order. |
| customNodePayloadNamespaces | readonly string[] | Every claimed payload namespace, deduplicated, in registration order. |
| customNodes | readonly unknown[] | |
| review | ReviewModuleContribution | null |
OoxmlPartinterfaceSource ↗
One XML part of the package, parsed into a canonical tree.
interface OoxmlPart| Member | Type | Summary |
|---|---|---|
| contentType | string | |
| id | string | |
| name | string | Canonical part name, e.g. `/word/document.xml`. |
| root | OoxmlElement |
ReviewCommentIteminterfaceSource ↗
One comment as a review card. A reply carries parentId; OOXML gives replies no separate element, so threads are reconstructed from that link.
interface ReviewCommentItem| Member | Type | Summary |
|---|---|---|
| comment | CommentRecord | |
| id | string | |
| kind | 'comment' | |
| orphaned | boolean | True when the file gave this comment no usable range. |
| parentId? | string | The comment this replies to, absent for a top-level comment. |
| parentRevisionId? | string | The REVISION this comment answers, when it covers exactly that change's characters. |
| range | ReviewRange | null | |
| replyIds | readonly string[] | Replies to this comment, in document order. Empty for a reply or a childless comment. |
| resolved | boolean |
ReviewCustomIteminterfaceSource ↗
A card contributed by a recognized custom node (defineCustomNode with a reviewCard hook), anchored at the node's range.
Informational, never resolvable: there is nothing to accept or reject, so the engine refuses those verbs on it. title and detail are HOST-authored (the definition's hook produced them), but attrs and text originate in a file an attacker controls — a surface renders every one of these as text, never markup.
interface ReviewCustomItem| Member | Type | Summary |
|---|---|---|
| attrs | Readonly<Record<string, string>> | Attrs decoded from the tag, after the definition's recognition hook. Untrusted input. |
| carded | boolean | Whether this node asked for a sidebar card. |
| data? | unknown | The payload the node's control binds to, after the definition validated it. |
| detail? | string | Card body, from the definition's `reviewCard` hook. |
| icon? | string | Glyph for this node in the collapsed rail, as an SVG path in a `0 -960 960 960` viewBox. |
| id | string | The SDT node's stable id in the canonical tree. |
| kind | 'custom' | |
| name | string | The definition's `name`. |
| range | ReviewRange | null | |
| tag | string | The raw `w:tag` the node was recognized from. Untrusted input. |
| text | string | The SDT's literal content text. Untrusted input. |
| title | string | Card title, from the definition's `reviewCard` hook. Empty when `carded` is false. |
ReviewModelInputinterfaceSource ↗
What the review queue derivation reads: one story part plus its comment parts.
interface ReviewModelInput| Member | Type | Summary |
|---|---|---|
| commentsExtendedPart? | OoxmlPart | undefined | `word/commentsExtended.xml`, absent when the package has none. |
| commentsPart? | OoxmlPart | undefined | `word/comments.xml`, absent when the package has none. |
| customNodePayloads? | ReadonlyMap<string, {
readonly nodeId: string;
readonly label: string;
readonly data: string;
}> | undefined | |
| customNodes? | readonly unknown[] | undefined | Custom node definitions from the module registry, forwarded OPAQUELY. |
| furnitureParts? | readonly OoxmlPart[] | undefined | Header/footer story parts, in section order. Their revisions and comment anchors join the queue: a tracked change in a header is a pending decision like any other, and a queue that only walked the body silently hid it from the rail AND from Accept All. |
| reportCustomNodeDiagnostic? | ((diagnostic: unknown) => void) | undefined | Where a capability package reports a node it could not read. Supplied per editor, so a page with two of them keeps their diagnostics apart. |
| storyPart | OoxmlPart | The story the ranges live in — the main document, a header, a note. |
ReviewModuleContributioninterfaceSource ↗
What a review module contributes: the queue derivation, and the revision display modes the editor may enter beyond the free tier's final-state projection.
interface ReviewModuleContribution| Member | Type | Summary |
|---|---|---|
| collectReviewItems | CollectReviewItems | The review queue derivation. |
| displayModes | readonly RevisionDisplayMode[] | Display modes this module unlocks (the free engine renders `proposed` only). |
| revisionItemsOfParagraph | (part: OoxmlPart, paragraphId: string) => readonly ReviewRevisionItem[] | Revisions wholly inside one paragraph — for the conservative local review patch after a text-local body edit. |
ReviewPositioninterfaceSource ↗
A position in the model offset space of one story.
interface ReviewPosition| Member | Type | Summary |
|---|---|---|
| offset | number | |
| paragraphId | string |
ReviewRangeinterfaceSource ↗
Where an item is anchored: a range in one story.
interface ReviewRange| Member | Type | Summary |
|---|---|---|
| end | ReviewPosition | |
| partName | string | |
| start | ReviewPosition |
ReviewRevisionIteminterfaceSource ↗
One tracked change as a review card.
Keyed per DECISION rather than per site: a revision spanning three ranges is one card, because accepting it accepts all three.
interface ReviewRevisionItem| Member | Type | Summary |
|---|---|---|
| address | RevisionAddress | The payload `acceptRevision` / `rejectRevision` take. |
| addresses | readonly RevisionAddress[] | EVERY address this decision covers, `address` first. |
| author | string | |
| date? | string | |
| id | string | Stable across renders and unique per DECISION, not per site. |
| kind | 'revision' | |
| pairedWith? | string | The other half of a move, or the other side of a delete/insert replacement. |
| ranges | readonly ReviewRange[] | Every site this decision touches, in document order. |
| readOnly | boolean | True when the engine cannot resolve this kind, so accept and reject must not be offered. |
| replacedRangeCount? | number | How many leading `ranges` are the STRUCK half of a replacement. |
| replacedText | string | The words a replacement removes. Empty for every other kind. |
| replyIds | readonly string[] | Comments answering this change, in document order. |
| revisionKind | ReviewRevisionKind | |
| text | string | Text the revision covers, for the card summary. Empty for changes with no characters. |
RevisionAddressinterfaceSource ↗
How a tracked change is addressed: its numeric id plus the PART it lives in.
Both, always — @w:id is unique only within a part, so an id alone names two revisions in any package with a header or a comments part.
interface RevisionAddress| Member | Type | Summary |
|---|---|---|
| author | string | |
| date? | string | Absent when the file wrote no `@w:date`; part of the identity either way. |
| id | string |
Type aliases (4)
CollectReviewItemstypeSource ↗
Derives the review queue — every pending revision decision and comment thread — from one story part plus its comment parts. Implemented by the pro review module; the free engine has no implementation and reports an empty queue.
type CollectReviewItems = (input: ReviewModelInput) => readonly ReviewItem[];ReviewItemtypeSource ↗
One pending decision in the review queue: a tracked change, a comment thread, or a pro custom-node card. Discriminate on kind.
type ReviewItem = ReviewRevisionItem | ReviewCommentItem | ReviewCustomItem;ReviewRevisionKindtypeSource ↗
What kind of decision a revision card represents.
Wider than the four content wrappers, because a reviewer has to be shown every pending decision, including the ones that decorate no characters. A card the surface cannot show is a change the reviewer never learns about — and acceptAllRevisions refuses if ANY revision in the document is one the engine cannot resolve, so an invisible one makes Accept All fail for a reason nothing on screen explains.
type ReviewRevisionKind = 'insert' | 'delete'
/**
* A deletion and an insertion that are one edit: text typed over a selection.
*
* Word shows these as a single `Replaced "x" with "y"` card, and resolving one half
* without the other is never what the reviewer meant — accepting the deletion alone
* leaves the replacement text unproposed, rejecting it alone leaves both.
*/
| 'replace' | 'moveFrom' | 'moveTo'
/** `w:rPrChange` / `w:pPrChange` — the words are unchanged, their formatting is not. */
| 'format'
/** `w:pPr/w:rPr/w:ins|w:del` — a paragraph split or merge. */
| 'paragraphMark'
/** A row, cell, section or grid revision. Supported row revisions are resolvable. */
| 'structural';RevisionDisplayModetypeSource ↗
Which revisions layout resolves before producing pages.
- all-markup shows both halves of every change. - proposed shows what the document becomes if every change is accepted. - original shows what it was before any of them.
The last two are specified as equal to accept-all and reject-all OUTPUT, which is what makes them testable, without either applying an op.
type RevisionDisplayMode = 'all-markup' | 'proposed' | 'original';