@forevka/wordcanvas 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist-lib/builder.js +5 -5
  2. package/dist-lib/{editorApp-Ct42Dpp1.js → editorApp-CRXveGOd.js} +1093 -1088
  3. package/dist-lib/{engine-Co8wGzPh.js → engine-CDHZLDvx.js} +1 -1
  4. package/dist-lib/{equationEditor-ufes6INd.js → equationEditor-C8O4iQY0.js} +5 -5
  5. package/dist-lib/export.js +3 -3
  6. package/dist-lib/generate-toc.js +5 -5
  7. package/dist-lib/import.js +2 -2
  8. package/dist-lib/{inlineExport-DmiMGSna.js → inlineExport-DufSrjh6.js} +1 -1
  9. package/dist-lib/{latex-Bw2eoY11.js → latex-BXHY96IY.js} +2 -2
  10. package/dist-lib/{parse-B7crTYAa.js → parse-BBIeOUPD.js} +3 -3
  11. package/dist-lib/{pipeline-CgTexI-5.js → pipeline-BR9aUw6T.js} +1 -1
  12. package/dist-lib/{pipeline-BxTfzdGp.js → pipeline-CsIYBlrl.js} +6 -6
  13. package/dist-lib/query.js +3 -3
  14. package/dist-lib/recalc-docx.js +2 -2
  15. package/dist-lib/{recalcToc-C0QoMNY9.js → recalcToc-4YmgOJlg.js} +2 -2
  16. package/dist-lib/{styleProps-Dw6DnP6p.js → styleProps-hQ-xcGCW.js} +1 -1
  17. package/dist-lib/{stylesheet-BZQ2DVSJ.js → stylesheet-oa_qg3op.js} +1 -1
  18. package/dist-lib/{symbolPicker-CC3eVi98.js → symbolPicker-BXTEUH3F.js} +1 -1
  19. package/dist-lib/{toc-BIVKqYZ1.js → toc-C4oeGLWP.js} +105 -94
  20. package/dist-lib/{webmcp-CnuYu6Wt.js → webmcp-CC54nQHq.js} +8 -8
  21. package/dist-lib/{wordcanvas-B4jNWfw7.js → wordcanvas-mL3V3lME.js} +3 -3
  22. package/dist-lib/wordcanvas.js +1 -1
  23. package/dist-node/{chunk-XHBY3PPO.js → chunk-2BASYQX6.js} +2 -2
  24. package/dist-node/{chunk-YOBNPVKS.js → chunk-Q2PZZE6W.js} +1 -1
  25. package/dist-node/chunk-Q2PZZE6W.js.map +7 -0
  26. package/dist-node/{chunk-RH7KMFDB.js → chunk-RLIU3RM7.js} +2 -2
  27. package/dist-node/{chunk-WSLT7ILY.js → chunk-VGI576RA.js} +2 -2
  28. package/dist-node/{engine-WTNLTPJA.js → engine-JO72FQWP.js} +3 -3
  29. package/dist-node/export.js +4 -4
  30. package/dist-node/generate-toc.js +4 -4
  31. package/dist-node/import.js +2 -2
  32. package/dist-node/recalc-docx.js +2 -2
  33. package/package.json +1 -1
  34. package/dist-node/chunk-YOBNPVKS.js.map +0 -7
  35. /package/dist-node/{chunk-XHBY3PPO.js.map → chunk-2BASYQX6.js.map} +0 -0
  36. /package/dist-node/{chunk-RH7KMFDB.js.map → chunk-RLIU3RM7.js.map} +0 -0
  37. /package/dist-node/{chunk-WSLT7ILY.js.map → chunk-VGI576RA.js.map} +0 -0
  38. /package/dist-node/{engine-WTNLTPJA.js.map → engine-JO72FQWP.js.map} +0 -0
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../shared/src/model/document.ts", "../../shared/src/model/text.ts", "../../shared/src/model/lists.ts", "../../shared/src/model/sdt.ts", "../../shared/src/model/defaults.ts", "../../shared/src/model/ops.ts", "../../shared/src/model/sections.ts", "../../shared/src/ids.ts", "../../shared/src/toc.ts", "../../shared/src/model/stylesheet.ts", "../../shared/src/model/math.ts", "../../shared/src/model/highlight.ts", "../../shared/src/fields.ts", "../../shared/src/fieldEval.ts"],
4
- "sourcesContent": ["// Layer 1: Document model \u2014 single source of truth. Pure data, no DOM/canvas imports.\r\n\r\nimport type { DocPosition } from \"./position\";\r\nimport type { MathEquation } from \"./math\";\r\n\r\n/** Underline line styles the model can paint + round-trip (OOXML w:u/@w:val).\r\n * Word defines more (and \"heavy\" weights); the importer folds those onto the\r\n * nearest of these, so paint/export stay closed over a small set. Absent\r\n * `underlineStyle` \u21D2 \"single\" (a plain solid line \u2014 the historical behavior). */\r\nexport type UnderlineStyle = \"single\" | \"double\" | \"thick\" | \"dotted\" | \"dash\" | \"dotDash\" | \"dotDotDash\" | \"wave\";\r\n\r\n/** Emphasis-mark style drawn above (or below, for `underDot`) each character\r\n * (OOXML w:em). Round-tripped; painting is optional (degrades to no mark). */\r\nexport type EmphasisMark = \"dot\" | \"comma\" | \"circle\" | \"underDot\";\r\n\r\n/** Proofing / typographic language tags (OOXML w:lang). Round-trip only \u2014 no\r\n * layout or paint effect in our model; in Word these drive spell-check, the\r\n * hyphenation dictionary, and CJK font resolution. Each field is a BCP-47 tag\r\n * (e.g. \"en-US\"); absent fields are omitted from the w:lang element on export.\r\n * Appears on nearly every run and in docDefaults/styles in real documents, so\r\n * preserving it keeps a Word \u2192 WordCanvas \u2192 Word round-trip from stripping the\r\n * document's language tags. */\r\nexport interface RunLang {\r\n /** w:lang/@w:val \u2014 Latin/Western language. */\r\n val?: string;\r\n /** w:lang/@w:eastAsia \u2014 East-Asian language. */\r\n eastAsia?: string;\r\n /** w:lang/@w:bidi \u2014 complex-script (bidi) language. */\r\n bidi?: string;\r\n}\r\n\r\nexport interface CharStyle {\r\n fontFamily: string;\r\n /** OOXML `w:rFonts/@w:cs` \u2014 complex-script (bidi) typeface, used by Word for\r\n * runs in complex scripts (Arabic, Hebrew, \u2026). Preserved through the `.docx`\r\n * round-trip; layout/paint use `fontFamily` (the ascii/hAnsi slot). A CSS font\r\n * stack like `fontFamily`. `| undefined` so a patch can strip it. */\r\n fontFamilyComplexScript?: string | undefined;\r\n /** OOXML `w:rFonts/@w:eastAsia` \u2014 East-Asian (CJK) typeface. Preserved through\r\n * the `.docx` round-trip; layout/paint use `fontFamily`. A CSS font stack like\r\n * `fontFamily`. `| undefined` so a patch can strip it. */\r\n fontFamilyEastAsia?: string | undefined;\r\n /** Proofing/typographic language (OOXML w:lang \u2014 @w:val/@w:eastAsia/@w:bidi).\r\n * Round-trip only; no layout/paint effect. Absent = inherit. `| undefined` so a\r\n * patch can strip it. See {@link RunLang}. */\r\n lang?: RunLang | undefined;\r\n fontSizePx: number;\r\n bold: boolean;\r\n italic: boolean;\r\n underline: boolean;\r\n /** Underline line style (OOXML w:u/@w:val) \u2014 only meaningful when `underline`\r\n * is true; absent \u21D2 \"single\". A run with `underline:false` ignores it. */\r\n underlineStyle?: UnderlineStyle | undefined;\r\n /** Explicit underline color (CSS hex, e.g. \"#ff0000\"), incl. theme colors\r\n * resolved at import. Absent \u21D2 the underline paints in the run's text color\r\n * (Word's \"auto\"). Only meaningful when `underline` is true. */\r\n underlineColor?: string | undefined;\r\n strikethrough: boolean;\r\n color: string;\r\n /** Hidden text (OOXML w:vanish). Preserved through round-trips but NEVER laid\r\n * out, painted, or reachable by the caret/selection \u2014 and protected from\r\n * deletion (it survives Select-All \u2192 Delete). Inert metadata, e.g. the\r\n * bookmark-anchor paragraphs generated reports hide. */\r\n hidden?: boolean;\r\n letterSpacingPx?: number;\r\n /** Background highlight (Word's text highlight). `| undefined` so patches can remove. */\r\n highlightColor?: string | undefined;\r\n /** Explicit \"no highlight\" override (OOXML `<w:highlight w:val=\"none\"/>`): the run's\r\n * character style carries a highlight and this run CLEARS it (issue #155). Distinct\r\n * from absent `highlightColor` (never set / inherit) so the clear re-emits `w:val=\"none\"`\r\n * on export instead of the style's highlight returning. Ignored when `highlightColor`\r\n * is set. */\r\n highlightCleared?: boolean | undefined;\r\n /** Sub/superscript: measured at 0.65\u00D7 size, baseline-shifted at paint time. */\r\n verticalAlign?: \"sub\" | \"super\" | undefined;\r\n /** Hyperlink target; linked runs paint blue+underlined, Ctrl+click opens. */\r\n link?: string | undefined;\r\n /** Footnote reference: this run IS the marker (its text is the note number,\r\n * kept in sync by the insert command's renumber pass; typically also\r\n * verticalAlign 'super'). Points into Document.footnotes. */\r\n footnoteRef?: string | undefined;\r\n /** Endnote reference: like `footnoteRef`, but the note body lives in\r\n * Document.endnotes and lays out at the END of the document (not the page\r\n * bottom). The run's text is the note number; typically verticalAlign 'super'. */\r\n endnoteRef?: string | undefined;\r\n /** Content-control membership (OOXML w:sdt) as an ORDERED ANCESTRY PATH,\r\n * outermost\u2192innermost. Contiguous runs sharing the same path form one inline\r\n * control; runs sharing a path PREFIX are nested inside the same outer\r\n * control(s). Properties for every id on the path live in `Document.sdts`.\r\n * This is the INLINE chain only \u2014 block-level controls put their ids on\r\n * `Block.sdtPath`; a run's full enclosing chain is `block.sdtPath ++ sdtPath`.\r\n * Mirrors `fieldId`/`Block.fieldId`; an invisible flat marker \u2014 no layout\r\n * effect. Treat the array as IMMUTABLE (always replace, never mutate in\r\n * place \u2014 shallow `{...style}` copies alias it). `| undefined` (never `[]`)\r\n * so removing the last control strips the marker. */\r\n sdtPath?: string[] | undefined;\r\n /** Inline-field membership (OOXML complex field): contiguous runs sharing a\r\n * fieldId are one inline field's RESULT (e.g. PAGE, DATE, IF); the definition\r\n * lives in `Document.fields`. The marker is what distinguishes a real field\r\n * result from literal text (e.g. a `{page}` token a user merely typed).\r\n * `| undefined` so removing a field can strip the marker. Mirrors `sdtPath`;\r\n * block-level fields use `Block.fieldId` instead. */\r\n fieldId?: string | undefined;\r\n /** Character-style reference (OOXML w:rStyle \u2192 a type===\"character\" NamedStyle).\r\n * REFERENCE ONLY: the concrete formatting still lives baked on the run (the\r\n * model is concrete, not a live cascade), so layout/paint ignore this \u2014 it\r\n * exists for docx round-trip and for \"what style is this run?\" UI. `| undefined`\r\n * so removing a character style can strip the marker. */\r\n charStyleId?: string | undefined;\r\n /** Inline equation payload (OOXML inline `m:oMath`). When present, this run is\r\n * a single OBJECT REPLACEMENT CHARACTER (U+FFFC) in `text` that renders as the\r\n * laid-out MathML \u2014 an inline replaced element. The AST travels on the run\r\n * (not a registry) so the layout prepare-cache stays self-contained; `styleEq`\r\n * compares it by reference so equation runs never merge with their neighbours.\r\n * Block (display) equations use `EquationBlock` instead. `| undefined` so a\r\n * patch can strip it. */\r\n equation?: MathEquation | undefined;\r\n /** Explicit right-to-left run (OOXML w:rPr/w:rtl). Forces this run's text to a\r\n * bidi-RTL embedding regardless of its characters, mirroring Word's per-run\r\n * \"rtl\" toggle. Absent = resolve direction from the characters' Unicode bidi\r\n * classes under the paragraph's base direction (the common case \u2014 Arabic/Hebrew\r\n * text reorders correctly without this flag). `| undefined` so a patch can\r\n * remove it. */\r\n rtl?: boolean | undefined;\r\n /** Double strikethrough (OOXML w:dstrike) \u2014 two parallel lines through the run,\r\n * independent of the single `strikethrough`. Painted (canvas + PDF). `| undefined`\r\n * so a patch can remove it. */\r\n doubleStrikethrough?: boolean | undefined;\r\n /** Baseline raise/lower in px (OOXML w:position, signed): POSITIVE raises the run\r\n * above the baseline, negative lowers it. Distinct from `verticalAlign` (sub/super\r\n * also shrinks the font) \u2014 `positionPx` only shifts, keeping the font size. Paint\r\n * adds it on top of any sub/super shift. Absent = on the baseline. */\r\n positionPx?: number | undefined;\r\n /** Kerning threshold in px (OOXML w:kern/@w:val, a min font size): kerning applies\r\n * at/above this size. Preserved for round-trip; no separate paint (the platform\r\n * text shaper already kerns). Absent = no explicit threshold. */\r\n kerningMinPx?: number | undefined;\r\n /** Character width scaling as a percentage (OOXML w:w/@w:val, 1..600; 100 = normal):\r\n * horizontally stretches (>100) or condenses (<100) each glyph WITHOUT changing its\r\n * height. Affects measurement and paint. Absent = 100 (no scaling). */\r\n widthScalePct?: number | undefined;\r\n /** Emphasis marks over each character (OOXML w:em). Round-tripped; paint is optional\r\n * (degrades to no mark). Absent = none. */\r\n emphasisMark?: EmphasisMark | undefined;\r\n /** Outlined text effect (OOXML w:outline) \u2014 glyphs drawn as outlines. Round-tripped;\r\n * degrades to normal text in paint. `| undefined` so a patch can remove it. */\r\n outline?: boolean | undefined;\r\n /** Drop-shadow text effect (OOXML w:shadow). Round-tripped; degrades to normal text. */\r\n shadow?: boolean | undefined;\r\n /** Embossed text effect (OOXML w:emboss). Round-tripped; degrades to normal text. */\r\n emboss?: boolean | undefined;\r\n /** Imprinted/engraved text effect (OOXML w:imprint). Round-tripped; degrades to\r\n * normal text. */\r\n imprint?: boolean | undefined;\r\n /** Run border \u2014 a box drawn around the run (OOXML w:bdr). Reuses the table\r\n * `CellBorder` value type (color + width + line style). Round-tripped; paint is\r\n * optional (degrades to no box). `| undefined` so a patch can remove it. */\r\n runBorder?: CellBorder | undefined;\r\n /** Fit-text target width in px (OOXML w:fitText/@w:val): Word compresses/expands the\r\n * run to occupy exactly this width. Round-tripped; degrades to natural width in\r\n * layout/paint. Absent = natural width. */\r\n fitTextPx?: number | undefined;\r\n /** All-caps display (OOXML w:caps). The model text stays as authored; the layout\r\n * and painters render every letter UPPERCASED (offset-transparent \u2014 caret and\r\n * measurement see the transformed glyphs). Common on headings/styles. `| undefined`\r\n * so a patch can clear it. Mutually reinforced by `smallCaps` (which also\r\n * uppercases, but shrinks the originally-lowercase letters). */\r\n caps?: boolean | undefined;\r\n /** Small-capitals display (OOXML w:smallCaps). Letters render UPPERCASED, but the\r\n * ones that were lowercase in the source are drawn at a reduced size (Word's small\r\n * caps). Like `caps`, the model text is untouched and the transform is\r\n * offset-transparent. Takes precedence over `caps` when both are set. `| undefined`\r\n * so a patch can clear it. */\r\n smallCaps?: boolean | undefined;\r\n /** Snap this run's glyph advances to the section's document grid (OOXML\r\n * run-level w:snapToGrid) \u2014 per-character grid adherence (inter-character\r\n * layout), an East-Asian typesetting aid. The paragraph-level\r\n * `ParaStyle.snapToGrid` instead governs inter-line layout. Word's default is\r\n * ON; an explicit `false` (w:val=\"0\") opts a run out. Round-trips; no layout\r\n * effect in our grid-less model (and unrelated to the editor's canvas \"snap to\r\n * grid\" view aid). Absent = default (ON). `| undefined` so a patch can strip it. */\r\n snapToGrid?: boolean | undefined;\r\n /** Symbol-font glyph (OOXML w:sym): `font` is the symbol font (e.g. \"Wingdings\")\r\n * and `char` is the UPPER-CASE hex code point Word stores (usually a Private-Use\r\n * value like \"F0E0\"). The run's `text` is the decoded character (so layout/paint\r\n * measure and draw it in `font`); on export the run re-emits w:sym instead of\r\n * w:t. `fontFamily` is set to `font` on import so the glyph paints correctly even\r\n * without consulting this marker \u2014 which exists for a faithful docx round-trip.\r\n * `| undefined` so a patch can strip it. */\r\n symbol?: { font: string; char: string } | undefined;\r\n}\r\n\r\n/** Structured document tag (Word content control) properties \u2014 a direct\r\n * mirror of w:sdtPr so the importer can map losslessly. */\r\nexport type SdtType = \"richText\" | \"plainText\" | \"checkbox\" | \"dropDown\" | \"comboBox\" | \"date\";\r\n\r\nexport interface SdtProps {\r\n type: SdtType;\r\n /** Title shown on the control's tab (w:alias). */\r\n alias?: string;\r\n /** Machine-readable tag (w:tag). */\r\n tag?: string;\r\n /** Content is currently the gray placeholder \u2014 typing replaces it whole. */\r\n placeholder?: boolean;\r\n /** Dropdown / combo box choices (w:listItem). */\r\n listItems?: { display: string; value: string }[];\r\n /** Date display format (w:date/@w:fullDate format, e.g. \"M/d/yyyy\"). */\r\n dateFormat?: string;\r\n /** Checkbox state (w14:checkbox). */\r\n checked?: boolean;\r\n /** w:lock=\"sdtContentLocked\" \u2014 contents cannot be edited. */\r\n lockContent?: boolean;\r\n /** w:lock=\"sdtLocked\" \u2014 the control cannot be deleted. */\r\n lockControl?: boolean;\r\n}\r\n\r\nexport interface ParaStyle {\r\n align: \"left\" | \"center\" | \"right\" | \"justify\";\r\n /** Base writing direction (OOXML w:pPr/w:bidi). \"rtl\" lays the paragraph out\r\n * right-to-left: the bidi base level is RTL (so a leading Latin word still sits\r\n * on the right), `align: \"left\"|\"right\"` are interpreted as START/END (mirrored),\r\n * and left/right indents swap to start/end. Absent = \"ltr\" (the default \u2014 every\r\n * existing document is unaffected). Resolved through the Stylesheet cascade like\r\n * `align`. */\r\n direction?: \"ltr\" | \"rtl\";\r\n lineHeight: number; // multiplier (used when lineRule is absent/\"auto\")\r\n /** Fixed line-spacing rule (docx w:spacing/@w:lineRule). Absent = `lineHeight`\r\n * is a multiplier of the single-line height (\"auto\" \u2014 the default every\r\n * existing document keeps). \"exact\" = the line is EXACTLY `lineHeightPx` tall,\r\n * clipping taller content; \"atLeast\" = at least `lineHeightPx`, growing for a\r\n * taller line. When set, `lineHeightPx` carries the fixed height and\r\n * `lineHeight` is ignored. */\r\n lineRule?: \"exact\" | \"atLeast\";\r\n /** Fixed line height in px \u2014 meaningful only alongside `lineRule`. */\r\n lineHeightPx?: number;\r\n spaceBeforePx: number;\r\n spaceAfterPx: number;\r\n /** Word's \"automatic\" paragraph spacing (OOXML w:beforeAutospacing / w:afterAutospacing,\r\n * the HTML-<p> behavior \u2014 issue #160). When set, `spaceBeforePx`/`spaceAfterPx` hold the\r\n * BAKED auto value (a fixed approximation; Word's per-context auto-value + HTML-style\r\n * margin collapsing are not reproduced), and these flags exist so export re-emits the\r\n * autospacing attributes. Absent = the explicit spacing value is authoritative. */\r\n spaceBeforeAuto?: boolean;\r\n spaceAfterAuto?: boolean;\r\n indentFirstLinePx: number;\r\n indentLeftPx: number;\r\n /** Right-edge indent (docx w:ind/@w:right|@w:end): narrows every line from the\r\n * right margin. Absent = 0. Drives the ruler's right-indent marker. */\r\n indentRightPx?: number;\r\n /** Never leave this block as the last on a page (headings). */\r\n keepWithNext?: boolean;\r\n /** Never split this paragraph across pages/columns (docx w:keepLines) \u2014 it\r\n * moves whole instead; only a paragraph taller than a page still splits. */\r\n keepLinesTogether?: boolean;\r\n /** Suppress before/after spacing between this paragraph and an adjacent\r\n * paragraph of the SAME style (docx w:contextualSpacing) \u2014 Word's default for\r\n * list styles, so same-style runs (list items, verse) sit tight while the\r\n * run's outer edges keep their spacing. Resolved through the Stylesheet\r\n * cascade like `align`. Absent = normal spacing on every edge. */\r\n contextualSpacing?: boolean;\r\n /** This paragraph starts a new page (Ctrl+Enter; docx w:pageBreakBefore). */\r\n pageBreakBefore?: boolean;\r\n /** List membership (docx w:numPr): definition ref + level 0..8.\r\n * Explicitly `| undefined` so a setParaStyle patch can REMOVE it. */\r\n list?: { listId: string; level: number } | undefined;\r\n /** Explicit \"remove numbering\" override (OOXML `<w:numPr><w:numId w:val=\"0\"/>`):\r\n * the paragraph's named style carries a list and this paragraph OPTS OUT of it\r\n * (issue #152). Distinct from absent `list` (never in a list / inherit): a\r\n * cleared paragraph re-emits `w:numId=\"0\"` so the opt-out survives a round-trip\r\n * instead of the style's list silently returning. Ignored when `list` is set. */\r\n listCleared?: boolean | undefined;\r\n namedStyle?: string; // resolved through Stylesheet cascade, e.g. \"heading1\"\r\n /** Effective outline level (docx w:outlineLvl), 0-8 = TOC levels 1-9 \u2014 resolved\r\n * through the paragraph-style cascade (heading styles carry it). Drives TOC\r\n * generation under the field's `\\u` switch and robust heading detection when\r\n * styles use opaque ids. Absent = body text (no outline level). */\r\n outlineLevel?: number;\r\n /** This paragraph ENDS a section (OOXML puts sectPr ON a paragraph). `props`\r\n * describes the section being terminated; blocks after it belong to the next\r\n * break's section, or to `Document.section` (the final/body sectPr). `type`\r\n * is the OOXML w:sectPr/w:type of the FOLLOWING section: \"nextPage\" starts it\r\n * on the next page (the default); \"evenPage\"/\"oddPage\" additionally force its\r\n * first page onto an even/odd page number, inserting a blank filler page when\r\n * the running page count has the wrong parity.\r\n * `| undefined` so a setParaStyle patch can remove the break. */\r\n sectionBreak?: { type: SectionBreakType; props: SectionPatch } | undefined;\r\n /** Ctrl+Shift+Enter: this paragraph starts the next newspaper column\r\n * (no-op in single-column sections, like Word). */\r\n columnBreakBefore?: boolean;\r\n /** Table-of-contents entry pointing at a heading paragraph. The page number\r\n * is PAINT-ONLY (engine post-pass) so it can never go stale; Ctrl+click\r\n * jumps to the target. `| undefined` so regeneration can clear it. */\r\n tocEntry?: { targetId: string; level: number } | undefined;\r\n /** Explicit tab stops (docx w:tabs), sorted by `posPx`. A `\\t` in run text\r\n * advances to the next stop past the current x; past the last explicit stop\r\n * (or with none) the layout falls back to a fixed default interval. */\r\n tabStops?: TabStop[];\r\n /** Paragraph borders (OOXML w:pBdr) \u2014 a box drawn around the paragraph. Each\r\n * edge reuses the table `CellBorder` value type (color + width + line style);\r\n * `between` is the rule Word draws between two consecutive paragraphs that\r\n * share identical borders. Absent edges draw no line. `| undefined` so a\r\n * setParaStyle patch can remove the borders. */\r\n borders?: ParaBorders | undefined;\r\n /** Explicit \"no border box\" override (OOXML empty `<w:pBdr/>` / all-nil edges):\r\n * the paragraph's named style carries a border box and this paragraph CLEARS it\r\n * (issue #153). Distinct from absent `borders` (never set / inherit) so the clear\r\n * re-emits an empty w:pBdr on export instead of the style's box returning. Draws\r\n * nothing (like absent); ignored when `borders` has edges. Mirrors the cell/table\r\n * \"explicit no borders\" handling (bordersSpecified). */\r\n bordersCleared?: boolean | undefined;\r\n /** Paragraph shading fill (OOXML paragraph-level w:shd), a CSS color painted\r\n * behind the paragraph's box. Mirrors the table cell `shading` value type.\r\n * Absent = no fill. `| undefined` so a patch can remove it. */\r\n shading?: string | undefined;\r\n /** Explicit \"no shading\" override (OOXML `<w:shd w:val=\"clear\" w:fill=\"auto\"/>`\r\n * \u2014 Word's \"Shading \u2192 No Color\"): the paragraph's named style carries a fill\r\n * and this paragraph CLEARS it (issue #147). Distinct from absent `shading`\r\n * (never set / inherit): a cleared paragraph re-emits the empty w:shd so the\r\n * clear survives a round-trip instead of the style's fill silently returning.\r\n * Painted as no fill (like absent `shading`); ignored when `shading` is set (a\r\n * concrete fill wins). `| undefined` so a patch can drop the marker. */\r\n shadingCleared?: boolean | undefined;\r\n /** Widow/orphan control (OOXML w:widowControl). Word's default is ON \u2014 keep\r\n * \u22652 lines together at a page boundary rather than stranding a lone first/last\r\n * line. Explicit `false` (w:widowControl w:val=\"0\") lets a single line break\r\n * away. Absent = default (ON). Honored by the pagination engine. */\r\n widowControl?: boolean;\r\n /** Exclude this paragraph from line numbering (OOXML w:suppressLineNumbers).\r\n * Round-trips; no layout effect here (line numbers aren't rendered). Absent = off. */\r\n suppressLineNumbers?: boolean;\r\n /** Vertical alignment of the glyphs on each line within its line box (OOXML\r\n * w:textAlignment): \"top\"/\"center\"/\"bottom\" hug the respective edge of a tall\r\n * line, \"baseline\" (Word's default, == absent) rides the shared baseline. Most\r\n * visible with extra line spacing or mixed font sizes. Honored by the engine. */\r\n textAlignment?: \"top\" | \"center\" | \"bottom\" | \"baseline\";\r\n /** Symmetric (mirrored) indents for facing-page layouts (OOXML w:mirrorIndents):\r\n * start/end indents swap on verso pages. Round-trips; visible only under mirrored\r\n * section margins. Absent = off. */\r\n mirrorIndents?: boolean;\r\n /** Automatically adjust the right indent to the document grid (OOXML\r\n * w:adjustRightInd). Round-trips; no layout effect in our grid-less model.\r\n * Absent = off. */\r\n adjustRightInd?: boolean;\r\n /** Snap this paragraph's LINES to the section's document grid (OOXML\r\n * paragraph-level w:snapToGrid) \u2014 whether the paragraph adheres to the grid's\r\n * lines-per-page pitch (inter-line layout), an East-Asian typesetting aid. The\r\n * run-level `CharStyle.snapToGrid` instead governs per-character advances\r\n * (inter-character layout). Word's default is ON; explicit `false` (w:val=\"0\")\r\n * opts the paragraph out. Round-trips; no layout effect in our grid-less model\r\n * (and unrelated to the editor's canvas \"snap to grid\" view aid). Absent =\r\n * default (ON). */\r\n snapToGrid?: boolean;\r\n /** Disable automatic hyphenation for this paragraph (OOXML w:suppressAutoHyphens).\r\n * Round-trips; no layout effect here (we don't auto-hyphenate). Absent = off. */\r\n suppressAutoHyphens?: boolean;\r\n /** East-Asian line-breaking (kinsoku shori) on/off TOGGLE (OOXML w:kinsoku).\r\n * Word's default is ON; explicit `false` (w:val=\"0\") disables the rules that\r\n * keep forbidden characters (e.g. closing punctuation) off line edges. The\r\n * engine performs kinsoku breaking regardless \u2014 this flag only round-trips the\r\n * explicit override. Absent = default (ON). */\r\n kinsoku?: boolean;\r\n /** Allow punctuation to hang past the right margin (OOXML w:overflowPunct), an\r\n * East-Asian typesetting feature. Round-trips; no layout effect here. Word's\r\n * default is ON; absent = default. */\r\n overflowPunct?: boolean;\r\n /** Allow a Latin word to break at ANY character rather than wrapping whole\r\n * (OOXML w:wordWrap), an East-Asian line-breaking option. Word's default is ON;\r\n * explicit `false` (w:val=\"0\") permits mid-word breaks. Round-trips; no layout\r\n * effect here. Absent = default (ON). */\r\n wordWrap?: boolean;\r\n /** Compress punctuation that falls at the start of a line (OOXML w:topLinePunct),\r\n * an East-Asian typesetting feature. Round-trips; no layout effect here.\r\n * Absent = off. */\r\n topLinePunct?: boolean;\r\n /** Automatically add spacing between East-Asian and Latin text (OOXML\r\n * w:autoSpaceDE). Word's default is ON; explicit `false` (w:val=\"0\") disables it.\r\n * Round-trips; no layout effect here. Absent = default (ON). */\r\n autoSpaceDE?: boolean;\r\n /** Automatically add spacing between East-Asian text and numbers (OOXML\r\n * w:autoSpaceDN). Word's default is ON; explicit `false` (w:val=\"0\") disables it.\r\n * Round-trips; no layout effect here. Absent = default (ON). */\r\n autoSpaceDN?: boolean;\r\n}\r\n\r\n/** Resolved per-edge paragraph borders (OOXML w:pBdr). Reuses the table\r\n * `CellBorder` value type per edge. An omitted edge draws no line. `between`\r\n * is the inter-paragraph rule (drawn like an inner bottom edge). */\r\nexport interface ParaBorders {\r\n top?: CellBorder;\r\n right?: CellBorder;\r\n bottom?: CellBorder;\r\n left?: CellBorder;\r\n between?: CellBorder;\r\n}\r\n\r\n/** Baked approximation of Word's \"automatic\" paragraph spacing (OOXML\r\n * w:beforeAutospacing/@w:afterAutospacing \u2014 issue #160): the before/after px used\r\n * when autospacing is on. Word computes it per context and collapses HTML-style;\r\n * we bake a flat ~1-line value so autospaced paragraphs don't collapse to 0. Shared\r\n * by the importer (bake) and the builder (author) so they stay in agreement. */\r\nexport const AUTO_PARA_SPACING_PX = 14;\r\n\r\nexport type TabAlign = \"left\" | \"center\" | \"right\" | \"decimal\";\r\nexport type TabLeader = \"none\" | \"dot\" | \"dash\" | \"underscore\";\r\n\r\nexport interface TabStop {\r\n /** Position from the start content edge, after the paragraph's start indent \u2014\r\n * the LEFT edge for an LTR paragraph, the RIGHT edge for an RTL (`w:bidi`) one. */\r\n posPx: number;\r\n /** Text alignment at the stop (default \"left\"). */\r\n align?: TabAlign;\r\n /** Filler drawn from the previous content to the stop (default \"none\"). */\r\n leader?: TabLeader;\r\n /** Explicit clear (OOXML `<w:tab w:val=\"clear\" w:pos=\"\u2026\"/>`) \u2014 REMOVES an inherited\r\n * tab stop at this position (issue #154). Not a real stop: the layout ignores it,\r\n * but export re-emits the clear so a style-provided tab the paragraph removed\r\n * doesn't reappear on re-import. When set, `align`/`leader` are irrelevant. */\r\n cleared?: boolean;\r\n}\r\n\r\n/** Style-homogeneous span of text. Adjacent equal-styled runs are merged on every edit. */\r\nexport interface Run {\r\n text: string;\r\n style: CharStyle;\r\n}\r\n\r\nexport interface Paragraph {\r\n kind: \"paragraph\";\r\n id: string; // stable \u2014 layout cache & undo key on this, never on index\r\n revision: number; // bumped by ops; invalidates the pretext prepare-cache\r\n runs: Run[];\r\n style: ParaStyle;\r\n /** Membership in a generic field's result region (see Document.fields). A\r\n * contiguous run of blocks sharing a fieldId IS the field's result; export\r\n * wraps them in the field's begin/instrText/separate\u2026end. Absent = ordinary\r\n * content. Like `tocEntry`/run `sdtPath`, an invisible flat marker \u2014 no layout\r\n * effect. `| undefined` so an op can clear it. */\r\n fieldId?: string | undefined;\r\n /** Block-level content-control ancestry (OOXML block-level w:sdt), outer\u2192inner.\r\n * A contiguous run of blocks sharing this path is wrapped by the control(s);\r\n * this represents controls around whole paragraphs/tables (incl. run-less\r\n * blocks). Inline controls inside the block put their ids on the runs'\r\n * `CharStyle.sdtPath` instead. Mirrors `fieldId`; invisible marker. Treat as\r\n * IMMUTABLE. `| undefined` (never `[]`). */\r\n sdtPath?: string[] | undefined;\r\n}\r\n\r\nexport interface ImageBlock {\r\n kind: \"image\";\r\n id: string;\r\n revision: number;\r\n /** Runtime image URL (a session-local `blob:` or an inline `data:` URL). NOT\r\n * portable \u2014 a `blob:` URL dies with the tab and means nothing to the server.\r\n * Persistence/replication addresses the bytes by `mediaId` instead and\r\n * rehydrates `src` on load (see shared/persist/media + serialize). */\r\n src: string;\r\n /** Content address of the image bytes (`sha256(bytes)` hex). The portable,\r\n * stable handle stored in snapshots and ops; absent only for legacy images\r\n * not yet registered in a MediaStore (then `src` may carry an inline data:\r\n * URL as a fallback). */\r\n mediaId?: string;\r\n /** Linked (\"Link to File\") image: the bytes live OUTSIDE the document, at this\r\n * URL (OOXML `a:blip r:link` \u2192 a `TargetMode=\"External\"` relationship). Unlike\r\n * an embedded image there is no `mediaId` and no packed bytes; `src` is set to\r\n * this URL for display, but `externalSrc` is the portable handle that survives\r\n * serialize (which blanks the runtime `src`). Export re-emits it as `r:link`. */\r\n externalSrc?: string;\r\n widthPx: number;\r\n heightPx: number;\r\n /** Crop insets (OOXML DrawingML a:srcRect), each a 0..1 fraction of the source\r\n * image trimmed off that edge \u2014 so `widthPx`/`heightPx` describe the displayed\r\n * (cropped) box and the visible source window is\r\n * [left, 1-right] \u00D7 [top, 1-bottom] of the original bytes. Absent = no crop.\r\n * Paint scales+clips to show only the window; export re-emits a:srcRect. */\r\n crop?: { left: number; top: number; right: number; bottom: number };\r\n align: \"left\" | \"center\" | \"right\";\r\n /** 'block' (default): occupies vertical space like a paragraph.\r\n * 'square': floats at the left/right margin (per align) and following text\r\n * flows around it \u2014 pretext's per-line maxWidth makes this affordable. */\r\n wrap?: \"block\" | \"square\";\r\n /** Absolutely-positioned anchored image (DOCX wp:anchor + wp:wrapNone) that\r\n * sits BEHIND (behind=true) or in front of the text. Unlike `wrap`, it does\r\n * NOT occupy vertical flow space and does NOT reflow surrounding text \u2014 it is\r\n * painted at `offset{X,Y}Px` from the `relFrom{H,V}` origin. Mutually\r\n * exclusive with `wrap`; absent = ordinary flow image. */\r\n anchor?: {\r\n behind: boolean;\r\n offsetXPx: number;\r\n offsetYPx: number;\r\n relFromH: \"page\" | \"margin\" | \"column\" | \"leftMargin\" | \"rightMargin\" | \"character\";\r\n relFromV: \"page\" | \"margin\" | \"paragraph\" | \"line\" | \"topMargin\" | \"bottomMargin\";\r\n decorative?: boolean;\r\n /** Stacking order among anchored images in the SAME layer (behind/front):\r\n * higher paints later (on top). Drives \"bring to front\"/\"send to back\";\r\n * maps to/from OOXML wp:anchor @relativeHeight. Default 0. */\r\n z?: number;\r\n };\r\n /** Field result membership \u2014 see Paragraph.fieldId. */\r\n fieldId?: string | undefined;\r\n /** Block-level content-control ancestry \u2014 see Paragraph.sdtPath. */\r\n sdtPath?: string[] | undefined;\r\n}\r\n\r\n/** Cells hold Blocks: paragraphs (first-class editing targets, located through\r\n * model/text.ts), images, and nested tables (rendered; their inner cells are\r\n * read-only \u2014 the paragraph locator goes one level deep). */\r\n/** One resolved cell-edge border. The importer collapses the OOXML cascade\r\n * (table style \u2192 w:tblBorders \u2192 w:tcBorders, plus inside/outside selection)\r\n * down to a concrete per-edge spec, so paint just draws what it's given. */\r\nexport interface CellBorder {\r\n /** CSS color, e.g. \"#000000\". */\r\n color: string;\r\n /** Line width in px (OOXML w:sz is eighths of a point \u2192 px). */\r\n widthPx: number;\r\n /** Default \"single\". \"none\" is expressed by omitting the edge entirely. */\r\n style?: \"single\" | \"double\" | \"dashed\" | \"dotted\";\r\n}\r\n\r\n/** Resolved per-edge borders for a cell. An omitted edge draws no line. */\r\nexport interface CellBorders {\r\n top?: CellBorder;\r\n right?: CellBorder;\r\n bottom?: CellBorder;\r\n left?: CellBorder;\r\n}\r\n\r\n/** Table-level default borders (OOXML w:tblPr/w:tblBorders). Extends the per-cell\r\n * edges with the two INTERIOR edges Word resolves between adjacent cells. The\r\n * importer cascades these onto individual cells for layout/paint; the table keeps\r\n * them so export re-emits the table-level defaults at tblPr level rather than only\r\n * the baked per-cell copies. */\r\nexport interface TableBorders extends CellBorders {\r\n /** Interior horizontal edge between vertically-adjacent cells (w:insideH). */\r\n insideH?: CellBorder;\r\n /** Interior vertical edge between horizontally-adjacent cells (w:insideV). */\r\n insideV?: CellBorder;\r\n}\r\n\r\n/** Inner cell padding in px, resolved from the OOXML cell-margin cascade\r\n * (w:tcMar over the table's w:tblCellMar over Word's defaults). Word's default\r\n * is 0 top/bottom and ~7.2px (108 twips) left/right \u2014 vertical padding is NOT\r\n * symmetric with horizontal, which is why a fixed all-sides pad makes rows too\r\n * tall. Absent = the engine's Word-matching default. */\r\nexport interface CellMargin {\r\n top: number;\r\n right: number;\r\n bottom: number;\r\n left: number;\r\n}\r\n\r\nexport interface TableCell {\r\n id: string;\r\n blocks: Block[];\r\n /** Horizontal merge: this cell covers N columns (default 1). */\r\n colSpan?: number;\r\n /** Vertical merge: this cell covers N rows (default 1). The cell lives in its\r\n * TOP row; the rows it spans into simply omit a cell for that grid column\r\n * (HTML rowspan semantics), so the importer must drop w:vMerge=\"continue\"\r\n * cells and bump this on the \"restart\" cell. */\r\n rowSpan?: number;\r\n /** Resolved background fill (CSS color) from w:shd. Absent = no fill. */\r\n shading?: string;\r\n /** Explicit \"No Color\" override (OOXML `<w:shd w:val=\"clear\" w:fill=\"auto\"/>`):\r\n * the cell clears a fill it would otherwise inherit from the table-level w:shd\r\n * or the table style (issue #150). Distinct from absent `shading` (never set /\r\n * inherit) so the clear re-emits on export instead of the inherited fill\r\n * returning. Painted as no fill; ignored when `shading` is set. A table-style\r\n * (re-)apply replaces it, like any direct cell shading (Word). */\r\n shadingCleared?: boolean;\r\n /** Resolved per-edge borders. Absent = renderer's default light grid (so\r\n * native/unstyled tables keep a visible grid); present = draw exactly these\r\n * edges, where an omitted edge means \"no border on that side\". */\r\n borders?: CellBorders;\r\n /** Resolved inner padding (px) from the w:tcMar/w:tblCellMar cascade. Absent =\r\n * engine default (Word's 0 vertical, ~7.2px horizontal). */\r\n margin?: CellMargin;\r\n /** Preferred cell width (OOXML w:tcW). Only consulted when the table is in an\r\n * autofit mode, where it clamps the cell's content-derived min/max width up to\r\n * this preference (Word semantics). `abs` = px (from dxa); `pct` = px resolved\r\n * per layout from a percentage of the table width. Absent = content-only. */\r\n preferredWidth?: { px: number; type: \"abs\" | \"pct\" };\r\n /** Vertical alignment of the cell's content within its box (OOXML w:tcPr/w:vAlign).\r\n * Absent = \"top\" (the historical default). For \"center\"/\"bottom\" the layout offsets\r\n * the block stack by the slack between content height and the cell's height \u2014 most\r\n * visible in a tall (rowSpan) or fixed-height cell with short content. */\r\n vAlign?: \"top\" | \"center\" | \"bottom\";\r\n /** Text flow direction inside the cell (OOXML w:tcPr/w:textDirection). Absent =\r\n * \"lrTb\" (normal horizontal left-to-right). \"tbRl\"/\"btLr\" are Word's rotated\r\n * (vertical) cell text, common in narrow header columns. Currently preserved\r\n * through import/export round-trips; the layout still flows the text\r\n * horizontally (vertical-text typesetting is out of scope for this pass). */\r\n textDirection?: \"lrTb\" | \"tbRl\" | \"btLr\" | \"lrTbV\" | \"tbRlV\" | \"tbLrV\";\r\n /** Suppress wrapping of the cell's content (OOXML w:tcPr/w:noWrap): in autofit\r\n * the cell prefers to widen rather than wrap. Round-tripped; absent = wrap. */\r\n noWrap?: boolean;\r\n /** Fit the cell's text to its width by inter-character spacing (OOXML\r\n * w:tcPr/w:tcFitText). Round-tripped; absent = off. */\r\n fitText?: boolean;\r\n /** Ignore the cell's end-of-cell mark when computing the minimum row height\r\n * (OOXML w:tcPr/w:hideMark) \u2014 lets a row shrink below one empty text line.\r\n * Round-tripped; absent = off. */\r\n hideMark?: boolean;\r\n}\r\n\r\n/** Row-level properties (OOXML `w:trPr`). Absent = no explicit row formatting. */\r\nexport interface RowProps {\r\n /** Fixed/minimum row height (OOXML `w:trHeight`). `value` is CSS px. `rule`:\r\n * \"atLeast\" \u2014 the row is AT LEAST this tall and grows with its content (the\r\n * common case); \"exact\" \u2014 the row is forced to exactly this height (taller\r\n * content is clipped). The \"auto\" rule (height as a pure hint) round-trips as\r\n * absent. */\r\n height?: { value: number; rule: \"atLeast\" | \"exact\" };\r\n /** Keep the whole row on one page \u2014 never split its cells across a page/column\r\n * break (OOXML `w:cantSplit`). */\r\n cantSplit?: boolean;\r\n /** Repeat this row as a header at the top of every page the table continues onto\r\n * (OOXML `w:tblHeader`). Honored for the LEADING contiguous header rows; a header\r\n * flag on a later row is preserved for round-trip but not repeated. */\r\n repeatHeader?: boolean;\r\n}\r\n\r\nexport interface TableRow {\r\n cells: TableCell[];\r\n /** Row-level properties (`w:trPr`): fixed/min height, cant-split, repeat-header.\r\n * Absent = none. */\r\n props?: RowProps;\r\n}\r\n\r\n/** Which conditional bands of a table style this table activates (OOXML w:tblLook).\r\n * Word's default turns on the header row and row banding. */\r\nexport interface TableCondOverrides {\r\n firstRow?: boolean;\r\n lastRow?: boolean;\r\n firstCol?: boolean;\r\n lastCol?: boolean;\r\n bandRows?: boolean;\r\n bandCols?: boolean;\r\n}\r\n\r\nexport interface TableBlock {\r\n kind: \"table\";\r\n id: string;\r\n revision: number;\r\n rows: TableRow[];\r\n /** Column widths as fractions of the content width (sum = 1). Absent = equal.\r\n * In autofit modes this is the last-known snapshot (used as the export grid\r\n * hint and the value the \"Fixed\" path / column drag edits); layout recomputes\r\n * the painted widths from cell content and does not depend on it. */\r\n colFractions?: number[];\r\n /** Column sizing strategy (OOXML w:tblLayout + w:tblW). Absent = \"fixed\", the\r\n * historical behavior: columns are `colFractions \u00D7 content width`. The autofit\r\n * modes derive column widths from cell content at layout time \u2014\r\n * \"autofitContents\" lets the table shrink below the content width to fit its\r\n * content; \"autofitWindow\" always fills the available width. */\r\n widthMode?: \"fixed\" | \"autofitContents\" | \"autofitWindow\";\r\n /** Preferred TOTAL table width (OOXML w:tblPr/w:tblW), honored in \"fixed\" mode\r\n * only. Absent = span the full content width (the historical behavior). `pct` is\r\n * 0..100 of the content width; `px` is absolute CSS px. Resolved and clamped to\r\n * [floor, contentWidth] at layout time; the autofit modes ignore it. */\r\n preferredWidth?: { type: \"pct\" | \"px\"; value: number };\r\n /** Horizontal alignment of the table within the content width (OOXML w:tblPr/w:jc),\r\n * applied whenever the table is narrower than the band. Absent = \"left\". */\r\n align?: \"left\" | \"center\" | \"right\";\r\n /** Table indent from the leading content edge (OOXML w:tblPr/w:tblInd), in px.\r\n * Shifts the whole table to the right (LTR) like a paragraph's left indent, on\r\n * top of any alignment offset. Absent = 0. */\r\n indentPx?: number;\r\n /** Render the table's columns in right-to-left visual order (OOXML\r\n * w:tblPr/w:bidiVisual): grid column 0 paints at the RIGHT edge and columns run\r\n * leftward. The logical row/cell order is unchanged \u2014 only the visual placement\r\n * mirrors. Absent = left-to-right. */\r\n bidiVisual?: boolean;\r\n /** Floating-table overlap behavior (OOXML w:tblPr/w:tblOverlap): \"never\" forbids\r\n * another floating table from overlapping this one. Preserved through round-trips;\r\n * absent = \"overlap\" (Word's default). */\r\n overlap?: \"never\" | \"overlap\";\r\n /** Table caption / title (OOXML w:tblPr/w:tblCaption) \u2014 accessibility metadata.\r\n * Round-tripped; absent = none. */\r\n caption?: string;\r\n /** Table description / alt text (OOXML w:tblPr/w:tblDescription) \u2014 accessibility\r\n * metadata. Round-tripped; absent = none. */\r\n description?: string;\r\n /** Table-level default borders (OOXML w:tblPr/w:tblBorders), including interior\r\n * edges. The importer cascades these onto each cell so layout/paint stay\r\n * per-cell; kept here so export hoists the table-level defaults back to tblPr\r\n * level instead of relying only on the baked per-cell copies. Absent = none. */\r\n defaultBorders?: TableBorders;\r\n /** Table-level default shading fill (OOXML w:tblPr/w:shd) \u2014 a CSS color applied to\r\n * every cell unless the cell overrides it. Absent = no table-level fill. */\r\n defaultShading?: string;\r\n /** Table-level default cell margins (OOXML w:tblPr/w:tblCellMar), the base each\r\n * cell's own w:tcMar overrides per side. Absent = Word's defaults. */\r\n defaultCellMargin?: CellMargin;\r\n /** Table-style reference (OOXML w:tblStyle \u2192 Document.tableStyles). The effective\r\n * per-cell formatting is baked onto the cells; this is kept for re-editing and\r\n * round-trip. Absent = no table style (direct cell formatting only). */\r\n styleId?: string | undefined;\r\n /** Which conditional bands of the referenced style are active (w:tblLook). */\r\n condOverrides?: TableCondOverrides;\r\n /** Field result membership \u2014 see Paragraph.fieldId. */\r\n fieldId?: string | undefined;\r\n /** Block-level content-control ancestry \u2014 see Paragraph.sdtPath. */\r\n sdtPath?: string[] | undefined;\r\n}\r\n\r\n/** A display (block) equation \u2014 Word's `m:oMathPara`. Stands on its own line(s)\r\n * like an image; the MathML AST lives directly on the block (no registry). */\r\nexport interface EquationBlock {\r\n kind: \"equation\";\r\n id: string;\r\n revision: number;\r\n /** The equation as a Presentation-MathML AST (the canonical form). */\r\n equation: MathEquation;\r\n /** Horizontal placement of the equation. Default \"center\" (Word display math). */\r\n align?: \"left\" | \"center\" | \"right\";\r\n /** Field result membership \u2014 see Paragraph.fieldId. */\r\n fieldId?: string | undefined;\r\n /** Block-level content-control ancestry \u2014 see Paragraph.sdtPath. */\r\n sdtPath?: string[] | undefined;\r\n}\r\n\r\n/** An embedder-defined custom block, drawn on canvas by a registered block type\r\n * (see `registerBlockType` in the frontend package). An atomic object \u2014 like an\r\n * image/equation it measures to a single box and places whole, and holds only a\r\n * JSON-serializable `data` payload (so snapshot serialize is free). It is a\r\n * first-class object in the editor: click-selectable and deletable. Its\r\n * `customType` selects the registered renderer. Content is drawn (not\r\n * text-editable), so it carries no caret and is skipped by geometry indexing. Has\r\n * no native OOXML: `.docx` export is lossy (a placeholder paragraph + a warning)\r\n * unless the registered type supplies `toOOXML`. */\r\nexport interface CustomBlock {\r\n kind: \"custom\";\r\n id: string;\r\n revision: number;\r\n /** The registered block-type key (see `registerBlockType({ type })`). */\r\n customType: string;\r\n /** Arbitrary JSON-serializable payload the type's measure/paint consume. */\r\n data: unknown;\r\n /** Field result membership \u2014 see Paragraph.fieldId. */\r\n fieldId?: string | undefined;\r\n /** Block-level content-control ancestry \u2014 see Paragraph.sdtPath. */\r\n sdtPath?: string[] | undefined;\r\n}\r\n\r\nexport type Block = Paragraph | ImageBlock | TableBlock | EquationBlock | CustomBlock;\r\n\r\n/** OOXML page-number / list format (the field `\\* <fmt>` switch). */\r\nexport type PageNumFmt = \"arabic\" | \"roman\" | \"Roman\" | \"alpha\" | \"Alpha\";\r\n/** IF comparison operators. */\r\nexport type IfOp = \"=\" | \"<>\" | \"<\" | \">\" | \"<=\" | \">=\";\r\n\r\n/** Typed, parsed form of a BUILT-IN field's definition \u2014 drives the field\r\n * constructor UI and the evaluator. `FieldDef.instruction` stays the verbatim\r\n * round-trip source of truth; `spec` is derived from it and re-synthesized back.\r\n * TOC is NOT here \u2014 it keeps its own `tocEntry`/`tocInstruction` path. */\r\nexport type FieldSpec =\r\n | { type: \"PAGE\"; numFmt?: PageNumFmt }\r\n | { type: \"NUMPAGES\"; numFmt?: PageNumFmt }\r\n | { type: \"DATE\"; format: string }\r\n | { type: \"TIME\"; format: string }\r\n | { type: \"IF\"; operandA: string; op: IfOp; operandB: string; trueRuns: Run[]; falseRuns: Run[] };\r\n\r\n/** A generic OOXML field tracked in the model: its verbatim instruction plus a\r\n * classification. CUSTOM (host-resolvable) fields and the BUILT-IN fields the\r\n * editor understands (PAGE/NUMPAGES/DATE/TIME/IF) are both tracked this way; TOC\r\n * keeps its own `tocEntry`/`tocInstruction` path. The field's result is the\r\n * contiguous run of BLOCKS carrying its `id` as `Block.fieldId` (region fields)\r\n * OR the contiguous RUNS carrying it as `CharStyle.fieldId` (inline fields). */\r\nexport interface FieldDef {\r\n id: string;\r\n /** Verbatim w:instrText (e.g. ` MYCHART \"sales-2026\" `, ` PAGE \\* roman `), re-emitted on export. */\r\n instruction: string;\r\n /** Field keyword, uppercased \u2014 the first instruction token (e.g. \"PAGE\", \"MYCHART\"). */\r\n name: string;\r\n kind: \"builtin\" | \"custom\";\r\n /** Parsed spec for built-in fields the editor understands; absent for opaque\r\n * custom (host-resolved) fields and built-ins we only round-trip verbatim. */\r\n spec?: FieldSpec;\r\n}\r\n\r\n/** The six margin-band stories: default header/footer plus the Word variants\r\n * (\"Different first page\" / \"Different odd & even\"). Container ops, the\r\n * paragraph locator, and the band layout all key on these names. */\r\nexport type BandContainer =\r\n | \"header\"\r\n | \"footer\"\r\n | \"headerFirst\"\r\n | \"headerEven\"\r\n | \"footerFirst\"\r\n | \"footerEven\";\r\n\r\nexport const BAND_CONTAINERS: readonly BandContainer[] = [\r\n \"header\",\r\n \"footer\",\r\n \"headerFirst\",\r\n \"headerEven\",\r\n \"footerFirst\",\r\n \"footerEven\",\r\n];\r\n\r\n/** One newspaper column's geometry (px). When a section supplies `cols`, it\r\n * overrides the equal-width division implied by `columns.count`/`gapPx`. The\r\n * array length MUST equal `columns.count`; `spaceAfterPx` is the gap to the\r\n * NEXT column (ignored on the last entry). */\r\nexport interface ColumnEntry {\r\n widthPx: number;\r\n spaceAfterPx: number;\r\n}\r\n\r\n/** One edge of a page border (w:pgBorders child). `style` maps to @w:val. */\r\nexport interface PageBorderEdge {\r\n style: \"single\" | \"double\" | \"dashed\" | \"dotted\" | \"thick\" | \"none\";\r\n /** w:sz is eighth-points (px = sz / 6 at 96 dpi). */\r\n widthPx: number;\r\n /** \"#rrggbb\"; OOXML \"auto\" maps to \"#000000\". */\r\n color: string;\r\n /** w:space (points) \u2014 offset from the page edge / text, converted to px. */\r\n spacePx?: number;\r\n}\r\n\r\n/** w:sectPr/w:pgBorders \u2014 page border box. Absent edges are not drawn. */\r\nexport interface PageBorders {\r\n top?: PageBorderEdge;\r\n right?: PageBorderEdge;\r\n bottom?: PageBorderEdge;\r\n left?: PageBorderEdge;\r\n /** w:pgBorders/@w:offsetFrom \u2014 \"page\" (from page edge) or \"text\" (from margin). */\r\n offsetFrom?: \"page\" | \"text\";\r\n}\r\n\r\n/** OOXML w:sectPr/w:type for a section START. \"nextPage\" begins the section on\r\n * the next page; \"evenPage\"/\"oddPage\" force its first page onto an even/odd page\r\n * number (a blank filler page is inserted when the running parity is wrong).\r\n * \"continuous\" is never modeled as a break here \u2014 geometry-preserving continuous\r\n * sections flow inline (see the importer). */\r\nexport type SectionBreakType = \"nextPage\" | \"evenPage\" | \"oddPage\";\r\n\r\n/** w:sectPr/w:lnNumType \u2014 line numbering printed in the page margin. Counts body\r\n * text lines per section; `countBy` controls which line numbers are shown. */\r\nexport interface LineNumbering {\r\n /** w:lnNumType/@w:countBy \u2014 print every Nth line number (default 1 = every line). */\r\n countBy?: number;\r\n /** w:lnNumType/@w:start \u2014 the first line's number (default 1). */\r\n start?: number;\r\n /** w:lnNumType/@w:restart \u2014 when the counter resets. \"newPage\" (Word's default)\r\n * restarts each page; \"newSection\" restarts at the section's first page;\r\n * \"continuous\" never restarts (counts straight through). */\r\n restart?: \"continuous\" | \"newPage\" | \"newSection\";\r\n /** w:lnNumType/@w:distance \u2014 gap (px) between the numbers and the text edge.\r\n * Absent = a small default. */\r\n distancePx?: number;\r\n}\r\n\r\n/** Per-section overrides carried on a section-break paragraph. Absent fields\r\n * inherit from `Document.section` \u2014 Word's \"link to previous\" for bands, and\r\n * shared page geometry unless the user changes it for one section. */\r\nexport interface SectionPatch {\r\n pageWidthPx?: number;\r\n pageHeightPx?: number;\r\n marginPx?: { top: number; right: number; bottom: number; left: number };\r\n /** `null` = explicitly single-column; absent = inherit `Document.section`.\r\n * `sep` draws a separator line between columns; `cols` overrides the\r\n * equal-width division with explicit per-column widths. */\r\n columns?: { count: number; gapPx: number; sep?: boolean; cols?: ColumnEntry[] } | null;\r\n /** Restart page numbering at this section (absent = continue counting). */\r\n pageNumberStart?: number;\r\n /** w:background/@w:color \u2014 page fill (\"#rrggbb\"). See `SectionProps`. */\r\n pageColorHex?: string;\r\n /** w:sectPr/w:pgBorders \u2014 page border box. */\r\n pageBorders?: PageBorders;\r\n /** w:pgMar/@w:header \u2014 distance (px) from the page TOP to the header's top edge. */\r\n headerDistancePx?: number;\r\n /** w:pgMar/@w:footer \u2014 distance (px) from the page BOTTOM to the footer's bottom\r\n * edge (ECMA-376). The footer grows upward from there. */\r\n footerDistancePx?: number;\r\n /** w:sectPr/w:lnNumType \u2014 line numbering for this section's body text. */\r\n lineNumbering?: LineNumbering;\r\n header?: Block[];\r\n footer?: Block[];\r\n headerFirst?: Block[];\r\n headerEven?: Block[];\r\n footerFirst?: Block[];\r\n footerEven?: Block[];\r\n}\r\n\r\nexport interface SectionProps {\r\n pageWidthPx: number;\r\n pageHeightPx: number;\r\n marginPx: { top: number; right: number; bottom: number; left: number };\r\n /** Newspaper columns: the content box divides into `count` boxes separated by\r\n * `gapPx`; flow fills column 1 top-to-bottom, then column 2, then the next\r\n * page. Absent = single column. `sep` draws a line between columns; `cols`\r\n * overrides the equal-width division with explicit per-column widths. */\r\n columns?: { count: number; gapPx: number; sep?: boolean; cols?: ColumnEntry[] };\r\n /** Restart page numbering at this section's first page ({page} tokens).\r\n * Absent = continue counting from the previous page. */\r\n pageNumberStart?: number;\r\n /** w:background/@w:color \u2014 page fill behind everything (\"#rrggbb\"). Absent =\r\n * white/no fill. NOTE: OOXML w:background is document-global (one element on\r\n * w:document); stored per-section for a uniform model/dialog, but export\r\n * reads it only from `Document.section`. */\r\n pageColorHex?: string;\r\n /** w:sectPr/w:pgBorders \u2014 page border box. Absent = no border. */\r\n pageBorders?: PageBorders;\r\n /** w:pgMar/@w:header \u2014 distance (px) from the page TOP to the header's top edge\r\n * (header grows down). Absent = center the band in the top margin. */\r\n headerDistancePx?: number;\r\n /** w:pgMar/@w:footer \u2014 distance (px) from the page BOTTOM to the footer's bottom\r\n * edge (footer grows up). Absent = center the band in the bottom margin. */\r\n footerDistancePx?: number;\r\n /** w:sectPr/w:lnNumType \u2014 line numbering printed in this section's margin.\r\n * Absent = no line numbers (the historical default). */\r\n lineNumbering?: LineNumbering;\r\n /** OOXML w:sectPr/w:type for the FINAL (body) section's start. \"evenPage\"/\"oddPage\"\r\n * force its first page onto an even/odd page number (mid-document sections carry\r\n * this on their `sectionBreak` paragraph instead). Absent \u21D2 \"nextPage\". */\r\n breakType?: SectionBreakType;\r\n /** Header/footer are full block stories (paragraphs, images, tables) laid out\r\n * by the same engine into the margin bands. {page}/{pages} tokens in run text\r\n * are substituted per page ({page:roman|Roman|alpha|Alpha} formats). The\r\n * First/Even variants override the default on the section's first page and\r\n * on even-numbered pages respectively (Word's band variant model). */\r\n header?: Block[];\r\n footer?: Block[];\r\n headerFirst?: Block[];\r\n headerEven?: Block[];\r\n footerFirst?: Block[];\r\n footerEven?: Block[];\r\n}\r\n\r\nexport interface Document {\r\n section: SectionProps;\r\n blocks: Block[];\r\n /** Named styles (Word's style gallery) \u2014 see model/stylesheet.ts. */\r\n stylesheet?: import(\"./stylesheet\").Stylesheet;\r\n /** List definitions keyed by id (docx numId space) \u2014 see model/lists.ts. */\r\n lists?: Record<string, import(\"./lists\").ListDefinition>;\r\n /** Table styles keyed by id (docx styleId space) \u2014 see model/tableStyles.ts. */\r\n tableStyles?: Record<string, import(\"./tableStyles\").TableStyle>;\r\n /** Footnote bodies keyed by ref id (docx footnotes.xml space). Each note is\r\n * a paragraph story laid out in the page-bottom footnote area; notes render\r\n * on whatever page their reference run lands on. */\r\n footnotes?: Record<string, Paragraph[]>;\r\n /** Endnote bodies keyed by ref id (docx endnotes.xml space). Each note is a\r\n * paragraph story collected at the END of the document, under a separator\r\n * rule, in reference order \u2014 Word's \"end of document\" endnote placement. */\r\n endnotes?: Record<string, Paragraph[]>;\r\n /** Content-control properties keyed by sdt id. Runs carry inline membership via\r\n * `CharStyle.sdtPath`; blocks carry block-level membership via `Block.sdtPath`.\r\n * Every id appearing on any path has an entry here. */\r\n sdts?: Record<string, SdtProps>;\r\n /** Bookmark name \u2192 its character RANGE (docx w:bookmarkStart/End). `start`/`end`\r\n * are positions (block id + UTF-16 offset) that may span paragraphs; a point\r\n * bookmark has start === end. The block may live in the body, a table cell, or\r\n * a header/footer band. Targets for in-document anchor links (\"#name\" \u2014 TOC\r\n * entries, cross-references) and the Bookmarks panel. */\r\n bookmarks?: Record<string, BookmarkRange>;\r\n /** The document's `TOC` field instruction (e.g. ` TOC \\o \"1-3\" \\h \\z \\u `),\r\n * captured on import so export re-emits it verbatim (honoring its switches)\r\n * instead of a hardcoded default. Absent when the document has no TOC field. */\r\n tocInstruction?: string;\r\n /** Block id of the paragraph holding the (empty/placeholder) `TOC` field, captured\r\n * on import so a headless render can BUILD the entries at the right spot. Absent\r\n * when there's no TOC field, or when the TOC already has entries (then the\r\n * existing tocEntry blocks mark the location). */\r\n tocAnchorBlockId?: string;\r\n /** Generic custom fields keyed by id (blocks carry the membership via\r\n * `fieldId`). Populated on import for non-built-in fields; export re-emits each\r\n * region as a real complex field; the editor refreshes it via `resolveField`.\r\n * Absent when the document has no custom fields. */\r\n fields?: Record<string, FieldDef>;\r\n /** Default tab interval in px (OOXML settings.xml w:defaultTabStop, twips\u2192px).\r\n * A `\\t` running past the last explicit tab stop advances to the next multiple\r\n * of this. Absent = the engine's Word-matching default (0.5in = 48px). */\r\n defaultTabStopPx?: number;\r\n /** Compatibility settings round-tripped from settings.xml w:compat/w:compatSetting\r\n * (the modern `name`/`uri`/`val` triples Word emits, e.g. compatibilityMode).\r\n * Preserved verbatim so a Word\u2192edit\u2192Word cycle keeps them. Absent = none. */\r\n compatSettings?: { name: string; uri: string; val: string }[];\r\n}\r\n\r\nexport interface BookmarkRange {\r\n start: DocPosition;\r\n end: DocPosition;\r\n}\r\n", "// Shared text helpers over the model: paragraph text access, grapheme/word\r\n// boundaries (Intl.Segmenter \u2014 the same segmentation pretext breaks lines on),\r\n// and style-at-position for Word-style inheritance when typing.\r\n\r\nimport type { BandContainer, Block, CharStyle, Document, Paragraph, Run } from \"./document\";\r\nimport { BAND_CONTAINERS } from \"./document\";\r\n\r\nexport const graphemes = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\r\nexport const words = new Intl.Segmenter(undefined, { granularity: \"word\" });\r\n\r\n/** Step-0 perf diagnostic (off by default, ~free when disabled). When `enabled`,\r\n * paragraphsOf/bandParagraphs tally how often they rebuild the paragraph list and\r\n * how many paragraph slots they walk+allocate \u2014 the exact work a WeakMap index\r\n * keyed on document identity would eliminate. Toggle via the export, read the\r\n * counters, reset between workloads. Used by frontend/src/perf/readsPerDoc.test.ts. */\r\nexport const _paraScanStats = {\r\n enabled: false,\r\n /** # of full-document traversals (paragraphsOf calls). */\r\n fullCalls: 0,\r\n /** Sum of full-list lengths returned (body + band paragraphs walked). */\r\n fullSlots: 0,\r\n /** # of bandParagraphs calls (incl. the O(N\u00B2) isBandParagraph filter). */\r\n bandCalls: 0,\r\n /** Sum of band-list lengths walked. */\r\n bandSlots: 0,\r\n};\r\nexport const resetParaScanStats = (): void => {\r\n _paraScanStats.fullCalls = 0;\r\n _paraScanStats.fullSlots = 0;\r\n _paraScanStats.bandCalls = 0;\r\n _paraScanStats.bandSlots = 0;\r\n};\r\n\r\n/** A paragraph whose every non-empty run is hidden (w:vanish): it has text but\r\n * none is visible. Such a paragraph is never laid out, caret-reachable, or\r\n * deletable \u2014 it's preserved metadata. A truly empty paragraph (no text at all)\r\n * is NOT hidden \u2014 it's a normal blank line. */\r\nexport const isHiddenParagraph = (p: Paragraph): boolean =>\r\n p.runs.some((r) => r.text.length > 0) && p.runs.every((r) => r.text.length === 0 || r.style.hidden === true);\r\n\r\nconst paragraphsInBlocks = (blocks: Block[], includeCells: boolean): Paragraph[] => {\r\n const out: Paragraph[] = [];\r\n for (const b of blocks) {\r\n if (b.kind === \"paragraph\") out.push(b);\r\n else if (b.kind === \"table\" && includeCells) {\r\n for (const row of b.rows) {\r\n for (const cell of row.cells) out.push(...paragraphsInBlocks(cell.blocks, includeCells));\r\n }\r\n }\r\n }\r\n return out;\r\n};\r\n\r\n/** The two band STORIES from the UI's point of view (story mode is entered per\r\n * header/footer area; which variant CONTAINER that resolves to depends on the\r\n * page \u2014 see Page.headerSource/footerSource). */\r\nexport type BandName = \"header\" | \"footer\";\r\n\r\n/** Editable paragraphs of a margin band container, document order \u2014 INCLUDING\r\n * band-table cells (imported footers are routinely tables holding text next\r\n * to a page-number paragraph). */\r\nexport const bandParagraphs = (doc: Document, band: BandContainer): Paragraph[] => {\r\n const list = paragraphsInBlocks(doc.section[band] ?? [], true);\r\n if (_paraScanStats.enabled) {\r\n _paraScanStats.bandCalls++;\r\n _paraScanStats.bandSlots += list.length;\r\n }\r\n return list;\r\n};\r\n\r\n/** Body paragraphs in document order, INCLUDING table-cell paragraphs (one\r\n * level deep). Excludes margin bands and footnote notes \u2014 the body story only.\r\n * Heading/TOC scans use this so headings inside table cells are found too. */\r\nexport const bodyParagraphs = (doc: Document): Paragraph[] => paragraphsInBlocks(doc.blocks, true);\r\n\r\n/** Per-document memoized index of the navigable-paragraph space. The model is\r\n * immutable \u2014 every edit returns a NEW Document object (path-clone) \u2014 so a\r\n * WeakMap keyed on document identity auto-invalidates: a fresh doc simply misses\r\n * the cache and the old entry is GC'd. Built once per doc, then `paragraphsOf`\r\n * is a cached-array return and `blockById`/`blockIndexOf` are O(1) Map lookups,\r\n * instead of re-walking the whole block tree (body + table cells + 6 bands +\r\n * footnotes/endnotes) on every call. Mirrors layout/geometry.ts's TreeIndex. */\r\ninterface DocParaIndex {\r\n list: Paragraph[];\r\n /** id \u2192 its index in `list` (FIRST occurrence, matching the old `.find`/\r\n * `.findIndex` semantics for the rare duplicate id). */\r\n byId: Map<string, number>;\r\n}\r\nconst paraIndexCache = new WeakMap<Document, DocParaIndex>();\r\n\r\nconst buildParaIndex = (doc: Document): DocParaIndex => {\r\n const list = [\r\n ...paragraphsInBlocks(doc.blocks, true),\r\n ...BAND_CONTAINERS.flatMap((band) => bandParagraphs(doc, band)),\r\n ...Object.values(doc.footnotes ?? {}).flat(),\r\n ...Object.values(doc.endnotes ?? {}).flat(),\r\n ];\r\n const byId = new Map<string, number>();\r\n for (let i = 0; i < list.length; i++) {\r\n const id = list[i]!.id;\r\n if (!byId.has(id)) byId.set(id, i); // keep first \u2014 matches old .find/.findIndex\r\n }\r\n if (_paraScanStats.enabled) {\r\n _paraScanStats.fullCalls++;\r\n _paraScanStats.fullSlots += list.length;\r\n }\r\n return { list, byId };\r\n};\r\n\r\nconst paraIndex = (doc: Document): DocParaIndex => {\r\n let idx = paraIndexCache.get(doc);\r\n if (!idx) {\r\n idx = buildParaIndex(doc);\r\n paraIndexCache.set(doc, idx);\r\n }\r\n return idx;\r\n};\r\n\r\n/** All editable paragraphs in document order: body (including table cells),\r\n * then every band story (header/footer + first/even variants). This is the\r\n * index space commands use. Cached per document identity (see DocParaIndex). */\r\nexport const paragraphsOf = (doc: Document): Paragraph[] => paraIndex(doc).list;\r\n\r\nexport const blockById = (doc: Document, blockId: string): Paragraph | undefined => {\r\n const idx = paraIndex(doc);\r\n const i = idx.byId.get(blockId);\r\n return i === undefined ? undefined : idx.list[i];\r\n};\r\n\r\nexport const blockIndexOf = (doc: Document, blockId: string): number => {\r\n const i = paraIndex(doc).byId.get(blockId);\r\n return i === undefined ? -1 : i;\r\n};\r\n\r\n/** The set of every band (header/footer + variants) paragraph id, memoized per\r\n * document identity. Lets a body-vs-band membership test be an O(1) Set lookup\r\n * instead of re-walking all six band stories per query \u2014 the selection\r\n * controller's caret navigation filters the body paragraphs against this on\r\n * every arrow key. Same WeakMap-on-identity invalidation as the para index. */\r\nconst bandIdCache = new WeakMap<Document, ReadonlySet<string>>();\r\nexport const bandParagraphIds = (doc: Document): ReadonlySet<string> => {\r\n let s = bandIdCache.get(doc);\r\n if (!s) {\r\n const built = new Set<string>();\r\n for (const band of BAND_CONTAINERS) for (const p of bandParagraphs(doc, band)) built.add(p.id);\r\n s = built;\r\n bandIdCache.set(doc, s);\r\n }\r\n return s;\r\n};\r\n\r\n/** Body navigation order: the paragraphs the caret can reach while editing the\r\n * body \u2014 `paragraphsOf` minus band paragraphs (they belong to their own header/\r\n * footer story) and hidden (w:vanish) paragraphs (never laid out, so the caret\r\n * skips them). Memoized per document identity: the selection controller derives\r\n * this on every caret move, and the filter (a full pass plus a per-paragraph\r\n * hidden test) is otherwise re-run several times per keystroke. Same\r\n * WeakMap-on-identity invalidation as `paragraphsOf`. Treat the result as\r\n * IMMUTABLE (callers read-only; see paragraphsOf). */\r\nconst navCache = new WeakMap<Document, Paragraph[]>();\r\nexport const navigableParagraphs = (doc: Document): Paragraph[] => {\r\n let list = navCache.get(doc);\r\n if (!list) {\r\n const bandIds = bandParagraphIds(doc);\r\n list = paragraphsOf(doc).filter((p) => !bandIds.has(p.id) && !isHiddenParagraph(p));\r\n navCache.set(doc, list);\r\n }\r\n return list;\r\n};\r\n\r\n/** Story (header/footer editing) navigation order for ONE band container:\r\n * `bandParagraphs(doc, band)` minus hidden (w:vanish) paragraphs the caret skips.\r\n * The body counterpart is `navigableParagraphs`; this is the per-band analogue the\r\n * selection controller derives on every caret move while editing a margin band.\r\n * Memoized per (document identity, band) \u2014 a nested `Map<BandContainer, \u2026>` per\r\n * doc, with the same WeakMap-on-identity invalidation. Treat the result as\r\n * IMMUTABLE (callers read-only). */\r\nconst navBandCache = new WeakMap<Document, Map<BandContainer, Paragraph[]>>();\r\nexport const navigableBandParagraphs = (doc: Document, band: BandContainer): Paragraph[] => {\r\n let perDoc = navBandCache.get(doc);\r\n if (!perDoc) {\r\n perDoc = new Map();\r\n navBandCache.set(doc, perDoc);\r\n }\r\n let list = perDoc.get(band);\r\n if (!list) {\r\n list = bandParagraphs(doc, band).filter((p) => !isHiddenParagraph(p));\r\n perDoc.set(band, list);\r\n }\r\n return list;\r\n};\r\n\r\n/** Every block id present anywhere in the doc \u2014 top-level blocks (paragraph,\r\n * image, table) in body + bands, table ids, and paragraph/image ids nested in\r\n * table cells. Used by the review layer's structural-suggestion GC, which keys\r\n * on whole-block identity rather than a text offset range. */\r\nconst collectBlockIds = (blocks: Block[], into: Set<string>): void => {\r\n for (const b of blocks) {\r\n into.add(b.id);\r\n if (b.kind === \"table\") {\r\n for (const row of b.rows) for (const cell of row.cells) collectBlockIds(cell.blocks, into);\r\n }\r\n }\r\n};\r\n\r\n/** Does a block with this id exist anywhere in the document (any kind, any\r\n * container, including table cells)? */\r\nexport const blockExists = (doc: Document, blockId: string): boolean => {\r\n const ids = new Set<string>();\r\n collectBlockIds(doc.blocks, ids);\r\n for (const band of BAND_CONTAINERS) collectBlockIds(doc.section[band] ?? [], ids);\r\n for (const paras of Object.values(doc.footnotes ?? {})) for (const p of paras) ids.add(p.id);\r\n for (const paras of Object.values(doc.endnotes ?? {})) for (const p of paras) ids.add(p.id);\r\n return ids.has(blockId);\r\n};\r\n\r\n// ---------------------------------------------------------------------------\r\n// Paragraph locator \u2014 content ops work on any paragraph, wherever it lives.\r\n\r\nexport type ParaLocation =\r\n | { kind: \"top\"; bi: number }\r\n | { kind: \"cell\"; where: \"body\" | BandContainer; bi: number; ri: number; ci: number; pi: number }\r\n | { kind: \"band\"; band: BandContainer; bi: number }\r\n | { kind: \"footnote\"; noteId: string; pi: number }\r\n | { kind: \"endnote\"; noteId: string; pi: number };\r\n\r\n/** Top-level block list of a container (\"body\" or a band story). */\r\nexport const containerListOf = (doc: Document, where: \"body\" | BandContainer): Block[] =>\r\n where === \"body\" ? doc.blocks : (doc.section[where] ?? []);\r\n\r\n/** id \u2192 ParaLocation for every locatable paragraph, memoized per document\r\n * identity (same WeakMap contract as the para index above: the model is\r\n * immutable, so any structural edit mints a new Document object and the stale\r\n * index falls away). Built in ONE walk over body + bands + notes, so callers\r\n * that used to pay a full-document scan per lookup (the hover adornment probes\r\n * it per mousemove; commands per dispatch) get an O(1) hit instead. Insertion\r\n * order mirrors the original scan order (body, bands, footnotes, endnotes;\r\n * first occurrence wins for a duplicate id). */\r\nconst locIndexCache = new WeakMap<Document, Map<string, ParaLocation>>();\r\n\r\nfunction buildLocIndex(doc: Document): Map<string, ParaLocation> {\r\n const map = new Map<string, ParaLocation>();\r\n const set = (id: string, loc: ParaLocation): void => {\r\n if (!map.has(id)) map.set(id, loc);\r\n };\r\n const walk = (blocks: Block[], where: \"body\" | BandContainer): void => {\r\n for (let bi = 0; bi < blocks.length; bi++) {\r\n const b = blocks[bi]!;\r\n if (b.kind === \"paragraph\") {\r\n set(b.id, where === \"body\" ? { kind: \"top\", bi } : { kind: \"band\", band: where, bi });\r\n } else if (b.kind === \"table\") {\r\n for (let ri = 0; ri < b.rows.length; ri++) {\r\n const row = b.rows[ri]!;\r\n for (let ci = 0; ci < row.cells.length; ci++) {\r\n const cell = row.cells[ci]!;\r\n for (let pi = 0; pi < cell.blocks.length; pi++) {\r\n const cb = cell.blocks[pi]!;\r\n // One level deep: paragraphs of nested tables stay read-only.\r\n if (cb.kind === \"paragraph\") set(cb.id, { kind: \"cell\", where, bi, ri, ci, pi });\r\n }\r\n }\r\n }\r\n }\r\n }\r\n };\r\n walk(doc.blocks, \"body\");\r\n for (const band of BAND_CONTAINERS) walk(doc.section[band] ?? [], band);\r\n for (const [noteId, paras] of Object.entries(doc.footnotes ?? {})) {\r\n for (let pi = 0; pi < paras.length; pi++) set(paras[pi]!.id, { kind: \"footnote\", noteId, pi });\r\n }\r\n for (const [noteId, paras] of Object.entries(doc.endnotes ?? {})) {\r\n for (let pi = 0; pi < paras.length; pi++) set(paras[pi]!.id, { kind: \"endnote\", noteId, pi });\r\n }\r\n return map;\r\n}\r\n\r\nexport function locateParagraph(doc: Document, blockId: string): ParaLocation | null {\r\n let idx = locIndexCache.get(doc);\r\n if (!idx) {\r\n idx = buildLocIndex(doc);\r\n locIndexCache.set(doc, idx);\r\n }\r\n return idx.get(blockId) ?? null;\r\n}\r\n\r\nexport function paragraphAt(doc: Document, loc: ParaLocation): Paragraph {\r\n if (loc.kind === \"top\") return doc.blocks[loc.bi] as Paragraph;\r\n if (loc.kind === \"band\") return doc.section[loc.band]![loc.bi] as Paragraph;\r\n if (loc.kind === \"footnote\") return doc.footnotes![loc.noteId]![loc.pi]!;\r\n if (loc.kind === \"endnote\") return doc.endnotes![loc.noteId]![loc.pi]!;\r\n const table = containerListOf(doc, loc.where)[loc.bi] as Extract<Block, { kind: \"table\" }>;\r\n return table.rows[loc.ri]!.cells[loc.ci]!.blocks[loc.pi] as Paragraph;\r\n}\r\n\r\n/** Immutable path-clone replace; bumps the containing table's revision too. */\r\nexport function replaceParagraphAt(doc: Document, loc: ParaLocation, p: Paragraph): Document {\r\n if (loc.kind === \"footnote\") {\r\n const paras = doc.footnotes![loc.noteId]!.slice();\r\n paras[loc.pi] = p;\r\n return { ...doc, footnotes: { ...doc.footnotes, [loc.noteId]: paras } };\r\n }\r\n if (loc.kind === \"endnote\") {\r\n const paras = doc.endnotes![loc.noteId]!.slice();\r\n paras[loc.pi] = p;\r\n return { ...doc, endnotes: { ...doc.endnotes, [loc.noteId]: paras } };\r\n }\r\n if (loc.kind === \"band\") {\r\n const blocks = (doc.section[loc.band] ?? []).slice();\r\n blocks[loc.bi] = p;\r\n return { ...doc, section: { ...doc.section, [loc.band]: blocks } };\r\n }\r\n if (loc.kind === \"top\") {\r\n const blocks = doc.blocks.slice();\r\n blocks[loc.bi] = p;\r\n return { ...doc, blocks };\r\n }\r\n const blocks = containerListOf(doc, loc.where).slice();\r\n const table = blocks[loc.bi] as Extract<Block, { kind: \"table\" }>;\r\n const rows = table.rows.slice();\r\n const row = { cells: rows[loc.ri]!.cells.slice() };\r\n const cell = { ...row.cells[loc.ci]!, blocks: row.cells[loc.ci]!.blocks.slice() };\r\n cell.blocks[loc.pi] = p;\r\n row.cells[loc.ci] = cell;\r\n rows[loc.ri] = row;\r\n blocks[loc.bi] = { ...table, rows, revision: table.revision + 1 };\r\n if (loc.where === \"body\") return { ...doc, blocks };\r\n return { ...doc, section: { ...doc.section, [loc.where]: blocks } };\r\n}\r\n\r\nexport const isInCell = (doc: Document, blockId: string): boolean =>\r\n locateParagraph(doc, blockId)?.kind === \"cell\";\r\n\r\nexport const textOfRuns = (runs: Run[]): string => runs.map((r) => r.text).join(\"\");\r\n\r\nexport const textOfBlock = (doc: Document, blockId: string): string => {\r\n const b = blockById(doc, blockId);\r\n return b ? textOfRuns(b.runs) : \"\";\r\n};\r\n\r\nexport function prevGrapheme(text: string, offset: number): number {\r\n let prev = 0;\r\n for (const s of graphemes.segment(text)) {\r\n if (s.index >= offset) break;\r\n prev = s.index;\r\n }\r\n return prev;\r\n}\r\n\r\nexport function nextGrapheme(text: string, offset: number): number {\r\n for (const s of graphemes.segment(text)) {\r\n const end = s.index + s.segment.length;\r\n if (end > offset) return end;\r\n }\r\n return text.length;\r\n}\r\n\r\nexport function prevWordStart(text: string, offset: number): number {\r\n let prev = 0;\r\n for (const s of words.segment(text)) {\r\n if (s.index >= offset) break;\r\n if (s.isWordLike) prev = s.index;\r\n }\r\n return prev;\r\n}\r\n\r\nexport function nextWordEnd(text: string, offset: number): number {\r\n for (const s of words.segment(text)) {\r\n const end = s.index + s.segment.length;\r\n if (s.isWordLike && end > offset) return end;\r\n }\r\n return text.length;\r\n}\r\n\r\n/** Style of the character before `offset` (Word: typing inherits what precedes\r\n * the caret), falling back to the first run's style. */\r\nexport function styleAtRuns(runs: Run[], offset: number): CharStyle | undefined {\r\n let cum = 0;\r\n for (const r of runs) {\r\n const end = cum + r.text.length;\r\n // offset-1 falls inside this run -> its style precedes the caret\r\n if (offset > cum && offset <= end && r.text.length > 0) return r.style;\r\n cum = end;\r\n }\r\n return runs[0]?.style;\r\n}\r\n\r\n/** Style of the character AT `index` (the run covering it), or undefined when out\r\n * of range. Distinct from styleAtRuns (which returns the char BEFORE a caret\r\n * offset): use this to inspect a specific character, e.g. an inline-equation\r\n * sentinel located by its run index. */\r\nexport function styleOfCharAt(runs: Run[], index: number): CharStyle | undefined {\r\n if (index < 0) return undefined;\r\n let cum = 0;\r\n for (const r of runs) {\r\n if (index >= cum && index < cum + r.text.length) return r.style;\r\n cum += r.text.length;\r\n }\r\n return undefined;\r\n}\r\n", "// List definitions \u2014 a direct mirror of OOXML numbering.xml abstractNum levels,\n// so the docx importer can feed Document.lists losslessly (id space = numId).\n// Markers are PAINT-ONLY: they live in the hanging indent and never affect line\n// breaking (matching Word's overlap behavior for very long markers).\n\nimport type { CharStyle } from \"./document\";\n\nexport type ListNumberFormat =\n | \"bullet\"\n | \"decimal\"\n | \"lowerLetter\"\n | \"upperLetter\"\n | \"lowerRoman\"\n | \"upperRoman\";\n\nexport interface ListLevel {\n format: ListNumberFormat;\n /** Marker pattern; %N is the counter of level N-1, formatted per THAT level\n * (OOXML \u00A717.9.11 semantics). E.g. \"%1.\" or \"%1.%2.%3\". Ignored for bullets. */\n text: string;\n bulletChar?: string;\n /** Text indent for paragraphs at this level (added to ParaStyle.indentLeftPx). */\n indentLeftPx: number;\n /** Marker hangs this far left of the text origin. */\n hangingPx: number;\n start: number;\n markerStyle?: Partial<CharStyle>;\n}\n\nexport interface ListDefinition {\n id: string;\n levels: ListLevel[]; // up to 9 (levels 0..8)\n}\n\n// ---------------------------------------------------------------------------\n// Number formatting\n\nconst ROMAN: [number, string][] = [\n [1000, \"m\"], [900, \"cm\"], [500, \"d\"], [400, \"cd\"], [100, \"c\"], [90, \"xc\"],\n [50, \"l\"], [40, \"xl\"], [10, \"x\"], [9, \"ix\"], [5, \"v\"], [4, \"iv\"], [1, \"i\"],\n];\n\nfunction toRoman(n: number): string {\n let out = \"\";\n let v = Math.max(1, n);\n for (const [value, sym] of ROMAN) {\n while (v >= value) {\n out += sym;\n v -= value;\n }\n }\n return out;\n}\n\nfunction toLetter(n: number): string {\n // 1->a .. 26->z, 27->aa (Word repeats: 27 is actually \"aa\")\n let out = \"\";\n let v = Math.max(1, n);\n while (v > 0) {\n v -= 1;\n out = String.fromCharCode(97 + (v % 26)) + out;\n v = Math.floor(v / 26);\n }\n return out;\n}\n\nexport function formatListNumber(n: number, format: ListNumberFormat): string {\n switch (format) {\n case \"decimal\": return String(n);\n case \"lowerLetter\": return toLetter(n);\n case \"upperLetter\": return toLetter(n).toUpperCase();\n case \"lowerRoman\": return toRoman(n);\n case \"upperRoman\": return toRoman(n).toUpperCase();\n case \"bullet\": return \"\";\n }\n}\n\n/** Render a level's marker from the counter stack (counters[k] = level k's value). */\nexport function markerText(def: ListDefinition, level: number, counters: number[]): string {\n const lvl = def.levels[Math.min(level, def.levels.length - 1)];\n if (!lvl) return \"\";\n if (lvl.format === \"bullet\") return lvl.bulletChar ?? DEFAULT_BULLET;\n return lvl.text.replace(/%(\\d)/g, (_, d: string) => {\n const k = Number(d) - 1;\n const refLevel = def.levels[Math.min(k, def.levels.length - 1)];\n const value = counters[k] ?? refLevel?.start ?? 1;\n return formatListNumber(value, refLevel?.format ?? \"decimal\");\n });\n}\n\n// ---------------------------------------------------------------------------\n// Default definitions (what the toolbar buttons create)\n\nexport const DEFAULT_BULLET_LIST_ID = \"bullets\";\nexport const DEFAULT_NUMBER_LIST_ID = \"numbers\";\nexport const DEFAULT_MULTILEVEL_LIST_ID = \"multilevel\";\n\n/** OOXML abstractNum holds up to 9 levels (0..8). */\nconst LIST_LEVELS = 9;\n/** Per-level text indent + step (px): level i sits at LIST_INDENT_STEP_PX*(i+1). */\nconst LIST_INDENT_STEP_PX = 24;\n/** How far the marker hangs left of the text origin, per list kind. */\nconst BULLET_HANGING_PX = 18;\nconst NUMBER_HANGING_PX = 22;\n\nconst DEFAULT_BULLET = \"\u2022\";\nconst BULLETS = [DEFAULT_BULLET, \"\u25E6\", \"\u25AA\"];\nconst NUMBER_FORMATS: ListNumberFormat[] = [\"decimal\", \"lowerLetter\", \"lowerRoman\"];\n\nconst levelIndentPx = (i: number): number => LIST_INDENT_STEP_PX * (i + 1);\n\nexport function defaultListDefinition(kind: \"bullet\" | \"decimal\"): ListDefinition {\n const levels: ListLevel[] = [];\n for (let i = 0; i < LIST_LEVELS; i++) {\n if (kind === \"bullet\") {\n levels.push({\n format: \"bullet\",\n text: \"\",\n bulletChar: BULLETS[i % BULLETS.length]!,\n indentLeftPx: levelIndentPx(i),\n hangingPx: BULLET_HANGING_PX,\n start: 1,\n });\n } else {\n levels.push({\n format: NUMBER_FORMATS[i % NUMBER_FORMATS.length]!,\n text: `%${i + 1}.`,\n indentLeftPx: levelIndentPx(i),\n hangingPx: NUMBER_HANGING_PX,\n start: 1,\n });\n }\n }\n return { id: kind === \"bullet\" ? DEFAULT_BULLET_LIST_ID : DEFAULT_NUMBER_LIST_ID, levels };\n}\n\n/** A single-glyph bullet list (every level uses `char`) \u2014 what the bullet\n * style picker applies. */\nexport function bulletListDefinition(id: string, char: string): ListDefinition {\n const levels: ListLevel[] = [];\n for (let i = 0; i < LIST_LEVELS; i++) {\n levels.push({ format: \"bullet\", text: \"\", bulletChar: char, indentLeftPx: levelIndentPx(i), hangingPx: BULLET_HANGING_PX, start: 1 });\n }\n return { id, levels };\n}\n\n/** A single-format numbered list (every level uses `format`), with `suffix`\n * after each counter (\".\" or \")\") \u2014 what the number style picker applies. */\nexport function numberListDefinition(id: string, format: ListNumberFormat, suffix: string): ListDefinition {\n const levels: ListLevel[] = [];\n for (let i = 0; i < LIST_LEVELS; i++) {\n levels.push({ format, text: `%${i + 1}${suffix}`, indentLeftPx: levelIndentPx(i), hangingPx: NUMBER_HANGING_PX, start: 1 });\n }\n return { id, levels };\n}\n\n/** Legal-style multilevel list: every level is decimal and its marker compounds\n * all ancestors \u2014 level 0 \"1.\", level 1 \"1.1.\", level 2 \"1.1.1.\", \u2026 (Word's\n * default Multilevel List). Tab/Shift+Tab walk the levels. */\nexport function multilevelListDefinition(): ListDefinition {\n const levels: ListLevel[] = [];\n for (let i = 0; i < LIST_LEVELS; i++) {\n const text = Array.from({ length: i + 1 }, (_, k) => `%${k + 1}`).join(\".\") + \".\";\n levels.push({\n format: \"decimal\",\n text, // \"%1.\", \"%1.%2.\", \"%1.%2.%3.\", \u2026\n indentLeftPx: levelIndentPx(i),\n hangingPx: LIST_INDENT_STEP_PX + i * 8, // compound markers widen with depth\n start: 1,\n });\n }\n return { id: DEFAULT_MULTILEVEL_LIST_ID, levels };\n}\n", "// Content-control (OOXML w:sdt) path helpers.\r\n//\r\n// SDT membership is an ORDERED ANCESTRY PATH (outer\u2192inner) so controls can nest:\r\n// - inline controls put their ids on `CharStyle.sdtPath` (within a paragraph);\r\n// - block-level controls put their ids on `Block.sdtPath` (whole paragraphs/\r\n// tables, incl. run-less blocks).\r\n// A run's full enclosing chain is `block.sdtPath ++ run.style.sdtPath`. The props\r\n// for every id live in `Document.sdts`. Paths are treated as immutable (always\r\n// replaced, never mutated in place) and are never stored as `[]` \u2014 use undefined.\r\n\r\nimport type { Block, CharStyle } from \"./document\";\r\n\r\n/** The innermost (most specific) inline control id at a run, or undefined. */\r\nexport function innermostSdtId(style: CharStyle): string | undefined {\r\n const p = style.sdtPath;\r\n return p && p.length > 0 ? p[p.length - 1] : undefined;\r\n}\r\n\r\n/** The innermost block-level control id wrapping a block, or undefined. */\r\nexport function blockInnermostSdtId(block: Block): string | undefined {\r\n const p = block.sdtPath;\r\n return p && p.length > 0 ? p[p.length - 1] : undefined;\r\n}\r\n\r\n/** A run's full enclosing control chain, outer\u2192inner: block ancestry (prefix)\r\n * followed by inline ancestry (suffix). Empty array if none. */\r\nexport function fullSdtChain(block: Block | undefined, style: CharStyle): string[] {\r\n const bp = block?.sdtPath ?? [];\r\n const ip = style.sdtPath ?? [];\r\n return bp.length === 0 ? ip.slice() : bp.concat(ip);\r\n}\r\n\r\n/** Path equality treating `undefined` and `[]` as equal (the empty path). */\r\nexport function sdtPathEq(a: string[] | undefined, b: string[] | undefined): boolean {\r\n const al = a?.length ?? 0;\r\n const bl = b?.length ?? 0;\r\n if (al !== bl) return false;\r\n for (let i = 0; i < al; i++) if (a![i] !== b![i]) return false;\r\n return true;\r\n}\r\n\r\n/** Length of the shared outer prefix of two paths (for LCP grouping on export). */\r\nexport function commonPrefixLen(a: string[], b: string[]): number {\r\n const n = Math.min(a.length, b.length);\r\n let i = 0;\r\n while (i < n && a[i] === b[i]) i++;\r\n return i;\r\n}\r\n\r\n/** Whether a run's inline path contains `id` (membership at any nesting level). */\r\nexport function inSdt(style: CharStyle, id: string): boolean {\r\n return style.sdtPath?.includes(id) ?? false;\r\n}\r\n\r\n/** Whether a block's path contains `id`. */\r\nexport function blockInSdt(block: Block, id: string): boolean {\r\n return block.sdtPath?.includes(id) ?? false;\r\n}\r\n\r\n/** Normalize a candidate path: undefined for empty, else a fresh array. */\r\nexport function normSdtPath(path: string[] | undefined): string[] | undefined {\r\n return path && path.length > 0 ? path.slice() : undefined;\r\n}\r\n\r\n/** Append a control id to a path (new array), for wrapping inside existing ones. */\r\nexport function pushSdt(path: string[] | undefined, id: string): string[] {\r\n return path && path.length > 0 ? [...path, id] : [id];\r\n}\r\n\r\n/** Remove ONE control id from a path (new array), preserving ancestry; returns\r\n * undefined when the path becomes empty. Used by \"remove control\". */\r\nexport function removeSdt(path: string[] | undefined, id: string): string[] | undefined {\r\n if (!path) return undefined;\r\n const next = path.filter((x) => x !== id);\r\n return next.length > 0 ? next : undefined;\r\n}\r\n\r\n/** The path prefix up to and INCLUDING `id` \u2014 the ancestry that newly inserted\r\n * content replacing control `id`'s contents must carry. Returns undefined if\r\n * `id` is not on the path. */\r\nexport function ancestryThrough(path: string[] | undefined, id: string): string[] | undefined {\r\n if (!path) return undefined;\r\n const idx = path.indexOf(id);\r\n return idx < 0 ? undefined : path.slice(0, idx + 1);\r\n}\r\n", "// The editor's default (\"Normal\") run + paragraph formatting, as CONCRETE\n// values. The model keeps runs/paragraphs concrete (every CharStyle/ParaStyle\n// field present), so every \"no style yet\" path \u2014 text insertion fallback,\n// paste, the sample document, the builder baselines, comment runs \u2014 needs a\n// full default to spread from. These are that single source of truth; keep them\n// in sync with the \"Normal\" style declared in makeDefaultStylesheet()\n// (stylesheet.ts), which is the same look expressed as a style delta.\n//\n// Embedders can OVERRIDE the library's built-in defaults (see EditorTypography\n// + the WordCanvas `overrideDefaultStyles` option). This only affects NEW/blank\n// documents and the fallback stylesheet \u2014 a loaded .docx keeps its own\n// w:docDefaults / Normal style (the importer bakes those in at import time).\n\nimport type { CharStyle, ParaStyle } from \"./document\";\n\n/** Per-instance typography override for the library's built-in defaults. Every\n * field is optional; omit to keep the built-in value. Drives both the concrete\n * default run/paragraph styles and the generated default stylesheet. */\nexport interface EditorTypography {\n /** Body (Normal) font family, e.g. \"Times New Roman, serif\". */\n fontFamily?: string;\n /** Body font size in px (96 dpi). */\n fontSizePx?: number;\n /** Body text color (any CSS color string). */\n color?: string;\n /** Default paragraph line height (multiplier). */\n lineHeight?: number;\n /** Heading font family (Title/Heading1/Heading2). Omit to keep the built-in. */\n headingFontFamily?: string;\n}\n\n/** Built-in body run formatting \u2014 Georgia 16px on near-black (#202124). */\nexport function makeDefaultCharStyle(t: EditorTypography = {}): CharStyle {\n return {\n fontFamily: t.fontFamily ?? \"Georgia, serif\",\n fontSizePx: t.fontSizePx ?? 16,\n bold: false,\n italic: false,\n underline: false,\n strikethrough: false,\n color: t.color ?? \"#202124\",\n };\n}\n\n/** Built-in paragraph formatting \u2014 left, 1.5 line height, 12px after. */\nexport function makeDefaultParaStyle(t: EditorTypography = {}): ParaStyle {\n return {\n align: \"left\",\n lineHeight: t.lineHeight ?? 1.5,\n spaceBeforePx: 0,\n spaceAfterPx: 12,\n indentFirstLinePx: 0,\n indentLeftPx: 0,\n };\n}\n\n/** Default body run formatting (built-in, no overrides). */\nexport const DEFAULT_CHAR_STYLE: CharStyle = makeDefaultCharStyle();\n\n/** Default paragraph formatting (built-in, no overrides). */\nexport const DEFAULT_PARA_STYLE: ParaStyle = makeDefaultParaStyle();\n", "// All mutation flows through operations. Applying an op returns the new document\r\n// (structurally shared), the op's exact INVERSE (undo is free), and a position\r\n// mapper so selections survive edits. This seam is also where OT/CRDT\r\n// collaboration would slot in later.\r\n\r\nimport type {\r\n BandContainer,\r\n Block,\r\n BookmarkRange,\r\n CharStyle,\r\n Document,\r\n EquationBlock,\r\n FieldDef,\r\n ImageBlock,\r\n ParaStyle,\r\n Paragraph,\r\n Run,\r\n RowProps,\r\n SdtProps,\r\n TableBlock,\r\n TableCell,\r\n TableRow,\r\n} from \"./document\";\r\nimport { BAND_CONTAINERS } from \"./document\";\r\nimport type { MathEquation } from \"./math\";\r\nimport { sdtPathEq } from \"./sdt\";\r\nimport { DEFAULT_CHAR_STYLE } from \"./defaults\";\r\nimport type { DocPosition } from \"./position\";\r\nimport {\r\n locateParagraph,\r\n paragraphAt,\r\n replaceParagraphAt,\r\n styleAtRuns,\r\n textOfRuns,\r\n type ParaLocation,\r\n} from \"./text\";\r\n\r\n/** setImageProps payload. `wrap` and `anchor` are mutually-exclusive states, so\r\n * each accepts `null` as an explicit \"clear this field\" sentinel (plain\r\n * `undefined` means \"leave unchanged\", which exactOptionalPropertyTypes also\r\n * needs to keep these distinguishable). */\r\nexport interface ImagePropsPatch {\r\n widthPx?: number;\r\n heightPx?: number;\r\n align?: ImageBlock[\"align\"];\r\n wrap?: ImageBlock[\"wrap\"] | null;\r\n anchor?: ImageBlock[\"anchor\"] | null;\r\n /** Crop insets (a:srcRect 0..1 fractions). `null` clears the crop. */\r\n crop?: ImageBlock[\"crop\"] | null;\r\n}\r\n\r\n/** setTableProps payload \u2014 table-LEVEL fields (w:tblPr): indent + the cascade\r\n * defaults (borders/shading/cell margins). Each key follows the setImageProps\r\n * convention: a value sets the field, `null` clears it, and an absent key leaves\r\n * it unchanged (exactOptionalPropertyTypes keeps the two distinguishable). */\r\nexport interface TablePropsPatch {\r\n indentPx?: number | null;\r\n defaultBorders?: import(\"./document\").TableBorders | null;\r\n defaultShading?: string | null;\r\n defaultCellMargin?: import(\"./document\").CellMargin | null;\r\n}\r\n\r\n/** Clamp crop insets to the documented invariant \u2014 each in [0,1) with a non-empty\r\n * visible window \u2014 so a malformed local/remote patch can't persist impossible\r\n * values (negative, NaN, or a fully-collapsed window) in the shared model. */\r\nfunction normalizeCrop(c: NonNullable<ImageBlock[\"crop\"]>): NonNullable<ImageBlock[\"crop\"]> {\r\n const clamp = (v: number): number => (Number.isFinite(v) ? Math.min(0.999, Math.max(0, v)) : 0);\r\n let { left, top, right, bottom } = { left: clamp(c.left), top: clamp(c.top), right: clamp(c.right), bottom: clamp(c.bottom) };\r\n if (left + right >= 1) {\r\n const k = 0.999 / (left + right);\r\n left *= k;\r\n right *= k;\r\n }\r\n if (top + bottom >= 1) {\r\n const k = 0.999 / (top + bottom);\r\n top *= k;\r\n bottom *= k;\r\n }\r\n return { left, top, right, bottom };\r\n}\r\n\r\nexport type Op =\r\n | { type: \"insertText\"; at: DocPosition; text: string; style?: CharStyle }\r\n | { type: \"insertRuns\"; at: DocPosition; runs: Run[] }\r\n | { type: \"deleteRange\"; blockId: string; start: number; end: number }\r\n | { type: \"setRuns\"; blockId: string; runs: Run[] }\r\n | { type: \"setParaStyle\"; blockId: string; patch: Partial<ParaStyle> }\r\n | { type: \"splitParagraph\"; at: DocPosition; newBlockId: string; newStyle?: ParaStyle; newSdtPath?: string[] }\r\n | { type: \"mergeParagraphs\"; firstBlockId: string }\r\n | { type: \"insertBlock\"; index: number; block: Block; where?: Container }\r\n | { type: \"removeBlock\"; blockId: string }\r\n | { type: \"setImageProps\"; blockId: string; patch: ImagePropsPatch }\r\n | { type: \"setEquation\"; blockId: string; equation: MathEquation }\r\n | { type: \"setEquationAlign\"; blockId: string; align: \"left\" | \"center\" | \"right\" }\r\n | { type: \"setTableRow\"; tableId: string; rowIndex: number; row: TableRow }\r\n | { type: \"setTableStructure\"; tableId: string; rows: TableRow[]; colFractions?: number[] }\r\n | { type: \"setTableStyleRef\"; tableId: string; styleId: string | null; condOverrides?: import(\"./document\").TableCondOverrides | null }\r\n | { type: \"setTableColFractions\"; blockId: string; fractions: number[] }\r\n | { type: \"setTableWidthMode\"; blockId: string; mode: TableBlock[\"widthMode\"] }\r\n | { type: \"setTablePreferredWidth\"; blockId: string; width: TableBlock[\"preferredWidth\"] | null }\r\n | { type: \"setTableAlign\"; blockId: string; align: TableBlock[\"align\"] | null }\r\n | { type: \"setTableProps\"; blockId: string; patch: TablePropsPatch }\r\n | { type: \"insertTableRow\"; tableId: string; rowIndex: number; row: TableRow }\r\n | { type: \"removeTableRow\"; tableId: string; rowIndex: number }\r\n | { type: \"setRowHeight\"; tableId: string; rowIndex: number; height: NonNullable<RowProps[\"height\"]> | null }\r\n | { type: \"insertTableColumn\"; tableId: string; colIndex: number; cells: TableCell[]; fractions?: number[] }\r\n | { type: \"removeTableColumn\"; tableId: string; colIndex: number }\r\n | { type: \"setStylesheet\"; stylesheet: import(\"./stylesheet\").Stylesheet }\r\n | { type: \"setTableStyleSheet\"; tableStyles: Record<string, import(\"./tableStyles\").TableStyle> }\r\n | { type: \"setListDefinition\"; listId: string; def: import(\"./lists\").ListDefinition | null }\r\n | { type: \"setSectionProps\"; geometry: SectionGeometry }\r\n | { type: \"setSectionBand\"; band: BandContainer; blocks: Block[] | null }\r\n | { type: \"setFootnote\"; noteId: string; paras: Paragraph[] | null }\r\n | { type: \"setEndnote\"; noteId: string; paras: Paragraph[] | null }\r\n | { type: \"setSdtProps\"; id: string; props: SdtProps | null }\r\n | { type: \"setField\"; id: string; def: FieldDef | null }\r\n | { type: \"setTocInstruction\"; instruction: string | null }\r\n | { type: \"setBookmark\"; name: string; range: BookmarkRange | null }\r\n | { type: \"setDocument\"; doc: Document };\r\n\r\n/** Page-setup fields of the final section (`doc.section`). Bands are NOT here \u2014\r\n * they change through container ops; mid-document sections change through\r\n * setParaStyle on their break paragraph's `sectionBreak.props`. */\r\nexport interface SectionGeometry {\r\n pageWidthPx: number;\r\n pageHeightPx: number;\r\n marginPx: { top: number; right: number; bottom: number; left: number };\r\n /** `null` = single column (the explicit \"off\" \u2014 SectionPatch distinguishes it\r\n * from \"inherit\"). `sep`/`cols` carry the separator line and per-column\r\n * widths respectively. */\r\n columns: { count: number; gapPx: number; sep?: boolean; cols?: import(\"./document\").ColumnEntry[] } | null;\r\n /** `null` = continue numbering from the previous section. */\r\n pageNumberStart: number | null;\r\n /** `null` = inherit (center band in margin). px from page top to header top. */\r\n headerDistancePx: number | null;\r\n /** `null` = inherit (center band in margin). px from page bottom to footer bottom. */\r\n footerDistancePx: number | null;\r\n /** `null` = no page fill. \"#rrggbb\". */\r\n pageColorHex: string | null;\r\n /** `null` = no page border. */\r\n pageBorders: import(\"./document\").PageBorders | null;\r\n /** OOXML w:sectPr/w:type for the section START. `null` = the default (\"nextPage\");\r\n * \"evenPage\"/\"oddPage\" force the section's first page onto an even/odd page. */\r\n breakType: import(\"./document\").SectionBreakType | null;\r\n /** w:sectPr/w:lnNumType \u2014 line numbering in the margin. `null` = off. */\r\n lineNumbering: import(\"./document\").LineNumbering | null;\r\n}\r\n\r\n/** Top-level block containers: the body, or one of the six margin-band stories\r\n * (default header/footer + first/even variants). */\r\nexport type Container = \"body\" | BandContainer;\r\n\r\nexport interface ApplyResult {\r\n doc: Document;\r\n inverse: Op;\r\n /** Remaps any stored position across this edit (selection, bookmarks). */\r\n mapPosition(pos: DocPosition): DocPosition;\r\n dirtyBlockIds: string[];\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Run-list surgery (pure helpers)\r\n\r\n/** Compare two w:sym markers by value (font + code point). */\r\nfunction symbolEq(a: CharStyle[\"symbol\"], b: CharStyle[\"symbol\"]): boolean {\r\n if (!a || !b) return !a === !b;\r\n return a.font === b.font && a.char === b.char;\r\n}\r\n\r\nexport function styleEq(a: CharStyle, b: CharStyle): boolean {\r\n return (\r\n a.fontFamily === b.fontFamily &&\r\n a.fontFamilyComplexScript === b.fontFamilyComplexScript &&\r\n a.fontFamilyEastAsia === b.fontFamilyEastAsia &&\r\n a.fontSizePx === b.fontSizePx &&\r\n a.bold === b.bold &&\r\n a.italic === b.italic &&\r\n a.underline === b.underline &&\r\n a.underlineStyle === b.underlineStyle &&\r\n a.underlineColor === b.underlineColor &&\r\n a.strikethrough === b.strikethrough &&\r\n a.color === b.color &&\r\n !!a.hidden === !!b.hidden && // hidden runs must never merge with visible ones\r\n (a.letterSpacingPx ?? 0) === (b.letterSpacingPx ?? 0) &&\r\n a.highlightColor === b.highlightColor &&\r\n a.verticalAlign === b.verticalAlign &&\r\n !!a.caps === !!b.caps && // case transforms change the rendered glyphs \u2014 never merge across\r\n !!a.smallCaps === !!b.smallCaps &&\r\n a.link === b.link &&\r\n a.footnoteRef === b.footnoteRef && // adjacent refs must never merge into one run\r\n sdtPathEq(a.sdtPath, b.sdtPath) && // content-control boundaries (incl. nesting) survive normalization\r\n a.fieldId === b.fieldId && // inline-field boundaries survive normalization\r\n a.equation === b.equation && // inline equations are atomic \u2014 never merge (reference identity)\r\n // Minor run typography & effects (w:rPr extras) \u2014 compared so styled runs never\r\n // merge with plain ones (and a struck-through pair keeps its boundary).\r\n !!a.doubleStrikethrough === !!b.doubleStrikethrough &&\r\n (a.positionPx ?? 0) === (b.positionPx ?? 0) &&\r\n (a.kerningMinPx ?? 0) === (b.kerningMinPx ?? 0) &&\r\n (a.widthScalePct ?? 100) === (b.widthScalePct ?? 100) &&\r\n a.emphasisMark === b.emphasisMark &&\r\n !!a.outline === !!b.outline &&\r\n !!a.shadow === !!b.shadow &&\r\n !!a.emboss === !!b.emboss &&\r\n !!a.imprint === !!b.imprint &&\r\n // w:snapToGrid defaults ON, so compare with a true default \u2014 an explicit OFF\r\n // run must not merge with a plain (default-on) neighbour (issue #161).\r\n (a.snapToGrid ?? true) === (b.snapToGrid ?? true) &&\r\n (a.fitTextPx ?? 0) === (b.fitTextPx ?? 0) &&\r\n langEq(a.lang, b.lang) && // proofing language (w:lang) \u2014 runs with different tags must not merge\r\n runBorderEq(a.runBorder, b.runBorder) &&\r\n symbolEq(a.symbol, b.symbol) // symbol glyphs (w:sym) carry font+codepoint \u2014 never merge with text\r\n );\r\n}\r\n\r\n/** Structural equality for proofing language (w:lang) \u2014 runs with different\r\n * val/eastAsia/bidi tags must not merge; absent === absent. */\r\nfunction langEq(a: CharStyle[\"lang\"], b: CharStyle[\"lang\"]): boolean {\r\n if (a === b) return true;\r\n if (!a || !b) return false;\r\n return a.val === b.val && a.eastAsia === b.eastAsia && a.bidi === b.bidi;\r\n}\r\n\r\n/** Structural equality for a run border (w:bdr) \u2014 runs with differently-styled\r\n * borders must not merge, but two imported runs with identical borders may. */\r\nfunction runBorderEq(a: CharStyle[\"runBorder\"], b: CharStyle[\"runBorder\"]): boolean {\r\n if (a === b) return true;\r\n if (!a || !b) return false;\r\n return a.color === b.color && a.widthPx === b.widthPx && a.style === b.style;\r\n}\r\n\r\n/** Merge equal-styled neighbors, drop empties. An all-empty paragraph keeps ONE\r\n * empty run so the paragraph mark still carries a style (Word behavior). */\r\nexport function normalizeRuns(runs: Run[], fallback: CharStyle): Run[] {\r\n const nonEmpty = runs.filter((r) => r.text.length > 0);\r\n if (nonEmpty.length === 0) return [{ text: \"\", style: runs[0]?.style ?? fallback }];\r\n const out: Run[] = [{ ...nonEmpty[0]! }];\r\n for (let i = 1; i < nonEmpty.length; i++) {\r\n const r = nonEmpty[i]!;\r\n const last = out[out.length - 1]!;\r\n if (styleEq(last.style, r.style)) last.text += r.text;\r\n else out.push({ ...r });\r\n }\r\n return out;\r\n}\r\n\r\n/** Split a run list at a UTF-16 offset. */\r\nexport function splitRunsAt(runs: Run[], offset: number): [Run[], Run[]] {\r\n const head: Run[] = [];\r\n const tail: Run[] = [];\r\n let cum = 0;\r\n for (const r of runs) {\r\n const end = cum + r.text.length;\r\n if (end <= offset) head.push(r);\r\n else if (cum >= offset) tail.push(r);\r\n else {\r\n head.push({ text: r.text.slice(0, offset - cum), style: r.style });\r\n tail.push({ text: r.text.slice(offset - cum), style: r.style });\r\n }\r\n cum = end;\r\n }\r\n return [head, tail];\r\n}\r\n\r\nexport function sliceRuns(runs: Run[], start: number, end: number): Run[] {\r\n const [, fromStart] = splitRunsAt(runs, start);\r\n const [middle] = splitRunsAt(fromStart, end - start);\r\n return middle;\r\n}\r\n\r\nfunction fallbackStyle(runs: Run[], offset: number): CharStyle {\r\n return styleAtRuns(runs, offset) ?? { ...DEFAULT_CHAR_STYLE };\r\n}\r\n\r\nexport function insertTextInRuns(runs: Run[], offset: number, text: string, style?: CharStyle): Run[] {\r\n const st = style ?? fallbackStyle(runs, offset);\r\n const [head, tail] = splitRunsAt(runs, offset);\r\n return normalizeRuns([...head, { text, style: st }, ...tail], st);\r\n}\r\n\r\nexport function insertRunsInRuns(runs: Run[], offset: number, inserted: Run[]): Run[] {\r\n const [head, tail] = splitRunsAt(runs, offset);\r\n return normalizeRuns([...head, ...inserted, ...tail], fallbackStyle(runs, offset));\r\n}\r\n\r\nexport function deleteInRuns(runs: Run[], start: number, end: number): Run[] {\r\n const [head, fromStart] = splitRunsAt(runs, start);\r\n const [, tail] = splitRunsAt(fromStart, end - start);\r\n return normalizeRuns([...head, ...tail], fallbackStyle(runs, start));\r\n}\r\n\r\nexport function applyStylePatchToRuns(\r\n runs: Run[],\r\n start: number,\r\n end: number,\r\n patch: Partial<CharStyle>,\r\n): Run[] {\r\n const [head, fromStart] = splitRunsAt(runs, start);\r\n const [middle, tail] = splitRunsAt(fromStart, end - start);\r\n const styled = middle.map((r) => ({ text: r.text, style: { ...r.style, ...patch } }));\r\n return normalizeRuns([...head, ...styled, ...tail], fallbackStyle(runs, start));\r\n}\r\n\r\n/** Rewrite the TEXT of runs in [start,end) through `fn`, preserving each run's\r\n * style. `fn` receives the whole covered string (so it can do context-aware\r\n * transforms like sentence case) and must return a string of the SAME length \u2014\r\n * if the length changes the slice is left untouched (run offsets must stay\r\n * stable). Used by change-case; transforms here are 1:1 per character. */\r\nexport function mapTextInRuns(\r\n runs: Run[],\r\n start: number,\r\n end: number,\r\n fn: (covered: string) => string,\r\n): Run[] {\r\n const [head, fromStart] = splitRunsAt(runs, start);\r\n const [middle, tail] = splitRunsAt(fromStart, end - start);\r\n const original = middle.map((r) => r.text).join(\"\");\r\n const next = fn(original);\r\n let i = 0;\r\n const out =\r\n next.length === original.length\r\n ? middle.map((r) => {\r\n const text = next.slice(i, i + r.text.length);\r\n i += r.text.length;\r\n return { text, style: r.style };\r\n })\r\n : middle;\r\n return normalizeRuns([...head, ...out, ...tail], fallbackStyle(runs, start));\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// applyOp\r\n\r\nconst identity = (p: DocPosition): DocPosition => p;\r\n\r\n/** Content ops accept ANY paragraph (top-level or table cell). */\r\nfunction mustLocate(doc: Document, blockId: string): { loc: ParaLocation; block: Paragraph } {\r\n const loc = locateParagraph(doc, blockId);\r\n if (!loc) throw new Error(`paragraph ${blockId} not found`);\r\n return { loc, block: paragraphAt(doc, loc) };\r\n}\r\n\r\n// ---- container plumbing: structural ops work on the body OR a band story ----\r\n\r\nexport function containerBlocks(doc: Document, where: Container): Block[] {\r\n return where === \"body\" ? doc.blocks : (doc.section[where] ?? []);\r\n}\r\n\r\nfunction withContainerBlocks(doc: Document, where: Container, blocks: Block[]): Document {\r\n if (where === \"body\") return { ...doc, blocks };\r\n return { ...doc, section: { ...doc.section, [where]: blocks } };\r\n}\r\n\r\n/** Which container holds this TOP-LEVEL block (paragraphs in table cells are\r\n * not top-level and return null). */\r\nexport function containerOf(doc: Document, blockId: string): { where: Container; index: number } | null {\r\n for (const where of [\"body\", ...BAND_CONTAINERS] as Container[]) {\r\n const index = containerBlocks(doc, where).findIndex((b) => b.id === blockId);\r\n if (index >= 0) return { where, index };\r\n }\r\n return null;\r\n}\r\n\r\nfunction mustTable(doc: Document, blockId: string): { where: Container; bi: number; block: TableBlock } {\r\n const found = containerOf(doc, blockId);\r\n const block = found ? containerBlocks(doc, found.where)[found.index] : undefined;\r\n if (!found || !block || block.kind !== \"table\") throw new Error(`table ${blockId} not found`);\r\n return { where: found.where, bi: found.index, block };\r\n}\r\n\r\nfunction replaceTable(doc: Document, where: Container, bi: number, table: TableBlock): Document {\r\n const blocks = containerBlocks(doc, where).slice();\r\n blocks[bi] = { ...table, revision: table.revision + 1 };\r\n return withContainerBlocks(doc, where, blocks);\r\n}\r\n\r\n/** Path-clone a table (body or band) replacing one cell's block list\r\n * (revision bumped). */\r\nfunction replaceCellBlocks(\r\n doc: Document,\r\n where: Container,\r\n bi: number,\r\n ri: number,\r\n ci: number,\r\n cellBlocks: Block[],\r\n): Document {\r\n const blocks = containerBlocks(doc, where).slice();\r\n const table = blocks[bi] as TableBlock;\r\n const rows = table.rows.slice();\r\n const row = { cells: rows[ri]!.cells.slice() };\r\n row.cells[ci] = { ...row.cells[ci]!, blocks: cellBlocks };\r\n rows[ri] = row;\r\n blocks[bi] = { ...table, rows, revision: table.revision + 1 };\r\n return withContainerBlocks(doc, where, blocks);\r\n}\r\n\r\n/** First caret-capable paragraph in a set of rows (cells may start with images). */\r\nexport function firstParagraphInRows(rows: TableRow[]): Paragraph | undefined {\r\n for (const row of rows) {\r\n for (const cell of row.cells) {\r\n for (const b of cell.blocks) if (b.kind === \"paragraph\") return b;\r\n }\r\n }\r\n return undefined;\r\n}\r\n\r\n/** Find an image block anywhere editable: top-level containers or table cells. */\r\nexport type ImageLocation =\r\n | { kind: \"top\"; where: Container; index: number; image: ImageBlock }\r\n | { kind: \"cell\"; bi: number; ri: number; ci: number; ii: number; image: ImageBlock };\r\n\r\nexport function locateImage(doc: Document, blockId: string): ImageLocation | null {\r\n const found = containerOf(doc, blockId);\r\n if (found) {\r\n const block = containerBlocks(doc, found.where)[found.index];\r\n if (block?.kind === \"image\") return { kind: \"top\", where: found.where, index: found.index, image: block };\r\n return null;\r\n }\r\n for (let bi = 0; bi < doc.blocks.length; bi++) {\r\n const b = doc.blocks[bi]!;\r\n if (b.kind !== \"table\") continue;\r\n for (let ri = 0; ri < b.rows.length; ri++) {\r\n const row = b.rows[ri]!;\r\n for (let ci = 0; ci < row.cells.length; ci++) {\r\n const cell = row.cells[ci]!;\r\n for (let ii = 0; ii < cell.blocks.length; ii++) {\r\n const cb = cell.blocks[ii]!;\r\n if (cb.kind === \"image\" && cb.id === blockId) return { kind: \"cell\", bi, ri, ci, ii, image: cb };\r\n }\r\n }\r\n }\r\n }\r\n return null;\r\n}\r\n\r\n/** Where a block lives: top-level in a container (body or a header/footer band)\r\n * or one level deep in a table cell. The path carries the container so callers\r\n * can clone the right story. */\r\nexport type BlockLocation<B extends Block> =\r\n | { kind: \"top\"; where: Container; index: number; block: B }\r\n | { kind: \"cell\"; where: Container; bi: number; ri: number; ci: number; ii: number; block: B };\r\n\r\n/** Find a block of a given kind anywhere editable \u2014 a top-level block of any\r\n * container (body or a header/footer band) OR a block nested one level deep in\r\n * a table cell (of a body OR band table). The container-aware generalization of\r\n * locateImage; equation editing/alignment/deletion resolve through it so display\r\n * equations imported into cells or bands stay live. Returns null when no block\r\n * with that id and kind exists. */\r\nexport function locateBlock<K extends Block[\"kind\"]>(\r\n doc: Document,\r\n blockId: string,\r\n blockKind: K,\r\n): BlockLocation<Extract<Block, { kind: K }>> | null {\r\n type B = Extract<Block, { kind: K }>;\r\n const found = containerOf(doc, blockId);\r\n if (found) {\r\n const block = containerBlocks(doc, found.where)[found.index];\r\n if (block?.kind === blockKind) {\r\n return { kind: \"top\", where: found.where, index: found.index, block: block as B };\r\n }\r\n return null; // id is a top-level block, but not of the requested kind\r\n }\r\n // Not top-level \u2014 scan table cells in the body and every band story.\r\n for (const where of [\"body\", ...BAND_CONTAINERS] as Container[]) {\r\n const blocks = containerBlocks(doc, where);\r\n for (let bi = 0; bi < blocks.length; bi++) {\r\n const b = blocks[bi]!;\r\n if (b.kind !== \"table\") continue;\r\n for (let ri = 0; ri < b.rows.length; ri++) {\r\n const row = b.rows[ri]!;\r\n for (let ci = 0; ci < row.cells.length; ci++) {\r\n const cell = row.cells[ci]!;\r\n for (let ii = 0; ii < cell.blocks.length; ii++) {\r\n const cb = cell.blocks[ii]!;\r\n if (cb.kind === blockKind && cb.id === blockId) {\r\n return { kind: \"cell\", where, bi, ri, ci, ii, block: cb as B };\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n return null;\r\n}\r\n\r\n/** Locate a display-equation block anywhere editable (body / band / table cell). */\r\nexport function locateEquation(doc: Document, blockId: string): BlockLocation<EquationBlock> | null {\r\n return locateBlock(doc, blockId, \"equation\");\r\n}\r\n\r\n/** Path-clone the document replacing a located block in place (top-level in any\r\n * container, or one level deep in a table cell). */\r\nfunction replaceLocatedBlock(doc: Document, loc: BlockLocation<Block>, block: Block): Document {\r\n if (loc.kind === \"top\") {\r\n const blocks = containerBlocks(doc, loc.where).slice();\r\n blocks[loc.index] = block;\r\n return withContainerBlocks(doc, loc.where, blocks);\r\n }\r\n const table = containerBlocks(doc, loc.where)[loc.bi] as TableBlock;\r\n const cellBlocks = table.rows[loc.ri]!.cells[loc.ci]!.blocks.slice();\r\n cellBlocks[loc.ii] = block;\r\n return replaceCellBlocks(doc, loc.where, loc.bi, loc.ri, loc.ci, cellBlocks);\r\n}\r\n\r\n/** The table's true grid-column count: the widest row measured in GRID columns,\r\n * not cells \u2014 colSpan widens a cell and a rowSpan from an earlier row leaves a\r\n * hole that this row's cells shift past (HTML table model). Counting cells\r\n * alone undercounts any row with a colSpan or a vertical-merge hole. */\r\nexport function gridColumnCount(t: TableBlock): number {\r\n let maxCols = 1;\r\n const rowsRemaining: number[] = [];\r\n for (const row of t.rows) {\r\n let col = 0;\r\n for (const cell of row.cells) {\r\n while ((rowsRemaining[col] ?? 0) > 0) col++;\r\n const span = Math.max(1, cell.colSpan ?? 1);\r\n const rowSpan = Math.max(1, cell.rowSpan ?? 1);\r\n if (rowSpan > 1) for (let k = 0; k < span; k++) rowsRemaining[col + k] = rowSpan;\r\n col += span;\r\n }\r\n maxCols = Math.max(maxCols, col);\r\n for (let c = 0; c < rowsRemaining.length; c++) if (rowsRemaining[c]! > 0) rowsRemaining[c]!--;\r\n }\r\n return maxCols;\r\n}\r\n\r\n/** Column fractions normalized to the table's column count. */\r\nexport function effectiveFractions(t: TableBlock): number[] {\r\n const n = gridColumnCount(t);\r\n if (t.colFractions && t.colFractions.length === n) return t.colFractions;\r\n return Array.from({ length: n }, () => 1 / n);\r\n}\r\n\r\nfunction bump(block: Paragraph, runs: Run[]): Paragraph {\r\n return { ...block, runs, revision: block.revision + 1 };\r\n}\r\n\r\nexport function applyOp(doc: Document, op: Op): ApplyResult {\r\n switch (op.type) {\r\n case \"insertText\": {\r\n const { loc, block } = mustLocate(doc, op.at.blockId);\r\n const runs = insertTextInRuns(block.runs, op.at.offset, op.text, op.style);\r\n const len = op.text.length;\r\n return {\r\n doc: replaceParagraphAt(doc, loc, bump(block, runs)),\r\n inverse: { type: \"deleteRange\", blockId: block.id, start: op.at.offset, end: op.at.offset + len },\r\n mapPosition: (p) =>\r\n p.blockId === block.id && p.offset >= op.at.offset\r\n ? { blockId: p.blockId, offset: p.offset + len }\r\n : p,\r\n dirtyBlockIds: [block.id],\r\n };\r\n }\r\n\r\n case \"insertRuns\": {\r\n const { loc, block } = mustLocate(doc, op.at.blockId);\r\n const len = textOfRuns(op.runs).length;\r\n const runs = insertRunsInRuns(block.runs, op.at.offset, op.runs);\r\n return {\r\n doc: replaceParagraphAt(doc, loc, bump(block, runs)),\r\n inverse: { type: \"deleteRange\", blockId: block.id, start: op.at.offset, end: op.at.offset + len },\r\n mapPosition: (p) =>\r\n p.blockId === block.id && p.offset >= op.at.offset\r\n ? { blockId: p.blockId, offset: p.offset + len }\r\n : p,\r\n dirtyBlockIds: [block.id],\r\n };\r\n }\r\n\r\n case \"deleteRange\": {\r\n const { loc, block } = mustLocate(doc, op.blockId);\r\n const removed = sliceRuns(block.runs, op.start, op.end);\r\n const runs = deleteInRuns(block.runs, op.start, op.end);\r\n const len = op.end - op.start;\r\n return {\r\n doc: replaceParagraphAt(doc, loc, bump(block, runs)),\r\n inverse: { type: \"insertRuns\", at: { blockId: block.id, offset: op.start }, runs: removed },\r\n mapPosition: (p) => {\r\n if (p.blockId !== block.id || p.offset <= op.start) return p;\r\n return { blockId: p.blockId, offset: p.offset >= op.end ? p.offset - len : op.start };\r\n },\r\n dirtyBlockIds: [block.id],\r\n };\r\n }\r\n\r\n case \"setRuns\": {\r\n const { loc, block } = mustLocate(doc, op.blockId);\r\n return {\r\n doc: replaceParagraphAt(doc, loc, bump(block, op.runs)),\r\n inverse: { type: \"setRuns\", blockId: block.id, runs: block.runs },\r\n mapPosition: identity, // callers use setRuns for length-preserving restyles\r\n dirtyBlockIds: [block.id],\r\n };\r\n }\r\n\r\n case \"setParaStyle\": {\r\n const { loc, block } = mustLocate(doc, op.blockId);\r\n const oldPatch: Partial<ParaStyle> = {};\r\n for (const key of Object.keys(op.patch) as (keyof ParaStyle)[]) {\r\n // @ts-expect-error \u2014 keyed copy of the previous values for the inverse\r\n oldPatch[key] = block.style[key];\r\n }\r\n const styled: Paragraph = {\r\n ...block,\r\n style: { ...block.style, ...op.patch },\r\n revision: block.revision + 1,\r\n };\r\n return {\r\n doc: replaceParagraphAt(doc, loc, styled),\r\n inverse: { type: \"setParaStyle\", blockId: block.id, patch: oldPatch },\r\n mapPosition: identity,\r\n dirtyBlockIds: [block.id],\r\n };\r\n }\r\n\r\n case \"splitParagraph\": {\r\n const loc = locateParagraph(doc, op.at.blockId);\r\n if (!loc) throw new Error(`paragraph ${op.at.blockId} not found`);\r\n const block = paragraphAt(doc, loc);\r\n const [headRuns, tailRuns] = splitRunsAt(block.runs, op.at.offset);\r\n const carry = fallbackStyle(block.runs, op.at.offset);\r\n const head = bump(block, normalizeRuns(headRuns, carry));\r\n const tail: Paragraph = {\r\n kind: \"paragraph\",\r\n id: op.newBlockId,\r\n revision: 0,\r\n runs: normalizeRuns(tailRuns, carry),\r\n style: op.newStyle ?? { ...block.style },\r\n // The tail's block-level content control is declared by the op (the split\r\n // command carries the source paragraph's path; the merge inverse restores\r\n // the original tail's). Absent \u2192 the tail belongs to no block control.\r\n ...(op.newSdtPath?.length ? { sdtPath: op.newSdtPath } : {}),\r\n };\r\n let next: Document;\r\n if (loc.kind === \"cell\") {\r\n const table = containerBlocks(doc, loc.where)[loc.bi] as TableBlock;\r\n const cellBlocks = table.rows[loc.ri]!.cells[loc.ci]!.blocks.slice();\r\n cellBlocks.splice(loc.pi, 1, head, tail);\r\n next = replaceCellBlocks(doc, loc.where, loc.bi, loc.ri, loc.ci, cellBlocks);\r\n } else if (loc.kind === \"footnote\") {\r\n const paras = doc.footnotes![loc.noteId]!.slice();\r\n paras.splice(loc.pi, 1, head, tail);\r\n next = { ...doc, footnotes: { ...doc.footnotes, [loc.noteId]: paras } };\r\n } else if (loc.kind === \"endnote\") {\r\n const paras = doc.endnotes![loc.noteId]!.slice();\r\n paras.splice(loc.pi, 1, head, tail);\r\n next = { ...doc, endnotes: { ...doc.endnotes, [loc.noteId]: paras } };\r\n } else {\r\n const where: Container = loc.kind === \"band\" ? loc.band : \"body\";\r\n const blocks = containerBlocks(doc, where).slice();\r\n blocks.splice(loc.bi, 1, head, tail);\r\n next = withContainerBlocks(doc, where, blocks);\r\n }\r\n return {\r\n doc: next,\r\n inverse: { type: \"mergeParagraphs\", firstBlockId: block.id },\r\n mapPosition: (p) =>\r\n p.blockId === block.id && p.offset >= op.at.offset\r\n ? { blockId: op.newBlockId, offset: p.offset - op.at.offset }\r\n : p,\r\n dirtyBlockIds: [block.id, op.newBlockId],\r\n };\r\n }\r\n\r\n case \"mergeParagraphs\": {\r\n const loc = locateParagraph(doc, op.firstBlockId);\r\n if (!loc) throw new Error(`paragraph ${op.firstBlockId} not found`);\r\n const block = paragraphAt(doc, loc);\r\n let nextPara: Paragraph;\r\n if (loc.kind === \"cell\") {\r\n const table = containerBlocks(doc, loc.where)[loc.bi] as TableBlock;\r\n const candidate = table.rows[loc.ri]!.cells[loc.ci]!.blocks[loc.pi + 1];\r\n if (!candidate || candidate.kind !== \"paragraph\") {\r\n throw new Error(\"mergeParagraphs: no next paragraph in cell\");\r\n }\r\n nextPara = candidate;\r\n } else if (loc.kind === \"footnote\") {\r\n const candidate = doc.footnotes![loc.noteId]![loc.pi + 1];\r\n if (!candidate) throw new Error(\"mergeParagraphs: no next paragraph in footnote\");\r\n nextPara = candidate;\r\n } else if (loc.kind === \"endnote\") {\r\n const candidate = doc.endnotes![loc.noteId]![loc.pi + 1];\r\n if (!candidate) throw new Error(\"mergeParagraphs: no next paragraph in endnote\");\r\n nextPara = candidate;\r\n } else {\r\n const where: Container = loc.kind === \"band\" ? loc.band : \"body\";\r\n const candidate = containerBlocks(doc, where)[loc.bi + 1];\r\n if (!candidate || candidate.kind !== \"paragraph\") {\r\n throw new Error(\"mergeParagraphs: no next paragraph\");\r\n }\r\n nextPara = candidate;\r\n }\r\n const headLen = textOfRuns(block.runs).length;\r\n const merged = bump(\r\n block,\r\n normalizeRuns([...block.runs, ...nextPara.runs], fallbackStyle(block.runs, headLen)),\r\n );\r\n let next: Document;\r\n if (loc.kind === \"cell\") {\r\n const table = containerBlocks(doc, loc.where)[loc.bi] as TableBlock;\r\n const cellBlocks = table.rows[loc.ri]!.cells[loc.ci]!.blocks.slice();\r\n cellBlocks.splice(loc.pi, 2, merged);\r\n next = replaceCellBlocks(doc, loc.where, loc.bi, loc.ri, loc.ci, cellBlocks);\r\n } else if (loc.kind === \"footnote\") {\r\n const paras = doc.footnotes![loc.noteId]!.slice();\r\n paras.splice(loc.pi, 2, merged);\r\n next = { ...doc, footnotes: { ...doc.footnotes, [loc.noteId]: paras } };\r\n } else if (loc.kind === \"endnote\") {\r\n const paras = doc.endnotes![loc.noteId]!.slice();\r\n paras.splice(loc.pi, 2, merged);\r\n next = { ...doc, endnotes: { ...doc.endnotes, [loc.noteId]: paras } };\r\n } else {\r\n const where: Container = loc.kind === \"band\" ? loc.band : \"body\";\r\n const blocks = containerBlocks(doc, where).slice();\r\n blocks.splice(loc.bi, 2, merged);\r\n next = withContainerBlocks(doc, where, blocks);\r\n }\r\n return {\r\n doc: next,\r\n inverse: {\r\n type: \"splitParagraph\",\r\n at: { blockId: block.id, offset: headLen },\r\n newBlockId: nextPara.id,\r\n newStyle: nextPara.style,\r\n // Restore the merged-away tail's block-level control on undo.\r\n ...(nextPara.sdtPath?.length ? { newSdtPath: nextPara.sdtPath } : {}),\r\n },\r\n mapPosition: (p) =>\r\n p.blockId === nextPara.id ? { blockId: block.id, offset: headLen + p.offset } : p,\r\n dirtyBlockIds: [block.id, nextPara.id],\r\n };\r\n }\r\n\r\n case \"insertBlock\": {\r\n const where = op.where ?? \"body\";\r\n const blocks = containerBlocks(doc, where).slice();\r\n blocks.splice(op.index, 0, op.block);\r\n return {\r\n doc: withContainerBlocks(doc, where, blocks),\r\n inverse: { type: \"removeBlock\", blockId: op.block.id },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.block.id],\r\n };\r\n }\r\n\r\n case \"removeBlock\": {\r\n const found = containerOf(doc, op.blockId);\r\n if (!found) throw new Error(`block ${op.blockId} not found`);\r\n const blocks = containerBlocks(doc, found.where).slice();\r\n const block = blocks[found.index]!;\r\n blocks.splice(found.index, 1);\r\n const neighbor = blocks[Math.min(found.index, blocks.length - 1)];\r\n return {\r\n doc: withContainerBlocks(doc, found.where, blocks),\r\n inverse: { type: \"insertBlock\", index: found.index, block, where: found.where },\r\n mapPosition: (p) =>\r\n p.blockId === op.blockId && neighbor\r\n ? { blockId: neighbor.id, offset: 0 }\r\n : p,\r\n dirtyBlockIds: [op.blockId],\r\n };\r\n }\r\n\r\n case \"setImageProps\": {\r\n const loc = locateImage(doc, op.blockId);\r\n if (!loc) throw new Error(`image ${op.blockId} not found`);\r\n const block = loc.image;\r\n // Inverse restores prior values; a field absent before is cleared (null)\r\n // on undo. wrap/anchor use null to mean \"clear\" (they're exclusive states).\r\n const oldPatch: ImagePropsPatch = {};\r\n if (op.patch.widthPx !== undefined) oldPatch.widthPx = block.widthPx;\r\n if (op.patch.heightPx !== undefined) oldPatch.heightPx = block.heightPx;\r\n if (op.patch.align !== undefined) oldPatch.align = block.align;\r\n if (op.patch.wrap !== undefined) oldPatch.wrap = block.wrap ?? null;\r\n if (op.patch.anchor !== undefined) oldPatch.anchor = block.anchor ?? null;\r\n if (op.patch.crop !== undefined) oldPatch.crop = block.crop ?? null;\r\n const updated: ImageBlock = { ...block, revision: block.revision + 1 };\r\n if (op.patch.widthPx !== undefined) updated.widthPx = op.patch.widthPx;\r\n if (op.patch.heightPx !== undefined) updated.heightPx = op.patch.heightPx;\r\n if (op.patch.align !== undefined) updated.align = op.patch.align;\r\n if (op.patch.wrap !== undefined) {\r\n if (op.patch.wrap === null) delete updated.wrap;\r\n else updated.wrap = op.patch.wrap;\r\n }\r\n if (op.patch.anchor !== undefined) {\r\n if (op.patch.anchor === null) delete updated.anchor;\r\n else updated.anchor = op.patch.anchor;\r\n }\r\n if (op.patch.crop !== undefined) {\r\n if (op.patch.crop === null) delete updated.crop;\r\n else updated.crop = normalizeCrop(op.patch.crop);\r\n }\r\n let next: Document;\r\n if (loc.kind === \"top\") {\r\n const blocks = containerBlocks(doc, loc.where).slice();\r\n blocks[loc.index] = updated;\r\n next = withContainerBlocks(doc, loc.where, blocks);\r\n } else {\r\n const table = doc.blocks[loc.bi] as TableBlock;\r\n const cellBlocks = table.rows[loc.ri]!.cells[loc.ci]!.blocks.slice();\r\n cellBlocks[loc.ii] = updated;\r\n next = replaceCellBlocks(doc, \"body\", loc.bi, loc.ri, loc.ci, cellBlocks); // locateImage cells are body-only\r\n\r\n }\r\n return {\r\n doc: next,\r\n inverse: { type: \"setImageProps\", blockId: op.blockId, patch: oldPatch },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.blockId],\r\n };\r\n }\r\n\r\n case \"setEquation\": {\r\n // Replace a display equation's MathML (the editor's Apply). Container-aware:\r\n // the equation may live in the body, a header/footer band, or a table cell.\r\n const loc = locateEquation(doc, op.blockId);\r\n if (!loc) throw new Error(`equation ${op.blockId} not found`);\r\n const old = loc.block.equation;\r\n const updated: EquationBlock = { ...loc.block, revision: loc.block.revision + 1, equation: op.equation };\r\n return {\r\n doc: replaceLocatedBlock(doc, loc, updated),\r\n inverse: { type: \"setEquation\", blockId: op.blockId, equation: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.blockId],\r\n };\r\n }\r\n\r\n case \"setEquationAlign\": {\r\n const loc = locateEquation(doc, op.blockId);\r\n if (!loc) throw new Error(`equation ${op.blockId} not found`);\r\n const old = loc.block.align ?? \"center\";\r\n const updated: EquationBlock = { ...loc.block, revision: loc.block.revision + 1, align: op.align };\r\n return {\r\n doc: replaceLocatedBlock(doc, loc, updated),\r\n inverse: { type: \"setEquationAlign\", blockId: op.blockId, align: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.blockId],\r\n };\r\n }\r\n\r\n case \"setTableColFractions\": {\r\n const { where, bi, block } = mustTable(doc, op.blockId);\r\n const old = effectiveFractions(block);\r\n return {\r\n doc: replaceTable(doc, where, bi, { ...block, colFractions: op.fractions }),\r\n inverse: { type: \"setTableColFractions\", blockId: op.blockId, fractions: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.blockId],\r\n };\r\n }\r\n\r\n case \"setTableWidthMode\": {\r\n const { where, bi, block } = mustTable(doc, op.blockId);\r\n const old = block.widthMode;\r\n const next: TableBlock = { ...block };\r\n if (op.mode && op.mode !== \"fixed\") next.widthMode = op.mode;\r\n else delete next.widthMode;\r\n return {\r\n doc: replaceTable(doc, where, bi, next),\r\n inverse: { type: \"setTableWidthMode\", blockId: op.blockId, mode: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.blockId],\r\n };\r\n }\r\n\r\n case \"setTablePreferredWidth\": {\r\n const { where, bi, block } = mustTable(doc, op.blockId);\r\n const old = block.preferredWidth;\r\n const next: TableBlock = { ...block };\r\n if (op.width) next.preferredWidth = op.width;\r\n else delete next.preferredWidth;\r\n return {\r\n doc: replaceTable(doc, where, bi, next),\r\n inverse: { type: \"setTablePreferredWidth\", blockId: op.blockId, width: old ?? null },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.blockId],\r\n };\r\n }\r\n\r\n case \"setTableAlign\": {\r\n const { where, bi, block } = mustTable(doc, op.blockId);\r\n const old = block.align;\r\n const next: TableBlock = { ...block };\r\n if (op.align && op.align !== \"left\") next.align = op.align;\r\n else delete next.align;\r\n return {\r\n doc: replaceTable(doc, where, bi, next),\r\n inverse: { type: \"setTableAlign\", blockId: op.blockId, align: old ?? null },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.blockId],\r\n };\r\n }\r\n\r\n case \"setTableProps\": {\r\n // Table-LEVEL props (indent + cascade defaults). Patch keys present are\r\n // applied (a value sets, `null` clears); the inverse captures each touched\r\n // field's prior value (null = was absent) so undo restores it exactly.\r\n const { where, bi, block } = mustTable(doc, op.blockId);\r\n const keys = Object.keys(op.patch) as (keyof TablePropsPatch)[];\r\n const oldPatch: TablePropsPatch = {};\r\n const next: Record<string, unknown> = { ...block };\r\n const blockRec = block as unknown as Record<string, unknown>;\r\n const oldRec = oldPatch as Record<string, unknown>;\r\n for (const key of keys) {\r\n oldRec[key] = blockRec[key] ?? null;\r\n const val = op.patch[key];\r\n if (val === undefined || val === null) delete next[key];\r\n else next[key] = val;\r\n }\r\n return {\r\n doc: replaceTable(doc, where, bi, next as unknown as TableBlock),\r\n inverse: { type: \"setTableProps\", blockId: op.blockId, patch: oldPatch },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.blockId],\r\n };\r\n }\r\n\r\n case \"setTableRow\": {\r\n const { where, bi, block } = mustTable(doc, op.tableId);\r\n const old = block.rows[op.rowIndex];\r\n if (!old) throw new Error(\"setTableRow: no such row\");\r\n const removedIds = new Set(old.cells.flatMap((c) => c.blocks.map((p) => p.id)));\r\n const rows = block.rows.slice();\r\n rows[op.rowIndex] = op.row;\r\n const fallback = firstParagraphInRows([op.row]);\r\n return {\r\n doc: replaceTable(doc, where, bi, { ...block, rows }),\r\n inverse: { type: \"setTableRow\", tableId: op.tableId, rowIndex: op.rowIndex, row: old },\r\n mapPosition: (p) => {\r\n if (!removedIds.has(p.blockId)) return p;\r\n // position survives if its paragraph still exists in the new row\r\n const kept = op.row.cells.some((c) => c.blocks.some((b) => b.id === p.blockId));\r\n return kept || !fallback ? p : { blockId: fallback.id, offset: 0 };\r\n },\r\n dirtyBlockIds: [op.tableId],\r\n };\r\n }\r\n\r\n case \"insertTableRow\": {\r\n const { where, bi, block } = mustTable(doc, op.tableId);\r\n const rows = block.rows.slice();\r\n rows.splice(op.rowIndex, 0, op.row);\r\n return {\r\n doc: replaceTable(doc, where, bi, { ...block, rows }),\r\n inverse: { type: \"removeTableRow\", tableId: op.tableId, rowIndex: op.rowIndex },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.tableId],\r\n };\r\n }\r\n\r\n case \"removeTableRow\": {\r\n const { where, bi, block } = mustTable(doc, op.tableId);\r\n const removed = block.rows[op.rowIndex];\r\n if (!removed) throw new Error(\"removeTableRow: no such row\");\r\n const rows = block.rows.slice();\r\n rows.splice(op.rowIndex, 1);\r\n const removedIds = new Set(removed.cells.flatMap((c) => c.blocks.map((p) => p.id)));\r\n const fallbackRow = rows[Math.min(op.rowIndex, rows.length - 1)];\r\n const fallback = fallbackRow ? firstParagraphInRows([fallbackRow]) : undefined;\r\n return {\r\n doc: replaceTable(doc, where, bi, { ...block, rows }),\r\n inverse: { type: \"insertTableRow\", tableId: op.tableId, rowIndex: op.rowIndex, row: removed },\r\n mapPosition: (p) =>\r\n removedIds.has(p.blockId) && fallback ? { blockId: fallback.id, offset: 0 } : p,\r\n dirtyBlockIds: [op.tableId],\r\n };\r\n }\r\n\r\n case \"setRowHeight\": {\r\n // Interactive row-drag height (w:trHeight). Mirrors the column op: replace a\r\n // single row's `props.height`, leaving the rest of its props untouched, and\r\n // invert to the prior height (null = none) for a free undo.\r\n const { where, bi, block } = mustTable(doc, op.tableId);\r\n const row = block.rows[op.rowIndex];\r\n if (!row) throw new Error(\"setRowHeight: no such row\");\r\n const old = row.props?.height ?? null;\r\n const nextRow: TableRow = { ...row };\r\n const props: RowProps = { ...(row.props ?? {}) };\r\n if (op.height) props.height = op.height;\r\n else delete props.height;\r\n if (Object.keys(props).length) nextRow.props = props;\r\n else delete nextRow.props;\r\n const rows = block.rows.slice();\r\n rows[op.rowIndex] = nextRow;\r\n return {\r\n doc: replaceTable(doc, where, bi, { ...block, rows }),\r\n inverse: { type: \"setRowHeight\", tableId: op.tableId, rowIndex: op.rowIndex, height: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.tableId],\r\n };\r\n }\r\n\r\n case \"setListDefinition\": {\r\n const old = doc.lists?.[op.listId] ?? null;\r\n const lists = { ...(doc.lists ?? {}) };\r\n if (op.def) lists[op.listId] = op.def;\r\n else delete lists[op.listId];\r\n return {\r\n doc: { ...doc, lists },\r\n inverse: { type: \"setListDefinition\", listId: op.listId, def: old },\r\n mapPosition: identity,\r\n // Indents come from the definition \u2192 every paragraph in the list must\r\n // re-measure; their (revision,width) line-cache keys change with width.\r\n dirtyBlockIds: [],\r\n };\r\n }\r\n\r\n case \"setTableStructure\": {\r\n const { where, bi, block } = mustTable(doc, op.tableId);\r\n const oldIds = new Set(\r\n block.rows.flatMap((r) => r.cells.flatMap((c) => c.blocks.map((p) => p.id))),\r\n );\r\n const fallback = firstParagraphInRows(op.rows);\r\n const next: TableBlock = { ...block, rows: op.rows };\r\n if (op.colFractions) next.colFractions = op.colFractions;\r\n else delete next.colFractions;\r\n const inverse: Op = { type: \"setTableStructure\", tableId: op.tableId, rows: block.rows };\r\n if (block.colFractions) inverse.colFractions = block.colFractions;\r\n return {\r\n doc: replaceTable(doc, where, bi, next),\r\n inverse,\r\n mapPosition: (p) => {\r\n if (!oldIds.has(p.blockId)) return p;\r\n const kept = op.rows.some((r) =>\r\n r.cells.some((c) => c.blocks.some((b) => b.id === p.blockId)),\r\n );\r\n return kept || !fallback ? p : { blockId: fallback.id, offset: 0 };\r\n },\r\n dirtyBlockIds: [op.tableId],\r\n };\r\n }\r\n\r\n case \"insertTableColumn\": {\r\n const { where, bi, block } = mustTable(doc, op.tableId);\r\n const snapshot: Op = { type: \"setTableStructure\", tableId: op.tableId, rows: block.rows };\r\n if (block.colFractions) snapshot.colFractions = block.colFractions;\r\n // Span-aware: a merged cell covering the insertion point grows by one\r\n // column instead of having a new cell slotted into its middle.\r\n const rows = block.rows.map((row, ri) => {\r\n const cells: TableCell[] = [];\r\n let col = 0;\r\n let inserted = false;\r\n for (const cell of row.cells) {\r\n const span = cell.colSpan ?? 1;\r\n if (!inserted && op.colIndex > col && op.colIndex < col + span) {\r\n cells.push({ ...cell, colSpan: span + 1 });\r\n inserted = true;\r\n } else if (!inserted && op.colIndex === col) {\r\n const fresh = op.cells[ri];\r\n if (fresh) cells.push(fresh);\r\n cells.push(cell);\r\n inserted = true;\r\n } else {\r\n cells.push(cell);\r\n }\r\n col += span;\r\n }\r\n if (!inserted) {\r\n const fresh = op.cells[ri];\r\n if (fresh) cells.push(fresh); // append at the right edge\r\n }\r\n return { cells };\r\n });\r\n let fractions = op.fractions;\r\n if (!fractions) {\r\n const old = effectiveFractions(block);\r\n const fresh = 1 / (old.length + 1);\r\n fractions = old.map((f) => f * (1 - fresh));\r\n fractions.splice(op.colIndex, 0, fresh);\r\n }\r\n return {\r\n doc: replaceTable(doc, where, bi, { ...block, rows, colFractions: fractions }),\r\n inverse: snapshot,\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.tableId],\r\n };\r\n }\r\n\r\n case \"setStylesheet\": {\r\n const old = doc.stylesheet ?? { styles: [], defaultStyleId: \"Normal\" };\r\n return {\r\n doc: { ...doc, stylesheet: op.stylesheet },\r\n inverse: { type: \"setStylesheet\", stylesheet: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [], // restyling paragraphs happens via setRuns/setParaStyle ops\r\n };\r\n }\r\n\r\n case \"setTableStyleSheet\": {\r\n const old = doc.tableStyles ?? {};\r\n return {\r\n doc: { ...doc, tableStyles: op.tableStyles },\r\n inverse: { type: \"setTableStyleSheet\", tableStyles: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [], // re-baking cells happens via the table-replace ops\r\n };\r\n }\r\n\r\n case \"setTableStyleRef\": {\r\n const { where, bi, block } = mustTable(doc, op.tableId);\r\n const oldStyleId = block.styleId ?? null;\r\n const oldCond = block.condOverrides ?? null;\r\n const next: TableBlock = { ...block };\r\n if (op.styleId === null) delete next.styleId;\r\n else next.styleId = op.styleId;\r\n if (op.condOverrides !== undefined) {\r\n if (op.condOverrides === null) delete next.condOverrides;\r\n else next.condOverrides = op.condOverrides;\r\n }\r\n return {\r\n doc: replaceTable(doc, where, bi, next),\r\n inverse: { type: \"setTableStyleRef\", tableId: op.tableId, styleId: oldStyleId, condOverrides: oldCond },\r\n mapPosition: identity,\r\n dirtyBlockIds: [op.tableId],\r\n };\r\n }\r\n\r\n case \"setSectionProps\": {\r\n const s = doc.section;\r\n const old: SectionGeometry = {\r\n pageWidthPx: s.pageWidthPx,\r\n pageHeightPx: s.pageHeightPx,\r\n marginPx: { ...s.marginPx },\r\n columns: s.columns\r\n ? { ...s.columns, ...(s.columns.cols ? { cols: s.columns.cols.map((c) => ({ ...c })) } : {}) }\r\n : null,\r\n pageNumberStart: s.pageNumberStart ?? null,\r\n headerDistancePx: s.headerDistancePx ?? null,\r\n footerDistancePx: s.footerDistancePx ?? null,\r\n pageColorHex: s.pageColorHex ?? null,\r\n pageBorders: s.pageBorders ?? null,\r\n breakType: s.breakType ?? null,\r\n lineNumbering: s.lineNumbering ? { ...s.lineNumbering } : null,\r\n };\r\n const next = {\r\n ...s,\r\n pageWidthPx: op.geometry.pageWidthPx,\r\n pageHeightPx: op.geometry.pageHeightPx,\r\n marginPx: op.geometry.marginPx,\r\n };\r\n if (op.geometry.columns) next.columns = op.geometry.columns;\r\n else delete next.columns;\r\n if (op.geometry.pageNumberStart !== null) next.pageNumberStart = op.geometry.pageNumberStart;\r\n else delete next.pageNumberStart;\r\n if (op.geometry.headerDistancePx !== null) next.headerDistancePx = op.geometry.headerDistancePx;\r\n else delete next.headerDistancePx;\r\n if (op.geometry.footerDistancePx !== null) next.footerDistancePx = op.geometry.footerDistancePx;\r\n else delete next.footerDistancePx;\r\n if (op.geometry.pageColorHex !== null) next.pageColorHex = op.geometry.pageColorHex;\r\n else delete next.pageColorHex;\r\n if (op.geometry.pageBorders !== null) next.pageBorders = op.geometry.pageBorders;\r\n else delete next.pageBorders;\r\n if (op.geometry.breakType !== null) next.breakType = op.geometry.breakType;\r\n else delete next.breakType;\r\n if (op.geometry.lineNumbering !== null) next.lineNumbering = op.geometry.lineNumbering;\r\n else delete next.lineNumbering;\r\n return {\r\n doc: { ...doc, section: next },\r\n inverse: { type: \"setSectionProps\", geometry: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [], // geometry change \u2192 full re-walk; line caches keyed by width\r\n };\r\n }\r\n\r\n case \"setFootnote\": {\r\n const old = doc.footnotes?.[op.noteId] ?? null;\r\n const footnotes = { ...(doc.footnotes ?? {}) };\r\n if (op.paras) footnotes[op.noteId] = op.paras;\r\n else delete footnotes[op.noteId];\r\n const removedIds = new Set((old ?? []).map((p) => p.id));\r\n const fallback = doc.blocks.find((b) => b.kind === \"paragraph\");\r\n return {\r\n doc: { ...doc, footnotes },\r\n inverse: { type: \"setFootnote\", noteId: op.noteId, paras: old },\r\n mapPosition: (p) => {\r\n if (!removedIds.has(p.blockId)) return p;\r\n const kept = (op.paras ?? []).some((b) => b.id === p.blockId);\r\n return kept || !fallback ? p : { blockId: fallback.id, offset: 0 };\r\n },\r\n dirtyBlockIds: [],\r\n };\r\n }\r\n\r\n case \"setEndnote\": {\r\n const old = doc.endnotes?.[op.noteId] ?? null;\r\n const endnotes = { ...(doc.endnotes ?? {}) };\r\n if (op.paras) endnotes[op.noteId] = op.paras;\r\n else delete endnotes[op.noteId];\r\n const removedIds = new Set((old ?? []).map((p) => p.id));\r\n const fallback = doc.blocks.find((b) => b.kind === \"paragraph\");\r\n return {\r\n doc: { ...doc, endnotes },\r\n inverse: { type: \"setEndnote\", noteId: op.noteId, paras: old },\r\n mapPosition: (p) => {\r\n if (!removedIds.has(p.blockId)) return p;\r\n const kept = (op.paras ?? []).some((b) => b.id === p.blockId);\r\n return kept || !fallback ? p : { blockId: fallback.id, offset: 0 };\r\n },\r\n dirtyBlockIds: [],\r\n };\r\n }\r\n\r\n case \"setSdtProps\": {\r\n const old = doc.sdts?.[op.id] ?? null;\r\n const sdts = { ...(doc.sdts ?? {}) };\r\n if (op.props) sdts[op.id] = op.props;\r\n else delete sdts[op.id];\r\n return {\r\n doc: { ...doc, sdts },\r\n inverse: { type: \"setSdtProps\", id: op.id, props: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [], // run markers change via setRuns/applyStylePatch ops\r\n };\r\n }\r\n\r\n case \"setField\": {\r\n const old = doc.fields?.[op.id] ?? null;\r\n const fields = { ...(doc.fields ?? {}) };\r\n if (op.def) fields[op.id] = op.def;\r\n else delete fields[op.id];\r\n return {\r\n doc: { ...doc, fields },\r\n inverse: { type: \"setField\", id: op.id, def: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [], // run/block fieldId markers change via setRuns/insert ops\r\n };\r\n }\r\n\r\n case \"setTocInstruction\": {\r\n const old = doc.tocInstruction ?? null;\r\n const next = { ...doc };\r\n if (op.instruction !== null) next.tocInstruction = op.instruction;\r\n else delete next.tocInstruction;\r\n return {\r\n doc: next,\r\n inverse: { type: \"setTocInstruction\", instruction: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [], // entries are regenerated via insert/removeBlock ops in the same tx\r\n };\r\n }\r\n\r\n case \"setBookmark\": {\r\n const old = doc.bookmarks?.[op.name] ?? null;\r\n const bookmarks = { ...(doc.bookmarks ?? {}) };\r\n if (op.range) bookmarks[op.name] = op.range;\r\n else delete bookmarks[op.name];\r\n return {\r\n doc: { ...doc, bookmarks },\r\n inverse: { type: \"setBookmark\", name: op.name, range: old },\r\n mapPosition: identity,\r\n dirtyBlockIds: [],\r\n };\r\n }\r\n\r\n case \"setDocument\": {\r\n // A coarse, whole-document replace \u2014 the inverse is the prior document, so it\r\n // undoes/redoes as one step. Used by DocumentEditor.append (merge) where a\r\n // granular op sequence can't express the change. Treated as LWW by transform;\r\n // positions can't be mapped across a wholesale swap (identity), and layout must\r\n // fully re-derive (no dirty subset).\r\n return {\r\n doc: op.doc,\r\n inverse: { type: \"setDocument\", doc },\r\n mapPosition: identity,\r\n dirtyBlockIds: [],\r\n };\r\n }\r\n\r\n case \"setSectionBand\": {\r\n const old = doc.section[op.band] ?? null;\r\n const section = { ...doc.section };\r\n if (op.blocks) section[op.band] = op.blocks;\r\n else delete section[op.band];\r\n const removedIds = new Set(\r\n (old ?? []).filter((b) => b.kind === \"paragraph\").map((b) => b.id),\r\n );\r\n const fallback = doc.blocks.find((b) => b.kind === \"paragraph\");\r\n return {\r\n doc: { ...doc, section },\r\n inverse: { type: \"setSectionBand\", band: op.band, blocks: old },\r\n mapPosition: (p) => {\r\n if (!removedIds.has(p.blockId)) return p;\r\n const kept = (op.blocks ?? []).some((b) => b.id === p.blockId);\r\n return kept || !fallback ? p : { blockId: fallback.id, offset: 0 };\r\n },\r\n dirtyBlockIds: [],\r\n };\r\n }\r\n\r\n case \"removeTableColumn\": {\r\n const { where, bi, block } = mustTable(doc, op.tableId);\r\n const snapshot: Op = { type: \"setTableStructure\", tableId: op.tableId, rows: block.rows };\r\n if (block.colFractions) snapshot.colFractions = block.colFractions;\r\n const oldFractions = effectiveFractions(block);\r\n const removedIds = new Set<string>();\r\n // Span-aware: a merged cell covering the removed column shrinks; an\r\n // unmerged cell at that column is dropped.\r\n const rows = block.rows.map((row) => {\r\n const cells: TableCell[] = [];\r\n let col = 0;\r\n for (const cell of row.cells) {\r\n const span = cell.colSpan ?? 1;\r\n if (op.colIndex >= col && op.colIndex < col + span) {\r\n if (span > 1) {\r\n const shrunk: TableCell = { ...cell };\r\n if (span - 1 > 1) shrunk.colSpan = span - 1;\r\n else delete shrunk.colSpan;\r\n cells.push(shrunk);\r\n } else {\r\n for (const p of cell.blocks) removedIds.add(p.id);\r\n }\r\n } else {\r\n cells.push(cell);\r\n }\r\n col += span;\r\n }\r\n return { cells };\r\n });\r\n const rest = oldFractions.filter((_, i) => i !== op.colIndex);\r\n const sum = rest.reduce((s, f) => s + f, 0) || 1;\r\n const fractions = rest.map((f) => f / sum);\r\n const fallback = firstParagraphInRows(rows);\r\n return {\r\n doc: replaceTable(doc, where, bi, { ...block, rows, colFractions: fractions }),\r\n inverse: snapshot,\r\n mapPosition: (p) =>\r\n removedIds.has(p.blockId) && fallback ? { blockId: fallback.id, offset: 0 } : p,\r\n dirtyBlockIds: [op.tableId],\r\n };\r\n }\r\n }\r\n}\r\n", "// Section resolution \u2014 pure model logic (no layout, no DOM). A section-break\r\n// paragraph TERMINATES a section (OOXML sectPr placement); blocks after the\r\n// last break belong to `doc.section`. Absent patch fields inherit from\r\n// `doc.section` (\"link to previous\"). Lives in the shared core so both the\r\n// layout engine and the document query API resolve sections the same way.\r\n\r\nimport type { Document, SectionBreakType, SectionPatch, SectionProps } from \"./document\";\r\nimport { BAND_CONTAINERS } from \"./document\";\r\n\r\nexport function effectiveSection(base: SectionProps, patch: SectionPatch): SectionProps {\r\n const out: SectionProps = {\r\n pageWidthPx: patch.pageWidthPx ?? base.pageWidthPx,\r\n pageHeightPx: patch.pageHeightPx ?? base.pageHeightPx,\r\n marginPx: patch.marginPx ?? base.marginPx,\r\n };\r\n // columns: undefined = inherit, null = explicitly single-column\r\n const columns = patch.columns === undefined ? base.columns : (patch.columns ?? undefined);\r\n if (columns) out.columns = columns;\r\n // page-number restart is a section's OWN property \u2014 never inherited\r\n if (patch.pageNumberStart !== undefined) out.pageNumberStart = patch.pageNumberStart;\r\n // Band distances inherit from the document section (the report sets them once).\r\n const headerDist = patch.headerDistancePx ?? base.headerDistancePx;\r\n if (headerDist !== undefined) out.headerDistancePx = headerDist;\r\n const footerDist = patch.footerDistancePx ?? base.footerDistancePx;\r\n if (footerDist !== undefined) out.footerDistancePx = footerDist;\r\n // Page fill & borders inherit from the document section unless overridden.\r\n const pageColorHex = patch.pageColorHex ?? base.pageColorHex;\r\n if (pageColorHex !== undefined) out.pageColorHex = pageColorHex;\r\n const pageBorders = patch.pageBorders ?? base.pageBorders;\r\n if (pageBorders !== undefined) out.pageBorders = pageBorders;\r\n // Line numbering is a section's OWN property (like page-number restart): a\r\n // section either declares its own w:lnNumType or has none \u2014 it never inherits.\r\n if (patch.lineNumbering !== undefined) out.lineNumbering = patch.lineNumbering;\r\n for (const key of BAND_CONTAINERS) {\r\n const blocks = patch[key] ?? base[key];\r\n if (blocks) out[key] = blocks;\r\n }\r\n return out;\r\n}\r\n\r\nexport interface ResolvedSection {\r\n /** 0-based position of this section within the document's section list. */\r\n index: number;\r\n props: SectionProps;\r\n /** Index (inclusive) of this section's FIRST top-level block. */\r\n startBlock: number;\r\n /** Index (inclusive) of this section's LAST top-level block. */\r\n endBlock: number;\r\n /** The OOXML w:type that governs how THIS section's first page begins\r\n * (\"nextPage\"/\"evenPage\"/\"oddPage\"). Read when the engine starts the section. */\r\n breakType: SectionBreakType;\r\n}\r\n\r\nexport function resolveSections(doc: Document): ResolvedSection[] {\r\n const out: ResolvedSection[] = [];\r\n let startBlock = 0;\r\n for (let i = 0; i < doc.blocks.length; i++) {\r\n const b = doc.blocks[i]!;\r\n if (b.kind === \"paragraph\" && b.style.sectionBreak) {\r\n out.push({\r\n index: out.length,\r\n props: effectiveSection(doc.section, b.style.sectionBreak.props),\r\n startBlock,\r\n endBlock: i,\r\n breakType: b.style.sectionBreak.type,\r\n });\r\n startBlock = i + 1;\r\n }\r\n }\r\n // The trailing body section keeps its own start type (even/odd parity), so a\r\n // document whose final section begins on a parity page is honored, not flattened.\r\n out.push({\r\n index: out.length,\r\n props: doc.section,\r\n startBlock,\r\n endBlock: doc.blocks.length - 1,\r\n breakType: doc.section.breakType ?? \"nextPage\",\r\n });\r\n return out;\r\n}\r\n", "// Globally-unique id minting for blocks, cells, and other model nodes.\n//\n// In single-user mode any unique string works. For multi-user collaboration two\n// clients must NEVER mint the same id (a collision would make one client's\n// paragraph silently alias another's), so ids are namespaced by a per-session\n// `siteId`:\n//\n// <siteId>-<counter base36> e.g. \"a3f9c1-0\", \"a3f9c1-1\", \u2026\n//\n// The counter is monotonic within a session; the siteId makes the whole space\n// disjoint from every other session. Replaying an op stream is deterministic\n// because generated ids are captured IN the op (splitParagraph.newBlockId,\n// insertBlock.block.id), so the consumer never re-mints them.\n//\n// Randomness lives at the edges (the frontend mints a siteId via crypto), not\n// here \u2014 this module stays pure and deterministic so it is trivially testable.\n\nexport interface IdGenerator {\n readonly siteId: string;\n /** Next unique id in this site's space. */\n next(): string;\n /** Current counter value (for persisting/resuming a session's id space). */\n count(): number;\n}\n\nexport function createIdGenerator(siteId: string, start = 0): IdGenerator {\n let n = start;\n return {\n siteId,\n next: () => `${siteId}-${(n++).toString(36)}`,\n count: () => n,\n };\n}\n\n// A process-wide default generator. The frontend reconfigures it at startup with\n// a real siteId (see configureIds); tests and any unconfigured context get the\n// deterministic \"local\" space.\nlet active: IdGenerator = createIdGenerator(\"local\");\n\n/** Point the default generator at a session's siteId. Call once at startup,\n * before any ids are minted. `start` resumes a prior session's counter. */\nexport function configureIds(siteId: string, start = 0): void {\n active = createIdGenerator(siteId, start);\n}\n\n/** Mint a fresh unique id from the active generator. */\nexport function freshId(): string {\n return active.next();\n}\n\n/** The active session's siteId (tags Changes so the server can order them). */\nexport function currentSiteId(): string {\n return active.siteId;\n}\n", "// Table-of-contents field instruction parsing + generation options. Pure and\r\n// layout-free, so import, export, and the backend route all share it.\r\n//\r\n// The OOXML `TOC` field instruction (e.g. ` TOC \\o \"1-3\" \\h \\z \\u `) governs WHICH\r\n// paragraphs become entries and some behaviors; the visual leader/right-tab live in\r\n// the TOC paragraph styles, not the instruction (see TocOptions).\r\n\r\nimport type { CharStyle, Document, ParaStyle, Paragraph } from \"./model/document\";\r\nimport { DEFAULT_CHAR_STYLE } from \"./model/defaults\";\r\nimport { bodyParagraphs, textOfRuns } from \"./model/text\";\r\nimport { sliceRuns } from \"./model/ops\";\r\nimport { freshId } from \"./ids\";\r\n\r\n/** Parsed `TOC` field switches. See ECMA-376 \u00A717.16.5.68. */\r\nexport interface TocSwitches {\r\n /** \\o \"1-3\" \u2014 include built-in heading levels in this (inclusive) range. */\r\n outlineRange?: { from: number; to: number };\r\n /** \\u \u2014 also include paragraphs with a directly-applied outline level. */\r\n useOutlineLevels: boolean;\r\n /** \\t \"StyleName,level,\u2026\" \u2014 custom paragraph styles mapped to TOC levels. */\r\n customStyles?: Record<string, number>;\r\n /** \\h \u2014 emit each entry as a hyperlink to its heading. */\r\n hyperlinks: boolean;\r\n /** \\n \"1-3\" \u2014 omit page numbers for levels in this (inclusive) range. */\r\n hidePageNumberRange?: { from: number; to: number };\r\n /** \\p \"sep\" \u2014 text between the entry label and the page number (default tab). */\r\n separator?: string;\r\n /** \\z \u2014 hide tab/leader/page numbers in Web layout view (display-only). */\r\n hideInWeb: boolean;\r\n}\r\n\r\nconst rangeOf = (s: string | undefined): { from: number; to: number } | undefined => {\r\n if (!s) return undefined;\r\n const m = s.match(/^(\\d+)\\s*-\\s*(\\d+)$/);\r\n if (m) return { from: Number(m[1]), to: Number(m[2]) };\r\n const n = s.match(/^\\d+$/) ? Number(s) : NaN;\r\n return Number.isNaN(n) ? undefined : { from: n, to: n };\r\n};\r\n\r\n/** Parse a `TOC` field instruction string into its switches. */\r\nexport function parseTocInstruction(instr: string): TocSwitches {\r\n const sw: TocSwitches = {\r\n useOutlineLevels: /\\\\u\\b/.test(instr),\r\n hyperlinks: /\\\\h\\b/.test(instr),\r\n hideInWeb: /\\\\z\\b/.test(instr),\r\n };\r\n const oRange = rangeOf(instr.match(/\\\\o\\s+\"([^\"]+)\"/)?.[1]);\r\n if (oRange) sw.outlineRange = oRange;\r\n const nRange = rangeOf(instr.match(/\\\\n\\s+\"?([0-9-]+)\"?/)?.[1]);\r\n if (nRange) sw.hidePageNumberRange = nRange;\r\n const sep = instr.match(/\\\\p\\s+\"([^\"]*)\"/)?.[1];\r\n if (sep !== undefined) sw.separator = sep;\r\n const t = instr.match(/\\\\t\\s+\"([^\"]+)\"/)?.[1];\r\n if (t) {\r\n const pairs = t.split(\",\");\r\n const map: Record<string, number> = {};\r\n for (let i = 0; i + 1 < pairs.length; i += 2) {\r\n const name = pairs[i]!.trim();\r\n const lvl = Number(pairs[i + 1]!.trim());\r\n if (name && lvl >= 1) map[name] = lvl;\r\n }\r\n if (Object.keys(map).length > 0) sw.customStyles = map;\r\n }\r\n return sw;\r\n}\r\n\r\n/** Synthesize a `TOC` field instruction from switches (inverse of\r\n * parseTocInstruction; round-trips the switches it models). Switch order follows\r\n * Word's own emit so a re-parse is stable. */\r\nexport function buildTocInstruction(sw: TocSwitches): string {\r\n const parts: string[] = [\"TOC\"];\r\n if (sw.outlineRange) parts.push(`\\\\o \"${sw.outlineRange.from}-${sw.outlineRange.to}\"`);\r\n if (sw.useOutlineLevels) parts.push(\"\\\\u\");\r\n if (sw.customStyles && Object.keys(sw.customStyles).length > 0) {\r\n const t = Object.entries(sw.customStyles).map(([name, lvl]) => `${name},${lvl}`).join(\",\");\r\n parts.push(`\\\\t \"${t}\"`);\r\n }\r\n if (sw.hyperlinks) parts.push(\"\\\\h\");\r\n if (sw.hidePageNumberRange) parts.push(`\\\\n \"${sw.hidePageNumberRange.from}-${sw.hidePageNumberRange.to}\"`);\r\n if (sw.separator !== undefined) parts.push(`\\\\p \"${sw.separator}\"`);\r\n if (sw.hideInWeb) parts.push(\"\\\\z\");\r\n return ` ${parts.join(\" \")} `;\r\n}\r\n\r\n/** Is `level` covered by an inclusive range (undefined range = no). */\r\nexport const inRange = (r: { from: number; to: number } | undefined, level: number): boolean =>\r\n !!r && level >= r.from && level <= r.to;\r\n\r\n// ---------------------------------------------------------------------------\r\n// Heading detection (shared by the editor's TOC generation and the headless\r\n// route) \u2014 robust to opaque style ids: tries the effective outline level, then\r\n// the resolved style NAME (\"Heading 1\"), then a `namedStyle` like \"heading1\",\r\n// then \\t custom-style mappings.\r\n\r\n/** The friendly name of a style id, following basedOn (for opaque numeric ids). */\r\nfunction styleNameOf(doc: Document, id: string | undefined): string | undefined {\r\n if (!id || !doc.stylesheet) return undefined;\r\n const byId = new Map(doc.stylesheet.styles.map((s) => [s.id, s]));\r\n let cur = byId.get(id);\r\n const seen = new Set<string>();\r\n while (cur && !seen.has(cur.id)) {\r\n if (cur.name && /(?:^|\\s)heading\\s*\\d/i.test(cur.name)) return cur.name;\r\n seen.add(cur.id);\r\n cur = cur.basedOn ? byId.get(cur.basedOn) : undefined;\r\n }\r\n return byId.get(id)?.name;\r\n}\r\n\r\n/** TOC level (1-based) for a paragraph, or null if it's not a heading. */\r\nexport function tocHeadingLevel(p: Paragraph, doc: Document, sw?: TocSwitches): number | null {\r\n const named = p.style.namedStyle;\r\n // \\t custom styles (match by opaque id or by friendly name).\r\n if (sw?.customStyles && named) {\r\n const lv = sw.customStyles[named] ?? sw.customStyles[styleNameOf(doc, named) ?? \"\"];\r\n if (lv) return lv;\r\n }\r\n if (p.style.outlineLevel !== undefined) return p.style.outlineLevel + 1;\r\n const direct = named?.match(/^heading\\s*(\\d)$/i);\r\n if (direct) return Number(direct[1]);\r\n const m = styleNameOf(doc, named)?.match(/(?:^|\\s)heading\\s*(\\d)/i);\r\n return m ? Number(m[1]) : null;\r\n}\r\n\r\n/** All TOC-eligible headings in document order (body + table cells), honoring the\r\n * switches' `\\o` range (when present) and `maxLevel`. */\r\nexport function detectTocHeadings(\r\n doc: Document,\r\n sw?: TocSwitches,\r\n maxLevel = 3,\r\n): { block: Paragraph; level: number }[] {\r\n const cap = sw?.outlineRange?.to ?? maxLevel;\r\n const out: { block: Paragraph; level: number }[] = [];\r\n for (const p of bodyParagraphs(doc)) {\r\n const level = tocHeadingLevel(p, doc, sw);\r\n if (level === null || level < 1 || level > cap) continue;\r\n if (sw?.outlineRange && (level < sw.outlineRange.from || level > sw.outlineRange.to)) continue;\r\n out.push({ block: p, level });\r\n }\r\n return out;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Imported-TOC unification: an imported TOC field flattens to plain paragraphs\r\n// (label + \"#anchor\" hyperlink + cached \"\\t<number>\" text). To round-trip it as a\r\n// LIVE field on export, mark those paragraphs as tocEntry and STRIP the cached\r\n// number text \u2014 so the number becomes layout-driven (paint decoration), exactly\r\n// like an editor-created TOC, and export wraps it in a real TOC/PAGEREF field.\r\n\r\n/** Mark a document's imported TOC entries as tocEntry and strip their cached page\r\n * numbers. Gated by the caller (only when a TOC field exists). Returns the count.\r\n * An entry is a body paragraph with a \"#anchor\" link resolving to a bookmarked\r\n * block plus a trailing \"\\t<number>\"; level is inferred from its left indent. */\r\nexport function markImportedTocEntries(doc: Document, indentStepPx = 20): number {\r\n if (!doc.bookmarks) return 0;\r\n const contentWidthPx = doc.section.pageWidthPx - doc.section.marginPx.left - doc.section.marginPx.right;\r\n let count = 0;\r\n for (const b of doc.blocks) {\r\n if (b.kind !== \"paragraph\" || b.style.tocEntry) continue;\r\n const anchor = b.runs.find((r) => r.style.link?.startsWith(\"#\"))?.style.link?.slice(1);\r\n if (!anchor) continue;\r\n const targetId = doc.bookmarks[anchor]?.start.blockId;\r\n if (!targetId) continue;\r\n const full = textOfRuns(b.runs);\r\n const m = full.match(/\\t\\d+\\s*$/); // trailing tab + page number\r\n if (!m) continue;\r\n b.runs = sliceRuns(b.runs, 0, full.length - m[0].length); // drop \"\\t<number>\"\r\n if (b.runs.length === 0) b.runs = [{ text: \"\", style: { ...TOC_BASE_CHAR } }];\r\n const level = Math.min(9, Math.max(1, Math.round((b.style.indentLeftPx || 0) / indentStepPx) + 1));\r\n b.style.tocEntry = { targetId, level };\r\n // Right-aligned dot leader so the painted/exported number aligns (if none set).\r\n if (!b.style.tabStops?.length && contentWidthPx > 0) {\r\n b.style.tabStops = [{ posPx: contentWidthPx, align: \"right\", leader: \"dot\" }];\r\n }\r\n b.revision++;\r\n count++;\r\n }\r\n return count;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// Generation options \u2014 everything that used to be hardcoded in\r\n// buildTocParagraphs, exposed so the editor AND the backend route can pass it.\r\n\r\nexport interface TocLevelStyle {\r\n char?: Partial<CharStyle>;\r\n para?: Partial<ParaStyle>;\r\n}\r\n\r\n/** TOC entries default to the body font/color, one notch smaller. */\r\nconst TOC_BASE_CHAR: CharStyle = { ...DEFAULT_CHAR_STYLE, fontSizePx: 13 };\r\n\r\n/** Per-level left-indent step (px) when none is supplied via TocOptions. */\r\nconst TOC_INDENT_STEP_PX = 20;\r\n\r\n/** Resolve the char + paragraph style for a TOC entry at `level`. Precedence:\r\n * built-in defaults < the document's own TOC style (`inherit`, from its TOC field\r\n * paragraph) < caller `TocOptions`. When `inherit` is given we DON'T impose the\r\n * built-in level-1 bold / size bumps \u2014 we respect the document's look (matching the\r\n * source Word doc); without it, the editor's default look is used. The para carries\r\n * the right-aligned leader tab stop (callers add `tocEntry`). */\r\nexport function tocEntryStyle(\r\n opts: TocOptions,\r\n level: number,\r\n contentWidthPx: number,\r\n inherit?: { char?: CharStyle | undefined; para?: ParaStyle | undefined },\r\n): { char: CharStyle; para: ParaStyle } {\r\n const indentStep = opts.indentStepPx ?? TOC_INDENT_STEP_PX;\r\n const leader = opts.leader ?? \"dot\";\r\n // Shared paragraph base for every entry, before indent + per-level overrides.\r\n const paraBase = { align: \"left\", lineHeight: 1.5, spaceBeforePx: 0, spaceAfterPx: 2, indentFirstLinePx: 0 } as const;\r\n let char: CharStyle;\r\n let para: ParaStyle;\r\n if (inherit && (inherit.char || inherit.para)) {\r\n char = { ...TOC_BASE_CHAR, ...inherit.char, ...opts.baseChar, ...opts.levels?.[level]?.char };\r\n const baseIndent = inherit.para?.indentLeftPx ?? 0;\r\n para = {\r\n ...paraBase,\r\n ...inherit.para,\r\n indentLeftPx: baseIndent + (level - 1) * indentStep,\r\n ...opts.levels?.[level]?.para,\r\n };\r\n } else {\r\n const base = { ...TOC_BASE_CHAR, ...opts.baseChar };\r\n char = { ...base, fontSizePx: level === 1 ? 14 : 13, bold: level === 1, ...opts.levels?.[level]?.char };\r\n para = {\r\n ...paraBase,\r\n indentLeftPx: (level - 1) * indentStep, ...opts.levels?.[level]?.para,\r\n };\r\n }\r\n if (leader !== \"none\" && contentWidthPx > 0) para.tabStops = [{ posPx: contentWidthPx, align: \"right\", leader }];\r\n else delete para.tabStops; // leader:\"none\" overrides any inherited leader tab\r\n return { char, para };\r\n}\r\n\r\n/** The TOC title paragraph's resolved styles, or null when titles are disabled. */\r\nexport function tocTitleStyle(opts: TocOptions): { text: string; char: CharStyle; para: ParaStyle } | null {\r\n if (opts.title === null) return null;\r\n const t = opts.title ?? {};\r\n const base = { ...TOC_BASE_CHAR, ...opts.baseChar };\r\n return {\r\n text: t.text ?? \"Table of Contents\",\r\n char: { ...base, fontSizePx: 20, bold: true, ...t.char },\r\n para: {\r\n align: \"left\", lineHeight: 1.4, spaceBeforePx: 8, spaceAfterPx: 12,\r\n indentFirstLinePx: 0, indentLeftPx: 0, namedStyle: t.namedStyle ?? \"tocTitle\", ...t.para,\r\n },\r\n };\r\n}\r\n\r\nexport interface TocOptions {\r\n /** TOC title paragraph; null/omit to skip a title. */\r\n title?: { text?: string; char?: Partial<CharStyle>; para?: Partial<ParaStyle>; namedStyle?: string } | null;\r\n /** Base char style applied to every entry before per-level overrides. */\r\n baseChar?: Partial<CharStyle>;\r\n /** Per-level (1-based) style overrides. */\r\n levels?: Record<number, TocLevelStyle>;\r\n /** Deepest heading level to include (default 3). */\r\n maxLevel?: number;\r\n /** Per-level left indent step in px (default 20). */\r\n indentStepPx?: number;\r\n /** Dot-leader between label and page number (default \"dot\"). */\r\n leader?: \"dot\" | \"dash\" | \"underscore\" | \"none\";\r\n /** Show page numbers (default true). */\r\n includePageNumbers?: boolean;\r\n /** Emit entries as hyperlinks (default true). */\r\n hyperlink?: boolean;\r\n}\r\n\r\n// ---------------------------------------------------------------------------\r\n// TOC generation \u2014 entries are REAL paragraphs tagged with `tocEntry` (so they\r\n// lay out, paint, hit-test, and export as a live field for free). Page numbers\r\n// are paint-only: the layout engine resolves each entry's number per relayout,\r\n// so they never go stale. Shared by the editor (insertTocCmd) and the headless\r\n// PDF/DOCX render path.\r\n\r\nconst TOC_LEVELS = 3;\r\n\r\n/** Build TOC paragraphs (optional title + one entry per heading) from a document's\r\n * headings. Every hardcoded look is an overridable `TocOptions` default. The page\r\n * number is paint-only (layout post-pass); a right-aligned dot-leader tab stop\r\n * right-aligns it. Mutates nothing. */\r\nexport function buildTocParagraphs(doc: Document, opts: TocOptions = {}): Paragraph[] {\r\n // Honor the document's own TOC field switches (\\o level range, \\t custom styles, \u2026)\r\n // so a C#-emitted ` TOC \\o \"1-5\" ` lists all five levels, not the default three.\r\n const sw = doc.tocInstruction ? parseTocInstruction(doc.tocInstruction) : undefined;\r\n const maxLevel = opts.maxLevel ?? sw?.outlineRange?.to ?? TOC_LEVELS;\r\n const contentWidthPx = doc.section.pageWidthPx - doc.section.marginPx.left - doc.section.marginPx.right;\r\n\r\n // Inherit the document's own TOC styling from the TOC field's paragraph (its pStyle,\r\n // e.g. \"TOC 1\", resolved on import \u2192 the doc's intended font/size/indent). Absent for\r\n // an editor-inserted TOC in a doc with no TOC field \u2192 the built-in default look.\r\n const anchor = doc.tocAnchorBlockId\r\n ? doc.blocks.find((b): b is Paragraph => b.kind === \"paragraph\" && b.id === doc.tocAnchorBlockId)\r\n : undefined;\r\n const inherit = anchor ? { char: anchor.runs[0]?.style, para: anchor.style } : undefined;\r\n\r\n const out: Paragraph[] = [];\r\n\r\n // Title: an explicit opts.title wins; otherwise add the default title only when we're\r\n // NOT filling an existing TOC field (a doc with a TOC field usually has its own title\r\n // heading already, so a generated one would duplicate it).\r\n const wantTitle = opts.title !== undefined ? opts.title !== null : anchor === undefined;\r\n if (wantTitle) {\r\n const title = tocTitleStyle(opts);\r\n if (title) {\r\n out.push({\r\n kind: \"paragraph\", id: freshId(), revision: 0,\r\n runs: [{ text: title.text, style: title.char }],\r\n style: title.para,\r\n });\r\n }\r\n }\r\n\r\n // Scan body AND table-cell paragraphs so headings inside tables are listed.\r\n for (const { block, level } of detectTocHeadings(doc, sw, maxLevel)) {\r\n const text = textOfRuns(block.runs).replace(/\\v/g, \" \").trim();\r\n if (text.length === 0) continue;\r\n const { char, para } = tocEntryStyle(opts, level, contentWidthPx, inherit);\r\n out.push({\r\n kind: \"paragraph\", id: freshId(), revision: 0,\r\n runs: [{ text, style: char }],\r\n style: { ...para, tocEntry: { targetId: block.id, level } },\r\n });\r\n }\r\n return out;\r\n}\r\n\r\n/** Fill an EMPTY TOC field: build entries from the document's headings and splice\r\n * them at the captured field location (`doc.tocAnchorBlockId`). Page numbers stay\r\n * paint-only \u2014 the layout engine resolves them on render. Returns a NEW doc (input\r\n * not mutated).\r\n *\r\n * If the TOC field ALREADY has entries (imported and marked as `tocEntry`), they are\r\n * PRESERVED untouched \u2014 they carry the source document's own styling and their page\r\n * numbers are recomputed by layout, so rebuilding from headings (with our defaults)\r\n * would only throw the author's TOC away. No headings / nowhere to anchor \u2192 unchanged. */\r\nexport function generateTocIntoDoc(\r\n doc: Document,\r\n opts: TocOptions = {},\r\n): { doc: Document; generated: number; headings: number } {\r\n // Already populated (e.g. a Word/C#-rendered TOC) \u2192 keep it as-is.\r\n if (doc.blocks.some((b) => b.kind === \"paragraph\" && b.style.tocEntry)) {\r\n return { doc, generated: 0, headings: 0 };\r\n }\r\n if (doc.tocAnchorBlockId === undefined) return { doc, generated: 0, headings: 0 };\r\n const at = doc.blocks.findIndex((b) => b.id === doc.tocAnchorBlockId);\r\n if (at < 0) return { doc, generated: 0, headings: 0 };\r\n\r\n const fresh = buildTocParagraphs(doc, opts);\r\n const headings = fresh.reduce((n, p) => n + (p.style.tocEntry ? 1 : 0), 0);\r\n if (headings === 0) return { doc, generated: 0, headings: 0 };\r\n\r\n const blocks = [...doc.blocks];\r\n blocks.splice(at, 1, ...fresh); // replace the empty TOC field paragraph\r\n return { doc: { ...doc, blocks }, generated: headings, headings };\r\n}\r\n", "// Named styles (Word's style gallery). The editor model keeps runs CONCRETE \u2014\n// styles here are templates + references, not a live cascade at render time:\n// - applying a style patches exactly the fields the style defines (direct\n// formatting on other fields survives, like Word),\n// - modifying a style re-patches every paragraph that references it,\n// - resolution walks the basedOn chain (editor-simple override semantics;\n// OOXML XOR-toggle fidelity lives in import/docx/styles.ts, which resolves\n// to concrete values at import time and preserves the styleId reference \u2014\n// the two systems share the same id space).\n\nimport type { CharStyle, Document, ParaStyle, Paragraph } from \"./document\";\nimport type { EditorTypography } from \"./defaults\";\nimport { paragraphsOf } from \"./text\";\n\nexport type NamedStyleType = \"paragraph\" | \"character\";\n\nexport interface NamedStyle {\n id: string; // shares the docx styleId space (\"Normal\", \"Heading1\", ...)\n name: string; // display name\n /** Paragraph vs character style (Word's two text-level style kinds). Optional\n * for back-compat: legacy stylesheets and defaultStylesheet() omit it and read\n * as \"paragraph\" via styleType(). Character styles store only `char` (empty\n * `para`) and apply to runs, not paragraphs. */\n type?: NamedStyleType;\n basedOn?: string;\n char: Partial<CharStyle>;\n para: Partial<ParaStyle>;\n}\n\nexport interface Stylesheet {\n styles: NamedStyle[];\n defaultStyleId: string;\n}\n\n/** A style's kind, defaulting untyped (legacy) styles to \"paragraph\". Read this\n * rather than `.type` directly so old documents keep working. */\nexport function styleType(s: NamedStyle): NamedStyleType {\n return s.type ?? \"paragraph\";\n}\n\nexport function styleById(sheet: Stylesheet, id: string): NamedStyle | undefined {\n return sheet.styles.find((s) => s.id === id);\n}\n\n/** Resolved templates: basedOn chain root\u2192leaf, defined fields override. */\nexport function resolveStyle(\n sheet: Stylesheet,\n id: string,\n): { char: Partial<CharStyle>; para: Partial<ParaStyle> } {\n const chain: NamedStyle[] = [];\n const seen = new Set<string>();\n for (let cur = styleById(sheet, id); cur && !seen.has(cur.id); cur = cur.basedOn ? styleById(sheet, cur.basedOn) : undefined) {\n seen.add(cur.id);\n chain.unshift(cur);\n }\n const char: Partial<CharStyle> = {};\n const para: Partial<ParaStyle> = {};\n for (const s of chain) {\n Object.assign(char, definedOnly(s.char));\n Object.assign(para, definedOnly(s.para));\n }\n return { char, para };\n}\n\n/** Resolved character template only: basedOn chain root\u2192leaf, defined `char`\n * fields override. Character styles have no `para`, so this is the run-level\n * equivalent of resolveStyle for type===\"character\" styles. */\nexport function resolveCharStyle(sheet: Stylesheet, id: string): Partial<CharStyle> {\n const chain: NamedStyle[] = [];\n const seen = new Set<string>();\n for (let cur = styleById(sheet, id); cur && !seen.has(cur.id); cur = cur.basedOn ? styleById(sheet, cur.basedOn) : undefined) {\n seen.add(cur.id);\n chain.unshift(cur);\n }\n const char: Partial<CharStyle> = {};\n for (const s of chain) Object.assign(char, definedOnly(s.char));\n return char;\n}\n\n/** Every style id that (transitively) lists `id` as its basedOn ancestor. Used\n * to keep a style out of its own descendants' \"Based on\" pickers. */\nexport function descendantsOf(sheet: Stylesheet, id: string): Set<string> {\n const out = new Set<string>();\n let grew = true;\n while (grew) {\n grew = false;\n for (const s of sheet.styles) {\n if (out.has(s.id)) continue;\n if (s.basedOn !== undefined && (s.basedOn === id || out.has(s.basedOn))) {\n out.add(s.id);\n grew = true;\n }\n }\n }\n return out;\n}\n\n/** Would setting `styleId`'s basedOn to `newBasedOn` create a cycle? True if\n * newBasedOn is the style itself or one of its descendants. A backstop for the\n * UI, which already excludes these from the picker. */\nexport function wouldCycle(sheet: Stylesheet, styleId: string, newBasedOn: string | undefined): boolean {\n if (!newBasedOn) return false;\n if (newBasedOn === styleId) return true;\n return descendantsOf(sheet, styleId).has(newBasedOn);\n}\n\n/** A canonical signature of a style's FORMATTING (everything but id + name):\n * type, basedOn, and its defined char/para deltas with keys sorted \u2014 so two\n * styles that differ only by name (or id) produce the same string. */\nfunction styleSignature(s: NamedStyle): string {\n const norm = (o: object): [string, unknown][] =>\n Object.entries(definedOnly(o)).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return JSON.stringify([styleType(s), s.basedOn ?? \"\", norm(s.char), norm(s.para)]);\n}\n\nexport interface StyleMergeResult {\n sheet: Stylesheet;\n /** mergedId \u2192 survivingId, for every style that was collapsed away. Empty when\n * there were no duplicates. */\n remap: Map<string, string>;\n}\n\n/** Collapse named styles that are identical except for their name/id: each group\n * of same-signature styles keeps ONE survivor (the default style if the group\n * contains it, otherwise the first in document order) and the rest are dropped,\n * their ids remapped to the survivor. basedOn pointers (and defaultStyleId) are\n * rewritten through the remap. Callers remap content references (paragraph\n * namedStyle / run charStyleId) using `remap`. */\nexport function mergeDuplicateNamedStyles(sheet: Stylesheet): StyleMergeResult {\n const remap = new Map<string, string>();\n const survivorBySig = new Map<string, string>();\n for (const s of sheet.styles) {\n const sig = styleSignature(s);\n const survivor = survivorBySig.get(sig);\n if (survivor === undefined) {\n survivorBySig.set(sig, s.id);\n } else if (s.id === sheet.defaultStyleId) {\n // The default style must survive \u2014 promote it and fold the prior survivor.\n survivorBySig.set(sig, s.id);\n remap.set(survivor, s.id);\n for (const [k, v] of remap) if (v === survivor) remap.set(k, s.id);\n } else {\n remap.set(s.id, survivor);\n }\n }\n if (remap.size === 0) return { sheet, remap };\n\n const resolveId = (id: string): string => remap.get(id) ?? id;\n const styles: NamedStyle[] = [];\n for (const s of sheet.styles) {\n if (remap.has(s.id)) continue; // collapsed away\n const next: NamedStyle = { ...s };\n if (next.basedOn !== undefined) {\n const rebased = resolveId(next.basedOn);\n if (rebased === next.id) delete next.basedOn; // self-reference after merge\n else next.basedOn = rebased;\n }\n styles.push(next);\n }\n return { sheet: { styles, defaultStyleId: resolveId(sheet.defaultStyleId) }, remap };\n}\n\nfunction definedOnly<T extends object>(o: T): Partial<T> {\n const out: Partial<T> = {};\n for (const [k, v] of Object.entries(o)) {\n if (v !== undefined) (out as Record<string, unknown>)[k] = v;\n }\n return out;\n}\n\n/** Every editable paragraph referencing a style \u2014 paragraphsOf already covers\n * body, table cells, and both margin bands in document order. */\nexport function paragraphsWithStyle(doc: Document, styleId: string): Paragraph[] {\n return paragraphsOf(doc).filter((p) => p.style.namedStyle === styleId);\n}\n\n/** The built-in starter stylesheet (Normal + Title/Subtitle/Heading/Quote/Code).\n * `t` overrides only the LIBRARY defaults \u2014 the body (Normal) font/size/color +\n * line height, and the heading font family. It is used for NEW/blank documents\n * and as the fallback when a document carries no stylesheet of its own; a loaded\n * .docx keeps its own w:docDefaults / Normal style (the importer bakes those in).\n * Zero-arg reproduces the historical default byte-for-byte. */\nexport function makeDefaultStylesheet(t: EditorTypography = {}): Stylesheet {\n const fontFamily = t.fontFamily ?? \"Georgia, serif\";\n const fontSizePx = t.fontSizePx ?? 16;\n const color = t.color ?? \"#202124\";\n const headingFont = t.headingFontFamily ?? \"Arial, sans-serif\";\n const base: Partial<ParaStyle> = { lineHeight: t.lineHeight ?? 1.5, spaceBeforePx: 0, spaceAfterPx: 12 };\n return {\n defaultStyleId: \"Normal\",\n styles: [\n {\n id: \"Normal\",\n name: \"Normal\",\n char: { fontFamily, fontSizePx, bold: false, italic: false, color },\n para: { ...base, align: \"left\", indentFirstLinePx: 0 },\n },\n {\n id: \"Title\",\n name: \"Title\",\n basedOn: \"Normal\",\n char: { fontFamily: headingFont, fontSizePx: 32, bold: true, color: \"#1a1a2e\" },\n para: { align: \"center\", spaceAfterPx: 4 },\n },\n {\n id: \"Subtitle\",\n name: \"Subtitle\",\n basedOn: \"Normal\",\n char: { italic: true, color: \"#5f6368\" },\n para: { align: \"center\", spaceAfterPx: 28 },\n },\n {\n id: \"Heading1\",\n name: \"Heading 1\",\n basedOn: \"Normal\",\n char: { fontFamily: headingFont, fontSizePx: 24, bold: true, color: \"#1a1a2e\" },\n para: { spaceBeforePx: 18, spaceAfterPx: 8, keepWithNext: true },\n },\n {\n id: \"Heading2\",\n name: \"Heading 2\",\n basedOn: \"Heading1\",\n char: { fontSizePx: 19 },\n para: { spaceBeforePx: 14, spaceAfterPx: 6 },\n },\n {\n id: \"Quote\",\n name: \"Quote\",\n basedOn: \"Normal\",\n char: { italic: true, color: \"#5f6368\" },\n para: { indentLeftPx: 36, spaceBeforePx: 8, spaceAfterPx: 8 },\n },\n {\n id: \"Code\",\n name: \"Code\",\n basedOn: \"Normal\",\n char: { fontFamily: \"Consolas, monospace\", fontSizePx: 14, color: \"#0b57d0\" },\n para: { lineHeight: 1.35 },\n },\n ],\n };\n}\n\n/** The built-in starter stylesheet with no overrides. */\nexport function defaultStylesheet(): Stylesheet {\n return makeDefaultStylesheet();\n}\n", "// Layer 1 (math): the canonical internal form for equations \u2014 a Presentation\r\n// MathML abstract syntax tree. Pure data (JSON-serializable, no DOM/canvas), so\r\n// it lives beside the rest of the document model and round-trips through\r\n// persistence for free.\r\n//\r\n// WHY MathML and not OMML: the editor, caret movement, and the typesetter all\r\n// operate on this structured tree. `.docx` stores math as OMML (the `m:`\r\n// namespace), so the import/export pipelines convert OMML <-> this AST at the\r\n// edges (see frontend/src/mathml). MathML strings (paste, the equation editor)\r\n// also parse to / serialize from this same tree.\r\n//\r\n// Design notes for the editor:\r\n// - Script kinds (sup/sub/subsup) are ONE node (`script`) with optional sub/sup.\r\n// - Limit kinds (under/over/underover) are ONE node (`limit`).\r\n// - Radical kinds (sqrt/root) are ONE node (`radical`) with optional index.\r\n// Unifying these keeps the visual editor generic: it edits SLOTS, not a\r\n// per-element zoo. `mathSlots`/`mapMathSlots` expose those slots uniformly.\r\n// - An empty `row` (no children) is a PLACEHOLDER SLOT \u2014 what the editor shows\r\n// as a dotted box you can tab into and type.\r\n\r\n/** MathML `mathvariant` \u2014 selects an alphanumeric style (blackboard, fraktur,\r\n * script, \u2026). Maps to/from OMML run style (`m:sty`) for the common cases. */\r\nexport type MathVariant =\r\n | \"normal\"\r\n | \"bold\"\r\n | \"italic\"\r\n | \"bold-italic\"\r\n | \"double-struck\"\r\n | \"fraktur\"\r\n | \"bold-fraktur\"\r\n | \"script\"\r\n | \"bold-script\"\r\n | \"sans-serif\"\r\n | \"bold-sans-serif\"\r\n | \"sans-serif-italic\"\r\n | \"sans-serif-bold-italic\"\r\n | \"monospace\";\r\n\r\n/** A horizontal list of sub-expressions (MathML `mrow`; also the canonical\r\n * wrapper for `math`/`mstyle`). An EMPTY row is an editable placeholder slot. */\r\nexport interface MathRow {\r\n type: \"row\";\r\n children: MathNode[];\r\n}\r\n\r\n/** Identifier \u2014 a variable or function name (MathML `mi`). Default rendering is\r\n * italic for single letters; `variant` overrides (e.g. blackboard-bold \u211D). */\r\nexport interface MathIdent {\r\n type: \"ident\";\r\n text: string;\r\n variant?: MathVariant;\r\n}\r\n\r\n/** Number literal (MathML `mn`). Rendered upright by default; `variant` overrides\r\n * the alphanumeric style (e.g. blackboard-bold `\uD835\uDFD9`, monospace digits). */\r\nexport interface MathNumber {\r\n type: \"number\";\r\n text: string;\r\n variant?: MathVariant;\r\n}\r\n\r\n/** Operator, relation, fence, or separator (MathML `mo`). `stretchy` lets a\r\n * delimiter/arrow grow to its context; `form` disambiguates spacing class. */\r\nexport interface MathOperator {\r\n type: \"op\";\r\n text: string;\r\n stretchy?: boolean;\r\n form?: \"prefix\" | \"infix\" | \"postfix\";\r\n}\r\n\r\n/** Literal text run inside math (MathML `mtext`) \u2014 upright, normal spacing. */\r\nexport interface MathText {\r\n type: \"text\";\r\n text: string;\r\n}\r\n\r\n/** Explicit spacing (MathML `mspace`), width in ems. */\r\nexport interface MathSpace {\r\n type: \"space\";\r\n widthEm?: number;\r\n}\r\n\r\n/** Fraction (MathML `mfrac`). `bevelled` = diagonal bar; `thickness: \"0\"` = no\r\n * bar (binomial coefficients `\\binom`). */\r\nexport interface MathFrac {\r\n type: \"frac\";\r\n num: MathNode;\r\n den: MathNode;\r\n bevelled?: boolean;\r\n thickness?: \"0\" | \"normal\";\r\n}\r\n\r\n/** Sub/superscript (MathML `msub`/`msup`/`msubsup`), unified: presence of\r\n * `sub`/`sup` selects the concrete element. At least one is set. */\r\nexport interface MathScript {\r\n type: \"script\";\r\n base: MathNode;\r\n sub?: MathNode;\r\n sup?: MathNode;\r\n}\r\n\r\n/** Radical (MathML `msqrt`/`mroot`). `index` present = nth root. */\r\nexport interface MathRadical {\r\n type: \"radical\";\r\n radicand: MathNode;\r\n index?: MathNode;\r\n}\r\n\r\n/** Fenced expression \u2014 delimiters around content (MathML `mfenced`, or OMML\r\n * `m:d`). Empty `open`/`close` strings mean \"no delimiter on that side\". */\r\nexport interface MathFenced {\r\n type: \"fenced\";\r\n open: string;\r\n close: string;\r\n /** Separator chars cycled between children when the content is a row. */\r\n separators?: string;\r\n child: MathNode;\r\n}\r\n\r\n/** Under/over scripts (MathML `munder`/`mover`/`munderover`), unified \u2014 limits,\r\n * overbars, hats, underbraces. `accent` keeps the OVER script tight to the base\r\n * (MathML `accent`); `accentUnder` does the same for the UNDER script (MathML\r\n * `accentunder`). The two are independent so an over-accent (hat) and an\r\n * under-accent (underbrace/underbar) never collapse to the same encoding. */\r\nexport interface MathLimit {\r\n type: \"limit\";\r\n base: MathNode;\r\n under?: MathNode;\r\n over?: MathNode;\r\n accent?: boolean;\r\n accentUnder?: boolean;\r\n}\r\n\r\n/** N-ary operator with limits (OMML `m:nary`: \u2211 \u220F \u222B \u2026). MathML has no single\r\n * element for this; it serializes to a scripted operator followed by the body.\r\n * `hideOp` renders just the scripted limits with no operator glyph. */\r\nexport interface MathNary {\r\n type: \"nary\";\r\n op: string;\r\n sub?: MathNode;\r\n sup?: MathNode;\r\n body: MathNode;\r\n hideOp?: boolean;\r\n}\r\n\r\n/** Matrix / array / cases (MathML `mtable`, OMML `m:m`). Row-major. */\r\nexport interface MathMatrix {\r\n type: \"matrix\";\r\n rows: MathNode[][];\r\n colAlign?: (\"left\" | \"center\" | \"right\")[];\r\n}\r\n\r\n/** Phantom \u2014 occupies space but paints nothing (MathML `mphantom`). */\r\nexport interface MathPhantom {\r\n type: \"phantom\";\r\n child: MathNode;\r\n}\r\n\r\n/** Integrity net for constructs outside the supported set: the original source\r\n * is kept verbatim so export re-emits it losslessly and nothing is silently\r\n * dropped. The typesetter renders a visible placeholder. Reaching this in\r\n * practice is a coverage bug to fix, not an accepted fallback. */\r\nexport interface MathUnknown {\r\n type: \"unknown\";\r\n omml?: string;\r\n mathml?: string;\r\n}\r\n\r\nexport type MathNode =\r\n | MathRow\r\n | MathIdent\r\n | MathNumber\r\n | MathOperator\r\n | MathText\r\n | MathSpace\r\n | MathFrac\r\n | MathScript\r\n | MathRadical\r\n | MathFenced\r\n | MathLimit\r\n | MathNary\r\n | MathMatrix\r\n | MathPhantom\r\n | MathUnknown;\r\n\r\n/** A whole equation as stored in the document: the root is always a `row`.\r\n * `display` = block (own line, centered); inline = sits in a text line. */\r\nexport interface MathEquation {\r\n root: MathRow;\r\n display: boolean;\r\n}\r\n\r\n// \u2500\u2500 Constructors / guards \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** An empty row \u2014 the editor's placeholder slot. */\r\nexport function emptyMathRow(): MathRow {\r\n return { type: \"row\", children: [] };\r\n}\r\n\r\n/** A row is a placeholder when it has no children (renders as a dotted box). */\r\nexport function isEmptyMathRow(n: MathNode): boolean {\r\n return n.type === \"row\" && n.children.length === 0;\r\n}\r\n\r\n/** Wrap a node in a row unless it already is one (normalizes slot contents). */\r\nexport function asMathRow(n: MathNode): MathRow {\r\n return n.type === \"row\" ? n : { type: \"row\", children: [n] };\r\n}\r\n\r\n// \u2500\u2500 Generic slot traversal (powers the visual editor + the typesetter) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Ordered child SLOTS of a node \u2014 the editable sub-expressions, in document\r\n * order. Leaf nodes (ident/number/op/text/space/unknown) have none. Used by\r\n * the editor for caret navigation and by converters/typesetter to recurse\r\n * without a per-type switch at every call site. */\r\nexport function mathSlots(n: MathNode): MathNode[] {\r\n switch (n.type) {\r\n case \"row\":\r\n return n.children;\r\n case \"frac\":\r\n return [n.num, n.den];\r\n case \"script\":\r\n return [n.base, ...(n.sub ? [n.sub] : []), ...(n.sup ? [n.sup] : [])];\r\n case \"radical\":\r\n return [n.radicand, ...(n.index ? [n.index] : [])];\r\n case \"fenced\":\r\n return [n.child];\r\n case \"limit\":\r\n return [n.base, ...(n.under ? [n.under] : []), ...(n.over ? [n.over] : [])];\r\n case \"nary\":\r\n return [...(n.sub ? [n.sub] : []), ...(n.sup ? [n.sup] : []), n.body];\r\n case \"matrix\":\r\n return n.rows.flat();\r\n case \"phantom\":\r\n return [n.child];\r\n default:\r\n return [];\r\n }\r\n}\r\n\r\n/** Rebuild `n` with every slot replaced by `f(slot)` \u2014 an IMMUTABLE structural\r\n * map (never mutates the input). The traversal order matches `mathSlots`. */\r\nexport function mapMathSlots(n: MathNode, f: (child: MathNode) => MathNode): MathNode {\r\n switch (n.type) {\r\n case \"row\":\r\n return { ...n, children: n.children.map(f) };\r\n case \"frac\":\r\n return { ...n, num: f(n.num), den: f(n.den) };\r\n case \"script\":\r\n return {\r\n ...n,\r\n base: f(n.base),\r\n ...(n.sub ? { sub: f(n.sub) } : {}),\r\n ...(n.sup ? { sup: f(n.sup) } : {}),\r\n };\r\n case \"radical\":\r\n return { ...n, radicand: f(n.radicand), ...(n.index ? { index: f(n.index) } : {}) };\r\n case \"fenced\":\r\n return { ...n, child: f(n.child) };\r\n case \"limit\":\r\n return {\r\n ...n,\r\n base: f(n.base),\r\n ...(n.under ? { under: f(n.under) } : {}),\r\n ...(n.over ? { over: f(n.over) } : {}),\r\n };\r\n case \"nary\":\r\n return {\r\n ...n,\r\n ...(n.sub ? { sub: f(n.sub) } : {}),\r\n ...(n.sup ? { sup: f(n.sup) } : {}),\r\n body: f(n.body),\r\n };\r\n case \"matrix\":\r\n return { ...n, rows: n.rows.map((r) => r.map(f)) };\r\n case \"phantom\":\r\n return { ...n, child: f(n.child) };\r\n default:\r\n return n;\r\n }\r\n}\r\n\r\n/** Depth-first visit of every node (self first, then slots). */\r\nexport function walkMath(n: MathNode, visit: (node: MathNode) => void): void {\r\n visit(n);\r\n for (const c of mathSlots(n)) walkMath(c, visit);\r\n}\r\n\r\n/** Plain-text approximation of an equation (for search, a11y, alt text). */\r\nexport function mathToPlainText(n: MathNode): string {\r\n switch (n.type) {\r\n case \"ident\":\r\n case \"number\":\r\n case \"op\":\r\n case \"text\":\r\n return n.text;\r\n case \"space\":\r\n return \" \";\r\n case \"frac\":\r\n return `${mathToPlainText(n.num)}/${mathToPlainText(n.den)}`;\r\n case \"fenced\":\r\n return `${n.open}${mathToPlainText(n.child)}${n.close}`;\r\n case \"nary\":\r\n return n.op + mathSlots(n).map(mathToPlainText).join(\"\");\r\n default:\r\n return mathSlots(n).map(mathToPlainText).join(\"\");\r\n }\r\n}\r\n", "// Word's 16 named highlight colors \u2014 the OOXML w:highlight/@w:val vocabulary and\r\n// the hex values the model stores in CharStyle.highlightColor. Single source for\r\n// the docx importer (name \u2192 hex) and exporter (hex \u2192 name), which previously\r\n// hand-maintained the two maps as mutual inverses (adding a color to one and not\r\n// the other silently lost round-trip fidelity for it).\r\n\r\n/** w:highlight name \u2192 the hex the model stores. */\r\nexport const HIGHLIGHT_HEX: Record<string, string> = {\r\n yellow: \"#ffff00\", green: \"#00ff00\", cyan: \"#00ffff\", magenta: \"#ff00ff\",\r\n blue: \"#0000ff\", red: \"#ff0000\", darkBlue: \"#000080\", darkCyan: \"#008080\",\r\n darkGreen: \"#008000\", darkMagenta: \"#800080\", darkRed: \"#800000\", darkYellow: \"#808000\",\r\n darkGray: \"#808080\", lightGray: \"#c0c0c0\", black: \"#000000\", white: \"#ffffff\",\r\n};\r\n\r\n/** model hex \u2192 w:highlight name (the derived inverse \u2014 never hand-edit). */\r\nexport const HIGHLIGHT_NAME: Record<string, string> = Object.fromEntries(\r\n Object.entries(HIGHLIGHT_HEX).map(([name, hex]) => [hex, name]),\r\n);\r\n", "// Generic OOXML field-instruction parsing. Pure and layout-free, so import,\n// export, and the editor command layer all share it.\n//\n// A \"field\" in OOXML is a region delimited by w:fldChar begin/separate/end runs\n// with a w:instrText instruction (e.g. ` TOC \\o \"1-3\" \\h `, ` PAGEREF _Toc1 \\h `,\n// ` MYCHART \"sales-2026\" `). Word ships a fixed set of BUILT-IN field types it\n// knows how to render; everything else is a developer-defined CUSTOM field whose\n// result the host computes (see Document.fields + the editor's resolveField hook).\n// This module only classifies an instruction and splits out its name + arguments;\n// TOC has its own richer parser in ./toc.\n\n/** A field instruction split into its keyword and arguments. */\nexport interface ParsedFieldInstruction {\n /** Field keyword, uppercased (e.g. \"TOC\", \"MYCHART\"). \"\" when the instruction\n * is blank. */\n name: string;\n /** Whitespace/quote-delimited tokens after the keyword (surrounding double\n * quotes stripped). Switches like `\\o` appear here verbatim. */\n args: string[];\n /** The instruction exactly as given (untrimmed). */\n raw: string;\n}\n\n/** Field keywords Word renders itself \u2014 NOT treated as host-resolvable custom\n * fields. TOC is built-in here too (the editor handles it on its own tocEntry\n * path), so it is never captured as a generic custom field. */\nconst BUILTIN_FIELDS = new Set([\n \"TOC\", \"PAGE\", \"NUMPAGES\", \"PAGEREF\", \"HYPERLINK\", \"REF\", \"SEQ\", \"STYLEREF\",\n \"DATE\", \"TIME\", \"CREATEDATE\", \"SAVEDATE\", \"PRINTDATE\", \"EDITTIME\", \"FILENAME\",\n \"FILESIZE\", \"AUTHOR\", \"LASTSAVEDBY\", \"TITLE\", \"SUBJECT\", \"KEYWORDS\", \"COMMENTS\",\n \"NUMWORDS\", \"NUMCHARS\", \"TOA\", \"INDEX\", \"XE\", \"TC\", \"TA\", \"RD\",\n \"FORMTEXT\", \"FORMCHECKBOX\", \"FORMDROPDOWN\", \"SYMBOL\", \"QUOTE\", \"IF\",\n \"DOCPROPERTY\", \"DOCVARIABLE\", \"MERGEFIELD\", \"ADDRESSBLOCK\", \"GREETINGLINE\",\n \"FILLIN\", \"ASK\", \"SET\", \"GOTOBUTTON\", \"MACROBUTTON\", \"EQ\", \"LISTNUM\",\n \"BIBLIOGRAPHY\", \"CITATION\", \"SECTION\", \"SECTIONPAGES\", \"AUTONUM\", \"AUTONUMLGL\",\n \"AUTONUMOUT\", \"INCLUDETEXT\", \"INCLUDEPICTURE\", \"LINK\", \"NOTEREF\",\n]);\n\n/** Is `name` one of Word's built-in field types (so NOT a custom field)? */\nexport function isBuiltinField(name: string): boolean {\n return BUILTIN_FIELDS.has(name.trim().toUpperCase());\n}\n\n/** Tokenize a field instruction, honoring double-quoted arguments. */\nfunction tokenizeField(instr: string): string[] {\n const out: string[] = [];\n const re = /\"([^\"]*)\"|(\\S+)/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(instr)) !== null) out.push(m[1] !== undefined ? m[1] : m[2]!);\n return out;\n}\n\n/** Parse a field instruction into its keyword + arguments. */\nexport function parseFieldInstruction(instr: string): ParsedFieldInstruction {\n const tokens = tokenizeField(instr);\n return { name: tokens.length > 0 ? tokens[0]!.toUpperCase() : \"\", args: tokens.slice(1), raw: instr };\n}\n\n/** Is this instruction a host-resolvable custom field (has a keyword and isn't a\n * Word built-in)? Drives import capture + the editor's \"Update Field\" menu. */\nexport function isCustomFieldInstruction(instr: string): boolean {\n const name = parseFieldInstruction(instr).name;\n return name.length > 0 && !isBuiltinField(name);\n}\n", "// Field evaluation \u2014 pure, layout-free, so import, the editor, and the headless\n// render all agree. Two result shapes:\n// \u2022 \"token\" \u2014 layout-dependent fields (PAGE/NUMPAGES). The result run holds a\n// `{page}`/`{pages}` token the layout engine re-substitutes per page (see\n// engine.ts substituteTokens). The run's CharStyle.fieldId is what marks it a\n// real field vs literal text.\n// \u2022 \"runs\" \u2014 static fields (DATE/TIME/IF), materialized once on insert/update.\n//\n// `spec` \u21C4 `instruction` round-trips via parseFieldSpec / buildInstruction; the\n// verbatim instruction stays the export source of truth (unknown switches survive).\n\nimport type { CharStyle, FieldSpec, IfOp, PageNumFmt, Run } from \"./model/document\";\nimport { parseFieldInstruction, type ParsedFieldInstruction } from \"./fields\";\nimport { textOfRuns } from \"./model/text\";\n\nexport interface FieldEvalCtx {\n /** \"Now\" for DATE/TIME \u2014 passed in so evaluation stays pure/deterministic. */\n now: Date;\n}\n\nexport type FieldEvalResult =\n | { kind: \"runs\"; runs: Run[] }\n | { kind: \"token\"; token: string };\n\nconst MONTHS = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"];\nconst DAYS = [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"];\n\nconst cloneRuns = (runs: Run[]): Run[] => runs.map((r) => ({ text: r.text, style: { ...r.style } }));\n\n/** Token suffix for a page-number format (matches engine.ts `{page:fmt}` grammar). */\nconst numFmtToken = (f: PageNumFmt | undefined): string =>\n f && f !== \"arabic\" ? `:${f}` : \"\";\n\n/** OOXML `\\* <fmt>` switch for a page-number format (matches export fieldInstr). */\nconst numFmtSwitch = (f: PageNumFmt | undefined): string =>\n f === \"roman\" ? \" \\\\* roman\"\n : f === \"Roman\" ? \" \\\\* ROMAN\"\n : f === \"alpha\" ? \" \\\\* alphabetic\"\n : f === \"Alpha\" ? \" \\\\* ALPHABETIC\"\n : \"\";\n\n/** Format a Date with a Word `\\@` picture (subset): yyyy/yy, MMMM/MMM/MM/M,\n * dddd/ddd/dd/d, HH/H, hh/h, mm/m, ss/s, AM/PM, am/pm. `'literal'` passes through. */\nexport function formatFieldDate(d: Date, fmt: string): string {\n const pad = (n: number, w = 2): string => String(n).padStart(w, \"0\");\n const h12 = ((d.getHours() + 11) % 12) + 1;\n let out = \"\";\n let i = 0;\n while (i < fmt.length) {\n if (fmt[i] === \"'\") {\n const end = fmt.indexOf(\"'\", i + 1);\n if (end < 0) { out += fmt.slice(i + 1); break; }\n out += fmt.slice(i + 1, end);\n i = end + 1;\n continue;\n }\n const rest = fmt.slice(i);\n const tok = (t: string): boolean => rest.startsWith(t);\n if (tok(\"yyyy\")) { out += pad(d.getFullYear(), 4); i += 4; }\n else if (tok(\"yy\")) { out += pad(d.getFullYear() % 100); i += 2; }\n else if (tok(\"MMMM\")) { out += MONTHS[d.getMonth()]; i += 4; }\n else if (tok(\"MMM\")) { out += MONTHS[d.getMonth()]!.slice(0, 3); i += 3; }\n else if (tok(\"MM\")) { out += pad(d.getMonth() + 1); i += 2; }\n else if (tok(\"M\")) { out += String(d.getMonth() + 1); i += 1; }\n else if (tok(\"dddd\")) { out += DAYS[d.getDay()]; i += 4; }\n else if (tok(\"ddd\")) { out += DAYS[d.getDay()]!.slice(0, 3); i += 3; }\n else if (tok(\"dd\")) { out += pad(d.getDate()); i += 2; }\n else if (tok(\"d\")) { out += String(d.getDate()); i += 1; }\n else if (tok(\"HH\")) { out += pad(d.getHours()); i += 2; }\n else if (tok(\"H\")) { out += String(d.getHours()); i += 1; }\n else if (tok(\"hh\")) { out += pad(h12); i += 2; }\n else if (tok(\"h\")) { out += String(h12); i += 1; }\n else if (tok(\"mm\")) { out += pad(d.getMinutes()); i += 2; }\n else if (tok(\"m\")) { out += String(d.getMinutes()); i += 1; }\n else if (tok(\"ss\")) { out += pad(d.getSeconds()); i += 2; }\n else if (tok(\"s\")) { out += String(d.getSeconds()); i += 1; }\n else if (tok(\"AM/PM\")) { out += d.getHours() < 12 ? \"AM\" : \"PM\"; i += 5; }\n else if (tok(\"am/pm\")) { out += d.getHours() < 12 ? \"am\" : \"pm\"; i += 5; }\n else { out += fmt[i]; i += 1; }\n }\n return out;\n}\n\n/** Compare two literal operands; numeric when both parse as finite numbers, else string. */\nfunction compare(a: string, op: IfOp, b: string): boolean {\n const na = Number(a);\n const nb = Number(b);\n const numeric = a.trim() !== \"\" && b.trim() !== \"\" && Number.isFinite(na) && Number.isFinite(nb);\n const [x, y]: [number | string, number | string] = numeric ? [na, nb] : [a, b];\n switch (op) {\n case \"=\": return x === y;\n case \"<>\": return x !== y;\n case \"<\": return x < y;\n case \">\": return x > y;\n case \"<=\": return x <= y;\n case \">=\": return x >= y;\n }\n}\n\n/** The chosen branch of an IF field, as fresh (cloned) runs. */\nexport function evaluateIf(spec: Extract<FieldSpec, { type: \"IF\" }>): Run[] {\n return cloneRuns(compare(spec.operandA, spec.op, spec.operandB) ? spec.trueRuns : spec.falseRuns);\n}\n\n/** Evaluate a built-in field to its display result. `baseStyle` styles DATE/TIME text. */\nexport function evaluateField(spec: FieldSpec, baseStyle: CharStyle, ctx: FieldEvalCtx): FieldEvalResult {\n switch (spec.type) {\n case \"PAGE\": return { kind: \"token\", token: `{page${numFmtToken(spec.numFmt)}}` };\n case \"NUMPAGES\": return { kind: \"token\", token: \"{pages}\" };\n case \"DATE\": return { kind: \"runs\", runs: [{ text: formatFieldDate(ctx.now, spec.format), style: { ...baseStyle } }] };\n case \"TIME\": return { kind: \"runs\", runs: [{ text: formatFieldDate(ctx.now, spec.format), style: { ...baseStyle } }] };\n case \"IF\": return { kind: \"runs\", runs: evaluateIf(spec) };\n }\n}\n\n// --- spec \u21C4 instruction --------------------------------------------------------\n\nconst IF_OPS = new Set<string>([\"=\", \"<>\", \"<\", \">\", \"<=\", \">=\"]);\n\nfunction numFmtFromInstr(raw: string): PageNumFmt | undefined {\n if (/\\\\\\*\\s*ROMAN\\b/.test(raw)) return \"Roman\";\n if (/\\\\\\*\\s*roman\\b/.test(raw)) return \"roman\";\n if (/\\\\\\*\\s*ALPHABETIC\\b/.test(raw)) return \"Alpha\";\n if (/\\\\\\*\\s*alphabetic\\b/.test(raw)) return \"alpha\";\n return undefined;\n}\n\n/** The `\\@ \"...\"` (or `\\@ token`) date/time picture from an instruction. */\nfunction atFormat(raw: string): string | undefined {\n return raw.match(/\\\\@\\s*\"([^\"]*)\"/)?.[1] ?? raw.match(/\\\\@\\s*(\\S+)/)?.[1];\n}\n\n/** Parse a built-in field instruction into a typed spec, or undefined (custom /\n * unsupported / unparseable). IF result runs use `baseStyle` (plain text). */\nexport function parseFieldSpec(parsed: ParsedFieldInstruction, baseStyle: CharStyle): FieldSpec | undefined {\n switch (parsed.name) {\n case \"PAGE\": { const f = numFmtFromInstr(parsed.raw); return f ? { type: \"PAGE\", numFmt: f } : { type: \"PAGE\" }; }\n case \"NUMPAGES\": { const f = numFmtFromInstr(parsed.raw); return f ? { type: \"NUMPAGES\", numFmt: f } : { type: \"NUMPAGES\" }; }\n case \"DATE\": return { type: \"DATE\", format: atFormat(parsed.raw) ?? \"M/d/yyyy\" };\n case \"TIME\": return { type: \"TIME\", format: atFormat(parsed.raw) ?? \"h:mm AM/PM\" };\n case \"IF\": {\n const [operandA, op, operandB, trueText, falseText] = parsed.args;\n if (operandA === undefined || op === undefined || operandB === undefined || !IF_OPS.has(op)) return undefined;\n return {\n type: \"IF\", operandA, op: op as IfOp, operandB,\n trueRuns: [{ text: trueText ?? \"\", style: { ...baseStyle } }],\n falseRuns: [{ text: falseText ?? \"\", style: { ...baseStyle } }],\n };\n }\n default: return undefined;\n }\n}\n\n/** Synthesize the verbatim instruction for a typed spec (inverse of parseFieldSpec). */\nexport function buildInstruction(spec: FieldSpec): string {\n switch (spec.type) {\n case \"PAGE\": return ` PAGE${numFmtSwitch(spec.numFmt)} \\\\* MERGEFORMAT `;\n case \"NUMPAGES\": return ` NUMPAGES \\\\* MERGEFORMAT `;\n case \"DATE\": return ` DATE \\\\@ \"${spec.format}\" `;\n case \"TIME\": return ` TIME \\\\@ \"${spec.format}\" `;\n case \"IF\": return ` IF ${spec.operandA} ${spec.op} ${spec.operandB} \"${textOfRuns(spec.trueRuns)}\" \"${textOfRuns(spec.falseRuns)}\" `;\n }\n}\n\n/** Convenience: build a field name from a spec (uppercased keyword). */\nexport function fieldName(spec: FieldSpec): string {\n return spec.type;\n}\n\nexport { parseFieldInstruction };\n"],
5
- "mappings": "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",
6
- "names": []
7
- }