@docx-editor.dev/core/binding

@docx-editor.dev/core/binding — the canonical tree ↔ ProseMirror binding.

Forward: project one revision of the tree into a ProseMirror doc. Reverse: explain an edited doc as the smallest set of tree ops, or REFUSE. The reverse direction never reconstructs the tree from the projection, so anything the projection does not model stays carried by the tree.

Functions (8)

bodyParagraphsfunctionSource ↗

Body paragraphs of a part, in document order.

declare function bodyParagraphs(part: OoxmlPart): OoxmlNode[];

docToTreeOpsfunctionSource ↗

Map an edited ProseMirror doc back into typed tree ops (task 6.2), or refuse (6.3).

Handles a paragraph count delta of 0 (in-place edits), +1 (a clean split) and -1 (a clean join). Anything else — a reorder, a multi-paragraph paste, a structural change combined with an edit elsewhere — is refused rather than approximated, because the approximation is what silently loses content.

declare function docToTreeOps(part: OoxmlPart, doc: Node): MapResult;

mountTreeSurfacefunctionSource ↗

Mount an editable ProseMirror view over a session's body story.

The reference binding, not the production surface — mountPaginatedSurface is what the shipped editor uses. This one exists to exercise the tree↔ProseMirror contract directly, without pagination or painting in the way.

Call [TreeSurface.destroy](TreeSurface.destroy) to release the view.

declare function mountTreeSurface(mount: HTMLElement, session: TreeDocxSession, options?: TreeSurfaceOptions): TreeSurface;

openTreeSessionfunctionSource ↗

Open DOCX bytes into a tree-backed session.

Returns a typed rejection rather than throwing: every failure here is a property of the FILE, and a host needs to tell "this is not a package" from "this package is malicious" from "this document has no body".

The read is BOUNDED — decompression ratio, part count, XML depth and element counts are all capped — because the bytes are untrusted by definition.

declare function openTreeSession(bytes: Uint8Array, options?: OpenTreeSessionOptions): OpenTreeSessionResult;

partHasNodefunctionSource ↗

Whether a tree node id is still present, for reconciliation checks.

declare function partHasNode(part: OoxmlPart, nodeId: string): boolean;

reconcileDocfunctionSource ↗

Rebuild a projected doc from a new tree revision, reusing untouched paragraphs (task 6.4).

A full reprojection is always CORRECT, so this only ever narrows the work: paragraphs the change did not name are carried over by reference, which lets ProseMirror keep their node identity and redraw nothing. When the change is structural — a paragraph created, deleted, split or joined — positions shift in ways a per-paragraph patch cannot express, so it falls back to a full projection rather than guessing.

Passing null means "no evidence available", which also takes the full path. Reconciling on partial evidence is how a projection silently diverges from the model.

declare function reconcileDoc(previousDoc: Node, part: OoxmlPart, change: {
    readonly dirty: readonly string[];
    readonly created: readonly string[];
    readonly deleted: readonly string[];
    readonly splitJoin: readonly unknown[];
} | null): Node;

runPropsOffunctionSource ↗

The accepted run properties carried by a text node, or an empty list.

declare function runPropsOf(node: Node): readonly OoxmlProperty[];

treeToDocfunctionSource ↗

Project one tree revision into a ProseMirror doc (task 6.1).

declare function treeToDoc(part: OoxmlPart): Node;

Interfaces (7)

DocumentStyleEntryinterfaceSource ↗

One w:style definition, projected for a style picker.

interface DocumentStyleEntry
MemberTypeSummary
namestring
previewDocumentStylePreview
styleIdstring
typestring

ParagraphAttrsinterfaceSource ↗

Attributes carried by a projected paragraph.

interface ParagraphAttrs
MemberTypeSummary
nodeIdstring | nullThe canonical tree node id this paragraph projects.
propsreadonly OoxmlProperty[]Accepted `w:pPr` children, as authored.

TreeApplyResultinterfaceSource ↗

What applying ops produced: whether it committed, and why not when it did not.

committed and rejected are separate flags because they are not opposites — a batch of zero ops neither commits nor is rejected.

interface TreeApplyResult
MemberTypeSummary
committedboolean
opCountnumber
reason?TreeBindingRejection | StoryTargetRejection | stringPresent when the edit was refused, so a host can report WHY rather than a silent no-op.
rejectedboolean

TreeDocxSessioninterfaceSource ↗

One open document: the canonical tree, and the only write path into it.

applyTreeOps is that path. Every mutation is a TreeDocOp addressed by node id plus UTF-16 offset, applied in one transaction, which is what makes cell and nested paragraphs ordinary rather than special cases.

Holds the whole package, not just the body — headers, footers, notes, comments and styles are all reachable through it, and parts the engine does not model are preserved verbatim.

interface TreeDocxSession
MemberTypeSummary
applyImagePropertiesApply image property tree ops plus hyperlink relationship wiring atomically.
applyPmDocMap an edited BODY doc to tree ops and commit them as ONE transaction.
applyTreeOpsCommit typed tree ops directly, as ONE transaction.
beginComposition
bodyTextBody text, paragraphs joined by newlines, read from the CANONICAL tree.
bookmarks`bookmarkName -> { paragraphId, offset }` over the main part, memoized per revision.
canRedo
canUndo
currentPackageThe current package with every opened story store merged in. Authority for layout resolution and save — never a swapped single-part view.
deleteCommentDelete a comment thread outright — body, thread state and story markers.
deleteImageDelete a picture drawing and collect orphaned media in one package undo unit.
documentFontsFont family names the document uses, from every `w:rFonts` in the CURRENT main part plus the styles and header/footer parts — validated, deduplicated, sorted. Memoized per package revision.
documentOutlineThe heading outline of the BODY story, in document order: paragraphs whose `w:pStyle` resolves to a heading through the styles part (built-in `heading N` name, or the style's own `w:outlineLvl` 0..8). Memoized per main-part revision — an edit can retitle, add or remove a heading, but the styles part cannot change in-session.
documentStylesThe `w:style` definitions of the styles part, validated and projected for a style picker. Memoized once: the styles part is immutable for the session's lifetime. A document without a styles part answers `[]`.
documentThemeColorsThe theme's ten picker colours (`a:clrScheme`), in Word's column order, or `[]` when the package has no complete scheme. Memoized once: the theme part is immutable for the session's lifetime.
documentThemeFontsThe theme part's Latin typefaces, for resolving `w:rFonts` theme references in layout.
editablebooleanWhether the body holds at least one editable paragraph.
effectiveRunDefaultsThe run formatting a paragraph's content INHERITS when it authors none: the paragraph style's `basedOn` chain, then `w:docDefaults`, with theme `rFonts` attributes resolved through the font scheme. `runProperties` (the span's own authored properties) lets a theme-only run-level `w:rFonts` resolve too. This is what lets a toolbar always show the effective font, the way Word does.
embeddedFontsThe faces the package EMBEDS (`word/fontTable.xml` embed relationships), deobfuscated — the only font source that needs neither a substitute nor a network. Extraction asserts nothing about validity; admitting a face is the font resource lane's job. Memoized once: the font table and font parts are immutable in-session.
endComposition
ensureHyperlinkRelationshipThe relationship id for an external hyperlink target on the part owning `scope` (default: body), minting one if that part has none, or `null` when the URL is refused or the scoped part cannot be resolved.
ensureListDefinition
ensureNumberingLevelDeclare `level` in the list definition `numId` names, with Word's default format for that depth, or answer false.
findTextEvery occurrence of `query` in the BODY story, in document order, addressed in the same offset vocabulary the tree ops and the surface selection use — so a match can be handed straight to `setSelection` without re-deriving anything.
hasReviewContentWhether the document carries review content — tracked changes or comment anchors — regardless of any review module. Derived from store vocabulary only (never the review model), memoized per revision: it is the free tier's honest "this document has more than you are seeing" signal.
headerFooterPartsThe resolved header/footer parts of the section, by variant (phase 2).
headerFooterPartsBySectionPer-section header/footer parts after OOXML inheritance, index-aligned with `enumerateDocumentSections`.
headerFooterResolutionBySectionPer-section resolution with declared-vs-inherited metadata for "Same as previous" chrome. Index-aligned with `headerFooterPartsBySection`.
insertCustomNode`scope` names the story the paragraph is in, and defaults to the body.
insertImageInsert a validated raster image as one package undo unit (task 12).
lastCommitWasStructuralWhether the last commit changed the BLOCK SEQUENCE (a split, join, insert or delete).
nodeIdOfCanonical node id for a `w14:paraId`, matched case-insensitively, or null.
numberingRootRoot of the numbering part tree (`w:numbering`), for list layout. Memoized once; `null` when the package has no numbering part. Numbering editing is a later slice.
packageRevisionPackage-wide revision — bumps on any story commit (body or HF).
paragraphAnchorsThe `w14:paraId` ↔ node-id index over the full editable set of the MAIN part, memoized per revision. Every editable paragraph carries a valid, part-unique id (established at open, maintained by the split appliers), so every paragraph is mapped. Header/footer paragraphs become addressable through `partFor` once their story store is open.
paragraphIdsCanonical node ids of the body paragraphs, in order.
paragraphIdsInCanonical node ids of paragraphs in a story scope. Defaults to the body. Header/footer scopes address the part `EditorScope { kind: 'headerFooter'; rId }` names.
paraIdOf`w14:paraId` of a canonical paragraph node id, verbatim, or null.
partThe current canonical BODY part — what layout reads for the main story.
partForThe current part for a story scope, or null when the target is refused.
projectDocProject the current BODY revision into a ProseMirror doc.
reconcileRe-project incrementally from the last committed change, reusing untouched paragraphs.
redo
relationshipTargetWhat the owning part's relationships answer for one `r:id` under `scope` (default: body): the authored target and whether it is external. `null` for an id the part does not declare, or when the scoped part is not open.
removeCustomNodeRemove a custom node and the payload it bound. `scope` names the story holding it and defaults to the body; a chip in a header is refused against the body store.
replaceImageReplace a picture drawing's embedded media in one package undo unit.
replyToCommentReply to a comment, or add one over a revision's range. Returns the new comment's id.
reviewItemsEvery pending review decision in the document, memoized per revision.
revisionBody-store revision (independent of header/footer store revisions).
revisionForPer-story revision, or null when the target is refused.
saveSerialize the whole package back to DOCX bytes.
setCommentResolvedResolve a comment thread, or reopen it. False when the document holds no such comment.
settingsRootRoot of the settings part tree (`w:settings`), for document-wide layout constants such as `w:defaultTabStop`. Memoized once; `null` when the package has no settings part. Settings editing is a later slice, so this is immutable for the session.
storyTextText of a story scope, paragraphs joined by newlines.
stylesRootRoot of the styles part tree, for layout's style cascade. Memoized once; `null` when the package has no styles part. Styles editing is a later slice, so this is immutable for the session.
subscribe
sweepCustomNodePayloadsDrop every payload no control binds, in the stores whose namespaces a module claims.
trackingSettingsWhat `settings.xml` says about tracking — `w:trackRevisions`, the tracked-changes protection, and the two do-not-track switches.
undoUndo the last entry, returning the selection to restore.

TreeSurfaceinterfaceSource ↗

A mounted ProseMirror view bound to a [TreeDocxSession](TreeDocxSession).

Every committed transaction becomes tree ops through the binding; a transaction the binding cannot explain is refused and reported through [TreeSurfaceState.lastRejection](TreeSurfaceState.lastRejection), leaving the tree untouched.

interface TreeSurface
MemberTypeSummary
destroy
redo
state
toggleRunPropertyToggle one accepted run property across the current selection.
undo
viewEditorView

TreeSurfaceOptionsinterfaceSource ↗

How a tree surface is mounted.

interface TreeSurfaceOptions
MemberTypeSummary
onChange?(state: TreeSurfaceState) => voidCalled after every commit or refusal, so a host can show revision and rejection state.

TreeSurfaceStateinterfaceSource ↗

Everything observable about a mounted tree surface.

lastRejection is part of the state rather than a thrown error because a refused edit is an ordinary outcome here — the binding refuses anything it cannot explain, and the host shows why instead of the edit vanishing.

interface TreeSurfaceState
MemberTypeSummary
canRedoboolean
canUndoboolean
lastRejectionstring | nullThe reason the last transaction was refused, or null when the last one committed.
revisionnumber

Type aliases (6)

MapResulttypeSource ↗

The ops explaining one edit, or the reason it could not be explained.

ops is the SMALLEST set that accounts for the difference, so anything the projection does not model — unknown nodes, lexical form, node identities — stays carried by the tree rather than round-tripping through the editor.

type MapResult = {
    readonly ok: true;
    readonly ops: readonly TreeDocOp[];
} | {
    readonly ok: false;
    readonly reason: TreeBindingRejection;
    readonly detail?: string;
};

OpenTreeSessionResulttypeSource ↗

An open session, or a typed refusal.

A result rather than a throw: every failure here is a property of the FILE, and a host needs to tell "this is not a package" from "this package is malicious" from "this document has no body".

type OpenTreeSessionResult = {
    readonly ok: true;
    readonly session: TreeDocxSession;
} | {
    readonly ok: false;
    readonly reason: TreeSessionRejection;
    readonly detail?: string;
};

StoryScopetypeSource ↗

Editable story target.

Body and headerFooter mirror EditorScope. Notes use one lazy store per notes part (notesPart) — not one store per note — resolved through safe document relationships.

type StoryScope = {
    readonly kind: 'body';
} | {
    readonly kind: 'headerFooter';
    readonly rId: string;
} | {
    readonly kind: 'notesPart';
    readonly noteKind: NoteKind;
};

StoryTargetRejectiontypeSource ↗

Why a story scope could not be resolved to a part.

Several of these are FILE-hostile shapes rather than caller mistakes: external-relationship and bad-relationship-target are how a crafted document tries to point a story at something outside the package, and both are refused rather than followed.

type StoryTargetRejection = 'unknown-scope' | 'dangling-relationship' | 'wrong-relationship-type' | 'external-relationship' | 'bad-relationship-target' | 'missing-part' | 'not-a-story-part' | 'too-many-story-stores';

TreeBindingRejectiontypeSource ↗

Why an edited projection could not be mapped back into tree ops.

A refusal, not a fallback. The reverse direction never RECONSTRUCTS the tree from the projection — it explains a difference — so a shape it cannot explain is rejected outright. A silently-dropped edit is worse than a refused one, because only the refusal can be reconciled.

type TreeBindingRejection = 'paragraph-count-unexplained' | 'paragraph-reordered' | 'unknown-paragraph-id' | 'unknown-content-moved' | 'unsupported-node' | 'split-not-clean' | 'join-not-clean';

TreeSessionRejectiontypeSource ↗

Why bytes could not be opened: any bounded-reader rejection, plus the package that parsed but carried no main document tree.

type TreeSessionRejection = OoxmlPackageRejection | 'no-main-document-tree';

Variables (2)

PROJECTION_ORIGINconstSource ↗

The origin a host should use when committing a reconciliation rather than a user edit.

PROJECTION_ORIGIN: "dev.docx-editor.core.origin.projection"

treeSchemaconstSource ↗

The ProseMirror schema the canonical tree projects into.

Deliberately minimal. It models only what an editing surface must manipulate directly — paragraphs, text, tabs, breaks, and run properties as marks — because everything it does NOT model stays on the tree and is preserved losslessly there. Widening this schema moves content out of the tree's custody, which is the opposite of what it is for.

The node and mark unions are written out rather than inferred from the spec below. Inferred, tsup's dts worker emits their members in an order that varies run to run, so the generated binding.api.md differed between builds of identical source and api:check failed at random. An explicit annotation pins the emitted order. Adding a node or mark to the spec means adding it here too — the compiler rejects the assignment otherwise.

treeSchema: Schema<'doc' | 'paragraph' | 'text' | 'tab' | 'hardBreak' | 'pageBreak' | 'unknownInline', 'runProps'>

On this page