import type { AnchorInfo, AnchorTargetRef, AnnotationUpdate, BlockMetadata, BulkEditResult, CharSpan, CommentListEntry, CrossBlockMatch, DiffEntry, DocumentAnnotation, DocxodusWasmExports, DocxSessionProjection, DocxSessionSettings, EditResult, EditSummary, FillOptions, FindOptions, FormatOp, HeaderFooterKind, NumberFormat, PageNumberField, PageNumberingOp, ParagraphBorderEdge, ParagraphFormatOp, TableBorderSpec, TableInsertOptions, TableMergeContent, TableShadingScope, ListFormat, GrepOptions, ListMembership, ReplaceOptions, RevisionListEntry, SectionInfo, TemplatePlaceholder, TextMatch } from "./types.js"; import { DiffFormat, ProjectionDepth, TrackedChangeMode } from "./types.js"; /** * Stateful in-memory DOCX editing session keyed by markdown-projection anchor ids. * Mirror of the .NET `DocxSession` surface. See * `docs/architecture/docx_mutation_api.md` for the surface contract, * anchor lifecycle, error catalog, and supported markdown subset. * * Sessions are not eligible for JS-side garbage collection — call {@link close} * (or use a `using` block under TypeScript 5.2+) when done. */ export declare class DocxSession { private readonly handle; private readonly wasm; /** @internal */ constructor(handle: number, wasm: DocxodusWasmExports["DocxSessionBridge"]); project(): DocxSessionProjection; /** * Project a slice of the document keyed off an anchor — useful for showing * one section to an LLM at a time without paying the cost of projecting the * whole document. * * - `ProjectionDepth.SelfOnly` — just the addressed block (one paragraph, * row, etc.). * - `ProjectionDepth.Subtree` — the block + descendants (e.g. a table with * all its rows/cells, but no following content). * - `ProjectionDepth.SubtreeAndFollowingSiblings` (default) — for headings * this returns the whole section (heading + content up to the next same- * or-higher heading); for non-headings it behaves like `Subtree`. * * @see docs/architecture/docx_mutation_api.md */ projectAnchor(anchorId: string, depth?: ProjectionDepth): DocxSessionProjection; /** * Render a single block to faithful HTML from the live session — the editor's * incremental per-block re-render after an edit. Resolves against the in-memory * document (no Save round-trip). `anchorId` is a block anchor (`kind:scope:unid`) * or the bare unid carried by a `data-anchor` attribute. Returns the block's HTML * element (no ``/`
` wrapper). */ renderBlock(anchorId: string, options?: { cssPrefix?: string; fabricateClasses?: boolean; }): string; replaceText(anchorId: string, markdown: string): EditResult; deleteBlock(anchorId: string): EditResult; /** Reorder one top-level paragraph/heading/list/table block relative to another. */ moveBlock(sourceAnchorId: string, targetAnchorId: string, position: "before" | "after"): EditResult; /** * Delete every top-level block-level sibling between `fromAnchorId` (inclusive) * and `toAnchorIdExclusive` (exclusive). Both anchors must share a direct * parent and live in the same package part. Returns a single `EditResult` * whose `removed` lists every anchor that was deleted. * * Records ONE undo snapshot — `undo()` restores the entire range. * * @see docs/architecture/docx_mutation_api.md#deleterange */ deleteRange(fromAnchorId: string, toAnchorIdExclusive: string): EditResult; /** * Delete a heading and everything below it up to (but not including) the next * heading at the same or higher level. The heading anchor must have `kind === "h"`. * * If the target is the last heading in its parent, the section extends to the * end of the parent (heading + everything after). * * @see docs/architecture/docx_mutation_api.md#deletesection */ deleteSection(headingAnchorId: string): EditResult; insertParagraph(anchorId: string, position: "before" | "after", markdown: string): EditResult; splitParagraph(anchorId: string, characterOffset: number): EditResult; mergeParagraphs(firstAnchorId: string, secondAnchorId: string): EditResult; /** * Insert an empty paragraph carrying a bottom border — an S-1-style horizontal rule — * before/after the block. `rule` styles the line (default: a single ≈1.5pt black rule). */ insertHorizontalRule(anchorId: string, position: "before" | "after", rule?: ParagraphBorderEdge): EditResult; /** * Insert a `rows`×`cols` table before/after the block. `options` controls borders, row-major * cell markdown, and cell alignment. The returned `EditResult.created` lists the cell-paragraph * anchors (row-major), so each cell can then be addressed to fill/format. */ insertTable(anchorId: string, position: "before" | "after", rows: number, cols: number, options?: TableInsertOptions): EditResult; /** * Table row/column editing, addressed by a cell-paragraph anchor (e.g. one returned from * {@link insertTable}'s `created`). Insert clones the reference row/column's widths and starts * empty (`created` lists the new cell-paragraph anchors); delete of the last row/column removes * the whole table. All four are grid-aware: inserting across a merge extends it, deleting * through one narrows it, and deleting a vertical merge's lead row promotes the next row to * carry it — the grid is never left ragged. */ insertTableRow(cellAnchorId: string, position: "before" | "after"): EditResult; insertTableColumn(cellAnchorId: string, position: "before" | "after"): EditResult; deleteTableRow(cellAnchorId: string): EditResult; deleteTableColumn(cellAnchorId: string): EditResult; /** * Merge the rectangle of cells anchored at `cellAnchorId` running `rowSpan` rows down × * `colSpan` cells right (Word's *Merge Cells*): `w:gridSpan` for the horizontal extent, * `w:vMerge` restart/continue for the vertical one. The rectangle must tile the same whole grid * columns in every row it covers and must not clip a vertical merge entering from above or * continuing below — a partial overlap fails with `invalid_table_merge` instead of tearing the * grid. `content` decides what happens to the absorbed cells' content (default `"append"`). */ mergeCells(cellAnchorId: string, rowSpan: number, colSpan: number, content?: TableMergeContent): EditResult; /** * Split the merged cell at `cellAnchorId` back into unit cells, dropping its `w:gridSpan` and * `w:vMerge` markup and restoring one cell per grid column (each taking its `w:tblGrid` width). * Addressing a vertical-merge continuation unmerges the whole run. A cell with no merge markup * fails with `invalid_table_merge`. */ unmergeCells(cellAnchorId: string): EditResult; /** * Table styling, addressed by a cell-paragraph anchor — the post-insert counterpart of * {@link insertTable}'s options (issue #315 Stage A). `setColumnWidths` retunes `w:tblGrid` + * every row's cell width (one positive twip value per column) and pins the table to fixed * layout, exactly as inserting with explicit `columnWidths` would. */ setColumnWidths(cellAnchorId: string, widthsTwips: number[]): EditResult; /** * Set the table-level borders (`w:tblPr/w:tblBorders`) of the table containing the anchor. * Only the edges named by `spec.scope` are written; the rest are left untouched. Style * `"none"` removes the targeted edges. Omitting `spec` writes a thin single border all round. */ setTableBorders(cellAnchorId: string, spec?: TableBorderSpec): EditResult; /** * Shade the cell containing the anchor — or, with scope `"row"`, every cell of its row * (header-row banding). `fill` is a hex RRGGBB triplet (leading '#' tolerated) or `"auto"`; * `null` removes the shading. */ setCellShading(cellAnchorId: string, fill: string | null, scope?: TableShadingScope): EditResult; /** * Mark (or unmark) the row containing the anchor as a repeating header row * (`w:trPr/w:tblHeader`), so a multi-page table re-shows it on every page. Word only honors * the flag on a run of rows starting at the table's first row. */ setRepeatHeaderRow(cellAnchorId: string, repeat: boolean): EditResult; /** * Set the running header story for the section that owns `anchorId` (any body block in that * section) to `markdown`. Creates the header part + `w:headerReference` if the story of `kind` * doesn't exist yet, else replaces its content. The created header-paragraph anchors (scope * `hdr{N}`) come back in `EditResult.created` — insert a page number into one with * {@link insertPageNumberField}. `"first"` sets the section's title-page flag; `"even"` sets the * document's even/odd-headers flag. */ setHeaderText(anchorId: string, kind: HeaderFooterKind, markdown: string): EditResult; /** Set the running footer story for the section that owns `anchorId` — see {@link setHeaderText}; * the created footer-paragraph anchors (scope `ftr{N}`) come back in `EditResult.created`. */ setFooterText(anchorId: string, kind: HeaderFooterKind, markdown: string): EditResult; /** * Append a page-number field to the paragraph `anchorId` — typically a header/footer paragraph * returned by {@link setFooterText}/{@link setHeaderText}. `"currentPage"` emits a PAGE field, * `"totalPages"` a NUMPAGES field (native complex field with a cached result). Center it by * setting the paragraph alignment ({@link setParagraphFormat}). Returns the paragraph anchor in * `modified`. * * `format` writes the field's own `\*` general-formatting switch (`PAGE \* roman` → `i, ii, iii`). * Omitting it — the default — emits a plain field, which is what Word inserts and what follows the * SECTION's format ({@link setPageNumbering}). Prefer the section setting for ordinary page * numbering: a switch here overrides it for this one field and keeps overriding it if the section * later changes. */ insertPageNumberField(anchorId: string, field?: PageNumberField, format?: NumberFormat): EditResult; /** * Set the page-numbering properties (`w:pgNumType`) of the section that owns `anchorId` (any body * block in that section) — Word's *Format Page Numbers…* dialog: which number the section starts * at and which format its pages use. Omitted fields on `op` are left unchanged, so the start can * be set without disturbing the format and vice versa. Creates the element, and a trailing * `w:sectPr`, if absent. * * Applying values the section already has is a successful no-op that does NOT consume undo * history — safe to call from a dropdown's change handler. */ setPageNumbering(anchorId: string, op: PageNumberingOp): EditResult; /** * Remove the section's page-numbering start/format: it reverts to continuing the previous * section's numbering in Word's default `1, 2, 3`. Chapter-numbering attributes * (`w:chapStyle`/`w:chapSep`) are preserved. A section with nothing to clear is a no-op. */ clearPageNumbering(anchorId: string): EditResult; /** * Make the `kind` header/footer stories of the section that owns `anchorId` actually RENDER: * `"first"` sets `w:titlePg`, `"even"` sets the document-global `w:evenAndOddHeaders`; * `"default"` needs no flag and succeeds as a no-op. Idempotent. * * {@link setHeaderText}/{@link setFooterText} set these flags while writing content, which covers * authoring a story from scratch — but NOT a document that already carries a first/even reference * with the flag absent (Word leaves exactly that behind when "Different first page" is switched * off). Editing such a story through the text ops otherwise yields header content that is present * but invisible. Note the `"even"` caveat from {@link setHeaderText}: the flag is document-global * and governs footers too. */ ensureHeaderFooterVisible(anchorId: string, kind: HeaderFooterKind): EditResult; /** * Create a footnote whose body is `markdown` and cite it from the body paragraph `anchorId`, at * `characterOffset` characters into that paragraph's text (0 = before all text, text length = * after all of it). On a document with no footnotes yet this also creates the footnotes part, * Word's two reserved separator notes, the `FootnoteText`/`FootnoteReference` styles and the * `w:footnotePr` settings declaration; otherwise the existing part is reused. The note id is * allocated above every id already used in the package, so non-contiguous ids can't collide. * * Returns the created note anchors in `EditResult.created` — the definition (kind `fn`) and its * paragraphs (kind `p`, scope `fn`) — so the note can immediately be edited with * {@link replaceText} or removed with {@link deleteBlock} (which also drops the body reference). * * Body paragraphs only: Word does not allow a note reference inside a header/footer story or * inside another note, so a non-body anchor fails with `anchorWrongKind`. */ insertFootnote(anchorId: string, characterOffset: number, markdown: string): EditResult; /** Create an endnote — see {@link insertFootnote}; writes the endnotes part and a * `w:endnoteReference`, and the created definition anchor has kind `en`. */ insertEndnote(anchorId: string, characterOffset: number, markdown: string): EditResult; /** * Add a **native Word comment** (real `w:comment` markup, visible in Word/Google * Docs/LibreOffice's Reviewing pane — not the {@link addAnnotation} overlay) on the body * paragraph `anchorId`. `span` selects the commented character range; `null` comments the * whole block. On a document with no comments yet this also creates the comments part and * the `CommentText`/`CommentReference` styles. `opts.date` (ISO-8601) is written only when * provided, keeping output deterministic by default. * * Returns the created definition anchor (kind `cmt`) and its paragraph anchors (kind `p`, * scope `cmt`) in `EditResult.created`, so the comment can immediately be edited with * {@link updateComment} or removed with {@link removeComment}. * * Body paragraphs only (Word has no comments-on-comments); a non-body anchor fails with * `anchor_wrong_kind`, a zero-length span with `empty_comment_span`. */ addComment(anchorId: string, span: CharSpan | null, author: string, markdown: string, opts?: { initials?: string; date?: string; }): EditResult; /** Add a native Word comment around the exact live extent of a tracked revision returned by * {@link listRevisions}. Accepting/rejecting the revision keeps the comment and either leaves * its range on surviving text or collapses it to a point. */ addCommentToRevision(revisionId: string, author: string, markdown: string, opts?: { initials?: string; date?: string; }): EditResult; /** * Add a native Word reply to `parentCommentAnchorId`. It adds an adjacent reference and * inherits the thread root's range through `w15:paraIdParent`; the required * `commentsExtended.xml` and `commentsIds.xml` parts are created when the parent was flat. */ addCommentReply(parentCommentAnchorId: string, author: string, markdown: string, opts?: { initials?: string; date?: string; }): EditResult; /** Replace a comment's body text, addressed by its definition anchor (kind `cmt`); the * comment's author/initials/date are preserved, as is the last paragraph's `w14:paraId` * (Word's reply-threading key). */ updateComment(commentAnchorId: string, markdown: string): EditResult; /** Resolve or reopen one comment (`false` reopens it). Flat comments are upgraded with the * paraId-keyed metadata Word uses, and the operation is undoable. */ setCommentResolved(commentAnchorId: string, resolved: boolean): EditResult; /** Remove a comment: the definition, its body marker triple everywhere in the package, and * any `commentsExtended`/`commentsIds` threading entries keyed by it. */ removeComment(commentAnchorId: string): EditResult; /** The document's native Word comments in comments-part order. */ listComments(): CommentListEntry[]; /** Markup-native tracked-revision listing, in document order across body, headers, * footers, footnotes, and endnotes. Ids are stable while the underlying markup * exists and address {@link acceptRevision}/{@link rejectRevision}; authors/dates * are the markup's own (no accept/reject re-diff). */ listRevisions(): RevisionListEntry[]; /** Accept ONE revision by the id {@link listRevisions} reported — insertions keep * their content, deletions are carried out, a move materializes at its destination, * a format change keeps the new properties. Undoable. */ acceptRevision(revisionId: string): EditResult; /** Reject ONE revision by id — the inverse of {@link acceptRevision}: insertions are * removed, deleted content is restored, a move stays at its source, a format change * restores the stored old properties. Undoable. */ rejectRevision(revisionId: string): EditResult; applyFormat(anchorId: string, span: CharSpan | null, op: FormatOp): EditResult; /** * Convenience: find `substring` in the anchor's flat text and apply `op` to the * first occurrence. Eliminates the offset-arithmetic trap from #138 — caller passes * the visible text they want formatted, the WASM-side resolves it to a CharSpan. */ applyFormatBySubstring(anchorId: string, substring: string, op: FormatOp): EditResult; /** * Convenience: apply `op` to the exact span of a {@link TextMatch} (typically from * {@link grep}). The match's `enclosingAnchor.id` + `span` address one specific * occurrence even when several identical needles share the same block. */ applyFormatToMatch(match: TextMatch, op: FormatOp): EditResult; setParagraphStyle(anchorId: string, styleId: string): EditResult; /** Set paragraph alignment / indent / page-break-before (omitted fields are left unchanged). */ setParagraphFormat(anchorId: string, op: ParagraphFormatOp): EditResult; setListLevel(anchorId: string, levelDelta: number): EditResult; removeListMembership(anchorId: string): EditResult; /** Make the paragraph a bullet/numbered list item, or remove list membership ("none"). */ applyListFormat(anchorId: string, kind: ListFormat): EditResult; /** Apply one list format across the contiguous sibling run from `firstAnchorId` to * `lastAnchorId` inclusive (either document order). Every member shares one `w:num` * instance so the numbering sequence stays intact; the whole range is a single undo step. */ applyListFormatRange(firstAnchorId: string, lastAnchorId: string, kind: ListFormat): EditResult; /** Restart the anchored list item's numbering at `value` — Word's *Set Numbering Value…*. * Writes a `w:startOverride` on a dedicated `w:num` instance and repoints the anchored item * plus every following member of its sequence, so a mid-list restart splits the sequence * exactly like Word (earlier items keep their numbers, the tail continues from `value`). */ setListStartOverride(anchorId: string, value: number): EditResult; /** Remove the numbering restart from the anchored item's whole sequence (the inverse of * {@link setListStartOverride}); the sequence reverts to the definition's own start. A * sequence with no override at the item's level is a successful no-op. */ clearListStartOverride(anchorId: string): EditResult; replaceCellContent(cellAnchorId: string, markdown: string): EditResult; readonly raw: { getXml: (anchorId: string) => string; insertXml: (anchorId: string, position: "before" | "after", xml: string) => EditResult; replaceXml: (anchorId: string, xml: string) => EditResult; }; /** * Searches the flat text of every paragraph/heading/list-item in scope for * matches of `pattern`, returning them in document order with the run * fragments each match spans. Lets callers rewrite a match in place while * preserving each fragment's formatting (bold/italic/hyperlink/etc.). * * `pattern` is a regular expression — use plain string equivalents wrapped * in `^` / `$` or pass literal text escaped via a helper. * * @see docs/architecture/docx_mutation_api.md#grep */ grep(pattern: string, options?: GrepOptions): TextMatch[]; /** * Like {@link grep}, but lets a single match span adjacent block-level * siblings (paragraphs/headings/list items) under the same parent. Block * boundaries appear in the matched text as `\n`, so `^`/`$` with the * Multiline flag anchor at boundaries and `.` won't cross unless Singleline * is set. * * Matches never cross OOXML package parts, container boundaries (body → * table cell), or non-paragraph siblings (a table between two paragraphs * breaks the run). Returned superset of {@link grep}: single-block matches * still appear with one slice. Filter `slices.length > 1` for cross-block only. * * @see docs/architecture/docx_mutation_api.md#grepcrossblock */ grepCrossBlock(pattern: string, options?: GrepOptions): CrossBlockMatch[]; /** * Finds every literal occurrence of `find` in the anchor's flat text and * replaces it with `replace`, preserving the surrounding run formatting that * the match didn't touch. Returns one `EditResult` per attempted match. * * Run-formatting contract: the replacement text inherits the formatting of * the FIRST run the match spanned. Middle/trailing runs keep their `w:rPr` * but lose the slice of text the match consumed. * * @see docs/architecture/docx_mutation_api.md#replacetextrange */ replaceTextRange(anchorId: string, find: string, replace: string, options?: ReplaceOptions): EditResult[]; /** * Replaces a specific Grep match in place — addresses the exact span by * `enclosingAnchor.id` + `span.{start,length}`, so identical needles in the * same paragraph (the template-fill case where five `[___]` placeholders * each get a different value) don't collide. */ replaceMatch(match: TextMatch, replace: string): EditResult; /** * Helper for {@link fillPlaceholders} `coalesceWhitespaceAroundEmptyFill` path — * mirrors the .NET `ReplaceMatchCoalescingNeighbors` rules. Inspects the chars * immediately surrounding the match via `match.contextBefore` / `contextAfter` * (so the option requires `contextChars >= 1`, the default) and expands the * deletion span to absorb whitespace / leading-space-before-punctuation / * matched-brackets where the patterns match. Falls back to literal-delete * when no neighbor pattern applies. * * Note: with `boundary: ContextBoundary.Bracket`, neighbor brackets are not * captured in context, so the bracket-coalesce rule won't fire on the JS side. * The .NET implementation reads flat text directly and handles that case; * callers who care should leave `boundary` at the default `Char`. */ private replaceMatchCoalescingNeighbors; /** * Replace the bracketed portion of a `TextMatch` with `newInner`, preserving any * prefix or suffix outside the brackets. Designed for `findPlaceholders` matches * like `$[___]` where the regex `\$?\[…\]` captures a leading `$`: * `replaceInner(match, "0.20")` yields `$0.20`, not `0.20`. * * Returns `MalformedMarkdown` if the match text does not contain balanced brackets. */ replaceInner(match: TextMatch, newInner: string): EditResult; /** * Picker-driven template fill. For every placeholder matching `options.kinds`, * calls `picker`; if the picker returns a non-null string, the placeholder is * replaced (with optional `$`-prefix preservation). Iterates until no more * placeholders match (or `maxPasses` is reached, or a pass makes zero changes) * — handles nested brackets that surface only after the inner ones are stripped. * * The TypeScript implementation mirrors the .NET `DocxSession.FillPlaceholders` * exactly. * * The picker is invoked synchronously by this loop on the JS side (it does * NOT run inside the WASM module). Async pickers are not supported: returning * a `Promise` will cause a `TypeError` at runtime inside the `$`-prefix * preservation branch (`Promise.startsWith is not a function`). For async * data, pre-build a lookup map before calling and have the picker read from * it synchronously. */ fillPlaceholders(picker: (p: TemplatePlaceholder) => string | null | undefined, options?: FillOptions): BulkEditResult; /** * Enumerate template placeholders in the document. Thin classifier over * {@link grep}: distinguishes `[___]` value blanks (`blank_fill`), * `[bracketed alternative clauses]` (`alternative_clause`), and * `[insert X]` / `[*italic hint*]` instructions (`instruction`). * * Combine kinds with bitwise OR: `PlaceholderKinds.BlankFill | PlaceholderKinds.Instruction`. * Default is `PlaceholderKinds.All`; default scope is body only (1). * * @see docs/architecture/docx_mutation_api.md#findplaceholders */ findPlaceholders(kinds?: number, scope?: number, contextChars?: number, boundary?: number): TemplatePlaceholder[]; /** * Returns a snapshot of edit-state introspection signals — placeholder counts, * underscore-run leftovers, footnote/comment counts. Useful for "am I done?" * verification at the end of an edit pipeline. */ getEditSummary(): EditSummary; /** * Discoverability alias for {@link findPlaceholders}. Same return shape. */ remainingPlaceholders(kinds?: number): TemplatePlaceholder[]; /** * Diff the document's current projection against the projection captured at * session construction time. * * Requires `captureInitialProjection: true` in {@link DocxSessionSettings} * (the default). Throws if not enabled. * * The return type depends on `format`: * - `DiffFormat.Json` (default) — structured anchor-keyed `DiffEntry[]`. * - `DiffFormat.Unified` — `patch(1)`-compatible unified-diff text; * empty string when nothing has changed. * - `DiffFormat.SideBySide` — two-column human-review text * (`diff -y` style). */ getDiff(format?: typeof DiffFormat.Json): DiffEntry[]; getDiff(format: typeof DiffFormat.Unified | typeof DiffFormat.SideBySide): string; /** * Resolves an annotation's range to the block-level markdown anchors covering * it, in document order. The bridge between Docxodus' read-side annotation API * and the write-side session: an agent that wants to edit "the indemnification * clause" looks the annotation up by id and gets the anchors it can hand to * {@link replaceText} / {@link deleteBlock} / {@link raw}. Returns an empty * list when the id is unknown or its bookmark is missing. * * v1 returns the enclosing block anchors — every paragraph/heading/list-item/ * cell/row/table whose subtree overlaps the bookmark range. Filter by * `kind === "p" | "h" | "li"` when you want only text-bearing blocks. * * @see docs/architecture/docx_mutation_api.md#findbyannotation */ findByAnnotation(annotationId: string): AnchorTargetRef[]; /** * Finds every annotation whose `labelId` matches and resolves each of their * ranges. The result is keyed by annotation id so callers can disambiguate * when the same label is applied to multiple regions (three "WARRANTY" * annotations on different paragraphs become three entries). Annotations * whose bookmark resolves to no anchors are omitted from the result. */ findByLabel(labelId: string): Record