/** * 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 { ColumnWidth } from "./viewport.js"; import type { BandWhich } from "./editor-headerfooter.js"; import type { CommentTarget } from "./editor-comments.js"; import { TrackedChangeMode } from "./types.js"; import type { CommentListEntry, FormattingInspection, HeaderFooterKind, HyperlinkInfo, ImageInsertOptions, ListFormat, NumberFormat, PageSetupOp, RevisionListEntry, SectionInfo, StyleInfo, TableBorderSpec, TableOfContentsOptions } from "./types.js"; /** The subset of WASM bridge exports the editor needs (as exposed on `window.Docxodus`). */ export interface DocxEditorExports { DocxSessionBridge: { GetVersion?: (handle: number) => string; 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; /** Native comment authoring (optional: older WASM bundles predate it; issue #580). */ AddComment?: (handle: number, anchor: string, spanJson: string, author: string, initials: string, date: string, markdown: string) => string; SetCommentResolved?: (handle: number, commentAnchor: string, resolved: boolean) => string; ListComments?: (handle: number) => string; AddCommentReply?: (handle: number, parentCommentAnchor: string, author: string, initials: string, date: string, markdown: string) => string; UpdateComment?: (handle: number, commentAnchor: string, markdown: string) => string; RemoveComment?: (handle: number, commentAnchor: string) => string; /** The editor's own render profile as one JSON object — the comment-aware twin of * RenderHtml / RenderBlockHtml / RenderBlocksHtml (optional: older bundles lack it). */ RenderEditorHtml?: (handle: number, optionsJson: string) => string; RenderEditorBlockHtml?: (handle: number, anchorId: string, optionsJson: string) => string; RenderEditorBlocksHtml?: (handle: number, anchorIdsJson: string, optionsJson: string) => string; RenderEditorChromeHtml?: (handle: number, optionsJson: string) => string; RenderEditorRangeHtml?: (handle: number, anchorIdsJson: string, optionsJson: string) => string; /** Review-mode controls (optional). */ SetTrackedChanges?: (handle: number, mode: number) => void; SetRevisionAuthor?: (handle: number, author: string) => void; ListRevisions?: (handle: number) => string; AcceptRevision?: (handle: number, revisionId: string) => string; RejectRevision?: (handle: number, revisionId: string) => string; AcceptAllRevisions?: (handle: number) => string; RejectAllRevisions?: (handle: number) => string; /** Links, images, references (optional). */ ListHyperlinks?: (handle: number, scopes: number) => string; AddHyperlink?: (handle: number, anchor: string, start: number, length: number, kind: string, target: string) => string; RemoveHyperlink?: (handle: number, hyperlinkId: string) => string; InsertImage?: (handle: number, anchor: string, characterOffset: number, imageBase64: string, optionsJson: string) => string; InsertTableOfContents?: (handle: number, anchor: string, pos: string, optionsJson: string) => string; /** Table cell structure and appearance (optional). */ MergeCells?: (handle: number, cellAnchor: string, rowSpan: number, colSpan: number, content: string) => string; UnmergeCells?: (handle: number, cellAnchor: string) => string; SetTableBorders?: (handle: number, cellAnchor: string, specJson: string) => string; SetCellShading?: (handle: number, cellAnchor: string, fill: string, scope: string) => string; SetRepeatHeaderRow?: (handle: number, cellAnchor: string, repeat: boolean) => string; /** Section page setup and header/footer flags (optional). */ SetPageSetup?: (handle: number, anchor: string, opJson: string) => string; SetHeaderFooterKindEnabled?: (handle: number, anchor: string, kind: string, enabled: boolean) => string; /** Introspection (optional). */ ListStyles?: (handle: number) => string; GetFormatting?: (handle: number, anchorId: string) => string; ReplaceTextRange?: (handle: number, anchor: string, find: string, replace: string, optionsJson: 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; /** Author-pinned page render scale (1.0 = 100%). Default 1. */ scale?: number; /** * How the continuous view sizes its text column. `"section"` (default) uses the width the * document's `w:sectPr` defines, so line breaking matches Word on every screen; `"fluid"` * lets the column follow the host, the pre-geometry behavior. */ columnWidth?: ColumnWidth; /** * Zoom the page down to fit a host narrower than it, the way a word processor's * fit-to-width does, instead of letting it overflow. Default true. */ fitToWidth?: boolean; /** * 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; /** * Render comments Word-style: the commented range highlighted inline and each thread as a * bubble in a gutter beside the page. Default true. With it off the document renders without * comment markup at all (the pre-gutter behaviour). */ comments?: boolean; /** Author stamped on comments posted from the gutter. Defaults to `revisionAuthor`. */ commentAuthor?: 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; /** Called when the caret enters or leaves a header/footer story (`null` = back in the body). */ onStoryChange?: (which: BandWhich | null) => void; /** Called after every comment-gutter layout with thread counts and the active thread. */ onCommentsChange?: (info: { threads: number; open: number; active: string | null; }) => void; } /** Options for {@link DocxEditor.openAsync}: the mount's window size and a progress callback. */ export interface DocxEditorOpenAsyncOptions extends DocxEditorOptions { /** * Body units (top-level paragraphs and tables) mounted per task before yielding to the event * loop. Default 24 — on the reference 17-page document that is a ~50 ms task. A window is * extended past the size so it never ends inside a border box the renderer groups adjacent * paragraphs into. */ windowSize?: number; /** Called after each window lands with the count of body units mounted so far. */ onProgress?: (mounted: number, total: number) => void; } /** * Word's Page Setup, as {@link DocxEditor.setPageSetup} takes it — the session's own * {@link PageSetupOp} (all twips; omit = unchanged), so the two cannot drift. */ export type EditorPageSetup = PageSetupOp; /** One hit from {@link DocxEditor.find}: a content-offset span inside an editable block. */ export interface EditorMatch { block: HTMLElement; start: number; length: number; } /** * 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 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; /** Whether hover or focus currently wants a handle for `blockDragSource`. * Tracked apart from the handle's `display`, which the viewport clip also drives: a handle * withdrawn because its block scrolled out of view has to come back when it scrolls in. */ private blockHandleWanted; private blockDragCleanup; /** Block boxes measured at drag start — see `BlockDropZone`. Empty when no drag is in flight. */ private dropZones; /** Combined scroll offset when `dropZones` was measured, and the scroller measured against. */ private dropZoneOrigin; private dropZoneScroller; 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; /** The comment gutter, when `options.comments` is on. */ private gutter; /** Story host the caret is in (page view / band), published for chrome via `onStoryChange`. */ private activeStory; /** Page geometry + fit-to-width zoom for the mounted document. Re-attached on every mount. */ private readonly viewport; /** 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 document without holding the main thread for the whole mount (issue #776). * * {@link open} renders and wires the entire document in one synchronous task, which on a * long document is the largest single block of a reading-and-editing session. This variant * pays only the session open up front, then mounts the document in windows of body units, * yielding to the event loop between them: the chrome — stylesheet, section wrappers with * their page geometry, footnote and endnote sections — comes from one cheap engine render * that sees no body content, and each window from the engine's block renderer, which lays * units out exactly as the full render does (the plan's border-box grouping is never split). * The DOM that results is the one {@link open} produces; blocks already mounted are editable * while later windows are still landing, and edits made meanwhile are honoured because every * window renders from the live session. * * Paginated mounts assemble the windows off-screen and paginate once at the end, since * pagination is a one-shot flow over the whole document; the main thread is still free * between windows, and `onProgress` lets a host show the fill. * * A bundle that predates the windowed renders mounts synchronously, exactly like {@link open}. */ static openAsync(container: HTMLElement, bytes: Uint8Array, exports: DocxEditorExports, options?: DocxEditorOpenAsyncOptions): Promise; private static resolveOptions; private static openSession; /** The steps every mount ends with once the body is in the DOM. */ private finishMount; /** * The windowed mount behind {@link openAsync}: chrome first, then body units in plan order, * a group-aligned window per task. See {@link openAsync} for the contract. */ private mountWindowed; /** * 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; /** Monotonic committed document version, when supported by the loaded engine. */ get version(): number | null; /** 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; /** * Move a fully rendered candidate into the host's stable public surface. * Internal editor chrome that binds directly to the container is recreated there; * document blocks and their listeners move with the DOM nodes. */ adoptContainer(container: HTMLElement): void; /** * The zoom the viewport is currently applying (1 = 100%). Below 1 the page is wider than the * host and has been scaled to fit rather than reflowed — the honest thing to show a user who * is wondering why a phone shows the whole page. */ get zoom(): number; /** * 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; /** * Repaint from the live session after it was mutated OUTSIDE the editor's own * commands — a host driving {@link sessionHandle} directly (an agent pipeline, * `raw.replaceXml`, a batch import). Continuous mode patches incrementally from * the render plan (a Unid-preserving single-block mutation repaints just that * block); paginated mode — or anything the reconciler cannot prove — remounts. * The editor cannot observe external mutations, so the host owns calling this * once per mutation batch. */ refresh(): void; /** 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; /** * The rectangle the floating handle may occupy: the window, narrowed by every ancestor that * clips its overflow. * * The handle is `position: fixed`, so it is placed in viewport coordinates and no ancestor's * overflow clips it for us. But the editor is routinely mounted inside a bounded scroller — * the ribbon puts its surface in one, inside a clipped card — and a block scrolled out of that * scroller has to take its handle with it rather than leave it parked over the host's chrome. * * Gated on the computed `overflow` rather than on whether an element scrolls right now, so the * answer does not depend on how much content happens to be loaded. */ private blockHandleClipRect; private showBlockHandle; private hideBlockHandle; private positionBlockHandle; /** Draw the drop line on `zone`'s requested edge, or take it away when there is no target. */ private paintDropIndicator; /** * Where to draw the line for an insertion on `position` of `zone` — the MIDDLE of the gap to * the neighbour on that side, not the zone's own border-box edge. A paragraph's `w:spacing` * becomes a CSS margin, which sits outside the box, so drawing on the edge underlines the * block's last line instead of reading as a gap between two blocks. Falls back to the raw edge * at the ends of the flow, and degrades to the same value when blocks are contiguous. */ private dropEdgeY; 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; /** Measure every movable block once, at drag start. See `BlockDropZone`. */ private captureDropZones; private scrollOffsetSum; /** How far the measured boxes have travelled since capture, from scrolling (drag autoscroll). */ private dropZoneShift; /** * Where a drop at `clientY` lands, or null when nothing there is legal. * * Resolution is by VERTICAL GEOMETRY over the measured blocks, not by which element the pointer * is over: the drag handle floats in the page margin, so a drag straight down the gutter — the * natural gesture — never crosses a paragraph box, and element hit testing gave those drags no * indicator and no drop at all. The nearest block by vertical distance is the target; the half * the pointer is in picks the side, snapped to the other side when only that one is legal * (a section break or a cross-block range usually makes exactly one side illegal). When neither * side is legal — the pointer is in a region this block cannot reach — there is no drop, and * nothing is drawn. */ private resolveDropAt; 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; /** Build the comment gutter over the container (called once, after the first mount). */ private createGutter; /** * 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. * * The body always gets its own `.docx-body-flow` wrapper — not only when bands are docked. * It is the sheet: the element the viewport gives page geometry to and zooms, and the one * the bands dock around. Without it the container would have to be both the scrolling host * and the scaled page, which are different boxes. */ 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; /** Batch-render `idsJson` anchors through the richest endpoint the bundle carries; the JSON * maps each anchor to its HTML (null when it failed to resolve), or carries `error`. */ private renderBlocksJson; /** 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's canonical tc anchor 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`. Reconciliation creates * the first notes section when needed and renumbers every affected citation in place. */ insertFootnote(markdown?: string): void; /** Cite a new endnote from the caret — see {@link insertFootnote}; writes the endnotes part. */ insertEndnote(markdown?: string): void; private insertNote; /** * Add a native Word comment on the current selection in the active block — or on the whole * block when the selection is collapsed (a null span comments the paragraph). The definition, * threading parts, and body-side range markers are written by the session's `AddComment` * (issue #300); the re-render shows the marker chrome the converter already draws. The * annotation type legal review runs on, finally authorable from the shipped surface * (issue #580). */ addComment(markdown?: string, author?: string, target?: CommentTarget): CommentListEntry | null; /** Reply to a thread root (or any comment) as a native Word reply (`commentsExtended`). */ addCommentReply(parentAnchorId: string, markdown: string, author?: string): boolean; /** Replace a comment's body text; author/date are preserved. */ updateComment(commentAnchorId: string, markdown: string): boolean; /** Delete a comment: the definition and its range markers everywhere. */ removeComment(commentAnchorId: string): boolean; /** What "New comment" would comment on right now: the active block and the selection in it. */ commentTarget(): CommentTarget | null; /** The live block for a full anchor id, or null when no mounted block resolves to it. */ private blockByAnchor; /** Open a draft comment bubble beside the selection (Word's "New Comment"). */ beginComment(): boolean; cancelComment(): void; /** Activate a thread by `cmt` anchor id or numeric comment id (null clears). */ activateComment(id: string | null): void; /** Step to the next (+1) / previous (−1) thread in document order. */ stepComment(direction: 1 | -1): CommentListEntry | null; /** The active thread root's anchor id, or null. */ get activeComment(): string | null; /** Show or hide the comment gutter (the markup stays in the document). */ showComments(visible: boolean): void; get commentsVisible(): boolean; /** Force the gutter to lay out now (tests). */ layoutComments(): void; /** The document's native comment threads (session truth), for review UIs — flat entries * with `parentAnchorId` linking replies and `resolved` carrying thread state. */ listComments(): CommentListEntry[]; /** Resolve or reopen a comment thread by its `cmt` anchor id (from {@link listComments}). * Resolution is thread metadata (`commentsExtended`), not body markup, so no re-render is * needed — a review UI re-reads {@link listComments} for the new state. */ setCommentResolved(commentAnchorId: string, resolved: boolean): boolean; 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" | "pageOfTotal"): 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; }; /** * Apply one inline `FormatOp` to the selection: a sub-range of the active block, the whole * block when the caret is collapsed, or every block of a multi-block selection. The last real * selection is used when a focus-stealing control (a colour picker) collapsed the live one. */ private applyInlineFormat; /** Font colour as a hex triplet (with or without '#'); `""` clears the explicit colour. */ setFontColor(hex: string): void; /** Word highlight colour name (`"yellow"`, `"green"`, …); `""` removes the highlight. */ setHighlight(name: string): void; setAllCaps(on: boolean): void; setSmallCaps(on: boolean): void; /** Word's "Clear All Formatting": drop every direct run property on the selection. */ clearFormatting(): void; /** The caret's rendered font size in points (from computed style), or null. */ fontSizeAtCaret(): number | null; /** Grow (+) or shrink (−) the selection's font size by `delta` points (Word's A↑ / A↓). */ adjustFontSize(delta: number): void; /** Line spacing as a multiple of single (1, 1.15, 1.5, 2 …) — `w:spacing/@w:line` under `auto`. */ setLineSpacing(multiple: number): void; /** Space before/after the paragraph, in POINTS (Word's Paragraph dialog units). */ setParagraphSpacing(op: { beforePt?: number; afterPt?: number; }): void; /** First-line indent in twips (0 = none); removes any hanging indent. */ setFirstLineIndent(twips: number): void; /** Hanging indent in twips (0 = none); removes any first-line indent. */ setHangingIndent(twips: number): void; /** * Make the active block (or every selected block) a list item of `kind` — the full Word * numbering gallery, not just bullets/decimal — or a plain paragraph with `"none"`. */ setListFormat(kind: ListFormat): void; /** The active block's list format (`"bullet"`, `"decimal"`, …), or null when it is not a list item. */ listFormatAtCaret(): string | null; /** Paragraph formatting (direct + effective) of the active block, for ribbon state. */ paragraphFormatting(): FormattingInspection | null; /** Wrap the selection in a hyperlink (external URL, or an internal bookmark name). */ insertHyperlink(target: string, kind?: "external" | "internal"): boolean; /** The hyperlink the caret (or selection start) sits in, or null. */ hyperlinkAtCaret(): HyperlinkInfo | null; /** Remove the hyperlink at the caret, keeping its text. */ removeHyperlink(): boolean; /** Insert an inline image (base64 bytes) at the caret in the active body block. */ insertImage(imageBase64: string, options?: ImageInsertOptions): boolean; /** Read a picked file and insert it as an inline image. */ insertImageFile(file: Blob, options?: ImageInsertOptions): Promise; /** Insert a table of contents (a real TOC field) before the active block. */ insertTableOfContents(options?: TableOfContentsOptions): boolean; /** Merge the active cell with `rowSpan`×`colSpan` neighbours (down and right). */ mergeCells(rowSpan: number, colSpan: number): void; /** Split a merged cell back into its grid cells. */ unmergeCells(): void; /** Set (or with `style: "none"` remove) the borders of the active cell's table. */ setTableBorders(spec: TableBorderSpec): void; /** Shade the active cell (or its row/column/table) with a hex fill; `""` clears. */ setCellShading(fill: string, scope?: "cell" | "row" | "column" | "table"): void; /** Repeat the active cell's row at the top of every page the table spans. */ setRepeatHeaderRow(repeat: boolean): void; /** Delete the whole table the caret is in. */ deleteTable(): void; /** How edits are being recorded right now. */ get trackedChanges(): TrackedChangeMode; /** * Switch tracked-changes mode mid-session (Word's "Track Changes" toggle). Undo history and * edits survive; the document re-renders so revisions show (or stop showing) inline. */ setTrackedChanges(mode: TrackedChangeMode): void; setRevisionAuthor(author: string): void; /** Every tracked revision in the document, from the session's registry. */ listRevisions(): RevisionListEntry[]; acceptRevision(revisionId: string): boolean; rejectRevision(revisionId: string): boolean; acceptAllRevisions(): boolean; rejectAllRevisions(): boolean; private resolveRevision; /** Rendered revision marks in document order — what Previous/Next step through. */ revisionElements(): HTMLElement[]; /** The registry entry a rendered revision mark belongs to (matched by block, then by text). */ revisionAt(el: HTMLElement): RevisionListEntry | null; /** Every occurrence of `query` in the editable blocks, in document order. */ find(query: string, options?: { matchCase?: boolean; }): EditorMatch[]; /** * Select a match, focus its block and scroll it into view — the caret lands on the hit, so * typing continues in the document. A find box that is still being typed into wants * `showFindMatches` instead; this is the commit step (closing the bar, jumping in to edit). */ selectMatch(match: EditorMatch): void; /** * Paint `matches` and scroll the one at `activeIndex` into view WITHOUT moving focus or the * caret. This is what a find field calls on every keystroke: the document shows where the hits * are and rides to the current one, while the keyboard stays in the search box. * * Falls back to the document selection where the CSS Custom Highlight API is missing — still * without focusing the block, so the next character typed goes to the search field either way. */ showFindMatches(matches: EditorMatch[], activeIndex: number): void; /** Drop the find painting (closing the find bar, or committing a match to the caret). */ clearFindMatches(): void; /** * Replace one match's text (formatting of the surrounding run is inherited). `focus: false` * leaves the keyboard where it is — a Replace button pressed from the find bar must not drag * the caret into the document mid-search. */ replaceMatch(match: EditorMatch, replacement: string, options?: { focus?: boolean; }): boolean; /** Replace every occurrence; returns how many were replaced. */ replaceAll(query: string, replacement: string, options?: { matchCase?: boolean; focus?: boolean; }): number; /** Set the author-pinned zoom (1 = 100%). Fit-to-width still caps it on narrow hosts. */ setZoom(scale: number): void; /** The zoom the user asked for (what a zoom control shows), before fit-to-width caps it. */ get requestedZoom(): number; /** Word count over the body's editable text. */ wordCount(): number; /** In page view, the active block's page and the page total; null in continuous view. */ pageInfo(): { page: number; total: number; } | null; /** The section governing the active block (or the first body block). */ sectionInfo(): SectionInfo | null; /** Word's Page Setup on the section holding the caret: size, orientation, margins. */ setPageSetup(op: EditorPageSetup): boolean; /** Word's "Different first page" (`first`) / "Different odd & even pages" (`even`). */ setHeaderFooterKindEnabled(kind: "first" | "even", enabled: boolean): boolean; headerFooterKindEnabled(kind: "first" | "even"): boolean; /** "First Page Header", "Footer", … for the story a band (or the active page area) shows. */ headerFooterStoryLabel(which: BandWhich): string; /** Put the caret in the header or footer (Word's "Go to Header / Go to Footer"). */ goToHeaderFooter(which: BandWhich): boolean; /** Leave the header/footer and put the caret back in the body. */ closeHeaderFooter(): void; /** Which story the caret is in, or null in the body. */ get activeStoryKind(): BandWhich | null; /** The document's style definitions (for a styles gallery); empty on older bundles. */ styles(): StyleInfo[]; /** The active block's paragraph style id (from the render), or null. */ styleAtCaret(): string | null; /** 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. */ /** The editor's render profile, as the comment-aware bridge endpoints take it. */ private editorRenderProfile; /** * Full-document HTML from the live session. Prefers the session-attached * `RenderEditorHtml` (comment-aware) or `RenderHtml` bridge — the saved bytes never cross the * JS/WASM boundary — and falls back to `ConvertDocxToHtmlComplete` over `bytes` (the opened * document on first paint, else a fresh save) for older WASM bundles. Every path uses the * same option profile, so the rendered HTML is identical. */ private renderFullHtml; /** Editable BODY blocks in document order (band blocks are enumerated by `ownerRoot`). In page * view a live story sits INSIDE a page box, so it is excluded here explicitly — a remount * rebuilds the pages without it, and a focus index that counted it would land one block off. */ 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 when an * existing list is not stampable; an absent list is the valid first-note case. */ 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; /** Re-append the gutter after a mount emptied the container (mounts replace `innerHTML`). */ private readoptGutter; } //# sourceMappingURL=editor.d.ts.map