@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
MemberTypeSummary
authorstring
blocksreadonly OoxmlElement[]Body paragraphs, as tree nodes, so the surface renders measured text rather than a string.
date?string
idstring
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
MemberTypeSummary
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.
idstringDiagnostic identity (`'review'`, `'custom-nodes'`); not a dispatch key.
onCustomNodeDiagnostic?(diagnostic: unknown) => voidTold when the recognition pass finds something wrong with a node in THIS editor's document.
review?ReviewModuleContributionReview 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
MemberTypeSummary
customNodeDiagnosticsreadonly ((diagnostic: unknown) => void)[]Every registered diagnostic listener, in registration order.
customNodePayloadNamespacesreadonly string[]Every claimed payload namespace, deduplicated, in registration order.
customNodesreadonly unknown[]
reviewReviewModuleContribution | null

OoxmlPartinterfaceSource ↗

One XML part of the package, parsed into a canonical tree.

interface OoxmlPart
MemberTypeSummary
contentTypestring
idstring
namestringCanonical part name, e.g. `/word/document.xml`.
rootOoxmlElement

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
MemberTypeSummary
commentCommentRecord
idstring
kind'comment'
orphanedbooleanTrue when the file gave this comment no usable range.
parentId?stringThe comment this replies to, absent for a top-level comment.
parentRevisionId?stringThe REVISION this comment answers, when it covers exactly that change's characters.
rangeReviewRange | null
replyIdsreadonly string[]Replies to this comment, in document order. Empty for a reply or a childless comment.
resolvedboolean

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
MemberTypeSummary
attrsReadonly<Record<string, string>>Attrs decoded from the tag, after the definition's recognition hook. Untrusted input.
cardedbooleanWhether this node asked for a sidebar card.
data?unknownThe payload the node's control binds to, after the definition validated it.
detail?stringCard body, from the definition's `reviewCard` hook.
icon?stringGlyph for this node in the collapsed rail, as an SVG path in a `0 -960 960 960` viewBox.
idstringThe SDT node's stable id in the canonical tree.
kind'custom'
namestringThe definition's `name`.
rangeReviewRange | null
tagstringThe raw `w:tag` the node was recognized from. Untrusted input.
textstringThe SDT's literal content text. Untrusted input.
titlestringCard 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
MemberTypeSummary
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[] | undefinedCustom node definitions from the module registry, forwarded OPAQUELY.
furnitureParts?readonly OoxmlPart[] | undefinedHeader/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) | undefinedWhere a capability package reports a node it could not read. Supplied per editor, so a page with two of them keeps their diagnostics apart.
storyPartOoxmlPartThe 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
MemberTypeSummary
collectReviewItemsCollectReviewItemsThe review queue derivation.
displayModesreadonly 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
MemberTypeSummary
offsetnumber
paragraphIdstring

ReviewRangeinterfaceSource ↗

Where an item is anchored: a range in one story.

interface ReviewRange
MemberTypeSummary
endReviewPosition
partNamestring
startReviewPosition

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
MemberTypeSummary
addressRevisionAddressThe payload `acceptRevision` / `rejectRevision` take.
addressesreadonly RevisionAddress[]EVERY address this decision covers, `address` first.
authorstring
date?string
idstringStable across renders and unique per DECISION, not per site.
kind'revision'
pairedWith?stringThe other half of a move, or the other side of a delete/insert replacement.
rangesreadonly ReviewRange[]Every site this decision touches, in document order.
readOnlybooleanTrue when the engine cannot resolve this kind, so accept and reject must not be offered.
replacedRangeCount?numberHow many leading `ranges` are the STRUCK half of a replacement.
replacedTextstringThe words a replacement removes. Empty for every other kind.
replyIdsreadonly string[]Comments answering this change, in document order.
revisionKindReviewRevisionKind
textstringText 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
MemberTypeSummary
authorstring
date?stringAbsent when the file wrote no `@w:date`; part of the identity either way.
idstring

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';

On this page