@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 (25)

CollaborationModuleContributioninterfaceSource ↗

What a collaboration module contributes: the replica the surface attaches.

interface CollaborationModuleContribution
MemberTypeSummary
sessionEditorCollaborationSessionA ready session. The host creates the Yjs room, then wraps it with `collaborationModule({ session })`.

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 last 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.

CompareDocumentsDialoginterfaceSource ↗

Compare documents dialog supplied by the review module.

interface CompareDocumentsDialog
MemberTypeSummary
destroy
open

CompareDocumentsDialogHostinterfaceSource ↗

What the Compare documents dialog may use: its session, mount, and strings.

interface CompareDocumentsDialogHost extends CompareDocumentsDialogSession
MemberTypeSummary
containerHTMLElement
translate?(key: string) => string | undefined

CompareDocumentsDialogSessioninterfaceSource ↗

One open Compare documents dialog. The editor owns it: the session aborts when the dialog closes, is replaced, or the document, mount, or editor goes away.

interface CompareDocumentsDialogSession
MemberTypeSummary
applyCompare the open document with the revised DOCX and record the differences in it as tracked changes, as one undoable step. Closes the session when the document changes. With no differences (`changed` false) or a refusal, the session stays open with a message to show.
canApplyWhether a compare can run now.
cancelClose the dialog. A running background compare stops, and nothing changes.
cancelablebooleanWhether `cancel()` can stop a running compare: true when compares run in the background. A compare that falls back to the main thread, because the worker never started, cannot stop.
defaultAuthorstringThe author name the dialog offers first: the editor's author, or an empty string.
originalHasTrackedChangesbooleanWhether the open document has tracked changes when the dialog opens. The compare treats them as accepted, so a dialog says so before the user compares.
signalAbortSignalAborts on close, replacement, load, detach, or destroy.

CompareDocumentsEngineOptionsinterfaceSource ↗

Options of one document compare.

interface CompareDocumentsEngineOptions
MemberTypeSummary
authorName?stringAuthor recorded on every revision. The engine picks a default when absent, empty, or only white space.
detectFormatChanges?booleanRecord formatting differences as formatting revisions. Default `true`.
removeDateAndTime?booleanOmit the revision timestamps. Default `false`.

CompareDocumentsProgressinterfaceSource ↗

How far a background compare is.

interface CompareDocumentsProgress
MemberTypeSummary
fractionnumberThe share of the work done, from 0 to 1. It never decreases.
phaseCompareDocumentsPhaseThe step that runs now.

CompareDocumentsRequestinterfaceSource ↗

What one compare request carries: the revised DOCX and its options.

interface CompareDocumentsRequest
MemberTypeSummary
authorName?stringAuthor recorded on every revision. When absent or blank, the editor's author. Without an editor author, the revised document's last author, then its creator, then `Comparison`.
detectFormatChanges?booleanRecord formatting differences as formatting revisions. Default `true`.
removeDateAndTime?booleanWrite the revisions without dates. Default `false`.
revisedUint8ArrayThe revised document's DOCX bytes. The open document is the original.

ContentTypeIndexinterfaceSource ↗

The resolved lookup: Override by case-folded part name, Default by case-insensitive extension.

interface ContentTypeIndex
MemberTypeSummary
defaultsReadonlyMap<string, string>ext key - single MIME (identical duplicates collapsed).
overridesReadonlyMap<string, string>case-folded part name - MIME.

EditorModuleinterfaceSource ↗

One registered capability module. Registration is construction-time (createDocxEditor({ modules })) and immutable for the instance's lifetime.

interface EditorModule
MemberTypeSummary
collaboration?CollaborationModuleContributionCollaboration replica. Absent, the editor stays single-user: local undo stays the authority, and `snapshot().collaborationStatus` is `'inactive'`.
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.
onDiagnostic?(event: EditorDiagnostic) => voidTold about every anonymous diagnostic of this editor, from its first open on.
review?ReviewModuleContributionReview capability: queue derivation, commands gate, display modes.
wantsDiagnostic?(kind: EditorDiagnosticKind) => booleanWhether `onDiagnostic` wants events of `kind` now. Absent, it wants every kind. The editor asks before work that only a diagnostic needs, such as timing key presses for `performance`, and skips that work when no listener wants it. Read often: keep it cheap.

EditorModuleRegistryinterfaceSource ↗

The resolved registry the editor instance holds: at most one review contribution and at most one collaboration contribution (first registration wins for each), all custom node definitions in registration order.

interface EditorModuleRegistry
MemberTypeSummary
collaborationCollaborationModuleContribution | null
customNodeDiagnosticsreadonly ((diagnostic: unknown) => void)[]Every registered diagnostic listener, in registration order.
customNodePayloadNamespacesreadonly string[]Every claimed payload namespace, deduplicated, in registration order.
customNodesreadonly unknown[]
diagnosticListenersreadonly ((event: EditorDiagnostic) => void)[]Every registered `onDiagnostic` listener, in registration order.
reviewReviewModuleContribution | null
wantsDiagnostic(kind: EditorDiagnosticKind) => booleanWhether any module with `onDiagnostic` wants events of `kind` now.

OoxmlExternalTargetinterfaceSource ↗

An external relationship target. Retained verbatim as authored evidence, with the sink-safety verdict alongside it. Never resolved against the package, never fetched.

interface OoxmlExternalTarget
MemberTypeSummary
idstring
ownerPartstring
rawTargetstring
sinkSafebooleanFalse when the target is not a safe sink (javascript:, file:, ...). Still not fetched.
typestring

OoxmlPackageinterfaceSource ↗

A loaded package: every XML part as a canonical tree, plus the non-XML parts kept verbatim.

The preservation model in one value. Modelled parts re-emit normalized; everything else is byte-identical, which is why an unrecognized part never costs a document anything.

interface OoxmlPackage
MemberTypeSummary
contentTypesContentTypeIndex
externalTargetsreadonly OoxmlExternalTarget[]
mainDocumentPartstringCanonical name of the part the root `officeDocument` relationship points at.
partBytesReadonlyMap<string, Uint8Array>Raw bytes of every entry, including the non-XML parts that have no tree.
partsReadonlyMap<string, OoxmlPart>Canonical trees, keyed by canonical part name. Non-XML parts are absent by design.
relationshipsReadonlyMap<string, readonly RelationshipRecord[]>Internal relationships by owner part, in authored order.

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

RelationshipRecordinterfaceSource ↗

One authored relationship, with nothing materialized away: owner part, id, type, raw target lexical form, mode, and position.

interface RelationshipRecord
MemberTypeSummary
idstring
ordernumber
ownerPartstring
rawTargetstring
targetModeTargetMode
typestring

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; }> | undefinedThe payload each of the story's controls binds to, keyed by the control's node id.
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.
stylesPart?OoxmlPart | undefinedShared style definitions. Their revisions have no paragraph range.

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.
compareDocuments?CompareDocumentsEngineDocument compare. Absent, compare commands refuse.
compareDocumentsInBackground?CompareDocumentsBackgroundEngineThe same compare off the main thread. When present, the Compare documents dialog uses it, reports progress, and can stop a running compare. If it rejects before its first progress report, the dialog compares with `compareDocuments` instead. The `compareDocuments` command stays synchronous and uses `compareDocuments`.
createCompareDocumentsDialog?(host: CompareDocumentsDialogHost) => CompareDocumentsDialogCreate the Compare documents dialog when a host opens it.
createRevisionMarkupDialog?(host: RevisionMarkupDialogHost) => RevisionMarkupDialogCreate local review settings chrome when a host opens the dialog.
displayModesreadonly ReviewDisplayMode[]Display modes this module unlocks (the free engine renders `proposed` only).
pane?ReviewPaneOptionsThe review pane settings the editor starts with. `setReviewPaneOptions` changes them later.
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
formattingChanges?readonly { readonly property: 'bold' | 'italic' | 'underline' | 'strike' | 'fontFamily' | 'fontSize' | 'color' | 'highlight' | 'alignment' | 'leftIndent' | 'rightIndent' | 'firstLineIndent' | 'hangingIndent' | 'spaceBefore' | 'spaceAfter' | 'direction'; readonly value: string | null; }[]Changed direct formatting values, ready for localized review summaries.
formattingKind?stringOOXML property-change element, used to resolve independently numbered formatting kinds.
formattingLanguages?readonly string[]Newly applied proofing language codes in a tracked formatting change.
idstringStable across renders and unique per decision within this editor instance.
kind'revision'
markDirection?'insert' | 'delete' | 'moveFrom' | 'moveTo'WHICH decision a `paragraphMark` records, absent for every other kind.
nestingnumberHow deeply this change is NESTED inside other changes, 0 for an unenclosed one.
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
structuralChanges?readonly ('rowInsert' | 'rowDelete' | 'cellInsert' | 'cellDelete' | 'cellMerge' | 'numberingInsert')[]Distinct structural operations covered by this decision, in document order.
textstringText the revision covers, for the card summary. Empty for changes with no characters.

RevisionAddressinterfaceSource ↗

How a tracked change is addressed: @w:id, @w:author and @w:date together, within a part.

All of them, 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 — and it is not author-scoped either, so (part, id) would merge two authors' distinct revisions. One logical revision is also deliberately many elements sharing an id, as a tracked row insertion is, which no uniqueness rule could express.

interface RevisionAddress
MemberTypeSummary
authorstring
date?stringAbsent when the file wrote no `@w:date`; part of the identity either way.
idstring

RevisionMarkupDialoginterfaceSource ↗

Viewer settings dialog supplied by the review module.

interface RevisionMarkupDialog
MemberTypeSummary
destroy
open

RevisionMarkupDialogHostinterfaceSource ↗

Local settings access for review chrome. No document mutations occur here.

interface RevisionMarkupDialogHost extends RevisionMarkupDialogSession
MemberTypeSummary
containerHTMLElement
translate?(key: string) => string | undefined

Type aliases (15)

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[];

CompareDocumentsBackgroundEnginetypeSource ↗

Runs a compare off the page's main thread, for example in a worker. The editor's Compare documents dialog uses it when the review module provides one. It reports progress, and it stops when signal aborts: the promise then rejects with the signal's reason.

type CompareDocumentsBackgroundEngine = (original: Uint8Array, revised: Uint8Array, options: CompareDocumentsEngineOptions, control: {
    readonly signal: AbortSignal;
    readonly onProgress: (progress: CompareDocumentsProgress) => void;
}) => Promise<CompareDocumentsBackgroundResult>;

CompareDocumentsBackgroundResulttypeSource ↗

Result of one background compare: like [CompareDocumentsEngineResult](CompareDocumentsEngineResult), as bytes.

type CompareDocumentsBackgroundResult = {
    readonly ok: true;
    readonly bytes: Uint8Array | null;
    readonly revisionCount: number;
    readonly changed: boolean;
} | {
    readonly ok: false;
    readonly code: CompareDocumentsEngineErrorCode;
    readonly reason?: CompareDocumentsRefusalReason;
    readonly message: string;
};

CompareDocumentsEnginetypeSource ↗

Compares the open document (the original) with a revised DOCX and returns the original with the differences as tracked changes. Synchronous, DOM-free, and free of side effects.

type CompareDocumentsEngine = (original: OoxmlPackage, revised: Uint8Array, options: CompareDocumentsEngineOptions) => CompareDocumentsEngineResult;

CompareDocumentsEngineErrorCodetypeSource ↗

Why the compare engine refused a compare, as a stable code: the codes that the engine, compareDocuments, and a background engine return. compareDocuments.errors.<code> in @docx-editor.dev/i18n translates every code.

type CompareDocumentsEngineErrorCode = 
/**
 * An option has a value of the wrong type, such as a `detectFormatChanges` that is not a
 * boolean, or the revised document is not a `Uint8Array`.
 */
'invalidOptions'
/** The original document is not a readable DOCX package. */
 | 'invalidOriginal'
/** The revised document is not a readable DOCX package. */
 | 'invalidRevised'
/** The documents exceed the compare's size limits. */
 | 'tooLarge'
/**
 * The original document enforces protection that the compare would break: read-only,
 * comments, forms, or tracked-changes protection. A compare also writes differences that
 * have no tracked form, such as style definitions and content control properties, so
 * tracked-changes protection refuses too.
 */
 | 'protected'
/** The differences include content that no tracked change can record. */
 | 'unsupportedContent';

CompareDocumentsEngineResulttypeSource ↗

Result of one document compare.

type CompareDocumentsEngineResult = {
    readonly ok: true;
    readonly package: OoxmlPackage;
    readonly revisionCount: number;
    readonly changed: boolean;
} | {
    readonly ok: false;
    readonly code: CompareDocumentsEngineErrorCode;
    readonly reason?: CompareDocumentsRefusalReason;
    readonly message: string;
};

CompareDocumentsErrorCodetypeSource ↗

Why a compare was refused, as a stable code. compareDocuments.errors.<code> in @docx-editor.dev/i18n translates every code. The engine and compareDocuments return [CompareDocumentsEngineErrorCode](CompareDocumentsEngineErrorCode) values. The editor command and dialog can also return the editor-only codes.

type CompareDocumentsErrorCode = CompareDocumentsEngineErrorCode
/** Editor only: no review module, no open document, or no mount. */
 | 'unavailable'
/** Editor only: a collaboration session is attached. */
 | 'collaboration'
/** Editor only: the document is open for viewing only. */
 | 'viewing'
/** Editor only: the dialog closed, or the compare was canceled, before it finished. */
 | 'closed'
/** Editor only: the revised file is a `Uint8Array` with no bytes. */
 | 'empty'
/**
 * Editor only: another compare of the same dialog runs, or a preview or an external document
 * refresh blocks writes to the document. Try again when it ends.
 */
 | 'busy'
/**
 * Editor only: the open document changed while a background compare ran, so its result
 * no longer applies. The editor never retries by itself.
 */
 | 'documentChanged'
/** Editor only: any other failure. */
 | 'failed';

CompareDocumentsOutcometypeSource ↗

The outcome of one compare.

type CompareDocumentsOutcome = {
    readonly ok: true;
    readonly revisionCount: number;
    readonly changed: boolean;
} | {
    readonly ok: false;
    readonly code: CompareDocumentsErrorCode;
    readonly reason?: CompareDocumentsRefusalReason;
    readonly message: string;
};

CompareDocumentsPhasetypeSource ↗

A step of a compare that runs in the background. The steps run in this order:

1. reading: read both documents. 2. comparing: find their differences. 3. checking: check that accepting and rejecting all changes give back each document. 4. applying: apply the result to the open document.

type CompareDocumentsPhase = 'reading' | 'comparing' | 'checking' | 'applying';

CompareDocumentsRefusalReasontypeSource ↗

What an unsupportedContent refusal is about, as a stable code. compareDocuments.reasons.<reason> in @docx-editor.dev/i18n translates every reason.

type CompareDocumentsRefusalReason = 
/** The documents differ inside a content control that is locked or bound to data. */
'lockedContent'
/** A document holds tracked changes that cannot be accepted, so its final state is unknown. */
 | 'unresolvedRevisions'
/** The documents differ in content with no tracked-change form, such as an embedded file. */
 | 'untrackedContent'
/** The revised document adds section or table settings that a reject cannot restore. */
 | 'unrecordedSettings'
/** A header or footer of an earlier section cannot be paired with one of the original. */
 | 'headerFooter'
/** The result needs a part type that the original document does not declare. */
 | 'packageStructure'
/**
 * The result fails the check: accepting all changes does not give the revised text, rejecting
 * them does not give the original text, or a tracked change cannot be accepted or rejected.
 */
 | 'unverified';

ReviewDisplayModetypeSource ↗

The views the review chrome offers, in Word's terms: the three layout projections plus Simple Markup, which reads as the proposed result with a red change bar beside every line a change touched — the document as accepting everything would leave it, and where.

type ReviewDisplayMode = RevisionDisplayMode | 'simple-markup';

ReviewItemtypeSource ↗

One pending decision in the review queue: a tracked change, a comment thread, or a pro custom-node card. Discriminate on kind.

The store's own derivation (collectReviewItems) only ever produces the first two; the kind: 'custom' card is contributed by the pro review module, which recognizes the node.

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 combined deletion and insertion decision. The built-in reader lists them separately,
 * unless a review query asks to pair replacements or a custom review provider combines them.
 */
 | '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';

TargetModetypeSource ↗

Whether a relationship points inside the package or out of it.

The security-relevant distinction: an External target is retained VERBATIM and never owner-resolved or fetched, because auto-loading a file-supplied remote target is a zero-click external fetch.

type TargetMode = 'Internal' | 'External';