@docx-editor.dev/pro

v2.1.3 · 2 published subpaths with full TypeScript signatures and JSDoc.

Subpaths

Package root

Registering a module is the whole enablement story: the review chrome slots light up through the same toolbarCommandState that disabled them, and the editor renders revisions as markup rather than the free tier's final-state projection.

Functions (17)

customNodeNamespacefunctionSource ↗

The namespace a definition's payloads live in.

Keyed on tagPrefix rather than on name, so one integrator's nodes share one store. A document with a citation and a figure carries one customXml part, not two.

declare function customNodeNamespace(definition: AnyCustomNodeDefinition): string;

customNodesModulefunctionSource ↗

Register custom node definitions with createDocxEditor({ modules }).

declare function customNodesModule(options: CustomNodesModuleOptions): EditorModule;

customNodesOffunctionSource ↗

Every recognized custom node in the editor's body, in document order, with its payload.

ts for (const node of customNodesOf(editor)) { const citation = Citation.dataOf(node); if (citation) index.add(citation.sourceId); }

Reads the body story. A node in a header or footer is not answered here — the same limit recognizeCustomNodes has, since it takes one part.

Derived fresh on every call from the canonical package: there is no cache to go stale, and no change event either, so re-read after an edit rather than holding the array.

declare function customNodesOf(editor: Editor, options?: CustomNodesOfOptions): readonly RecognizedCustomNode[];

customNodeXmlfunctionSource ↗

The run-level w:sdt markup for one custom node, ready to splice between runs inside a w:p (the w prefix must be bound to the WordprocessingML main namespace, as it is in every word/document.xml).

Refuses the same inputs insertCustomNode refuses (the 64-character w:tag cap), so a server cannot author a chip the editor would not have.

ts const sdt = customNodeXml(citation, { sourceId: 'src_9f3' }, '(Smith 2024)'); if (sdt.ok) template.replace('{{citation}}', sdt.xml);

declare function customNodeXml<Schema extends StandardSchemaV1 | undefined = undefined>(definition: CustomNodeDefinition<Schema>, attrs: Readonly<Record<string, string>>, text: string, options?: CustomNodeXmlOptions<Schema>): CustomNodeXmlResult;

decodeCustomNodeTagfunctionSource ↗

Decode a w:tag value, or null when it is not a custom-node tag.

Null rather than throwing: every inline SDT in every opened document flows through this, and most SDTs in the wild are ordinary Word content controls whose tags mean something else entirely.

declare function decodeCustomNodeTag(tag: string): DecodedCustomNodeTag | null;

defineCustomNodefunctionSource ↗

Validate and freeze a definition. Throws on a shape mistake — author error, not file input.

declare function defineCustomNode<Schema extends StandardSchemaV1 | undefined = undefined>(definition: CustomNodeDefinition<Schema>): CustomNode<Schema>;

encodeCustomNodeTagfunctionSource ↗

Encode a node identity into a w:tag value, refusing the Word length cap.

declare function encodeCustomNodeTag(prefix: string, name: string, attrs: Readonly<Record<string, string>>): EncodeTagResult;

insertCustomNodefunctionSource ↗

Insert one custom node. Returns the engine's typed result: refusals carry the engine's own reason (tag overflow, offset out of range, viewing mode, …), and a payload the schema refused carries the failing fields in issues.

ts insertCustomNode(editor, citation, { data: { sourceId: 'src_9f3', year: 2024 } });

declare function insertCustomNode<Schema extends StandardSchemaV1 | undefined = undefined>(editor: Editor, definition: CustomNodeDefinition<Schema>, input?: CustomNodeInput<Schema>): CustomNodeWriteOutcome;

isCustomNodeDefinitionfunctionSource ↗

Whether an opaque registry value is a custom-node definition.

The engine carries registered definitions as unknowns (getCustomNodeDefinitions), so every pro surface that reads them back narrows through this ONE guard.

declare function isCustomNodeDefinition(candidate: unknown): candidate is AnyCustomNodeDefinition;

parseCustomNodeDatafunctionSource ↗

Parse a payload out of a data part and validate it against the definition's schema.

Synchronous on purpose. This runs inside the read path, where recognition happens for every node in the document before anything paints, and an async boundary there would mean a document that renders its chips a frame later than its text. A schema with an async refinement is refused (async) rather than awaited, so the limitation is visible instead of silent.

A payload with no schema comes back as the parsed JSON, typed unknown — the host asked for no guarantees and gets none, rather than getting a lie. It is also a NULL-PROTOTYPE object on that path, where a schema-validated one is whatever the validator rebuilt: hasOwnProperty and instanceof Object do not hold on the former.

declare function parseCustomNodeData<Schema extends StandardSchemaV1 | undefined>(schema: Schema, raw: string): CustomNodeDataResult<Schema extends StandardSchemaV1 ? InferSchemaOutput<Schema> : unknown>;

prepareForExportfunctionSource ↗

Apply every definition's preserveOnExport to a document, and answer the bytes to ship.

The BYTES form, for a caller with no editor: a request handler, a queue worker, a build step, a document customNodeXml authored server-side. In a browser with an editor mounted, reach for [saveForExport](saveForExport) instead — it reads the definitions off the editor, so a node cannot leave because the list passed here forgot it.

true (the default) leaves a node untouched. 'text' unwraps the control, keeping the words and dropping the tag, the binding and the payload. false removes the node with its content. A tag no definition claims is never touched — this is a host applying its own policy to its own markup, not a scrub of the document.

ts const generated = await renderContract(order); const outgoing = prepareForExport(generated, [Clause, InternalNote]); if (outgoing.ok) await email.attach(outgoing.bytes);

Applied to EVERY story, so a chip in a header is treated like a chip in the body. The payload stores hang off the main document part, which is where Word enumerates its data store from, so that is the only part they are cleaned up against.

declare function prepareForExport(bytes: Uint8Array, definitions: readonly AnyCustomNodeDefinition[], options?: DocumentExportOptions): DocumentExportResult;

recognizeCustomNodesfunctionSource ↗

declare function recognizeCustomNodes(part: OoxmlPart, definitions: readonly AnyCustomNodeDefinition[], options?: RecognizeCustomNodesOptions): RecognizedCustomNode[];

removeCustomNodefunctionSource ↗

Delete one custom node — wrapper AND content, one undo step.

The default contentLocked chip deletes fine (the lock guards its characters, not its existence); a sdtLocked/sdtContentLocked wrapper refuses with the engine's reason.

declare function removeCustomNode(editor: Editor, nodeId: string): CustomNodeWriteOutcome;

reviewModulefunctionSource ↗

Build the review module. Construction never validates the key and never touches the network.

declare function reviewModule(options?: ReviewModuleOptions): EditorModule;

saveForExportfunctionSource ↗

Save the document as the copy that leaves your system.

NOT THE DEFAULT PATH. A custom node is an ordinary inline content control: Word and Word Online both render its text and hand the tag, binding and payload back unchanged, so editor.save() already produces a file that opens correctly for a recipient who has never heard of this library. This is for the narrower case where the recipient should not have the nodes at all — internal annotations that must not leave, markup that means nothing outside your system.

editor.save() is the copy you keep — every node intact, reopens here with the chips working. This is its pair: the same document with each definition's preserveOnExport applied, for a download, an attachment, or a hand-off you want stripped.

```ts await storage.put(docId, new Uint8Array(await editor.save())); // yours

const outgoing = await saveForExport(editor); // theirs if (outgoing.ok) download(outgoing.bytes); ```

Store the saved bytes, never these. What the export stripped is gone for good: unwrapped text does not become a node again. Produce this copy fresh from the saved one each time you hand one out.

Definitions come from the editor's registered modules, so a node cannot leave because a list forgot it. On a server, where there is no editor, use prepareForExport with an explicit list.

declare function saveForExport(editor: Editor, options?: SaveForExportOptions): Promise<DocumentExportResult>;

serializeCustomNodeDatafunctionSource ↗

Serialize a payload for a data part. Refuses what cannot round-trip through JSON.

NOT symmetric with [parseCustomNodeData](parseCustomNodeData): a key named __proto__, constructor or prototype is written here and dropped on the way back, because a payload arriving from a file is the hazard and a payload leaving this process is not. A host that needs those keys needs a different name for them.

declare function serializeCustomNodeData(value: unknown): CustomNodeDataResult<string>;

updateCustomNodefunctionSource ↗

Replace one custom node in place: the node is removed and a fresh one is inserted at its own span — ONE transaction, one undo step, recognized by construction like insertCustomNode.

ts updateCustomNode(editor, citation, node.nodeId, { data: { ...citation, year: 2025 } });

declare function updateCustomNode<Schema extends StandardSchemaV1 | undefined = undefined>(editor: Editor, definition: CustomNodeDefinition<Schema>, nodeId: string, update?: CustomNodeUpdate<Schema>): CustomNodeWriteOutcome;

Interfaces (21)

ActivatedCustomNodeinterfaceSource ↗

A chip activation: identity + attrs, plus where it sits.

attrs are the definition's OWN shape — the raw tag decode has already been through fromDocx, exactly as the review derivation runs it, so every surface (click, hover, edit, cards) sees one attrs vocabulary. text and nodeId are present when the surface could resolve them (a registered review module resolves both).

interface ActivatedCustomNode
MemberTypeSummary
attrsReadonly<Record<string, string>>
data?unknownThe node's payload, when the surface could resolve one.
namestring
nodeId?stringThe SDT node's canonical id — the address `removeContentControl` takes.
rectDOMRectViewport-relative rect of the chip's boundary, for anchoring host UI.
tagstring
text?stringThe node's literal content text, when resolvable.

CustomNodeinterfaceSource ↗

A definition, plus what [defineCustomNode](defineCustomNode) attaches to it.

You author a [CustomNodeDefinition](CustomNodeDefinition); you are handed one of these. The difference is dataOf, which cannot be written by hand because it closes over the schema you just declared.

interface CustomNode<Schema extends StandardSchemaV1 | undefined = any> extends CustomNodeDefinition<Schema>
MemberTypeSummary
dataOf(node: { readonly name?: string; readonly data?: unknown; } | null | undefined) => InferSchemaOutput<Schema> | undefinedThis node's payload, from a surface that carries every definition's under one type.

CustomNodeDefinitioninterfaceSource ↗

One integrator-defined inline node, anchored on a run-level SDT whose w:tag carries its identity.

A definition claims a tag PREFIX, so acme recognizes every acme:* tag. An SDT whose prefix no definition claims stays literal — which is also what the free tier and Word itself render, so an unrecognized node never loses content or locks editing.

Build one with [defineCustomNode](defineCustomNode), which validates the shape, then register it through [customNodesModule](customNodesModule).

interface CustomNodeDefinition<Schema extends StandardSchemaV1 | undefined = any>
```ts
const citation = defineCustomNode({
  name: 'citation',
  tagPrefix: 'acme',
  chrome: { color: '#2563eb' },
  onClick: (node) => openCitation(node.attrs.key),
});
```
MemberTypeSummary
chrome?{ readonly color?: string; }Chip appearance, HOST-authored (never file data). `color` tints the chip and its border; applied by `CustomNodeChrome` from `@docx-editor.dev/pro/react`.
fromDocx?(input: { readonly attrs: Readonly<Record<string, string>>; readonly text: string; readonly data?: InferSchemaOutput<Schema>; }) => Readonly<Record<string, string>> | nullRecognition hook. Receives the decoded attrs and the SDT's literal text (so label drift from Word edits is visible) and returns the attrs the node should carry — or null to leave this SDT unrecognized and literal.
label?stringDisplay name for chrome — the "Edit label" context-menu row. Defaults to `name`. Host-authored, never file data; provide a localized string.
namestringNode type name — the second segment of the tag (`<prefix>:<name>?…`).
onClick?(node: ActivatedCustomNode) => voidClick on the painted chip. UI state belongs in `CustomNodeChrome`'s `onNodeClick`.
onEdit?(node: ActivatedCustomNode) => voidThe "Edit label" row the context menu shows at the top when the right-click lands on the node's chip. The HOST owns the dialog.
onHover?(node: ActivatedCustomNode) => voidPointer enters the painted chip.
payloadNamespace?stringThe customXml store this definition's payloads live in.
preserveOnExport?boolean | 'text'What happens to this node when a document is exported OUTSIDE the system that made it.
reviewCard?(node: { readonly attrs: Readonly<Record<string, string>>; readonly text: string; readonly data?: InferSchemaOutput<Schema>; }) => { readonly title: string; readonly detail?: string; readonly icon?: string; } | nullContribute a card to the review sidebar for every recognized node of this definition, anchored at the node's range. Return null to skip one node.
schema?SchemaThe shape of this node's payload, as a zod (or valibot, or arktype) schema.
tagAttrs?(data: InferSchemaOutput<Schema>) => Readonly<Record<string, string>>Extra identity to put in the `w:tag`, from the payload. Rarely needed.
tagPrefixstringTag prefix this definition claims (`acme` claims `acme:*`). No colons.
text?(data: InferSchemaOutput<Schema>) => stringWhat the document SHOWS for this node, from its payload.

CustomNodeDiagnosticinterfaceSource ↗

Something worth telling an integrator about a document, which is never worth throwing over.

A payload arrives from a file the sender wrote, so "it did not match the schema" is an ordinary property of an ordinary document — not an exception. It is reported and the node still renders.

interface CustomNodeDiagnostic
MemberTypeSummary
code'payload-invalid' | 'payload-missing'`payload-invalid` — a payload was found and did not match the schema. `payload-missing` — the control's binding names a store node the document does not hold.
issuesreadonly string[]Human-readable, one per failing field. Never rendered as markup by this package.
namestringThe definition whose schema refused it.
nodeIdstringThe control's canonical node id, so a host can locate it.

CustomNodeInputinterfaceSource ↗

What a node says and where it goes — one object, so the parts cannot be passed in the wrong order or get out of step.

A definition with text needs only data: what the document shows is computed from it.

ts insertCustomNode(editor, Citation, { data: citation }); // text derives insertCustomNode(editor, Tag, { attrs: { id: 'x' }, text: '[tag]' }); // no payload

interface CustomNodeInput<Schema extends StandardSchemaV1 | undefined = any>
MemberTypeSummary
alias?string`w:alias` — the human title Word shows on the control, and the chrome's floating label.
at?{ readonly paragraphId: string; readonly offset: number; }Where to insert. Omitted, the node lands at the current selection HEAD — the programmatic mirror of "type a citation at the caret".
attrs?Readonly<Record<string, string>>The `w:tag` attrs. Derived by the definition's `tagAttrs` when it declares one.
data?InferSchemaInput<Schema>The node's payload: everything that does not fit in 64 characters of `w:tag`.
lock?false | 'sdtLocked' | 'sdtContentLocked' | 'contentLocked'The `w:lock` written on the control. Defaults to `contentLocked` — the text is locked so the label cannot drift out of sync with the attrs by inline typing, while the node itself stays DELETABLE as one unit, in the editor and in Word alike. `false` writes no lock; `sdtContentLocked` also forbids deleting the node.
text?stringWhat the document shows. Derived by the definition's `text` when it declares one.

CustomNodeIssueinterfaceSource ↗

One field a payload got wrong.

path is the route to it — ['authors', 0, 'name'] — which is what a form needs to find the input to mark. Empty for an issue about the payload as a whole.

interface CustomNodeIssue
MemberTypeSummary
messagestring
pathreadonly (string | number)[]
pointerstring`authors.0.name`, for a log line or a message. Derived from `path`.

CustomNodePayloadSourceinterfaceSource ↗

A payload as the store holds it, before any schema has looked at it. Untrusted file input.

interface CustomNodePayloadSource
MemberTypeSummary
datastring
labelstring
nodeIdstring

CustomNodesModuleOptionsinterfaceSource ↗

How [customNodesModule](customNodesModule) is configured.

interface CustomNodesModuleOptions extends ProLicenseOptions
MemberTypeSummary
nodesreadonly AnyCustomNodeDefinition[]The definitions this editor recognizes. A tag prefix no definition claims stays literal.
onDiagnostic?(diagnostic: CustomNodeDiagnostic) => voidTold about a document, never about a bug: a payload that failed its schema, so far.

CustomNodesOfOptionsinterfaceSource ↗

How [customNodesOf](customNodesOf) narrows what it answers.

interface CustomNodesOfOptions
MemberTypeSummary
nodes?readonly AnyCustomNodeDefinition[]Which definitions to recognize. Defaults to everything registered on the editor, which is what a host almost always wants — passing a subset answers only those.
onDiagnostic?(diagnostic: CustomNodeDiagnostic) => voidTold about a node whose payload could not be read.

CustomNodeUpdateinterfaceSource ↗

How [updateCustomNode](updateCustomNode) rewrites the control it replaces.

The same shape [CustomNodeInput](CustomNodeInput) takes, minus at — an update happens where the node already is — and with data able to be null.

interface CustomNodeUpdate<Schema extends StandardSchemaV1 | undefined = undefined> extends Omit<CustomNodeInput<Schema>, 'at' | 'data'>
MemberTypeSummary
data?InferSchemaInput<Schema> | nullThe payload the rewritten node carries.

CustomNodeXmlOptionsinterfaceSource ↗

How [customNodeXml](customNodeXml) writes the control, for server-side templating where there is no editor instance to insert through.

interface CustomNodeXmlOptions<Schema extends StandardSchemaV1 | undefined = undefined>
MemberTypeSummary
alias?string`w:alias` — the human title Word shows on the control.
data?InferSchemaInput<Schema>The node's payload — everything past the 64 characters `w:tag` holds.
id?number`w:id` — Word's own numeric control identity (positive, non-zero). Defaults to a deterministic FNV-1a over the tag and text; pass one to control it. Two IDENTICAL nodes in one document then share an id — Word tolerates this and regenerates on open, but a caller splicing many copies of the same node can pass distinct ids.
lock?false | 'sdtLocked' | 'sdtContentLocked' | 'contentLocked'`w:lock`. Defaults to `contentLocked`, matching `insertCustomNode`. `false` omits it.
nodeId?stringThe node's id inside the store, which the binding's xpath quotes. Defaults to `cx1`.
storeIndex?numberWhich `/customXml/itemN.xml` this store claims. Defaults to 1.

CustomNodeXmlStoreinterfaceSource ↗

The package a spliced payload needs, as parts a caller adds to the zip it is assembling.

Everything here is required. A control carrying a w:dataBinding whose store is missing is a document Word opens and offers to repair, and repairing it throws the control away.

interface CustomNodeXmlStore
MemberTypeSummary
contentTypeOverride{ readonly partName: string; readonly contentType: string; }The content-type Override the properties part needs. `itemN.xml` needs none.
itemPartNamestring`/customXml/itemN.xml` — the payload itself. Rides the package's `xml` content-type default.
itemXmlstring
propsPartNamestring`/customXml/itemPropsN.xml` — carries the `ds:itemID` the binding quotes.
propsXmlstring
relationshipsreadonly { readonly from: string; readonly type: string; readonly target: string; }[]Relationships the caller must declare, each from the part named to the target named.
storeItemIdstringThe `ds:itemID`, which is also the `w:storeItemID` already written into the markup.

DecodedCustomNodeTaginterfaceSource ↗

A w:tag value parsed back into the identity it encodes.

Everything here comes from a file an attacker fully controls. attrs is a null-prototype object and the decoder refuses the prototype-polluting names outright, but the VALUES are still untrusted: never build DOM, URLs or CSS from them without sanitizing.

interface DecodedCustomNodeTag
MemberTypeSummary
attrsReadonly<Record<string, string>>Query-string attrs, on a null-prototype object. Untrusted file input.
namestringThe node type name, matching a [CustomNodeDefinition.name](CustomNodeDefinition.name).
prefixstringThe prefix segment — which definition claims this node.

DocumentExportOptionsinterfaceSource ↗

How [saveForExport](saveForExport) and [prepareForExport](prepareForExport) treat this copy.

interface DocumentExportOptions
MemberTypeSummary
destination?DocumentDestinationDefaults to `external`, because that is what calling an export function means.

ProLicenseOptionsinterfaceSource ↗

Accepted by every pro entry point.

interface ProLicenseOptions
MemberTypeSummary
licenseKey?stringYour license key from docx-editor.dev. Optional in v1: unlicensed use in development and evaluation is permitted, production use requires a license (see LICENSE.md) — the package trusts you either way.

RecognizeCustomNodesOptionsinterfaceSource ↗

Every recognized custom node in one story, in document order.

Tag-prefix keyed, exactly as the change specifies: an inline SDT whose tag decodes to a registered <prefix>:<name> pair is offered to that definition's fromDocx; everything else — foreign tags, unregistered prefixes, a fromDocx veto — stays a literal SDT.

interface RecognizeCustomNodesOptions
MemberTypeSummary
onDiagnostic?(diagnostic: CustomNodeDiagnostic) => voidTold about a node whose payload could not be read. Omitted, nothing is reported.
payloads?ReadonlyMap<string, CustomNodePayloadSource>The payload each control binds, from `customNodePayloadsByControl`.

RecognizedCustomNodeinterfaceSource ↗

A recognized custom node: one inline SDT whose tag matched a definition.

interface RecognizedCustomNode
MemberTypeSummary
attrsReadonly<Record<string, string>>Attrs after the definition's `fromDocx` had its say. Untrusted input.
data?unknownThe payload the node's control binds to, validated against the definition's `schema`.
namestringThe definition's `name`.
nodeIdstringThe SDT node's stable id in the canonical tree.
tagstringThe raw `w:tag` the node was recognized from.
textstringThe SDT's literal content text — what Word users see and may have edited.

ReviewModuleOptionsinterfaceSource ↗

How [reviewModule](reviewModule) is configured. Carries only the licence key today, so reviewModule() with no argument is the ordinary call.

interface ReviewModuleOptions extends ProLicenseOptions

SaveForExportOptionsinterfaceSource ↗

How [saveForExport](saveForExport) treats this copy.

interface SaveForExportOptions extends DocumentExportOptions
MemberTypeSummary
nodes?readonly AnyCustomNodeDefinition[]Which definitions to apply. Defaults to everything registered on the editor, which is the answer a host almost always wants — passing a subset leaves the rest untouched, and an untouched node travels whole.

StandardSchemaIssueinterfaceSource ↗

One validation failure. path is what tells a host WHICH field was wrong.

interface StandardSchemaIssue
MemberTypeSummary
messagestring
path?readonly (PropertyKey | { readonly key: PropertyKey; })[] | undefined

StandardSchemaV1interfaceSource ↗

The Standard Schema interface, vendored.

Any zod, valibot or arktype schema satisfies it. See https://standardschema.dev.

Reduced to the parts used here rather than copied: the spec's namespace, Props, SuccessResult/FailureResult, PathSegment and InferInput are all absent. Assignability with a real schema is what matters, and is checked against zod in the tests.

interface StandardSchemaV1<Input = unknown, Output = Input>
MemberTypeSummary
"~standard"{ readonly version: 1; readonly vendor: string; readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>; readonly types?: { readonly input: Input; readonly output: Output; } | undefined; }

Type aliases (11)

AnyCustomNodeDefinitiontypeSource ↗

A definition of any payload shape, spelled out.

The AUTHORED shape, which is what every collection and every internal helper takes: they read name, schema, text and preserveOnExport and never need dataOf. A [CustomNode](CustomNode) is assignable to it, so defineCustomNode's result goes wherever this is asked for.

The same thing bare CustomNodeDefinition already means — the interface defaults its parameter to any for exactly this reason. CustomNodeDefinition<Schema> is INVARIANT in Schema, because the schema's output type appears in the PARAMETER of fromDocx and reviewCard; that is what makes those hooks typed, and it also means two definitions with different schemas are not assignable to one another. Had the default been undefined, the obvious annotation — const nodes: CustomNodeDefinition[] = [citation, figure] — would fail with a message naming neither the cause nor this alias.

The cost, stated plainly: data is unchecked wherever a definition is held under this type. Pull one out of a registry and insertCustomNode(editor, def, attrs, text, { data }) accepts any shape at all. Payload typing lives where the definition is WRITTEN — defineCustomNode infers the schema, and its hooks are typed from it.

type AnyCustomNodeDefinition = CustomNodeDefinition;

CustomNodeDataRejectiontypeSource ↗

Why a payload was refused.

malformed is not valid JSON at all; invalid parsed but did not match the schema; async is a schema whose validation returns a promise, which cannot be used here (see below).

type CustomNodeDataRejection = 'malformed' | 'invalid' | 'async';

CustomNodeDataResulttypeSource ↗

What [parseCustomNodeData](parseCustomNodeData) answers.

type CustomNodeDataResult<Output> = {
    readonly ok: true;
    readonly value: Output;
} | {
    readonly ok: false;
    readonly reason: CustomNodeDataRejection;
    readonly issues: readonly string[];
};

CustomNodeWriteOutcometypeSource ↗

What [insertCustomNode](insertCustomNode) and [updateCustomNode](updateCustomNode) answer.

The engine's ExecResult, plus issues when the refusal was a schema failure. A host that only branches on ok sees no difference.

type CustomNodeWriteOutcome = (Extract<ExecResult, {
    ok: true;
}> & {
    readonly nodeId?: string;
}) | (Extract<ExecResult, {
    ok: false;
}> & {
    readonly issues?: readonly CustomNodeIssue[];
});

CustomNodeXmlResulttypeSource ↗

What [customNodeXml](customNodeXml) answers: the w:sdt markup, or a refusal.

Refuses exactly what insertCustomNode refuses — chiefly the 64-character w:tag cap — so a server cannot author a chip the editor itself would not have accepted.

type CustomNodeXmlResult = {
    readonly ok: true;
    readonly xml: string;
    readonly store?: CustomNodeXmlStore;
} | {
    readonly ok: false;
    readonly code: 'invalidArgs';
    readonly reason: string;
};

DocumentDestinationtypeSource ↗

Where this copy of the document is going.

The distinction the whole option exists for, made explicit at the call site so a host writes intent rather than remembering which function strips:

- internal — the copy you keep. Your own storage, your own system, a draft a user will reopen HERE. Nothing is stripped, because a stripped copy reopens as plain text and the chips are gone for good. - external — the copy that leaves. A download, an email attachment, a hand-off to someone who does not run this library. preserveOnExport decides what travels.

A UI with one Download button and a "keep our markup" checkbox drives both from one call.

type DocumentDestination = 'internal' | 'external';

DocumentExportResulttypeSource ↗

The exported document, or the reason there is not one.

A refusal answers no bytes. "Stripping failed, here are the bytes anyway" is the one outcome that must not be possible: a caller would ship the markup it asked to remove and have been told the export succeeded.

type DocumentExportResult = {
    readonly ok: true;
    readonly bytes: Uint8Array;
    readonly unwrapped: number;
    readonly removed: number;
} | {
    readonly ok: false;
    readonly reason: string;
};

EncodeTagResulttypeSource ↗

What [encodeCustomNodeTag](encodeCustomNodeTag) answers: the encoded tag, or a refusal.

The only refusal is tag-overflow. Word stores at most [MAX_TAG_LENGTH](MAX_TAG_LENGTH) characters in w:tag, and truncating would silently change a node's IDENTITY, so an oversized payload is reported with its length rather than trimmed.

type EncodeTagResult = {
    readonly ok: true;
    readonly tag: string;
} | {
    readonly ok: false;
    readonly reason: 'tag-overflow';
    readonly length: number;
};

InferSchemaInputtypeSource ↗

The type a schema ACCEPTS, which is what a write has to satisfy.

Different from the output whenever the schema transforms — a zod .default() or .transform() takes one shape and produces another — so a write typed by the output would reject the very value the schema was written to accept.

type InferSchemaInput<Schema> = 0 extends 1 & Schema ? any : Schema extends StandardSchemaV1<infer Input, unknown> ? Input : unknown;

InferSchemaOutputtypeSource ↗

The type a schema produces, for a definition to hand back to its host.

unknown for a definition with no schema, which is the honest description of an unchecked payload — not never, which would make the field unusable rather than merely unguaranteed.

type InferSchemaOutput<Schema> = 0 extends 1 & Schema ? any : Schema extends StandardSchemaV1<unknown, infer Output> ? Output : unknown;

StandardSchemaResulttypeSource ↗

What a Standard Schema validation answers.

type StandardSchemaResult<Output> = {
    readonly value: Output;
    readonly issues?: undefined;
} | {
    readonly issues: readonly StandardSchemaIssue[];
};

Variables (3)

CUSTOM_NODE_STORE_ROOTconstSource ↗

The local name of every payload store this library authors.

CUSTOM_NODE_STORE_ROOT = "docxEditor"

MAX_CUSTOM_NODE_DATA_LENGTHconstSource ↗

The largest payload this will parse, in UTF-16 code units.

A file-supplied length must never reach an allocation, and JSON.parse on a hostile string is the allocation. 256 KB is far past any legitimate chip payload and far short of anything that hurts.

MAX_CUSTOM_NODE_DATA_LENGTH: number

MAX_TAG_LENGTHconstSource ↗

Word refuses to store more than 64 characters in w:tag.

MAX_TAG_LENGTH = 64

On this page