@docx-editor.dev/core/contracts/document
@docx-editor.dev/core/contracts/document — the document-level edit and query vocabulary.
The subset that means something without a live editor: what an automation host or an LLM tool can ask for and change.
A CONTRACT module, not a barrel. contracts/editor builds the Editor command and query surfaces on top of these, so they cannot live in the package root: the root re-exports runtime from ../editor, and a contract importing the root would invert the dependency — safe today only because that import is type-only, and one accidental value import away from pulling the painted engine into a server bundle.
Interfaces (27)
ApplyResultinterfaceSource ↗
What applying a batch of edits produced: the new document, and a per-edit verdict.
results is positionally aligned with the input, so an edit that failed is identified by its index rather than by anything the caller has to correlate.
interface ApplyResult| Member | Type | Summary |
|---|---|---|
| doc | DocxDocument | |
| results | ExecResult[] | One per edit, positionally aligned with the input. |
ContentControlinterfaceSource ↗
Structured document tag (w:sdt).
interface ContentControl| Member | Type | Summary |
|---|---|---|
| alias? | string | |
| content | readonly Block[] | |
| controlType | ContentControlType | |
| id | string | |
| kind | 'contentControl' | |
| locked? | boolean | |
| tag? | string |
ContentControlFilterinterfaceSource ↗
Narrows a content-control query. Fields combine with AND; an empty filter matches every control.
interface ContentControlFilter| Member | Type | Summary |
|---|---|---|
| alias? | string | |
| controlType? | ContentControlType | |
| tag? | string |
ContentControlSummaryinterfaceSource ↗
A content control reduced to what a listing needs: its identity, kind, and lock state.
interface ContentControlSummary| Member | Type | Summary |
|---|---|---|
| alias? | string | |
| controlType | ContentControlType | |
| id | string | |
| locked? | boolean | |
| tag? | string |
DocAnchorinterfaceSource ↗
The LLM- and JSON-facing address for a piece of a document.
paraId is the 8-hex w14:paraId, matched case-insensitively. search is a phrase that must match EXACTLY ONCE inside that paragraph; ambiguous or missing matches fail with 'ambiguous' / 'notFound' rather than falling back to first-match.
Offset-based addressing was tried and abandoned: an agent cannot compute a character offset it has not seen, and offsets do not survive concurrent edits. Do not reintroduce { blockId, offset }.
interface DocAnchor| Member | Type | Summary |
|---|---|---|
| occurrence? | number | Opt-in disambiguation. Omit to require uniqueness. |
| paraId | string | |
| search? | string |
DocAnchorRangeinterfaceSource ↗
A position in one story: a paragraph and a UTF-16 offset inside it.
The same offset space the ops and the caret use, so an anchor read here can be handed straight back to a selection without re-deriving anything.
interface DocAnchorRange| Member | Type | Summary |
|---|---|---|
| endOffset | number | |
| endParagraphId | string | May sit in a later paragraph: the range markers are independent elements. |
| part | string | Canonical part name of the story the range lives in, e.g. `/word/document.xml`. |
| startOffset | number | |
| startParagraphId | string |
DocCommentinterfaceSource ↗
One comment or reply, as comments.xml records it.
A reply is a comment with a parentId; OOXML gives replies no separate element, so the thread is reconstructed from that link rather than from nesting.
interface DocComment| Member | Type | Summary |
|---|---|---|
| anchor? | DocAnchorRange | Where the comment is anchored, absent when the file gave it no usable range. |
| author | string | |
| date? | string | OPTIONAL, because `CT_Comment` makes `@w:date` optional and files omit it. A comment with no date is a comment, not a defect, and fabricating one is a content change. |
| id | string | |
| orphaned? | boolean | True when the file gave this comment no usable range — a reference with no markers, or a start with no end. Reported rather than dropped: a reviewer's remark vanishing silently is worse than one that says it lost its text. |
| parentId? | string | |
| resolved? | boolean | |
| text | string |
DocEditsinterfaceSource ↗
The document-executable edit vocabulary.
An interface rather than a closed union so extensions can widen it by declaration merging. A sealed union cannot be extended by a plugin, and the runtime dispatch is already registry-backed.
interface DocEdits| Member | Type | Summary |
|---|---|---|
| acceptAllRevisions | Record<never, never> | |
| acceptRevision | {
id: number;
part?: 'body' | 'footnote' | 'endnote';
noteId?: number;
} | |
| addComment | {
target: DocTarget;
text: string;
author: string;
} | |
| addRepeatingSectionItem | {
target: DocTarget;
index?: number;
} | |
| adjustIndent | {
target: DocTarget;
direction: 'increase' | 'decrease';
} | Word's Increase/Decrease Indent. |
| applyFormatting | {
target: DocTarget;
marks: RunFormatting;
} | |
| applyVariables | {
values: Record<string, string>;
} | |
| deleteText | {
target: DocTarget;
} | |
| insertBreak | {
target: DocTarget;
kind: 'page' | 'column' | 'line' | 'section';
} | |
| insertHyperlink | {
target: DocTarget;
href: string;
text?: string;
} | |
| insertImage | {
target: DocTarget;
data: Uint8Array;
extent?: Extent;
} | |
| insertTable | {
target: DocTarget;
rows: number;
cols: number;
} | |
| insertText | {
target: DocTarget;
text: string;
} | |
| mergeParagraphs | {
target: DocTarget;
} | |
| proposeDeletion | {
target: DocTarget;
author: string;
} | |
| proposeInsertion | {
target: DocTarget;
text: string;
author: string;
} | |
| proposeReplacement | {
target: DocTarget;
replaceWith: string;
author: string;
} | Authored family. `author` is required: tracked-ness is verb identity, not a boolean flag, so there is no global trackChanges toggle to forget. |
| rejectAllRevisions | Record<never, never> | |
| rejectRevision | {
id: number;
part?: 'body' | 'footnote' | 'endnote';
noteId?: number;
} | |
| removeContentControl | {
target: DocTarget;
} | |
| removeHyperlink | {
target: DocTarget;
} | |
| removeRepeatingSectionItem | {
target: DocTarget;
index: number;
} | |
| replaceText | {
target: DocTarget;
text: string;
} | |
| replyComment | {
commentId: string;
text: string;
author: string;
} | |
| resolveComment | {
commentId: string;
} | |
| setContentControlValue | {
target: DocTarget;
value: string;
} | |
| setParagraphStyle | {
target: DocTarget;
styleId: string;
} | |
| setVariable | {
name: string;
value: string;
} | |
| splitParagraph | {
target: DocTarget;
} | |
| toggleList | {
target: DocTarget;
kind: 'bullet' | 'ordered';
} | Word's Bullets and Numbering. |
DocLocationinterfaceSource ↗
Structural addressing for content the paraId map cannot reach.
interface DocLocation| Member | Type | Summary |
|---|---|---|
| container | ContainerRef | |
| offset? | number | |
| path | number[] | Block indices, descending into tables and content controls. |
DocQueriesinterfaceSource ↗
The document-readable query vocabulary, keyed identically to [DocQueryResults](DocQueryResults).
An interface rather than a closed union for the same reason [DocEdits](DocEdits) is: an extension widens it by declaration merging, and the runtime dispatch is registry-backed.
interface DocQueries| Member | Type | Summary |
|---|---|---|
| comments | {
resolved?: boolean;
} | |
| contentControls | {
filter?: ContentControlFilter;
} | |
| findText | {
text: string;
container?: ContainerRef;
} | |
| paragraphs | {
container?: ContainerRef;
} | |
| revisions | {
part?: 'body' | 'footnote' | 'endnote';
} | |
| styles | Record<never, never> | |
| variables | Record<never, never> |
DocQueryResultsinterfaceSource ↗
What each query returns. Keyed identically to DocQueries.
interface DocQueryResults| Member | Type | Summary |
|---|---|---|
| comments | readonly DocComment[] | |
| contentControls | readonly ContentControlSummary[] | |
| findText | readonly DocRange[] | |
| paragraphs | readonly ParagraphSummary[] | |
| revisions | readonly Revision[] | |
| styles | StyleDefinitions | |
| variables | Readonly<Record<string, string>> |
DocRangeinterfaceSource ↗
A span between two positions. The endpoints may be addressed either way, independently.
interface DocRange| Member | Type | Summary |
|---|---|---|
| from | DocAnchor | DocLocation | |
| to | DocAnchor | DocLocation |
DocumentBodyinterfaceSource ↗
The main story: its blocks in reading order, plus the sections derived from them.
interface DocumentBody| Member | Type | Summary |
|---|---|---|
| content | readonly Block[] | |
| sections | readonly Section[] | Derived, not stored: recomputed on read from section-break markers and section inheritance. Never treat it as a spreadable field. |
DocxDocumentinterfaceSource ↗
A parsed .docx.
NOT JSON-round-trippable: it holds Maps and Dates, plus an internal side-table of verbatim XML used for lossless round-tripping. Use toJSON / fromJSON before sending it over JSON-RPC or handing it to a model.
interface DocxDocument| Member | Type | Summary |
|---|---|---|
| body | DocumentBody | |
| comments | readonly DocComment[] | |
| revisions | readonly Revision[] | |
| styles | StyleDefinitions | |
| theme? | Theme |
ExtentinterfaceSource ↗
A size in EMUs, the unit DrawingML stores extents in. 914400 EMU = 1 inch.
interface Extent| Member | Type | Summary |
|---|---|---|
| heightEmu | number | |
| widthEmu | number |
HeaderFooterSetinterfaceSource ↗
A section's three header (or footer) variants, each a relationship id.
All optional: a variant a section does not declare inherits the previous section's, and one absent everywhere means the document simply has none.
interface HeaderFooterSet| Member | Type | Summary |
|---|---|---|
| default? | string | |
| even? | string | |
| first? | string |
IndentFormattinginterfaceSource ↗
Indent at the selection, in twips.
Unlike every other field here, this does NOT go absent when the selection's paragraphs disagree. A ruler has to draw its handles somewhere, and Word draws them at the FIRST selected paragraph's values — Select All is the commonest indent gesture, and hiding the handles for it would be worse than showing one paragraph's truth. The values are therefore always the first touched paragraph's, and [mixed](mixed) records per field whether the rest agree.
firstLine is ONE SIGNED offset: negative is a hanging indent. OOXML spells it as two mutually exclusive attributes and this collapses them hanging-wins (§17.3.1.12), which is the model Word itself keeps.
Absent inside a table. The value would be correct there, but it is measured from the cell's content edge while a ruler is drawn against the page's margin, and the ruler does not know the cell.
interface IndentFormatting| Member | Type | Summary |
|---|---|---|
| firstLine | number | First-line offset from [left](left), signed. Negative is a hanging indent. |
| left | number | Left indent, signed. Negative pulls text into the margin, as Word allows. |
| mixed | {
readonly left: boolean;
readonly right: boolean;
readonly firstLine: boolean;
} | Per field, whether the selection's paragraphs disagree about it. |
| right | number | Right indent, signed. |
ParagraphinterfaceSource ↗
One paragraph: its runs, the style it names, and its list membership.
interface Paragraph| Member | Type | Summary |
|---|---|---|
| kind | 'paragraph' | |
| numbering? | NumberingRef | |
| paraId? | string | `w14:paraId`. The stable handle `DocAnchor` addresses. |
| runs | readonly Run[] | |
| styleId? | string |
ParagraphSummaryinterfaceSource ↗
A paragraph reduced to what a listing needs: its stable handle, its text, and its style.
paraId is what a follow-up edit addresses, so a summary without one names a paragraph the file gave no w14:paraId and that a DocAnchor cannot reach.
interface ParagraphSummary| Member | Type | Summary |
|---|---|---|
| paraId? | string | |
| styleId? | string | |
| text | string |
RevisioninterfaceSource ↗
One tracked change.
Addressing needs BOTH id and part: @w:id is unique only within a part, so an id alone names two revisions in any package that has a header or a comments part.
interface Revision| Member | Type | Summary |
|---|---|---|
| author | string | |
| date? | string | OPTIONAL. `CT_TrackChange` requires `@w:id` and `@w:author` and makes `@w:date` optional, and producers that omit it are ordinary. Requiring it here forced either a fabricated date — a content change — or dropping the revision from the list. |
| id | number | Numeric, and unique only WITHIN a part. Pair with `part` to address one. |
| part | string | REQUIRED, and a canonical PART NAME rather than a three-value enum. |
| type | RevisionType |
RunFormattinginterfaceSource ↗
Character formatting, and — at selection level — the paragraph properties a toolbar reads alongside it.
One type serves both roles so a toolbar reads alignment, style and script state from the same object as bold and italic. On a [Run](Run) the selection-level fields stay absent: a run has no alignment or paragraph style of its own.
interface RunFormatting| Member | Type | Summary |
|---|---|---|
| alignment? | 'left' | 'center' | 'right' | 'both' | Paragraph alignment at the selection. `both` is OOXML's spelling of justify. |
| bold? | boolean | |
| color? | ColorValue | |
| fontFamily? | string | |
| fontSizePt? | number | |
| highlight? | string | |
| indent? | IndentFormatting | The EFFECTIVE paragraph indent at the selection — cascade and numbering merge included, so a numbered item that authors no `w:ind` reports the indent its list definition gives it. Absent when nothing is loaded, or when the selection is inside a table (see [IndentFormatting](IndentFormatting)). |
| italic? | boolean | |
| lineSpacing? | {
readonly rule: 'multiple' | 'exact' | 'atLeast';
readonly value: number;
} | Line spacing at the selection, in the unit its rule implies — LINES for `multiple`, points for `exact` and `atLeast`. The same vocabulary `setLineSpacing` takes, so a control can show what it reads and send back what it shows. Absent when the selection's paragraphs disagree or state no line spacing. |
| spaceAfterPt? | number | |
| spaceBeforePt? | number | Space above and below the paragraph at the selection, in points. |
| strike? | boolean | |
| styleId? | string | Paragraph style id (`w:pStyle`) at the selection. |
| subscript? | boolean | |
| superscript? | boolean | |
| underline? | boolean |
SectioninterfaceSource ↗
One section: the page it lays out on, and the header/footer stories it declares.
interface Section| Member | Type | Summary |
|---|---|---|
| footers | HeaderFooterSet | |
| headers | HeaderFooterSet | |
| properties | SectionProperties |
SectionPropertiesinterfaceSource ↗
w:sectPr: the page a section lays out on. Twips throughout, as the file stores them.
interface SectionProperties| Member | Type | Summary |
|---|---|---|
| columns? | {
count: number;
gapTwips: number;
} | |
| margins | PageMargins | |
| pageSize | {
widthTwips: number;
heightTwips: number;
} | |
| titlePage? | boolean | `w:titlePg` — whether the section's first page takes the `first` header/footer variant. |
StyleDefinitioninterfaceSource ↗
One style: the ID content references, the name a reader sees, and its inheritance link.
interface StyleDefinition| Member | Type | Summary |
|---|---|---|
| basedOn? | string | `w:basedOn` — the style this one inherits from. Absent at the root of a chain. |
| id | string | |
| name | string |
StyleDefinitionsinterfaceSource ↗
styles.xml, split by the three style families that address separately.
Keyed by style ID rather than by the name a reader sees, because the ID is what a paragraph or run actually references.
interface StyleDefinitions| Member | Type | Summary |
|---|---|---|
| character | ReadonlyMap<string, StyleDefinition> | |
| paragraph | ReadonlyMap<string, StyleDefinition> | |
| table | ReadonlyMap<string, StyleDefinition> |
TableinterfaceSource ↗
A table: its rows, and the table style they resolve through.
interface Table| Member | Type | Summary |
|---|---|---|
| kind | 'table' | |
| rows | readonly TableRow[] | |
| styleId? | string |
ThemeinterfaceSource ↗
theme1.xml — what a [ColorValue](ColorValue) of kind theme resolves against.
interface Theme| Member | Type | Summary |
|---|---|---|
| colorScheme | ThemeColorScheme | |
| fontScheme? | Record<string, string> |
Type aliases (11)
BlocktypeSource ↗
Anything that can sit at block level in a story. Discriminate on kind.
type Block = Paragraph | Table | ContentControl;ColorValuetypeSource ↗
A colour as the FILE expresses it, not as a resolved RGB string.
A theme colour stays a theme reference — slot plus tint or shade — so that changing the theme repaints the document the way Word does. Flattening to hex at read time would freeze the resolved value and break that link. auto is Word's "let the renderer decide", usually black on white.
type ColorValue = {
readonly kind: 'hex';
readonly value: string;
} | {
readonly kind: 'theme';
readonly slot: string;
readonly tint?: number;
readonly shade?: number;
} | {
readonly kind: 'auto';
};ContainerReftypeSource ↗
Which STORY a location belongs to.
The body is one container; every header, footer and note is another. A path alone is ambiguous without it, because block index 0 exists in every story a document has.
type ContainerRef = {
part: 'body';
} | {
part: 'header' | 'footer';
rId: string;
} | {
part: 'footnote' | 'endnote';
noteId: number;
};ContentControlTypetypeSource ↗
Which kind of control a w:sdt is, and therefore what a value written into it must be.
type ContentControlType = 'richText' | 'plainText' | 'checkbox' | 'dropdown' | 'comboBox' | 'date' | 'picture' | 'repeatingSection';DocEdittypeSource ↗
One edit, as a discriminated union derived from [DocEdits](DocEdits).
Adding a key to DocEdits — including by declaration merging from a plugin — widens this automatically, so the union never drifts from the vocabulary it is built out of.
type DocEdit = {
[K in keyof DocEdits]: {
type: K;
} & DocEdits[K];
}[keyof DocEdits];DocQuerytypeSource ↗
One query, as a discriminated union derived from [DocQueries](DocQueries).
type DocQuery = {
[K in keyof DocQueries]: {
type: K;
} & DocQueries[K];
}[keyof DocQueries];DocTargettypeSource ↗
Anything an operation can be pointed at: a paragraph-relative [DocAnchor](DocAnchor), a structural [DocLocation](DocLocation), or a [DocRange](DocRange) spanning two of them.
type DocTarget = DocAnchor | DocLocation | DocRange;ExecErrorCodetypeSource ↗
Why a write was refused, as a value to branch on.
Deliberately finer-grained than a boolean: "no-op", "target not found" and "content control is locked" are different outcomes, and a caller retrying the first should not retry the third.
type ExecErrorCode = 'notFound' | 'ambiguous' | 'locked' | 'bound' | 'typeMismatch' | 'kindMismatch' | 'outOfBounds' | 'unsupported' | 'invalidArgs';ExecResulttypeSource ↗
Every write returns this rather than boolean.
A bare boolean cannot distinguish "no-op" from "target not found" from "content control is locked", and the editor layer already throws eight distinct ContentControl error classes that a boolean would flatten.
type ExecResult = {
ok: true;
changed: boolean;
} | {
ok: false;
code: ExecErrorCode;
reason: string;
target?: DocTarget;
};RevisionTypetypeSource ↗
What kind of decision a revision represents.
Wider than insert/delete/format, and it has to be. w:moveFrom/w:moveTo are not a deletion and an insertion — resolving one half alone duplicates or loses the content; w:pPr/w:rPr/w:ins|w:del decorates no characters at all and merges paragraphs when resolved; a row or cell revision is structural. A reviewer shown only three kinds is a reviewer who never learns about the rest.
type RevisionType = 'insert' | 'delete'
/** A deletion and an insertion that are one edit: text typed over a selection. */
| '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. */
| 'structural';ThemeColorSchemetypeSource ↗
Theme colour slots (accent1, dk1, lt2, …) to hex.
type ThemeColorScheme = Readonly<Record<string, string>>;