/** * DocxEditor — a framework-agnostic, in-browser DOCX block editor. * * Architecture (see docs/architecture/ir_editor_feasibility.md, "Option B"): * - model-of-record: a live DocxSession in WASM (lossless save); * - rendering: WmlToHtmlConverter HTML (faithful) stamped with data-anchor; * - editing: each block is contenteditable; on commit, the edit goes through * DocxSession by anchor, then ONLY that block is re-rendered from the live * session (session-attached RenderBlockHtml) and patched into the DOM. * * The IR/anchor system is the addressing spine; the live OOXML is the truth. * This is the pure-TypeScript core; a React wrapper can sit on top. * * MVP scope: per-block, commit-on-blur editing of paragraphs/headings. An edited * block's content is replaced from its plain text (inline formatting within an * edited block is not preserved — a documented MVP limit); UNTOUCHED blocks keep * full fidelity, and save() is lossless for them. */ import type { BandWhich } from "./editor-headerfooter.js"; import { TrackedChangeMode } from "./types.js"; import type { HeaderFooterKind, NumberFormat } from "./types.js"; /** The subset of WASM bridge exports the editor needs (as exposed on `window.Docxodus`). */ export interface DocxEditorExports { DocxSessionBridge: { OpenSession: (bytes: Uint8Array, settingsJson: string) => number; CloseSession: (handle: number) => void; CreateBlankDocx: () => Uint8Array; Project: (handle: number) => string; ReplaceText: (handle: number, anchor: string, md: string) => string; ReplaceTextAtSpan: (handle: number, anchor: string, spanStart: number, spanLength: number, replace: string) => string; SplitParagraph: (handle: number, anchor: string, offset: number) => string; MergeParagraphs: (handle: number, first: string, second: string) => string; DeleteBlock: (handle: number, anchor: string) => string; MoveBlock?: (handle: number, sourceAnchor: string, targetAnchor: string, pos: string) => string; /** JSON `{anchorId, before, after}[]`: where a block may legally move, per side. Optional — * without it the drag UI offers every block and lets the engine refuse, as it did before. */ ValidMoveTargets?: (handle: number, sourceAnchor: string) => string; InsertHorizontalRule: (handle: number, anchor: string, pos: string, ruleJson: string) => string; InsertTable: (handle: number, anchor: string, pos: string, rows: number, cols: number, optionsJson: string) => string; InsertTableRow: (handle: number, cellAnchor: string, pos: string) => string; InsertTableColumn: (handle: number, cellAnchor: string, pos: string) => string; DeleteTableRow: (handle: number, cellAnchor: string) => string; DeleteTableColumn: (handle: number, cellAnchor: string) => string; ApplyFormat: (handle: number, anchor: string, spanJson: string, opJson: string) => string; SetParagraphStyle: (handle: number, anchor: string, styleId: string) => string; SetParagraphFormat: (handle: number, anchor: string, opJson: string) => string; ApplyListFormat: (handle: number, anchor: string, kind: string) => string; SetListLevel: (handle: number, anchor: string, delta: number) => string; GetListMembership: (handle: number, anchor: string) => string; RenderBlockHtml: (handle: number, anchorId: string, cssPrefix: string, fabricateClasses: boolean) => string; RenderBlockHtmlForReview?: (handle: number, anchorId: string, cssPrefix: string, fabricateClasses: boolean, renderTrackedChanges: boolean) => string; /** Session-attached full-document render (optional: older WASM bundles lack it). */ RenderHtml?: (handle: number, cssPrefix: string, fabricateClasses: boolean, paginated: boolean, scale: number) => string; RenderHtmlForReview?: (handle: number, cssPrefix: string, fabricateClasses: boolean, paginated: boolean, scale: number, renderTrackedChanges: boolean) => string; Save: (handle: number) => Uint8Array; /** Save keeping the projector's Unid bookkeeping — remount only, never a user download. * Optional so a bridge predating it still satisfies this type. */ SaveWithAnchorIds?: (handle: number) => Uint8Array; Undo: (handle: number) => boolean; Redo: (handle: number) => boolean; /** Header/footer region: the section a body anchor belongs to (kind → part mapping). */ GetSectionInfo: (handle: number, anchorId: string) => string; SetHeaderText: (handle: number, anchor: string, kind: string, markdown: string) => string; SetFooterText: (handle: number, anchor: string, kind: string, markdown: string) => string; InsertPageNumberField: (handle: number, anchor: string, field: string, format: string) => string; EnsureHeaderFooterVisible: (handle: number, anchor: string, kind: string) => string; SetPageNumbering: (handle: number, anchor: string, opJson: string) => string; ClearPageNumbering: (handle: number, anchor: string) => string; /** Note authoring (optional: older WASM bundles predate it). */ InsertFootnote?: (handle: number, anchor: string, characterOffset: number, markdown: string) => string; InsertEndnote?: (handle: number, anchor: string, characterOffset: number, markdown: string) => string; /** Incremental-reconcile endpoints (optional: older WASM bundles predate them; * the editor falls back to full remounts / full projections without them). */ ListBlocks?: (handle: number) => string; ListRenderedBlocks?: (handle: number, renderTrackedChanges: boolean) => string; ListNotes?: (handle: number, endnotes: boolean) => string; ListAnchors?: (handle: number) => string; RenderBlocksHtml?: (handle: number, anchorIdsJson: string, cssPrefix: string, fabricateClasses: boolean) => string; RenderBlocksHtmlForReview?: (handle: number, anchorIdsJson: string, cssPrefix: string, fabricateClasses: boolean, renderTrackedChanges: boolean) => string; }; DocumentConverter: { ConvertDocxToHtmlComplete: (...args: any[]) => string; }; } export interface DocxEditorOptions { /** CSS class prefix for rendered HTML. Default "docx-". */ cssPrefix?: string; /** * Fabricate CSS classes (vs inline styles). Default FALSE for the editor: a per-block * re-render must be self-contained, but fabricated class names are per-conversion and * have no matching stylesheet on the page, so re-rendered blocks would lose styling. * Inline styles keep every block's formatting intact on incremental re-render. */ fabricateClasses?: boolean; /** Make paragraph/heading blocks editable. Default true. */ editable?: boolean; /** Render block-flow pages (page boxes via pagination.ts) vs a continuous view. Default false. */ paginated?: boolean; /** Page render scale for paginated mode (1.0 = 100%). Default 1. */ scale?: number; /** * Render docked Header/Footer editing bands around the body flow. Default FALSE — with it off * the editor's DOM is unchanged, so existing consumers are unaffected. When on, the body flow * is wrapped in a `.docx-body-flow` element that becomes the edit root, keeping band blocks out * of the body's block list (which indexes remount focus). */ headerFooter?: boolean; /** Enable the editor-owned block drag handle. Default false (the ribbon defaults it to true). */ blockDrag?: boolean; /** How editor mutations are recorded. RenderInline enables native Word revisions. */ trackedChanges?: TrackedChangeMode; /** Author stamped on native Word revisions. Default "docxodus". */ revisionAuthor?: string; /** Called after a block edit commits (with the affected anchor). */ onEdit?: (info: { anchorId: string; unid: string; }) => void; /** Called after a successful block move. */ onMove?: (info: { sourceAnchorId: string; destinationAnchorId: string; }) => void; } /** * Serialize a block's inline content to the projector's markdown subset, preserving * bold / italic / links (emphasis detected via computed style). Used so an edit keeps * the block's formatting instead of flattening it to plain text. Formatting the markdown * subset cannot express (font size/color) is still dropped on an edited block. */ export declare function serializeInlineMarkdown(block: HTMLElement): string; export type FormatKey = "bold" | "italic" | "underline" | "strike" | "code" | "superscript" | "subscript"; /** Paragraph alignment passed to DocxEditor.setAlignment. */ export type EditorAlignment = "left" | "center" | "right" | "justify"; export declare class DocxEditor { private readonly exports; private readonly container; private readonly handle; private readonly options; /** Map a block's current bare unid → its full kind:scope:unid (DocxSession anchor). */ private readonly unidToFullId; /** The element whose [data-anchor] descendants are the editable blocks (container or page container). */ private editRoot; /** The most recently focused editable block — the target for ribbon/format commands. */ private activeBlock; private closed; /** * Re-entrancy guard for node replacement. Replacing a contenteditable block that still holds * focus removes the focused node, which fires a SYNCHRONOUS `blur` → re-enters commitBlock; the * interleaved second replaceWith then throws NotFoundError ("node ... no longer a child") and the * structural edit (split/merge/format) is lost. While this flag is set, commitBlock no-ops. */ private replacing; /** Editor-owned block-move chrome. It is deliberately outside the rendered unit tree. */ private blockDragHandle; private blockDropIndicator; private blockMoveMenu; private blockMoveLive; private blockDragSource; private blockDragCleanup; private blockDragTargetCleanup; private blockDragging; private blockDragPointerDown; /** Anchors the current drag source may legally move next to, per the engine's own rules. * Null when the bridge predates ValidMoveTargets — then every block is offered, as before. */ private blockMoveTargets; /** Memoized `ValidMoveTargets` answers, keyed by source anchor and dropped whenever an edit * lands. The legal-target set is a property of the document, so hovering back and forth over * the same blocks between edits must not re-ask the engine. */ private blockMoveTargetCache; /** Cancels the pending idle prefetch of the hovered block's targets — see `showBlockHandle`. */ private blockMoveTargetPrefetch; /** Why the last move was refused, verbatim from the engine — diagnostics, not announcement copy. */ lastMoveError: string | null; /** * The last real (non-collapsed) text selection inside an editable block. A toolbar control that * must take focus to be used — the font-size combobox — blurs the block and collapses the live * selection, so without this an operation triggered from such a control could only target the * whole paragraph (S-1 smoke-test finding 3). Refreshed whenever a non-empty selection sits in a * block, and cleared when a caret is collapsed inside a block (so it never goes stale). */ private lastSelection; /** * Stable bookmark for a selection spanning independent editable blocks. Native controls such as * a font-size combobox can take focus and collapse the live DOM selection; block ids + content * offsets let the command restore that same range before applying. This is the multi-block * counterpart to lastSelection above. */ private lastCrossBlockSelection; /** State for the mouse-selection bridge between independent contenteditable block hosts. */ private dragSelection; /** The docked header/footer bands, when `options.headerFooter` is on. */ private region; /** Why the last reconcile() fell back to a full remount (null = it patched). For * diagnostics/specs; not part of the public API. */ private lastReconcileFallback; private constructor(); /** Track the last meaningful selection so focus-stealing toolbar controls can still target it. */ private readonly onSelectionChange; /** Start tracking a normal primary-button text drag inside one editable block. */ private readonly onMouseDown; /** * Apply the latest cross-block endpoint after the browser finishes its native mousemove * selection update. Firefox rewrites Selection back into the originating contenteditable after * event dispatch even when mousemove is cancelled; writing in requestAnimationFrame wins that * race and happens before paint. Coalescing also avoids rebuilding a Range for every raw pointer * event when the mouse is moving faster than the display can refresh. */ private queueDragSelection; /** Cancel a queued repaint and discard the current gesture. */ private clearDragSelection; /** * Browsers fence native mouse selection at a contenteditable host boundary. Once a drag reaches * another block in the same OOXML story, take over just that gesture and create the cross-block * Selection the editor's existing multi-block command path consumes. Intra-block selection stays * entirely native, and separate hosts remain intact for safe per-anchor commits. */ private readonly onMouseMove; /** Commit the final cross-block endpoint before releasing the gesture state. */ private readonly onMouseUp; /** The editable block (contenteditable [data-anchor]) containing `node`, if any, within this editor. * Fenced by `container`, not `editRoot`, so header/footer band blocks — which live outside the * body edit root by design — also register. The fence still rejects other editors on the page. */ private editableBlockOf; /** * The root owning `el`'s sibling block list: its header/footer band's story container, else the * body edit root. Keeps a multi-block selection from spanning a band and the body, whose block * lists belong to different OOXML parts. */ private ownerRoot; /** True when `el` is a header/footer band block rather than a body block. */ private isBandBlock; /** * Repaint after an edit to `block` that would otherwise remount the whole document: a band * repaints only itself (a story is one to three paragraphs), leaving the body DOM — and the * user's place in it — untouched; a body edit reconciles incrementally. `forceRemount` is * for ops whose repaint provably needs whole-document context the reconciler cannot see: * list membership/level changes (sibling numbering shifts without sibling XML changing) * and border-div regrouping (HR insert, clearBorders). */ private refreshAfter; /** Open a document, render it into `container`, and wire up editing. */ static open(container: HTMLElement, bytes: Uint8Array, exports: DocxEditorExports, options?: DocxEditorOptions): DocxEditor; /** * Open a fresh, blank document (a "New document" — single empty paragraph, Normal style, * US-Letter section) and wire up editing. The seed bytes come from the WASM bridge so the * result opens cleanly in Word too. */ static openBlank(container: HTMLElement, exports: DocxEditorExports, options?: DocxEditorOptions): DocxEditor; /** Lossless DOCX bytes reflecting all edits. */ save(): Uint8Array; /** Release the underlying WASM session. The editor is unusable afterward. */ close(): void; /** * Switch between continuous and paginated rendering WITHOUT losing edits. Re-renders from the * LIVE session, so every committed edit (and the undo/redo history) survives the toggle — unlike * re-opening the original bytes, which silently discards session edits. No-op if already `value`. */ setPaginated(value: boolean): void; /** The editor's current DOM (for inspection/tests). */ get root(): HTMLElement; /** * The live `DocxSession` handle backing this editor — the model of record. * * Surfaced because chrome around the editor (the anchor rail) reports it as engine * state, and reaching into the private field from a host page only worked because * the bundle erases TypeScript's visibility. */ get sessionHandle(): number; /** Move one top-level body block relative to another and repaint from the live session. */ moveBlock(sourceAnchorId: string, targetAnchorId: string, position: "before" | "after"): boolean; /** Resolve any rendered descendant to its top-level body move unit (tables stay whole). */ private blockUnitOf; private isMovableBlockUnit; private currentBlockDragSource; private showBlockHandle; private hideBlockHandle; private positionBlockHandle; private showDropIndicator; private hideDropIndicator; private announceBlockMove; /** * Ask the engine which anchors the current source may move next to. One call per drag source, * so the UI offers only drops MoveBlock will accept — a document with section breaks is * partitioned into regions, and drawing an indicator across one only to fail the drop was the * behaviour this replaces. Null (no bridge support) keeps the previous offer-everything path. */ private refreshBlockMoveTargets; /** * This block's legal destinations, from the memo when it is there. Returns `undefined` — not * `null` — for "not asked yet", so a caller can tell an unknown answer from the engine's * "no bridge support, offer everything" one. */ private blockMoveTargetsFor; /** * Ask for the hovered block's destinations off the interaction path, and hide the handle if * the answer comes back empty and that block is still the one under the pointer. The handle * therefore appears immediately on hover and withdraws a beat later on the rare immovable * block, instead of every hover paying for the query up front. */ private prefetchBlockMoveTargets; /** * Whether `unit` is a legal destination for the current drag source — for a SPECIFIC side when * one is given. A cross-block range or a section break between the blocks can make one side * legal and the other not, so "this target is reachable" is not enough to pick a position. */ private isValidMoveTarget; /** * The side of `unit` a drop at `clientY` should land on: the half the pointer is in, snapped to * the other side when only that one is legal. Snapping rather than refusing keeps a reachable * target usable — the illegal side is usually illegal only because a section break or a * cross-block range sits between the two blocks on that side. */ private dropPositionFor; private refreshBlockDropTargets; private closeBlockMoveMenu; private openBlockMoveMenu; /** * Resolve a move-menu action to the block it should land against, considering only destinations * the engine accepts. "Top"/"bottom" therefore mean the ends of the source's own movable region * — on a document with section breaks that is the section, not the document, which is what makes * the commands work at all there instead of always failing. */ private blockMoveDestination; private runBlockMoveMenuAction; /** The nearest ancestor that actually scrolls the document flow, for drag autoscroll. */ private scrollContainer; /** Mount one floating handle; PDD owns pointer dragging while the menu owns keyboard moves. */ private setupBlockDrag; private teardownBlockDrag; private assertOpen; /** * Rebuild unid → full-anchor-id from the live session projection. * * Unids are CONTENT-ADDRESSED, so blocks with identical content in DIFFERENT parts collide — * e.g. a document with empty default/first/even header stories has one unid for several * header parts. A collision must resolve to the BODY entry: body blocks carry only * `data-anchor` (the bare unid) and have nothing else to resolve through, whereas a * header/footer band block carries its full anchor in `data-hf-anchor` and is resolved from * that (see `anchorIdOf`). Letting a non-body scope win here would silently redirect a body * edit into a header part. */ private refreshAnchorMap; /** * The full `kind:scope:unid` anchor for a rendered block. A header/footer band block carries * its own — the unid map cannot disambiguate one, since several parts' story paragraphs can * share a content-addressed unid (a real Word document with empty default/first/even stories * does exactly that, and a unid-keyed lookup would land the edit in the wrong header part). */ private anchorIdOf; /** Build the header/footer region (called once, before the first mount). */ private createRegion; /** * Point the bands at the section governing the current focus (or the first body block). * Called after every mount, and on focus of a body block, so a multi-section document shows * the stories that actually apply where the caret is. */ private syncRegionToBody; /** * Insert the bands around `bodyRoot` and make `bodyRoot` the edit root. Band blocks must stay * OUT of the edit root: `editableList()`/`blockIndex()` enumerate it to compute remount focus * indices, and band blocks in that list would shift every index. */ private dockBands; /** Continuous (non-paginated) mount: inject the converter's styles + body, wire blocks. */ private mountHtml; /** Paginated mount: flow blocks into page boxes via pagination.ts, wire the page clones. */ private mountPaginated; private wireBlocks; private wireBlock; /** * Replace `oldEl` with `newNodes`, suppressing the re-entrant blur→commit that removing a focused * block fires (see `replacing`), AND tolerating the case where a synchronous blur during focus * transfer detaches `oldEl` between the caller's checks and here. `replaceWith` then throws * NotFoundError ("node … no longer a child … moved in a blur event handler") — `isConnected` * alone doesn't catch this race. The session is already updated, so a skipped/failed visual swap * leaves correct content (the typed DOM); the next commit or remount reconciles it. This is why * the catch is silent rather than rethrowing — there's no lost data, only a deferred re-render. * Returns true if the swap happened. */ private replaceNode; /** Commit a block edit on blur: diff → run-preserving session op → re-render only this block. */ private commitBlock; private onKeydown; /** Editable cells in visual document order, excluding cells from any nested table. */ private tableCellsFor; /** First addressable paragraph in a cell, fenced against nested-table descendants. */ private editableInCell; /** Word-style cell navigation: Shift+Tab goes back, Tab goes forward, and Tab at the * final cell appends a row before entering its first cell. The first cell is a hard * boundary for Shift+Tab so focus never leaks out of the editor table. */ private navigateTableCell; /** Shift+Enter: insert an intra-paragraph line break at the caret. Delegates to the * native `insertLineBreak` command, which inserts a
AND positions the caret * after it correctly (handling the browser's bogus trailing-
rule) so typing * continues on the new line. Commits (on blur) as a w:br via the " \n" hard break * the serializer emits for a
. */ private insertLineBreakAtCaret; /** Enter: split the block at the caret into two paragraphs. */ private splitAtCaret; /** Backspace at block start: merge this block into the previous one. */ private mergeWithPrevious; /** * Apply the block's pending text change to the session with full inline-formatting fidelity. * Diffs the committed content text (markers + bidi excluded) against the current content text * and rewrites only the changed span via ReplaceTextAtSpan — every untouched run keeps its exact * rPr, and typed text inherits the boundary run's formatting. Returns the parsed EditResult, or * null when there is no change. Empty/whitespace-only baselines (e.g. the placeholder space the * converter renders for an empty paragraph, whose DOM text doesn't line up with the session's * empty run text) are rebuilt via ReplaceText — there is no inline formatting to preserve there. */ private commitTextChange; /** Flush a block's current (uncommitted) text to the session; returns the live full id. */ private syncBlock; /** Render a block by anchor and parse it into a detached element (null on error). */ private renderInto; private get renderTrackedChanges(); private renderBlockHtml; private renderPlanJson; /** Render two blocks in ONE batched bridge call when the bundle carries RenderBlocksHtml — * the per-render shell/converter setup is paid once instead of twice, which matters on the * Enter path (split renders both halves synchronously under the keystroke). Falls back to * two per-block renders on older bundles. Output is renderInto-identical per block (both * routes share the same extraction). */ private renderTwo; /** The editable block immediately before `el` within its own root, or null. */ private previousEditable; private parseEdit; /** * Drop the memoized `ValidMoveTargets` answers. Which blocks a block may move next to is a * fact about the DOCUMENT, so it survives hovering but not editing — and the two places a * document changes are `parseEdit` (every mutation that returns an `EditResult`) and * undo/redo, which return a bare boolean and so cannot go through it. */ private invalidateBlockMoveTargets; /** * Restore a cross-block selection after a native toolbar control took focus. The bookmark uses * stable anchor ids and content offsets, so it also survives incremental block swaps. */ private restoreCrossBlockSelection; /** Editable blocks the current selection covers, in document order. Uses Range.comparePoint * (robust to a selection boundary that normalized onto a wrapper element rather than a block * or text node — Range.intersectsNode misses the end block at a `(block, childCount)` boundary). * A collapsed or single-block selection yields just the active block. */ private selectedBlocks; /** The selection's span within `block`, clipped to the block (for inline ops across blocks): * the first block runs selection-start→end-of-block, middle blocks are whole, the last block * runs start-of-block→selection-end. Returns null for a whole-block apply. */ private blockSpanForSelection; /** Apply an inline ApplyFormat op to each block's slice of the selection, then reconcile * the DOM incrementally (see {@link finishMultiBlockOp} — a full remount costs a whole- * document convert, seconds on a large doc, where per-block swaps are ~10 ms each). * Returns false (caller falls back to the single-block path) for a 1-block selection. */ private applyInlineOpAcrossBlocks; /** Apply a whole-block (paragraph-level) op to each selected block, then reconcile the DOM * incrementally ({@link finishMultiBlockOp}). `forceRemount` is for ops whose rendering * needs whole-document context (e.g. border-div regrouping after clearBorders). Returns * false for a 1-block selection (caller uses the single-block path). */ private applyParagraphOpAcrossBlocks; /** Snapshot each selected block's identity + selection slice BEFORE any session op runs * (ops never mutate the DOM, so spans captured here stay valid until the swap phase). */ private multiBlockTargets; /** * Reconcile the DOM after a multi-block op. Fidelity-identical to the single-block path by * construction: each edited block is swapped for its own session-attached single-block * render — exactly what format()/setFontSize()/applyParagraphFormat() do for one block — * so a multi-block apply is N single-block applies, not one whole-document re-render * (which froze the UI for the full-document convert time on every ribbon action). Falls * back to ONE full remount when the op touched a list item (numbering continuation needs * whole-document context) or the caller forced it. Restores the cross-block selection so * consecutive ribbon actions (center, then bold) keep targeting the same range. */ private finishMultiBlockOp; /** * Toggle (or set) an inline format on the current selection in the active block. * A selection spanning multiple blocks applies to each. With no selection, applies to * the whole paragraph. Routes through DocxSession (`ApplyFormat`) so it is lossless and * supports underline/strike, not just markdown. */ format(key: FormatKey, value?: boolean): void; /** * Set the font size (in points) of the current selection in the active block; with no * selection, applies to the whole paragraph. `pts <= 0` clears the explicit size. Routes * through DocxSession `ApplyFormat` (`w:sz`), so it is lossless and survives save. */ setFontSize(pts: number): void; /** * Set the font family of the current selection in the active block; with no selection, * applies to the whole paragraph. `""` clears the explicit font (inherits the style/default). * Routes through DocxSession `ApplyFormat` (`w:rFonts`), so it is lossless and survives save. * Multi-block + last-selection plumbing matches {@link setFontSize} (a focus-stealing font * dropdown still applies to the real sub-range). */ setFontFamily(name: string): void; /** Set paragraph alignment (left/center/right/justify) on the active block. */ setAlignment(alignment: EditorAlignment): void; /** * Insert an S-1-style horizontal rule (an empty paragraph with a bottom border) after the * active block. `weight` is the rule thickness in eighths of a point (default 12 ≈ 1.5pt). * Re-renders fully (a new block needs whole-document context to lay out). */ insertHorizontalRule(weight?: number, style?: string, position?: "above" | "below"): void; /** * Insert a `rows`×`cols` table after the active block. `options.cellContents` (row-major * markdown) seeds the cells, `options.borderless` makes an invisible layout table, and * `options.cellAlignment` aligns every cell. Re-renders fully (tables need document context). */ insertTable(rows: number, cols: number, options?: { borderless?: boolean; cellContents?: string[]; cellAlignment?: EditorAlignment; columnWidths?: number[]; }): void; /** Run a table-structure op on the active cell (a cell-paragraph block) and re-render. */ private tableEdit; /** Insert a row above/below the active cell's row. No-op outside a table. */ insertTableRow(where: "above" | "below"): void; /** Insert a column left/right of the active cell's column. No-op outside a table. */ insertTableColumn(where: "left" | "right"): void; /** Delete the active cell's row (deleting the last row removes the table). No-op outside a table. */ deleteTableRow(): void; /** Delete the active cell's column (deleting the last column removes the table). No-op outside a table. */ deleteTableColumn(): void; /** * Indent/outdent the active block. On a LIST item this changes the list NESTING LEVEL * (`SetListLevel`) so numbering nests (e.g. 1, 2 → a sub-level) rather than the item just * shifting sideways with flat numbering. On a plain paragraph it adjusts the left indent by * `deltaTwips` (default ±720 = 0.5"), clamped at 0. */ indent(deltaTwips?: number): void; /** Change the active list item's nesting level by `delta` (+1 deeper, −1 shallower). */ private setListLevel; /** Toggle (or set) page-break-before on the active block. */ pageBreakBefore(value?: boolean): void; /** * Toggle the active block between a bullet/numbered list item and a plain paragraph. * Clicking the same kind it already is removes the list; any other state applies the kind. */ toggleList(kind: "bullet" | "decimal"): void; /** Clear all paragraph borders (e.g. remove an inserted horizontal rule) on the active block — * or every block in a multi-block selection. The engine/wire already accept `clearBorders`; * this surfaces it on the editor so an HR border is removable (S-1 smoke-test finding 1b). */ clearParagraphBorders(): void; /** * Delete the active block (e.g. a stray empty paragraph left above/below a table). Routes * through DocxSession `DeleteBlock` + re-render, focusing the previous block. No-op when the * caret is inside a table (remove cells via the table toolbar's delete row/column instead) and * no-op when it is the only editable block (don't empty the document). Closes the S-1 * smoke-test "no block-delete affordance" gap. */ deleteBlock(): void; /** * Cite a new footnote from the caret position in the active body block. The note definition is * created (writing the whole Word scaffold — part, reserved separator notes, settings * declaration, styles — on a document that has none yet) and its body renders as ordinary * editable `data-anchor` blocks in the notes section, so editing it afterwards needs no new op. * * Body blocks only: Word disallows a note reference inside a header/footer story or inside * another note, and the session rejects those with `AnchorWrongKind`. Remounts, because a new * note renumbers the citations after it and can add a whole part. */ insertFootnote(markdown?: string): void; /** Cite a new endnote from the caret — see {@link insertFootnote}; writes the endnotes part. */ insertEndnote(markdown?: string): void; private insertNote; private applyParagraphFormat; /** Set the paragraph style of the active block — or of every block in a multi-block selection * (e.g. "Heading1", "Heading2", "Normal"). */ setParagraphStyle(styleId: string): void; /** Undo the last edit (incremental repaint; falls back to a full re-render). */ undo(): void; /** Redo the last undone edit (incremental repaint; falls back to a full re-render). */ redo(): void; /** * Select which story kind a band edits (`"default"` / `"first"` / `"even"`). A kind with no * existing part is created empty, so the band always presents something editable. `"first"` * sets the section's `w:titlePg`; `"even"` sets the document-global `w:evenAndOddHeaders` * (which also governs footers — the band surfaces that caveat inline). */ setHeaderFooterKind(which: BandWhich, kind: HeaderFooterKind): void; /** The story kind a band is currently editing, or null when the region is off. */ headerFooterKind(which: BandWhich): HeaderFooterKind | null; /** * Append a page-number field to the focused header/footer story paragraph (falling back to the * band's last paragraph — Word's convention). No-op outside a band. */ insertPageNumber(field?: "currentPage" | "totalPages"): void; /** * Set the page numbering of the section the bands describe (`w:pgNumType`) — Word's *Format Page * Numbers…*: `start` restarts numbering at that number, `format` chooses `1, 2, 3` vs * `i, ii, iii` etc. Omitted fields are left unchanged. Requires the header/footer region * (`{ headerFooter: true }`); a no-op otherwise. * * Inserted page-number fields are plain, so they render through this. The editor's own view still * shows each field's cached result — Word recomputes on open — but `{ paginated: true }` * substitutes the real per-page number and so reflects the change immediately. */ setPageNumbering(op: { start?: number; format?: NumberFormat; }): void; /** Remove the section's page-numbering start/format: it reverts to continuing the previous * section's numbering in Word's default `1, 2, 3`. */ clearPageNumbering(): void; /** This section's page numbering as the document currently states it — `{}` when the section * sets neither (it continues the previous section in the default format). */ pageNumbering(): { start?: number; format?: NumberFormat; }; /** Which inline formats the current selection carries — for ribbon button highlighting. */ queryFormatState(): Record; /** Re-render one block from the live session by EditResult ref, swapping it in place. */ private swapBlock; /** * Full-document HTML from the live session. Prefers the session-attached * `RenderHtml` bridge — the saved bytes never cross the JS/WASM boundary * (two multi-MB copies per remount on a large doc) — and falls back to * Save + ConvertDocxToHtmlComplete for older WASM bundles. Both paths use * the same option profile, so the rendered HTML is identical. */ private renderFullHtml; /** Editable BODY blocks in document order (band blocks are enumerated by `ownerRoot`). */ private editableList; private blockIndex; /** * True when an edit produced or touched a list item (kind "li"). List markers and * numbering CONTINUATION need whole-document context, which a single-block render lacks * (every item would render as "1."), so such edits re-render the whole document. */ private affectsList; /** True when the bridge carries the reconcile trio and the mode allows patching. */ private canReconcile; /** The body's top-level unit nodes in document order: `[data-anchor]` elements not * nested in another unit (cell paragraphs collapse into their table) and not in the * notes sections. */ private bodyUnitNodes; /** The DOM diff token for a body unit node (see editor-reconcile.tokenOf). */ private static domTokenOf; /** The kind a body unit node would have in the plan (only 'li'/'tbl' matter to the * remount guard). */ private static domKindOf; private static listMarkerText; /** * Incrementally patch the DOM from the session's render plan; falls back to * {@link remount} whenever it cannot prove the patch correct. Same focus contract * as remount. */ private reconcile; /** The patch itself. Returns false to request the remount fallback. */ private reconcileCore; private bail; /** The generated single-child wrapper chain around a unit node (a table's alignment *
). Climbs while the parent is an anchor-less DIV whose ONLY element child is * the current node — never a section div (multi-child) or the edit root. */ private unitWrapperOf; /** The `[data-anchor]` element of a fresh render root (the root itself for a leaf * block, its descendant for a wrapper-shaped render like a table's align div). */ private static anchorElOf; /** Insert/remove/swap body unit nodes per the diff. Returns `true` on success or a * bail-reason string (parent ambiguity, order violation, wrapper semantics) — the * session is already correct, so bailing just means a full repaint. */ private applyBodyDiff; /** Wire a freshly rendered unit root (and its nested blocks) and stamp the unit's * content signature on its `[data-anchor]` element — the element the next * reconcile's DOM walk reads tokens from. */ private wireUnit; /** Old-sequence diff state for one notes section. `null` requests remount (DOM not * stampable/consistent). */ private notesDiff; /** Apply a notes-section diff: rebuild the `
    `'s li list, preserving kept nodes. */ private applyNotesDiff; /** Build a notes-section `
  1. ` for a freshly rendered note — replicating the * converter's chrome (id/value are re-stamped by the renumber pass; the backref * goes inside the last paragraph, matching RenderFootnoteItem). */ private buildNoteLi; /** * Rewrite position-derived note chrome from the session's citation-ordered note * list: the k-th marker in document order IS note k (ids ascend in reference * order), so marker sup text, hrefs/ids, li ids/values and backref hrefs are all * re-derived positionally. Pure attribute/text patching of generated chrome. */ private renumberNoteChrome; /** After an incremental block swap, stale marker chrome in the swapped node (the * throwaway render numbers citations from 1) is repaired in place. */ private maybeRenumberNotes; /** Stamp the DOM state the reconciler diffs against: container signatures on body * tables and `data-note-anchor` + signature on notes-section items. Called after * every full mount; reconcile stamps its own insertions. */ private stampPlanState; /** * Full re-render from current session state (after undo/redo, and after list edits where * single-block rendering can't compute numbering). Optionally focus the editable block at * `focusIndex` (caret at start, or end if `caretAtEnd`) — addressed by index because a * block's content-hashed unid changes across the save/reproject a remount performs. */ private remount; } //# sourceMappingURL=editor.d.ts.map