@docx-editor.dev/core 0.0.1-placeholder → 2.0.1

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 (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +92 -2
  3. package/THIRD_PARTY_NOTICES.md +18 -0
  4. package/dist/automation.cjs +1 -0
  5. package/dist/automation.d.cts +74 -0
  6. package/dist/automation.d.ts +74 -0
  7. package/dist/automation.js +1 -0
  8. package/dist/binding.cjs +1 -0
  9. package/dist/binding.d.cts +82 -0
  10. package/dist/binding.d.ts +82 -0
  11. package/dist/binding.js +1 -0
  12. package/dist/chunk-5KSCB2OZ.js +2 -0
  13. package/dist/chunk-BSC44TIT.js +5 -0
  14. package/dist/chunk-DLPL5DAG.js +12 -0
  15. package/dist/chunk-DOEIARE2.js +2 -0
  16. package/dist/chunk-G7YCQBZE.cjs +5 -0
  17. package/dist/chunk-HBL3NNH5.cjs +1 -0
  18. package/dist/chunk-HR7K3W6A.js +1 -0
  19. package/dist/chunk-HVAV2JSN.js +11 -0
  20. package/dist/chunk-MYH2HCUJ.cjs +2 -0
  21. package/dist/chunk-NSM75TN6.cjs +1 -0
  22. package/dist/chunk-PHDVKUWZ.js +1 -0
  23. package/dist/chunk-QADYGK5L.cjs +12 -0
  24. package/dist/chunk-REZUWYB7.js +1 -0
  25. package/dist/chunk-RNBHTPW7.js +10 -0
  26. package/dist/chunk-UD2ZXLFI.cjs +2 -0
  27. package/dist/chunk-VYAVK2AH.cjs +1 -0
  28. package/dist/chunk-YUK46A2N.cjs +11 -0
  29. package/dist/chunk-ZMIX42J7.cjs +10 -0
  30. package/dist/chunk-ZT55Q52J.cjs +2 -0
  31. package/dist/chunk-ZUALAR4M.js +2 -0
  32. package/dist/content-control-walk-CbTqHq6O.d.cts +56 -0
  33. package/dist/content-control-walk-D06GijR0.d.ts +56 -0
  34. package/dist/contracts/document.cjs +1 -0
  35. package/dist/contracts/document.d.cts +246 -0
  36. package/dist/contracts/document.d.ts +246 -0
  37. package/dist/contracts/document.js +0 -0
  38. package/dist/contracts/editor.cjs +1 -0
  39. package/dist/contracts/editor.d.cts +11 -0
  40. package/dist/contracts/editor.d.ts +11 -0
  41. package/dist/contracts/editor.js +1 -0
  42. package/dist/contracts/interaction.cjs +1 -0
  43. package/dist/contracts/interaction.d.cts +11 -0
  44. package/dist/contracts/interaction.d.ts +11 -0
  45. package/dist/contracts/interaction.js +0 -0
  46. package/dist/contracts/modules.cjs +1 -0
  47. package/dist/contracts/modules.d.cts +90 -0
  48. package/dist/contracts/modules.d.ts +90 -0
  49. package/dist/contracts/modules.js +1 -0
  50. package/dist/contracts/types.cjs +1 -0
  51. package/dist/contracts/types.d.cts +452 -0
  52. package/dist/contracts/types.d.ts +452 -0
  53. package/dist/contracts/types.js +0 -0
  54. package/dist/editor-C1QRi1v4.d.ts +1671 -0
  55. package/dist/editor-D0UDock4.d.cts +1671 -0
  56. package/dist/editor.cjs +1 -0
  57. package/dist/editor.css +6017 -0
  58. package/dist/editor.d.cts +271 -0
  59. package/dist/editor.d.ts +271 -0
  60. package/dist/editor.js +1 -0
  61. package/dist/hf-references-BdjsQNE_.d.ts +71 -0
  62. package/dist/hf-references-CgEOSw3i.d.cts +71 -0
  63. package/dist/image-resources-BJnTpG66.d.ts +195 -0
  64. package/dist/image-resources-BpIydXtU.d.cts +195 -0
  65. package/dist/index-CrW-IxXt.d.cts +2515 -0
  66. package/dist/index-ZqBEOrTV.d.ts +2515 -0
  67. package/dist/index.cjs +1 -0
  68. package/dist/index.d.cts +22 -0
  69. package/dist/index.d.ts +22 -0
  70. package/dist/index.js +1 -0
  71. package/dist/layout.cjs +1 -0
  72. package/dist/layout.d.cts +2316 -0
  73. package/dist/layout.d.ts +2316 -0
  74. package/dist/layout.js +1 -0
  75. package/dist/note-nodes-BPSTVDMr.d.ts +108 -0
  76. package/dist/note-nodes-C6DLxlvj.d.cts +108 -0
  77. package/dist/note-properties-CnmCoory.d.cts +68 -0
  78. package/dist/note-properties-m75hmP70.d.ts +68 -0
  79. package/dist/ooxml-package-CR5us6ad.d.cts +354 -0
  80. package/dist/ooxml-package-D_CtywTW.d.ts +354 -0
  81. package/dist/ooxml-tree-BU0e3DVi.d.cts +871 -0
  82. package/dist/ooxml-tree-BU0e3DVi.d.ts +871 -0
  83. package/dist/output.cjs +1 -0
  84. package/dist/output.d.cts +158 -0
  85. package/dist/output.d.ts +158 -0
  86. package/dist/output.js +1 -0
  87. package/dist/protocol-CAkOBBGE.d.cts +1201 -0
  88. package/dist/protocol-oxHaWmt4.d.ts +1201 -0
  89. package/dist/review-support-DS8DYX2a.d.cts +326 -0
  90. package/dist/review-support-Fy3vdIea.d.ts +326 -0
  91. package/dist/revision-projection-72ZApe-g.d.cts +84 -0
  92. package/dist/revision-projection-DHAkcD_L.d.ts +84 -0
  93. package/dist/semantic-cell-selection-CZjBSmMv.d.cts +427 -0
  94. package/dist/semantic-cell-selection-DsmHmLJ5.d.ts +427 -0
  95. package/dist/semantic-interaction-D-Dszxrn.d.cts +187 -0
  96. package/dist/semantic-interaction-Dy-dCUFF.d.ts +187 -0
  97. package/dist/semantic-paint-drawings-BA3z9jl8.d.ts +21 -0
  98. package/dist/semantic-paint-drawings-D7qbB0E2.d.cts +21 -0
  99. package/dist/semantic-records-CJR3BvHs.d.cts +1432 -0
  100. package/dist/semantic-records-CWodBJsz.d.ts +1432 -0
  101. package/dist/shaped-measurer-B-TAtcQU.d.ts +613 -0
  102. package/dist/shaped-measurer-CNIT_svZ.d.cts +613 -0
  103. package/dist/store.cjs +1 -0
  104. package/dist/store.d.cts +2247 -0
  105. package/dist/store.d.ts +2247 -0
  106. package/dist/store.js +1 -0
  107. package/dist/tree-op-types-CwOeiaQ8.d.ts +1431 -0
  108. package/dist/tree-op-types-DMiW6nIH.d.cts +1431 -0
  109. package/dist/tree-package-store-D5S7VKs5.d.ts +708 -0
  110. package/dist/tree-package-store-DOKMtCnh.d.cts +708 -0
  111. package/dist/tree-session-CiUel5Kz.d.ts +542 -0
  112. package/dist/tree-session-D47_imvR.d.cts +542 -0
  113. package/dist/tree-store-CxifqwIv.d.cts +273 -0
  114. package/dist/tree-store-hLtjeyWE.d.ts +273 -0
  115. package/package.json +105 -15
  116. package/index.js +0 -1
@@ -0,0 +1,1432 @@
1
+ import { O as OoxmlProperty, j as DrawingAccessibility, S as SourceCrop, k as DrawingTransform, V as VectorShapeProjection, f as DrawingHorizontalReferenceFrame, g as DrawingVerticalReferenceFrame, b as ImageWrapTarget, D as DrawingProjection, l as TableBorderStyle } from './tree-op-types-CwOeiaQ8.js';
2
+ import { O as OoxmlNode, o as OoxmlDrawingNode, a as OoxmlElement } from './ooxml-tree-BU0e3DVi.js';
3
+ import { I as ImageResourceState } from './image-resources-BJnTpG66.js';
4
+ import { a as RevisionAttribution } from './revision-projection-DHAkcD_L.js';
5
+
6
+ /** Whether a paragraph must start a new page (`w:pageBreakBefore`). */
7
+ declare function paragraphBreaksBefore(props: readonly OoxmlProperty[]): boolean;
8
+ /**
9
+ * Soft ceiling matching the spike's resolved-style limit (31_680 twips ≈ 22"). Beyond
10
+ * that an attacker-authored spacing would push pagination into pathological page counts.
11
+ */
12
+ declare const MAX_PARAGRAPH_SPACING_PT: number;
13
+ /** Soft ceiling on border width (96 eighths = 12pt). Word's UI tops out well below this. */
14
+ declare const MAX_BORDER_WIDTH_PT = 12;
15
+ /** Soft ceiling on border-to-text gap (`w:space`, already in points). */
16
+ declare const MAX_BORDER_SPACE_PT = 3168;
17
+ /**
18
+ * A paragraph's resolved space before and after, in points.
19
+ *
20
+ * Already collapsed against `w:contextualSpacing`, so adjacent same-style paragraphs that suppress
21
+ * their gap arrive here with it removed rather than leaving that to whoever stacks them.
22
+ */
23
+ interface ParagraphSpacing {
24
+ /** `w:spacing/@before`, in points. */
25
+ readonly before: number;
26
+ /** `w:spacing/@after`, in points. */
27
+ readonly after: number;
28
+ }
29
+ /**
30
+ * Resolved line spacing (`w:spacing/@line` + `@lineRule`, ECMA-376 17.3.1.33).
31
+ *
32
+ * `auto` is the interesting one: `@line` is 240ths of a line, so 240 is single, 360 is
33
+ * one-and-a-half, 480 is double — and Word's own Normal style since 2013 is 259, i.e.
34
+ * 1.08. A document laid out at a flat single spacing is ~8% tight on EVERY line, which
35
+ * moves every page break, so this is not a cosmetic detail.
36
+ *
37
+ * `exact` fixes the line box at `@line` twips and lets tall glyphs clip, the way Word
38
+ * does. `atLeast` uses it as a floor.
39
+ */
40
+ type LineSpacingRule = 'auto' | 'exact' | 'atLeast';
41
+ /**
42
+ * Resolved line spacing: the rule, and the value it applies.
43
+ *
44
+ * `value` means different things per rule — 240ths of a line under `auto`, points under `exact`
45
+ * and `atLeast` — which is why the two travel together and neither is useful alone.
46
+ */
47
+ interface ParagraphLineSpacing {
48
+ readonly rule: LineSpacingRule;
49
+ /** `auto`: the 240ths-of-a-line multiplier numerator. Otherwise points. */
50
+ readonly value: number;
51
+ }
52
+ /** Single spacing: what a paragraph that says nothing gets. */
53
+ declare const SINGLE_LINE_SPACING: ParagraphLineSpacing;
54
+ /**
55
+ * One resolved `w:pBdr` edge: its style, colour, thickness and gap.
56
+ *
57
+ * `widthPt` and `spacePt` are already converted and CLAMPED — both come from a file, and an
58
+ * unbounded border width becomes a layout dimension.
59
+ */
60
+ interface ParagraphBorderEdge {
61
+ /** Authored `ST_Border` value (`single`, `dashed`, …). */
62
+ readonly val: string;
63
+ /** RRGGBB, or null when auto/missing (paint defaults to black). */
64
+ readonly color: string | null;
65
+ /** Border thickness in points (`w:sz` is eighths of a point). */
66
+ readonly widthPt: number;
67
+ /** Gap from text to the rule, in points (`w:space`). */
68
+ readonly spacePt: number;
69
+ /**
70
+ * `w:shadow` — Word offsets a drop shadow behind the rule.
71
+ *
72
+ * Present only when authored true, so an edge that says nothing keeps the shape earlier
73
+ * fixtures assert. Resolved and carried; drawing it is deferred.
74
+ */
75
+ readonly shadow?: true;
76
+ }
77
+ /** The six `CT_PBdr` children, in schema order (ECMA-376 §17.3.1.24). */
78
+ declare const PARAGRAPH_BORDER_SIDES: readonly ["top", "left", "bottom", "right", "between", "bar"];
79
+ /**
80
+ * Which of the six `CT_PBdr` edges.
81
+ *
82
+ * Four are physical box edges; `between` and `bar` are group-relative, drawn only where
83
+ * consecutive paragraphs share a border definition.
84
+ */
85
+ type ParagraphBorderSide = (typeof PARAGRAPH_BORDER_SIDES)[number];
86
+ /**
87
+ * A paragraph's resolved `w:pBdr` (ECMA-376 §17.3.1.24).
88
+ *
89
+ * `top`/`left`/`bottom`/`right` are the four physical edges of the box. The other two are
90
+ * group-relative: `between` draws at a boundary INSIDE a run of consecutive paragraphs whose
91
+ * border settings are identical, and `bar` is the vertical change-bar rule beside the
92
+ * paragraph, drawn whether or not the paragraph groups with its neighbours.
93
+ */
94
+ interface ParagraphBorders {
95
+ readonly top?: ParagraphBorderEdge;
96
+ readonly left?: ParagraphBorderEdge;
97
+ readonly bottom?: ParagraphBorderEdge;
98
+ readonly right?: ParagraphBorderEdge;
99
+ readonly between?: ParagraphBorderEdge;
100
+ readonly bar?: ParagraphBorderEdge;
101
+ }
102
+ /**
103
+ * Resolve `w:spacing` before/after from flat paragraph properties.
104
+ *
105
+ * Line spacing (`w:line` / `w:lineRule`) is a separate concern — it changes measured line
106
+ * height, not the gap between paragraphs — and is not resolved here.
107
+ */
108
+ declare function paragraphSpacing(props: readonly OoxmlProperty[]): ParagraphSpacing;
109
+ /**
110
+ * Resolve `w:line` / `w:lineRule` from flat paragraph properties.
111
+ *
112
+ * Merged per attribute for the same reason as before/after: `w:spacing` is one element
113
+ * carrying independent attributes, and a style that states only `@line` must not reset the
114
+ * rule an earlier entry in the cascade set.
115
+ */
116
+ declare function paragraphLineSpacing(props: readonly OoxmlProperty[]): ParagraphLineSpacing;
117
+ /**
118
+ * Apply resolved line spacing to a line's natural (glyph-derived) box.
119
+ *
120
+ * Word places `auto` / `atLeast` extras BELOW the line (the last line's multiple spacing
121
+ * still separates it from the next paragraph). Putting that delta above inverted cover-page
122
+ * rhythm: `w:line="460"` on "between" opened a large gap above the word and almost none
123
+ * before "MERIDIAN". `exact` taller than the glyphs centers the text (ECMA-376 17.3.1.33).
124
+ * An `exact` box smaller than the glyphs keeps the baseline inside so clipped text still
125
+ * sits on it.
126
+ */
127
+ declare function applyLineSpacing(spacing: ParagraphLineSpacing, naturalHeight: number, naturalBaseline: number): {
128
+ height: number;
129
+ baseline: number;
130
+ };
131
+ /**
132
+ * `w:contextualSpacing` (17.3.1.9): drop before/after between paragraphs of the SAME
133
+ * style. Word's built-in `ListParagraph` sets it, so every list authored in Word gets a
134
+ * paragraph gap between items without this.
135
+ */
136
+ declare function paragraphContextualSpacing(props: readonly OoxmlProperty[]): boolean;
137
+ /**
138
+ * Resolve `w:pBdr` from the paragraph-properties node.
139
+ *
140
+ * Nested — every edge is a child of `pBdr`, not an attribute — so this reads the typed tree
141
+ * rather than the flattened `OoxmlProperty[]` bag `propertiesOf` builds for leaf props.
142
+ */
143
+ declare function paragraphBorders(pPr: OoxmlNode | undefined): ParagraphBorders;
144
+ /**
145
+ * `w:pBdr` after the style cascade: a later `w:pBdr` replaces an earlier one WHOLESALE.
146
+ *
147
+ * Word does not merge edges across the cascade. A style that states only `w:bottom` discards
148
+ * the box its `basedOn` ancestor declared, so folding edge by edge would leave a lone
149
+ * underline surrounded by a box no one authored. Absence inherits; `nil`/`none` clear.
150
+ */
151
+ declare function cascadedParagraphBorders(paragraphPropertyNodes: readonly OoxmlNode[]): ParagraphBorders;
152
+ /**
153
+ * Visual stroke thickness layout publishes for one edge (points).
154
+ *
155
+ * Compound `ST_Border` values (`double`, …) use the shared inflated band so a thin
156
+ * `w:sz="3"` double still occupies a visible double-line box — matching table borders.
157
+ */
158
+ declare function paragraphBorderStrokeWidthPt(edge: ParagraphBorderEdge): number;
159
+ /**
160
+ * Extent one border edge occupies away from the text it decorates: gap plus rule, in points.
161
+ *
162
+ * Vertically that is flow height — a top rule pushes the first line down, a bottom rule holds
163
+ * the page open below the last one — so pagination has to see it. Horizontally it is
164
+ * publish-only: Word draws left/right paragraph rules OUTSIDE the text column and never
165
+ * re-breaks the lines, which is why adding a box to a paragraph in Word does not reflow it.
166
+ */
167
+ declare function paragraphBorderExtentPt(edge: ParagraphBorderEdge | undefined): number;
168
+ /** Vertical extent a bottom border adds below the last line (gap + rule). */
169
+ declare function bottomBorderExtentPt(edge: ParagraphBorderEdge | undefined): number;
170
+ /**
171
+ * Identity of a paragraph's border set, for the `w:between` group rule.
172
+ *
173
+ * Word treats consecutive paragraphs whose border settings are IDENTICAL as ONE bordered
174
+ * block: the top rule draws above the first, the bottom rule below the last, and each
175
+ * interior boundary gets `w:between` or nothing (§17.3.1.24). That is why applying a box to
176
+ * three selected paragraphs in Word draws one box and not three.
177
+ *
178
+ * Empty string means "no borders", which never groups with anything.
179
+ */
180
+ declare function paragraphBordersFingerprint(borders: ParagraphBorders): string;
181
+ /**
182
+ * Gap to insert before a paragraph once the previous paragraph's `after` is already in the
183
+ * flow cursor — Word takes the larger of the two rather than summing them.
184
+ */
185
+ declare function collapsedSpaceBefore(before: number, previousAfter: number): number;
186
+ /**
187
+ * Applied before-spacing for placement (Word 2013+ / compat mode 15).
188
+ *
189
+ * Adjacent before/after still collapse to the larger gap, but before is dropped entirely when
190
+ * the paragraph begins at the top of a page mid-section. The first paragraph of a document or
191
+ * section retains before. Callers publish this applied value on the fragment so shading, borders,
192
+ * selection, and paint share one geometry.
193
+ */
194
+ declare function appliedSpaceBefore(before: number, previousAfter: number, atTopOfPage: boolean, firstParagraphOfSection: boolean): number;
195
+
196
+ /** Soft ceiling matching Word's practical custom-tab UI limit. */
197
+ declare const MAX_TAB_STOPS = 64;
198
+ /**
199
+ * Soft ceiling on a tab position (31_680 twips ≈ 22"), matching paragraph-spacing bounds so
200
+ * a hostile stop cannot shove layout into pathological widths.
201
+ */
202
+ declare const MAX_TAB_POSITION_TWIPS = 31680;
203
+ /** OOXML / Word default when `w:settings/w:defaultTabStop` is absent: 720 twips = 0.5". */
204
+ declare const DEFAULT_TAB_INTERVAL_TWIPS = 720;
205
+ /** The default tab interval in points — {@link DEFAULT_TAB_INTERVAL_TWIPS} converted. */
206
+ declare const DEFAULT_TAB_INTERVAL_PT: number;
207
+ /**
208
+ * How a tab stop positions the text that follows it.
209
+ *
210
+ * Only `left` is a plain cursor jump. The other three size the tab glyph from the MEASURED
211
+ * following segment, so its end, centre or decimal point lands on the stop.
212
+ */
213
+ type TabAlignment = 'left' | 'center' | 'right' | 'decimal';
214
+ /**
215
+ * `w:tab/@w:leader` (ECMA-376 §17.3.1.38, ST_TabTlc): the character repeated across the
216
+ * space a tab reserves. `none` is the default and is represented by an absent leader.
217
+ *
218
+ * This is the difference between a Word table of contents and a column of headings floating
219
+ * next to a column of page numbers, so it is carried through layout to paint rather than
220
+ * dropped as a geometry-irrelevant attribute.
221
+ */
222
+ type TabLeader = 'dot' | 'hyphen' | 'underscore' | 'heavy' | 'middleDot';
223
+ /**
224
+ * The character each leader repeats (§17.3.1.38, ST_TabTlc).
225
+ *
226
+ * Lives with the type rather than with the painter because LAYOUT has to measure it: a
227
+ * leader is the same character typed over and over, and the only way to space it the way
228
+ * typing it would is to ask the measurer how wide it actually is.
229
+ */
230
+ declare const TAB_LEADER_GLYPH: ReadonlyMap<TabLeader, string>;
231
+ /** One authored `w:tab`: where it sits, how it aligns, and what fills the gap. */
232
+ interface TabStop {
233
+ /** Position from the paragraph content origin, in points. */
234
+ readonly positionPt: number;
235
+ readonly alignment: TabAlignment;
236
+ /** Absent for `none` — the schema default and the overwhelming majority of stops. */
237
+ readonly leader?: TabLeader;
238
+ }
239
+ /**
240
+ * A paragraph's tab stops after the style cascade, with the default interval that applies past
241
+ * the last explicit one.
242
+ */
243
+ interface ResolvedTabStops {
244
+ /** Custom stops sorted by ascending position. */
245
+ readonly stops: readonly TabStop[];
246
+ /** Default-tab interval in points (always positive and bounded). */
247
+ readonly defaultIntervalPt: number;
248
+ }
249
+ /** The frozen "no custom stops" value, so a paragraph without tabs mints no object. */
250
+ declare const EMPTY_TAB_STOPS: ResolvedTabStops;
251
+ /**
252
+ * Resolve tab stops from cascaded `w:pPr` nodes (docDefaults → style chain → direct).
253
+ *
254
+ * Each `w:tabs` merges with `clear` support; absence inherits. The leader travels with the
255
+ * stop that declared it — a `clear` at the same position discards both together.
256
+ */
257
+ declare function cascadedTabStops(paragraphPropertyNodes: readonly OoxmlNode[]): ResolvedTabStops;
258
+ /** Direct `w:pPr` only — used when no style cascade table is present. */
259
+ declare function paragraphTabStops(pPr: OoxmlNode | undefined): ResolvedTabStops;
260
+ /**
261
+ * Republish stops under a document-wide default-tab interval (`w:defaultTabStop`).
262
+ *
263
+ * The cascade resolves stops from the paragraph's own property chain, which cannot see
264
+ * `settings.xml`; the interval is a document constant that arrives from the session. Returns
265
+ * the input unchanged when nothing moves, so a cache-key fingerprint stays stable.
266
+ */
267
+ declare function withDefaultTabInterval(tabs: ResolvedTabStops, defaultIntervalPt: number | undefined): ResolvedTabStops;
268
+ /**
269
+ * Read `w:settings/w:defaultTabStop` (ECMA-376 §17.15.1.25), in points.
270
+ *
271
+ * Word's own interval, not a constant: a metric-locale template writes `w:val="1134"` (2cm)
272
+ * and every default-interval tab in the document lands on that grid instead of the 0.5"
273
+ * one. The value is FILE-DERIVED, so a non-integer, non-positive or out-of-range `val` falls
274
+ * back to the schema default rather than being trusted into layout arithmetic.
275
+ *
276
+ * `ST_TwipsMeasure` also admits a universal measure (`"2cm"`); Word writes plain twips, and
277
+ * the spelled form falls back to the default rather than being parsed here.
278
+ */
279
+ declare function defaultTabIntervalFromSettings(settings: OoxmlNode | null | undefined): number;
280
+ /** Where one tab character lands: the resolved x, and the stop that decided it. */
281
+ interface TabDestination {
282
+ readonly positionPt: number;
283
+ readonly alignment: TabAlignment;
284
+ /** Leader of the stop that was reached; absent for `none` and for default-interval tabs. */
285
+ readonly leader?: TabLeader;
286
+ }
287
+ /**
288
+ * Next tab destination strictly past `currentX`, preferring custom stops then the default
289
+ * interval. Destination is clamped to `rightEdge` so stops cannot escape the content box.
290
+ */
291
+ declare function nextTabDestination(tabs: ResolvedTabStops, currentX: number, rightEdge: number): TabDestination;
292
+ /**
293
+ * Width of a tab glyph so the following segment lands on the destination.
294
+ *
295
+ * `segmentWidth` / `decimalOffset` are already measured in points. Decimal offset is the
296
+ * advance from the segment start to the decimal point (0 when none — treated like right).
297
+ */
298
+ declare function tabAdvanceWidth(alignment: TabAlignment, currentX: number, destinationX: number, segmentWidth: number, decimalOffset: number): number;
299
+ /**
300
+ * Stable fingerprint for layout cache keys — nested `w:tabs` are not in flat `OoxmlProperty`
301
+ * bags, so style-inherited stops must be named explicitly or breaks would collide.
302
+ */
303
+ declare function tabStopsFingerprint(tabs: ResolvedTabStops): string;
304
+
305
+ /** `w:vertAlign` — script position, which also scales the run's effective size. */
306
+ type VerticalAlign = 'baseline' | 'superscript' | 'subscript';
307
+ /** A resolved underline: its variant, and its colour when it does not follow the text. */
308
+ interface ResolvedUnderline {
309
+ /** The authored `ST_Underline` variant. */
310
+ readonly variant: string;
311
+ /** RRGGBB, or null when the underline follows the text colour. */
312
+ readonly color: string | null;
313
+ }
314
+ /**
315
+ * A run's character properties after the full cascade, in the units layout works in.
316
+ *
317
+ * Points rather than half-points, RRGGBB rather than theme references — everything already
318
+ * resolved, so measurement and paint never re-run the cascade per glyph.
319
+ */
320
+ interface ResolvedRunStyle {
321
+ readonly fontFamily: string | null;
322
+ /** Points. `w:sz` is half-points, so 22 becomes 11. */
323
+ readonly fontSizePt: number;
324
+ /** RRGGBB, or null for the inherited/automatic colour. */
325
+ readonly color: string | null;
326
+ readonly bold: boolean;
327
+ readonly italic: boolean;
328
+ readonly underline: ResolvedUnderline | null;
329
+ readonly strike: boolean;
330
+ readonly doubleStrike: boolean;
331
+ /** An `ST_HighlightColor` name, or null. */
332
+ readonly highlight: string | null;
333
+ /**
334
+ * Character shading fill (`w:rPr/w:shd`), validated RRGGBB, or null.
335
+ *
336
+ * Paint applies this as the glyph-box background; a recognised highlight overrides it.
337
+ * Measurement ignores shading.
338
+ */
339
+ readonly shading: string | null;
340
+ readonly verticalAlign: VerticalAlign;
341
+ /** `w:position`, in points. Positive raises the baseline. */
342
+ readonly baselineShiftPt: number;
343
+ readonly caps: boolean;
344
+ readonly smallCaps: boolean;
345
+ /** `w:spacing`, in points. Added to every advance. */
346
+ readonly characterSpacingPt: number;
347
+ /** `w:w`, as a percentage. 100 is unscaled. */
348
+ readonly horizontalScalePercent: number;
349
+ /** `w:kern`, in points: the size at or above which kerning applies. 0 disables it. */
350
+ readonly kerningMinPt: number;
351
+ /**
352
+ * `w:vanish` (ECMA-376 §17.3.2.45): the run is hidden text.
353
+ *
354
+ * Word does not draw it AND does not paginate it — hidden index or comment text takes no
355
+ * space at all. So this cannot be a paint-time opacity: a hidden run that is still measured
356
+ * pushes every following line, and every following page break, to the wrong place. Layout
357
+ * drops the content instead (see `piecesOfParagraph`).
358
+ *
359
+ * `w:specVanish` (§17.3.2.36) is a different property — an always-hidden paragraph mark on
360
+ * a heading — and never sets this.
361
+ */
362
+ readonly hidden: boolean;
363
+ }
364
+ /** The style a run inherits when it authors nothing. */
365
+ declare const DEFAULT_RUN_STYLE: ResolvedRunStyle;
366
+ /**
367
+ * The theme part's two Latin typefaces, for resolving `w:rFonts` theme attributes.
368
+ *
369
+ * Structurally identical to the binding lane's `DocumentThemeFonts` and assignable from it.
370
+ * Declared here so this lane reads two validated strings rather than the theme tree.
371
+ */
372
+ interface ThemeFonts {
373
+ /** `a:majorFont` latin typeface — headings. */
374
+ readonly major: string | null;
375
+ /** `a:minorFont` latin typeface — body text. */
376
+ readonly minor: string | null;
377
+ }
378
+ /**
379
+ * Resolve one run's direct formatting.
380
+ *
381
+ * Unrecognised values are DROPPED rather than guessed: a `w:sz` of `"large"` leaves the
382
+ * default size rather than inventing one, because a wrong measurement moves every glyph
383
+ * after it and a missing one is visible immediately.
384
+ *
385
+ * `themeFonts` resolves `w:rFonts` theme references. Absent, a theme-only `rFonts` leaves
386
+ * the family inherited — which is what every run of a theme-fonted document does, so the
387
+ * whole document falls back to the surface default face.
388
+ */
389
+ declare function resolveRunStyle(props: readonly OoxmlProperty[], themeFonts?: ThemeFonts): ResolvedRunStyle;
390
+ /** The text as it is DRAWN, after case transforms. Measurement must use this, not the source. */
391
+ declare function displayText(text: string, style: ResolvedRunStyle): string;
392
+ /** Measure run text the way layout breaks lines and paints glyphs (caps/small-caps aware). */
393
+ declare function measureDisplayText(text: string, style: ResolvedRunStyle, measurer: TextMeasurer): number;
394
+ /** Whether two resolved styles are identical, for span merging and cache keys. */
395
+ declare function runStylesEqual(a: ResolvedRunStyle, b: ResolvedRunStyle): boolean;
396
+ /**
397
+ * How far a run's glyphs are lifted off the line's baseline, in points. Positive is up.
398
+ *
399
+ * Super and subscript move the GLYPHS without moving the run's box, so the box keeps tiling
400
+ * the line and the selection band stays continuous. Anything drawing at the glyphs — the
401
+ * painter, and the caret — has to apply this itself, and from one place, or the two drift.
402
+ */
403
+ declare function baselineShiftPtOf(style: ResolvedRunStyle): number;
404
+
405
+ interface DrawingPoint {
406
+ readonly x: number;
407
+ readonly y: number;
408
+ }
409
+ interface DrawingInsets {
410
+ readonly top: number;
411
+ readonly right: number;
412
+ readonly bottom: number;
413
+ readonly left: number;
414
+ }
415
+ type DrawingClipFallback = 'none' | 'unsupported-preset';
416
+ interface DrawingGeometry {
417
+ readonly contentBounds: LayoutBox;
418
+ readonly paintBounds: LayoutBox;
419
+ readonly hitBounds: LayoutBox;
420
+ readonly transformedCorners: readonly DrawingPoint[];
421
+ readonly clipPolygon: readonly DrawingPoint[] | null;
422
+ readonly clipFallback: DrawingClipFallback;
423
+ readonly effectInsets: DrawingInsets;
424
+ }
425
+
426
+ interface InlineDrawingRecord {
427
+ readonly kind: 'inlineDrawing';
428
+ readonly drawingNodeId: string;
429
+ readonly paragraphId: string;
430
+ readonly ownerPartName: string;
431
+ readonly start: number;
432
+ /** Left edge of the extent box (slot + distL). */
433
+ readonly x: number;
434
+ readonly y: number;
435
+ readonly width: number;
436
+ readonly height: number;
437
+ readonly distL: number;
438
+ readonly distR: number;
439
+ readonly distT: number;
440
+ readonly distB: number;
441
+ /** Caret/hit advance start (slot left, before distL). */
442
+ readonly advanceStart: number;
443
+ /** Caret/hit advance end (slot + totalWidth). */
444
+ readonly advanceEnd: number;
445
+ readonly baselineOffset: number;
446
+ readonly paintBounds: LayoutBox;
447
+ readonly hitBounds: LayoutBox;
448
+ readonly geometry: DrawingGeometry;
449
+ readonly resource: ImageResourceState;
450
+ readonly accessibility: DrawingAccessibility;
451
+ /** Sanitized external hyperlink projection; inert until an explicit gesture activates it. */
452
+ readonly hyperlinkHref: string | null;
453
+ readonly effects: Readonly<{
454
+ readonly grayscale: boolean;
455
+ readonly brightness: number;
456
+ readonly contrast: number;
457
+ }>;
458
+ readonly crop: SourceCrop;
459
+ readonly transform: DrawingTransform;
460
+ /** Fixed non-picture graphic kind for refusal labels (`chart`, `group`, …); null for pictures. */
461
+ readonly placeholderGraphicKind: string | null;
462
+ /** Typed solid-geometry payload for a renderable `wps:wsp` shape; null otherwise. */
463
+ readonly vectorShape: VectorShapeProjection | null;
464
+ }
465
+ interface InlineDrawingLayoutContext {
466
+ readonly ownerPartName: string;
467
+ /** Precomputed run-level atom id (`w:drawing` or MC wrapper) → projection. */
468
+ readonly projectionForAtom?: (atomNodeId: string) => DrawingProjection | null;
469
+ readonly project: (drawing: OoxmlDrawingNode) => DrawingProjection | null;
470
+ readonly resourceOf: (projection: DrawingProjection) => ImageResourceState;
471
+ }
472
+ /** Named fallback when a positioning frame cannot be resolved (OpenSpec 4.6). */
473
+ type AnchoredDrawingLayoutFallback = 'unresolvable-frame' | 'page-defer-exhausted';
474
+ interface AnchoredDrawingRecord extends Omit<InlineDrawingRecord, 'kind' | 'baselineOffset' | 'advanceStart' | 'advanceEnd' | 'distL' | 'distR' | 'distT' | 'distB'> {
475
+ readonly kind: 'anchoredDrawing';
476
+ readonly anchorParagraphId: string;
477
+ readonly horizontalFrame: DrawingHorizontalReferenceFrame;
478
+ readonly verticalFrame: DrawingVerticalReferenceFrame;
479
+ readonly horizontalFrameOrigin: number;
480
+ readonly verticalFrameOrigin: number;
481
+ readonly behindDocument: boolean;
482
+ readonly allowOverlap: boolean;
483
+ readonly layoutInCell: boolean;
484
+ readonly relativeHeight: number;
485
+ readonly wrap: Exclude<ImageWrapTarget, 'inline'>;
486
+ readonly layoutFallback?: AnchoredDrawingLayoutFallback;
487
+ /** Canonical document traversal index within the owner story part. */
488
+ readonly sourceOrder?: number;
489
+ }
490
+
491
+ /**
492
+ * Page-field evaluation context for furniture projection.
493
+ *
494
+ * `pageNumber` is the displayed PAGE value after section `w:pgNumType/@w:start` (1-based).
495
+ * `pageCount` is document NUMPAGES. `sectionPageCount` is SECTIONPAGES for the attached
496
+ * section. `format` is the authored `w:pgNumType/@w:fmt` applied only to PAGE.
497
+ */
498
+ interface FieldPageContext {
499
+ readonly pageNumber: number;
500
+ readonly pageCount: number;
501
+ /** SECTIONPAGES; defaults to `pageCount` when omitted (single-section callers). */
502
+ readonly sectionPageCount?: number;
503
+ /** Authored ST_NumberFormat for PAGE; absent → decimal. */
504
+ readonly format?: string;
505
+ }
506
+ /** A half-open model-offset range, in the paragraph's own UTF-16 offset space. */
507
+ interface ModelRange {
508
+ readonly start: number;
509
+ readonly end: number;
510
+ }
511
+ /**
512
+ * How layout turns a typed `w:hyperlink` node into the sanitized record spans carry.
513
+ *
514
+ * Injected rather than computed here because resolving `r:id` needs the PACKAGE's
515
+ * relationships and this module only ever sees one part's tree. `null` means the caller
516
+ * declined to project — the runs still measure and paint, they simply carry no link, which is
517
+ * the right degradation: text is never lost for want of a target.
518
+ */
519
+ type HyperlinkProjector = (link: OoxmlNode) => SpanLinkRecord | null;
520
+ /**
521
+ * Format a displayed PAGE value through the shared ST_NumberFormat resolver.
522
+ *
523
+ * Unknown / script-specific formats fall back to decimal (same convention as list markers).
524
+ * `none` / `bullet` are meaningless for page numbers and also fall back to decimal so a
525
+ * hostile fmt cannot blank the furniture.
526
+ */
527
+ declare function formatPageNumber(value: number, format: string | undefined): string;
528
+
529
+ /** Test-only mutable counters; omitted by production call sites. Not package-public. */
530
+ interface TableBorderGridResolveWork {
531
+ /** Sparse intervals emitted (not dense column slots). */
532
+ ownershipSlotsWritten: number;
533
+ columnLookups: number;
534
+ }
535
+ /** Shared remaining interval budget for nested table finalization in one layout. */
536
+ interface TableBorderOwnershipBudget {
537
+ intervalsRemaining: number;
538
+ }
539
+
540
+ /** Which physical edge of a box a border sits on. */
541
+ type TableBorderSideName = 'top' | 'right' | 'bottom' | 'left';
542
+ /**
543
+ * One border edge in one of three states.
544
+ *
545
+ * `omitted` and `none` are NOT the same: an omitted edge inherits from the table style or the
546
+ * neighbouring cell, while an explicit `none` wins the conflict and draws nothing. Collapsing
547
+ * them would let a style's border reappear where the author removed it.
548
+ */
549
+ type TableBorderSide = {
550
+ readonly state: 'omitted';
551
+ } | {
552
+ readonly state: 'none';
553
+ } | {
554
+ readonly state: 'edge';
555
+ readonly style: TableBorderStyle;
556
+ /** Validated RRGGBB, or null for auto/missing (paint defaults to black). */
557
+ readonly color: string | null;
558
+ /** Thickness in points (`w:sz` is eighths of a point). */
559
+ readonly widthPt: number;
560
+ };
561
+ /** A table's six authored border edges: four outer, plus the two interior intervals. */
562
+ interface TableBorderBox {
563
+ readonly top: TableBorderSide;
564
+ readonly left: TableBorderSide;
565
+ readonly bottom: TableBorderSide;
566
+ readonly right: TableBorderSide;
567
+ readonly insideH: TableBorderSide;
568
+ readonly insideV: TableBorderSide;
569
+ }
570
+ /** The four resolved edges of one cell, after conflict resolution against its neighbours. */
571
+ interface CellBorderBox {
572
+ readonly top: TableBorderSide;
573
+ readonly left: TableBorderSide;
574
+ readonly bottom: TableBorderSide;
575
+ readonly right: TableBorderSide;
576
+ }
577
+ /** Final edge winner; absent means the side/interval is not drawn. */
578
+ interface ResolvedTableBorderEdge {
579
+ readonly style: TableBorderStyle;
580
+ readonly color: string | null;
581
+ readonly widthPt: number;
582
+ }
583
+ /**
584
+ * Conflict winner over one grid interval of a cell side.
585
+ *
586
+ * `gridStart`/`gridEnd` are absolute grid column indices for horizontal sides and absolute
587
+ * row indices for vertical sides (half-open). `startPt`/`endPt` are cell-local along-axis
588
+ * positions in layout points.
589
+ */
590
+ interface ResolvedTableBorderEdgeSegment {
591
+ readonly side: TableBorderSideName;
592
+ readonly gridStart: number;
593
+ readonly gridEnd: number;
594
+ readonly startPt: number;
595
+ readonly endPt: number;
596
+ readonly edge: ResolvedTableBorderEdge;
597
+ }
598
+ /**
599
+ * One published stroke rectangle in cell-local layout points.
600
+ *
601
+ * Paint multiplies x/y/width/height by scale and draws — no metrics, gaps, or corner math.
602
+ */
603
+ interface TableBorderStrokeRecord {
604
+ readonly side: TableBorderSideName;
605
+ readonly role: 'outer' | 'inner' | 'middle' | 'edge';
606
+ readonly color: string | null;
607
+ /** CSS keyword for this stroke (compound strokes are always solid). */
608
+ readonly cssStyle: 'solid' | 'dashed' | 'dotted';
609
+ readonly x: number;
610
+ readonly y: number;
611
+ readonly width: number;
612
+ readonly height: number;
613
+ }
614
+ /**
615
+ * Layout-owned cell borders after conflict resolution and compound expansion.
616
+ *
617
+ * Convenience `top`/`left`/`bottom`/`right` are set when that side has a single uniform
618
+ * full-span winner (existing consumers / CSS simple edges). Multi-interval winners live
619
+ * only on `edgeSegments`. Compound geometry is always on `strokes`.
620
+ */
621
+ interface ResolvedCellBorders {
622
+ readonly top?: ResolvedTableBorderEdge;
623
+ readonly left?: ResolvedTableBorderEdge;
624
+ readonly bottom?: ResolvedTableBorderEdge;
625
+ readonly right?: ResolvedTableBorderEdge;
626
+ readonly edgeSegments?: readonly ResolvedTableBorderEdgeSegment[];
627
+ readonly strokes?: readonly TableBorderStrokeRecord[];
628
+ }
629
+ /** Soft cap on published stroke rectangles per cell (security / pathological spans). */
630
+ declare const MAX_TABLE_BORDER_STROKES = 256;
631
+ /** Read one OOXML border child into the three-state model. */
632
+ declare function readBorderSide(node: OoxmlElement | undefined): TableBorderSide;
633
+ /**
634
+ * Read a table's `w:tblBorders`, dropping or clamping hostile values.
635
+ *
636
+ * Widths and colours come from a file: an out-of-range `w:sz` becomes a layout dimension, so it is
637
+ * bounded here rather than downstream.
638
+ */
639
+ declare function readTableBorders(tblPr: OoxmlElement | undefined): TableBorderBox;
640
+ /** Read one cell's `w:tcBorders`, under the same bounds {@link readTableBorders} applies. */
641
+ declare function readCellBorders(tcPr: OoxmlElement | undefined): CellBorderBox;
642
+ /**
643
+ * Conflict weight: the authored width in eighths of a point, and nothing else.
644
+ *
645
+ * Word-matching, not conformance — §17.4.39 (`w:tblBorders`) and §17.4.66 (`w:tcBorders`)
646
+ * describe the elements and specify no conflict algorithm. Word picks the heavier RULE, so
647
+ * a 6pt dashed rule beats a hairline single. Folding the style rank into the weight (an
648
+ * `sz × border-number` product) made a 0.5pt double outrank a 1pt single and made every
649
+ * dashed or dotted rule weigh 1 regardless of `w:sz`, which erased its width entirely.
650
+ */
651
+ declare function borderWeight(side: TableBorderSide): number;
652
+ /**
653
+ * Pick the winner between two candidates on a shared grid line (zero cell spacing).
654
+ *
655
+ * `none` loses to any edge; two `none`/omitted yield omitted (no paint). Width decides;
656
+ * equal widths rank by style, then prefer the darker color, then `preferFirst`
657
+ * (reading-order / first candidate).
658
+ */
659
+ declare function resolveBorderConflict(first: TableBorderSide, second: TableBorderSide, preferFirst?: boolean): TableBorderSide;
660
+ interface BorderGridCell {
661
+ readonly gridColumn: number;
662
+ readonly gridSpan: number;
663
+ readonly vMergeContinue: boolean;
664
+ readonly borders: CellBorderBox;
665
+ /** Set on restart cells that visually span into later rows. */
666
+ readonly mergeRowSpan?: number;
667
+ }
668
+ /**
669
+ * The absolute grid a table's borders are drawn on: column widths, and per-row tops and heights.
670
+ *
671
+ * Shared by every edge so adjacent cells resolve to the SAME line, rather than each computing its
672
+ * own and leaving a hairline gap between them.
673
+ */
674
+ interface BorderGridGeometry {
675
+ /** Absolute column widths for the whole table (points). */
676
+ readonly columnWidthsPt: readonly number[];
677
+ /**
678
+ * Per laid-out row: absolute top and height in the same coordinate space as cell boxes
679
+ * (only relative differences matter for vertical edge segmentation).
680
+ */
681
+ readonly rowBands: readonly {
682
+ readonly y: number;
683
+ readonly height: number;
684
+ }[];
685
+ /** Per laid-out cell: width/height after vMerge expansion (cell-local stroke space). */
686
+ readonly cellBoxes: readonly (readonly {
687
+ readonly width: number;
688
+ readonly height: number;
689
+ }[])[];
690
+ }
691
+ /**
692
+ * Resolve borders for every cell in a laid-out table fragment.
693
+ *
694
+ * Shared vertical edges: conflict(left.right, right.left) → assigned to the left cell,
695
+ * segmented per row when a vMerge restart faces differing neighbors.
696
+ * Shared horizontal edges: conflict(above.bottom, below.top) → assigned to the above cell,
697
+ * segmented per grid column when a gridSpan cell faces differing below neighbors; vMerge
698
+ * interior seams are suppressed per interval.
699
+ *
700
+ * When `geometry` is provided, compound edges expand into explicit stroke records with
701
+ * corner-adjusted endpoints in cell-local points.
702
+ */
703
+ declare function resolveTableCellBorderGrid(rows: readonly (readonly BorderGridCell[])[], table: TableBorderBox, columnCount: number, geometry?: BorderGridGeometry, work?: TableBorderGridResolveWork, ownershipBudget?: TableBorderOwnershipBudget): ResolvedCellBorders[][];
704
+ /** Width contribution of a resolved edge for content inset / row sizing. */
705
+ declare function borderExtentPt(edge: ResolvedTableBorderEdge | TableBorderSide | undefined): number;
706
+
707
+ /** A half-open UTF-16 range inside one paragraph, addressed by its canonical node id. */
708
+ interface SourceRange {
709
+ readonly paragraphId: string;
710
+ readonly start: number;
711
+ readonly end: number;
712
+ }
713
+ /**
714
+ * A rectangle in layout POINTS.
715
+ *
716
+ * Points everywhere in this layer — twips convert at property-read boundaries and CSS pixels at
717
+ * paint. A box carrying either of those would eventually be added to one carrying the other.
718
+ */
719
+ interface LayoutBox {
720
+ readonly x: number;
721
+ readonly y: number;
722
+ readonly width: number;
723
+ readonly height: number;
724
+ }
725
+ /**
726
+ * A bottom paragraph border as layout published it.
727
+ *
728
+ * `box` is the rule's geometry in the same coordinate space as the fragment (page-content
729
+ * relative). Paint positions from this box and MUST NOT remeasure the border.
730
+ */
731
+ interface ParagraphBottomBorderRecord {
732
+ readonly edge: ParagraphBorderEdge;
733
+ readonly box: LayoutBox;
734
+ }
735
+ /**
736
+ * One `w:pBdr` rule as layout published it.
737
+ *
738
+ * `box` is the STROKE rectangle in the same coordinate space as the fragment, so paint sets a
739
+ * position and a colour and nothing else. It matters that paint cannot derive these itself:
740
+ * Word draws the side rules OUTSIDE the text column, so `box.x` on a `left`/`bar` stroke is
741
+ * left of the fragment box and a painter reasoning from the fragment alone would put it
742
+ * inside the text.
743
+ */
744
+ interface ParagraphBorderStrokeRecord {
745
+ readonly side: ParagraphBorderSide;
746
+ readonly edge: ParagraphBorderEdge;
747
+ readonly box: LayoutBox;
748
+ }
749
+ /**
750
+ * The hyperlink a span sits inside, as layout resolved it.
751
+ *
752
+ * Already SANITIZED: `href` is the runtime projection produced once at the trust boundary,
753
+ * and `null` means the link is inert — a refused scheme, or a relationship the package does
754
+ * not declare. Paint, hit-testing and the popover consume this and never the authored target,
755
+ * so there is exactly one place a file-derived URL becomes something a browser can follow.
756
+ *
757
+ * `id` is the `w:hyperlink` node's canonical id, which is what makes the spans of one link
758
+ * recognisable as one link across the several lines it wraps onto — and what an unlink or a
759
+ * retarget addresses.
760
+ */
761
+ interface SpanLinkRecord {
762
+ readonly id: string;
763
+ readonly kind: 'external' | 'internal' | 'unresolved';
764
+ /** Sanitized runtime projection: an absolute URL, `#anchor`, or null when inert. */
765
+ readonly href: string | null;
766
+ /** Bookmark name for an internal link, so navigation need not re-parse the fragment. */
767
+ readonly anchor?: string;
768
+ /** `w:tooltip` — paint puts it on the anchor's `title`. */
769
+ readonly tooltip?: string;
770
+ }
771
+ /** A run of text on one line sharing identical resolved formatting. */
772
+ interface StyleSpanRecord {
773
+ readonly range: SourceRange;
774
+ readonly text: string;
775
+ /** The run's authored properties, retained as evidence. */
776
+ readonly props: readonly OoxmlProperty[];
777
+ /**
778
+ * The same properties RESOLVED — one unit system, defaults applied.
779
+ *
780
+ * Carried on the span so the measurer, the span and the painter all read one resolution
781
+ * rather than each deriving its own. Two derivations that disagree by a fraction of a
782
+ * point put the caret where no glyph is.
783
+ */
784
+ readonly style: ResolvedRunStyle;
785
+ readonly box: LayoutBox;
786
+ /**
787
+ * Cumulative advances from {@link box}.x to each UTF-16 caret boundary in {@link text}.
788
+ *
789
+ * Length is `text.length + 1` (both endpoints). Layout publishes these so hit-testing and
790
+ * the caret read the same per-cluster edges the span was measured with, rather than
791
+ * re-measuring a prefix at interaction time or interpolating across {@link box}.width —
792
+ * OpenSpec task 13.5. Absent on older records; consumers fall back to the measurer.
793
+ */
794
+ readonly caretEdges?: readonly number[];
795
+ /**
796
+ * `w:tab/@w:leader` of the stop a `\t` span advanced to (ECMA-376 §17.3.1.38).
797
+ *
798
+ * Only ever set on a tab span, and only for a non-`none` leader. Paint repeats the glyph
799
+ * across the advance THIS box already reserved — the leader adds no width of its own, so
800
+ * it can never move the text that follows it.
801
+ */
802
+ readonly tabLeader?: TabLeader;
803
+ /**
804
+ * Advance of ONE leader glyph in this run's face, in points, measured by layout.
805
+ *
806
+ * Paint cannot ask a font how wide a character is, so without this it had to guess and
807
+ * deliberately overfill, leaving the dots at whatever spacing an over-long string
808
+ * happened to produce. Measured, the leader is spaced exactly as the same character
809
+ * typed there would be — which is what Word draws.
810
+ */
811
+ readonly tabLeaderAdvancePt?: number;
812
+ /**
813
+ * The hyperlink this span belongs to, or absent for ordinary text.
814
+ *
815
+ * Carried on the SPAN rather than looked up at paint time because a link that wraps
816
+ * produces one set of spans per line, and paint has no paragraph to walk — only the line
817
+ * it was handed.
818
+ */
819
+ readonly link?: SpanLinkRecord;
820
+ /**
821
+ * Horizontal jump, in points, that a floating object's wrap zone forced before this span.
822
+ *
823
+ * A line that resumes on the far side of a float is still ONE line: its spans keep running
824
+ * model offsets and share a selection band. Only layout knows the jump is an obstacle
825
+ * rather than justification slack, so it publishes it here. Paint reserves it with an inert
826
+ * advance and excludes it from the word spacing it reconstructs — inferring the difference
827
+ * from the gap alone spread the float's whole width across every space on the line.
828
+ */
829
+ readonly wrapAdvanceBefore?: number;
830
+ /**
831
+ * The revision wrappers this text sits inside, outermost first, absent when untracked.
832
+ *
833
+ * Carried on the span for the same reason `style` is: paint and the review surface must read
834
+ * ONE attribution. A sidebar that re-derived which revision covers a span by walking the tree
835
+ * could disagree with what was painted, and the card would point at the wrong text.
836
+ */
837
+ readonly revisions?: readonly RevisionAttribution[];
838
+ /**
839
+ * Live PAGE/NUMPAGES/SECTIONPAGES projection (layout-time evaluated text).
840
+ *
841
+ * Not model-editable until typed fields land: paint treats these as atomic furniture and
842
+ * selection mapping refuses them the way it refuses markers.
843
+ */
844
+ readonly projected?: boolean;
845
+ /**
846
+ * Footnote/endnote navigation metadata for projected note atoms.
847
+ *
848
+ * Paint tags body citations (`to-note`) and note-body marks (`to-body`) so React chrome
849
+ * can jump without owning layout logic.
850
+ */
851
+ readonly noteNav?: {
852
+ readonly scopeId: string;
853
+ readonly direction: 'to-note' | 'to-body';
854
+ };
855
+ }
856
+ /**
857
+ * One laid-out line: its geometry, its baseline, and the styled spans it renders.
858
+ *
859
+ * The unit hit-testing and caret placement resolve against. Geometry comes from HERE, never from
860
+ * the DOM, which is what lets an empty paragraph still get a caret.
861
+ */
862
+ interface LineRecord {
863
+ readonly id: string;
864
+ readonly range: SourceRange;
865
+ readonly spans: readonly StyleSpanRecord[];
866
+ readonly box: LayoutBox;
867
+ /**
868
+ * Where the line's content actually starts, after alignment and the first-line indent.
869
+ *
870
+ * {@link box} is the content BAND the line was broken against — its `x` is the indented
871
+ * column edge and its `width` the available measure, neither of which moves with
872
+ * `w:jc`. Alignment is otherwise expressed only as x offsets on the span boxes, so a line
873
+ * with no spans (an empty paragraph) had no aligned origin at all: paint, hit testing and
874
+ * the caret each fell back to `box.x` and drew a centred empty paragraph's caret hard
875
+ * against the left margin, where it stayed until the first character was typed.
876
+ *
877
+ * Equal to the first span's x whenever there is one, so it is the single origin every
878
+ * consumer can read without a spans-or-box fallback of its own.
879
+ */
880
+ readonly contentX: number;
881
+ /** Distance from the line box top to the text baseline. */
882
+ readonly baseline: number;
883
+ /**
884
+ * Space ABOVE the glyph band inside {@link box} (exact centering, not auto/atLeast).
885
+ *
886
+ * `auto` / `atLeast` extras grow the box BELOW the glyphs — paint puts that depth in
887
+ * padding-bottom. {@link baseline} is measured from the line top and already includes
888
+ * this above-band when present.
889
+ *
890
+ * Published by layout rather than recovered by paint: three paint sites each deriving it
891
+ * separately is how they came to disagree about where a line's text sits.
892
+ */
893
+ readonly leading: number;
894
+ /**
895
+ * Auto/atLeast line-spacing depth BELOW the glyph band, inside {@link box}.
896
+ *
897
+ * The complement of {@link leading}: exact spacing centres the glyphs and moves the
898
+ * baseline down, while auto/atLeast leave the band at the top and grow the box beneath it.
899
+ * So the band a consumer needs is `box.height - trailingSpacing - leading`, and
900
+ * subtracting `leading` alone is right only under the exact rule.
901
+ *
902
+ * A line WITH spans carries its band in the span heights too. An empty paragraph carries
903
+ * nothing, which is why the caret, paint's `padding-bottom` and the content-control
904
+ * boundary all need this published rather than recovered from the box.
905
+ *
906
+ * Zero under the exact rule and on lines holding drawings, where the box is authored.
907
+ * Absent on lines published before this was measured; treat as zero.
908
+ */
909
+ readonly trailingSpacing?: number;
910
+ /**
911
+ * Model ranges on this line covering DELETED content, absent when there is none.
912
+ *
913
+ * The caret steps over these rather than entering them: text typed inside a deletion exists
914
+ * in neither the original nor the proposal, and there is no valid tree for the result.
915
+ *
916
+ * Recorded even in display modes that lay the deletion out invisibly, because the offsets
917
+ * exist in the model in every mode and an offset-by-offset walk would otherwise stop at
918
+ * positions with no glyph.
919
+ */
920
+ readonly deletedRanges?: readonly ModelRange[];
921
+ /**
922
+ * Inline drawings on this line, absent when there are none.
923
+ *
924
+ * Each occupies one UTF-16 model unit at {@link InlineDrawingRecord.start}. Hidden drawings
925
+ * are omitted — they remain in the tree and projection but publish no geometry.
926
+ */
927
+ readonly drawings?: readonly InlineDrawingRecord[];
928
+ }
929
+ /**
930
+ * A paragraph's resolved indent in points, in the vocabulary `w:ind` uses.
931
+ *
932
+ * `left`/`right` are signed; `hanging` is not (`ST_TwipsMeasure`). `firstLine` is signed
933
+ * even though the schema declares it unsigned, because Word's model keeps one signed
934
+ * first-line indent and this engine follows it.
935
+ */
936
+ interface ParagraphIndent {
937
+ readonly left: number;
938
+ readonly right: number;
939
+ readonly firstLine: number;
940
+ readonly hanging: number;
941
+ }
942
+ /**
943
+ * The part of one paragraph that sits on one page.
944
+ *
945
+ * A paragraph that crosses a page boundary produces several fragments that all name the SAME
946
+ * `paragraphId`, which is what lets selection and hit-testing treat it as one paragraph while
947
+ * pagination treats it as two boxes.
948
+ */
949
+ interface ParagraphFragmentRecord {
950
+ readonly kind: 'paragraph';
951
+ readonly id: string;
952
+ readonly paragraphId: string;
953
+ /** 0 for the first fragment of the paragraph, 1 for its continuation, and so on. */
954
+ readonly fragmentIndex: number;
955
+ readonly range: SourceRange;
956
+ readonly props: readonly OoxmlProperty[];
957
+ /**
958
+ * Before/after spacing applied to THIS fragment, in points.
959
+ *
960
+ * Continuations carry `before: 0`; only the final fragment carries `after`. The numbers
961
+ * already reflect Word's adjacent-collapse against the previous paragraph's after.
962
+ */
963
+ readonly spacing: ParagraphSpacing;
964
+ /**
965
+ * The paragraph's EFFECTIVE indent in points, cascade and numbering merge included.
966
+ *
967
+ * Published because it is not recoverable from anything else here. `props` carries the
968
+ * cascaded `w:ind`, but a list paragraph's indent comes from `numbering.xml` and is merged
969
+ * in after the cascade, so a numbered item that authors no `w:ind` reads zero there while
970
+ * its text sits indented. The geometry is no better an answer: `box.x` is cell-relative
971
+ * inside a table and is displaced by float zones.
972
+ *
973
+ * `firstLine` and `hanging` are kept as OOXML spells them. A consumer wanting the one
974
+ * signed first-line offset Word models takes `hanging > 0 ? -hanging : firstLine` —
975
+ * hanging WINS, it is not summed (ECMA-376 §17.3.1.12).
976
+ */
977
+ readonly indent: ParagraphIndent;
978
+ /** Bottom rule on the final fragment when `w:pBdr/w:bottom` resolves; absent otherwise. */
979
+ readonly bottomBorder?: ParagraphBottomBorderRecord;
980
+ /**
981
+ * Every `w:pBdr` rule this fragment draws, in paint order.
982
+ *
983
+ * A paragraph split across pages opens and closes exactly once: the `top` stroke rides the
984
+ * first fragment, the closing stroke the last. `bottomBorder` remains the bottom rule alone
985
+ * — a `between` rule closing a grouped paragraph is not one.
986
+ */
987
+ readonly borders?: readonly ParagraphBorderStrokeRecord[];
988
+ /**
989
+ * Validated 6-hex paragraph shading fill (`w:pPr/w:shd`), absent for none/auto.
990
+ *
991
+ * Paint fills {@link shadingBox}. Measurement ignores it.
992
+ */
993
+ readonly shading?: string;
994
+ /**
995
+ * Geometry of the paragraph shading band when {@link shading} is set. Absent when there
996
+ * is no fill.
997
+ *
998
+ * TWO SHAPES, because Word fills two different things. An UNBORDERED paragraph is filled
999
+ * across its line boxes (indent-aware width), and before/after spacing stays unfilled. A
1000
+ * paragraph that publishes any {@link borders} stroke is filled across the rectangle those
1001
+ * strokes draw instead — `w:space` padding and rule extent included — so a callout's fill
1002
+ * reaches its frame rather than leaving a pale stripe floating inside an empty box. Paint
1003
+ * draws the strokes after the fill, so the frame is never covered.
1004
+ */
1005
+ readonly shadingBox?: LayoutBox;
1006
+ /**
1007
+ * The revision on this paragraph's own MARK (`w:pPr/w:rPr/w:ins|w:del`), absent when there
1008
+ * is none.
1009
+ *
1010
+ * Carried on the fragment rather than on a span because it decorates no characters: the
1011
+ * pilcrow was inserted or deleted, which is how a split or a merge is recorded. Only the
1012
+ * FINAL fragment carries it — the mark lives at the end of the paragraph, so a paragraph
1013
+ * split across pages must not draw two of them.
1014
+ */
1015
+ readonly markRevision?: RevisionAttribution;
1016
+ /**
1017
+ * List marker painted in the hanging-indent slot of the FIRST fragment only.
1018
+ *
1019
+ * Not part of model text: no UTF-16 range, never contributes to caret/selection offsets,
1020
+ * and must not be serialised back into the paragraph.
1021
+ */
1022
+ readonly marker?: ListMarkerRecord;
1023
+ readonly lines: readonly LineRecord[];
1024
+ readonly box: LayoutBox;
1025
+ }
1026
+ /**
1027
+ * A numbering marker as layout published it.
1028
+ *
1029
+ * Geometry is in the same coordinate space as the fragment (page-content or cell-content
1030
+ * relative). Paint positions from this box and MUST NOT remeasure the marker.
1031
+ */
1032
+ interface ListMarkerRecord {
1033
+ readonly text: string;
1034
+ readonly style: ResolvedRunStyle;
1035
+ readonly box: LayoutBox;
1036
+ /**
1037
+ * The `w:ilvl` this marker was resolved at, 0..8.
1038
+ *
1039
+ * Published because the level, not the geometry, is what Increase/Decrease Indent moves
1040
+ * on a list paragraph — demoting an item re-resolves its format from `numbering.xml`,
1041
+ * which is why a bullet becomes a hollow circle and a `1.` becomes an `a.`.
1042
+ */
1043
+ readonly level: number;
1044
+ /**
1045
+ * The `w:numId` this marker resolved through.
1046
+ *
1047
+ * Published with the level because the two together are what identifies a list: whether
1048
+ * a demote is even possible depends on which levels THIS definition declares, and a
1049
+ * document may hold several lists whose level 0 looks identical.
1050
+ */
1051
+ readonly numId: string;
1052
+ /** `w:numFmt` of the resolved level — `bullet` or a numbering format. */
1053
+ readonly numFmt: string;
1054
+ }
1055
+ /**
1056
+ * The part of one table that sits on one page.
1057
+ *
1058
+ * A table that crosses a page boundary produces one fragment per page it spans, which is
1059
+ * what lets it checkpoint like a paragraph: the flow loop places whole fragments, and
1060
+ * resuming after a table needs no knowledge of its interior.
1061
+ */
1062
+ interface TableFragmentRecord {
1063
+ readonly kind: 'table';
1064
+ readonly id: string;
1065
+ /** Canonical node id of the `w:tbl`. */
1066
+ readonly tableId: string;
1067
+ /** 0 for the first page the table touches, 1 for its continuation, and so on. */
1068
+ readonly fragmentIndex: number;
1069
+ /** Nesting depth: 0 for body-level tables, increasing for nested tables. */
1070
+ readonly nestingDepth: number;
1071
+ /**
1072
+ * Resolved column boundary x positions in table-local points, left edge through right edge.
1073
+ * Length is column count + 1.
1074
+ */
1075
+ readonly columnEdges: readonly number[];
1076
+ readonly rows: readonly TableRowFragmentRecord[];
1077
+ readonly box: LayoutBox;
1078
+ }
1079
+ /**
1080
+ * One table row on one page.
1081
+ *
1082
+ * A FRAGMENT, not the row: a row split across a page break appears once per page it touches, and
1083
+ * a repeated header row appears on every page of its table.
1084
+ */
1085
+ interface TableRowFragmentRecord {
1086
+ /** Canonical node id of the `w:tr`. */
1087
+ readonly id: string;
1088
+ /** Pending tracked row insertion/deletion, when authored in `w:trPr`. */
1089
+ readonly revisionKind?: 'insert' | 'delete';
1090
+ /**
1091
+ * The `w:trPr/w:ins|w:del` attribution — the `(id, author, date)` triple the review model
1092
+ * addresses this decision by, so painted rows can carry it the way revision spans do.
1093
+ */
1094
+ readonly revisionId?: string;
1095
+ readonly revisionAuthor?: string;
1096
+ readonly revisionDate?: string;
1097
+ /** Authored row ordinal within the table; repeats share the original row's index. */
1098
+ readonly rowIndex: number;
1099
+ /**
1100
+ * True for a `w:tblHeader` row RE-EMITTED at the top of a continuation page. Painted,
1101
+ * but excluded from interaction walks so each caret stop exists exactly once.
1102
+ */
1103
+ readonly isHeaderRepeat: boolean;
1104
+ /**
1105
+ * True when this record continues a row that already emitted content on a prior page
1106
+ * (cell content fragmented at a paragraph/line boundary). Same `id` as the lead fragment.
1107
+ */
1108
+ readonly isContinuation?: boolean;
1109
+ readonly cells: readonly TableCellFragmentRecord[];
1110
+ readonly box: LayoutBox;
1111
+ }
1112
+ /**
1113
+ * One table cell on one page, already resolved against the grid.
1114
+ *
1115
+ * `gridSpan` is clamped at read time, because it comes from a file and an unclamped span is a
1116
+ * loop bound an attacker controls. A vertical-merge continuation paints its box but holds no
1117
+ * blocks — its content belongs to the cell that started the merge.
1118
+ */
1119
+ interface TableCellFragmentRecord {
1120
+ /** Canonical node id of the `w:tc`. */
1121
+ readonly id: string;
1122
+ /** First grid column this cell occupies. */
1123
+ readonly gridColumn: number;
1124
+ /** Canonical `w:gridCol` node id for this cell's start column, when authored. */
1125
+ readonly gridColumnId?: string;
1126
+ /** Grid columns spanned, already clamped at read time. */
1127
+ readonly gridSpan: number;
1128
+ /** A vertical-merge continuation paints its box but holds no blocks. */
1129
+ readonly vMergeContinue: boolean;
1130
+ /**
1131
+ * When true, paint skips borders/fill/content for this cell (vMerge continue). Grid
1132
+ * bookkeeping and the box remain so selection/geometry walks stay consistent.
1133
+ */
1134
+ readonly paintInert?: boolean;
1135
+ /** Number of rows this restart cell visually spans (1 when not a vertical merge). */
1136
+ readonly rowSpan?: number;
1137
+ /** Validated 6-hex cell shading fill, absent for none/auto. */
1138
+ readonly shading?: string;
1139
+ /**
1140
+ * Layout-owned resolved borders after collapsed conflict resolution.
1141
+ *
1142
+ * Includes convenience per-side edges, per-grid-interval winners, and explicit compound
1143
+ * stroke rectangles in cell-local points. Paint only scales and draws — it must not
1144
+ * invent stroke/gap/corner geometry.
1145
+ */
1146
+ readonly borders?: ResolvedCellBorders;
1147
+ /** Nested blocks in reading order; recursion carries nested tables. */
1148
+ readonly blocks: readonly BlockFragmentRecord[];
1149
+ readonly box: LayoutBox;
1150
+ }
1151
+ /** A top-level (or cell-level) block fragment, discriminated by `kind`. */
1152
+ type BlockFragmentRecord = ParagraphFragmentRecord | TableFragmentRecord;
1153
+ /**
1154
+ * One header or footer story as it sits on one page.
1155
+ *
1156
+ * `box` is absolute (sheet coordinates) and sized to the story's FLOW height — never to
1157
+ * any anchored-object extent, which is the rule that keeps a decorated header's hit area
1158
+ * from covering the body. `fragments` are story-relative (origin at the box's top-left).
1159
+ * Baseline furniture may be shared across pages of the same variant when the story has no
1160
+ * allowlisted PAGE/NUMPAGES/SECTIONPAGES fields; after page-field finalize, projections are
1161
+ * per page (or per distinct field values for count-only stories).
1162
+ */
1163
+ interface HeaderFooterStoryRecord {
1164
+ readonly kind: 'header' | 'footer';
1165
+ readonly variant: 'default' | 'first' | 'even';
1166
+ readonly partName: string;
1167
+ /**
1168
+ * Main-document relationship id that resolves to this part (`EditorScope.rId`).
1169
+ *
1170
+ * Present when the furniture source could name the relationship; scoped editing binds
1171
+ * on this id so shared parts stay one story across pages/sections.
1172
+ */
1173
+ readonly rId?: string;
1174
+ readonly box: LayoutBox;
1175
+ readonly fragments: readonly BlockFragmentRecord[];
1176
+ /** Anchored drawings owned by this story, in story-relative coordinates. */
1177
+ readonly anchoredDrawings?: readonly AnchoredDrawingRecord[];
1178
+ /**
1179
+ * Transient projector used between furniture attach and document-level page-field
1180
+ * finalize. Absent on published layout records after finalize.
1181
+ */
1182
+ readonly pageFieldProjector?: (context: {
1183
+ readonly pageNumber: number;
1184
+ readonly pageCount: number;
1185
+ readonly sectionPageCount?: number;
1186
+ readonly format?: string;
1187
+ }) => HeaderFooterStoryRecord;
1188
+ }
1189
+ /**
1190
+ * One footnote or endnote story as it sits on one page (or continuation page).
1191
+ *
1192
+ * Notes are ordinary editable stories — NOT `[data-docx-hf]` furniture. `box` is absolute
1193
+ * (sheet coordinates). `fragments` are story-relative. Separators are nonselectable paint
1194
+ * geometry owned by the parent {@link NoteAreaRecord}.
1195
+ */
1196
+ interface NoteStoryRecord {
1197
+ readonly noteKind: 'footnote' | 'endnote';
1198
+ readonly noteId: number;
1199
+ /** `footnote:N` / `endnote:N` — EditorScope note id. */
1200
+ readonly scopeId: string;
1201
+ /** Derived display mark for this occurrence; null when customMarkFollows / continuation. */
1202
+ readonly mark: string | null;
1203
+ /** True when this is a continuation fragment (no leading mark). */
1204
+ readonly continuation?: boolean;
1205
+ readonly box: LayoutBox;
1206
+ readonly fragments: readonly BlockFragmentRecord[];
1207
+ }
1208
+ /**
1209
+ * Footnote / endnote area on one page: separator + stacked note stories.
1210
+ *
1211
+ * `placement` records how the area was positioned. `fallbackReason` is set when the
1212
+ * bounded reflow loop exhausted and layout kept the reference with its note on a later
1213
+ * page (D12 named fallback).
1214
+ */
1215
+ interface NoteAreaRecord {
1216
+ readonly kind: 'footnotes' | 'endnotes';
1217
+ readonly placement: 'pageBottom' | 'beneathText' | 'sectEnd' | 'docEnd';
1218
+ readonly box: LayoutBox;
1219
+ /** Separator rule / authored separator story; absent when no notes on this page. */
1220
+ readonly separator?: {
1221
+ readonly kind: 'separator' | 'continuationSeparator';
1222
+ readonly box: LayoutBox;
1223
+ readonly fragments: readonly BlockFragmentRecord[];
1224
+ readonly synthetic: boolean;
1225
+ /** Layout-owned single/double rule when marker-only or synthetic; absent for authored stories. */
1226
+ readonly ruleStyle?: 'single' | 'double';
1227
+ };
1228
+ readonly notes: readonly NoteStoryRecord[];
1229
+ readonly fallbackReason?: string;
1230
+ }
1231
+ /**
1232
+ * Note-stream ownership for overflow sheets created by note pagination.
1233
+ *
1234
+ * - `footnote-drain`: continuation pages that exist only to finish pageBottom footnotes —
1235
+ * not free body hosts for sectEnd/docEnd endnotes.
1236
+ * - `endnote-overflow`: sheets inserted to finish sectEnd/docEnd collections.
1237
+ */
1238
+ type PageNoteStream = 'footnote-drain' | 'endnote-overflow';
1239
+ /**
1240
+ * Raw `w:lock/@w:val` on one control, or `unlocked` when absent / unrecognised.
1241
+ *
1242
+ * Effective permissions across a nesting chain are the union of every ancestor's lock on two
1243
+ * axes (content edit / removal); see {@link ContentControlBoundaryRecord.effectiveLock}.
1244
+ */
1245
+ type ContentControlLock = 'unlocked' | 'sdtLocked' | 'contentLocked' | 'sdtContentLocked';
1246
+ /**
1247
+ * Mapped control type for layout / chrome — same members as the shipped public
1248
+ * `ContentControlType`. Untyped and preserved-only kinds report as `richText`.
1249
+ */
1250
+ type ContentControlMappedType = 'richText' | 'plainText' | 'checkbox' | 'dropdown' | 'comboBox' | 'date' | 'picture' | 'repeatingSection';
1251
+ /** Where the control sits in the tree relative to its content. */
1252
+ type ContentControlLevel = 'block' | 'inline' | 'row' | 'cell';
1253
+ /**
1254
+ * One piece of a control's content geometry.
1255
+ *
1256
+ * A block control that crosses a page break publishes one fragment per page rather than a
1257
+ * single rectangle covering the inter-page gap. An inline control publishes one fragment per
1258
+ * LINE it touches, covering the text's vertical extent (line-spacing leading excluded), so a
1259
+ * wrapped control never claims the words beside it. Coordinates match fragment boxes
1260
+ * (page-content space).
1261
+ */
1262
+ interface ContentControlGeometryFragment {
1263
+ readonly pageIndex: number;
1264
+ readonly box: LayoutBox;
1265
+ }
1266
+ /**
1267
+ * Layout-published boundary for one content control (`w:sdt` / typed `contentControl`).
1268
+ *
1269
+ * Chrome, lock feedback, and hit resolution read this record — never painted DOM. The wrapper
1270
+ * itself is not a layout box; `fragments` cover the content that already flowed in place.
1271
+ */
1272
+ interface ContentControlBoundaryRecord {
1273
+ /** Canonical node id of the control wrapper — not `w:id`. */
1274
+ readonly id: string;
1275
+ readonly alias?: string;
1276
+ readonly tag?: string;
1277
+ readonly controlType: ContentControlMappedType;
1278
+ /** This control's own `w:lock`, before ancestor union. */
1279
+ readonly lock: ContentControlLock;
1280
+ /**
1281
+ * Nested lock union with every ancestor control, collapsed back to a single `ST_Lock`
1282
+ * vocabulary value (both axes locked → `sdtContentLocked`).
1283
+ */
1284
+ readonly effectiveLock: ContentControlLock;
1285
+ /** `w:showingPlcHdr` is present on the control's properties. */
1286
+ readonly placeholder: boolean;
1287
+ /** `w:dataBinding` is present — content edits are refused as bound. */
1288
+ readonly bound: boolean;
1289
+ /** 0 for a top-level control; increments through nested wrappers under the shared nesting bound. */
1290
+ readonly nestingDepth: number;
1291
+ readonly level: ContentControlLevel;
1292
+ readonly fragments: readonly ContentControlGeometryFragment[];
1293
+ }
1294
+ /**
1295
+ * One laid-out page: the sheet, the content area, and everything that landed on it.
1296
+ *
1297
+ * Page identity is REUSED across incremental passes — a pass that changes nothing returns the
1298
+ * previous records by reference, which is what lets paint skip untouched pages entirely.
1299
+ */
1300
+ interface PageRecord {
1301
+ readonly id: string;
1302
+ readonly index: number;
1303
+ /** The whole sheet. */
1304
+ readonly box: LayoutBox;
1305
+ /** The area inside the margins that content flows into. */
1306
+ readonly contentBox: LayoutBox;
1307
+ readonly fragments: readonly BlockFragmentRecord[];
1308
+ /** Layout-owned vertical rules requested by `w:cols/@w:sep`, content-box relative. */
1309
+ readonly columnSeparators?: readonly LayoutBox[];
1310
+ /** Page-content anchored drawings on this sheet, absent when there are none. */
1311
+ readonly anchoredDrawings?: readonly AnchoredDrawingRecord[];
1312
+ /** Page furniture for this page's variant, absent when the document declares none. */
1313
+ readonly header?: HeaderFooterStoryRecord;
1314
+ readonly footer?: HeaderFooterStoryRecord;
1315
+ /** Footnotes reserved on this page (pageBottom / beneathText / continuations). */
1316
+ readonly footnotes?: NoteAreaRecord;
1317
+ /** Endnotes collected on this page (sectEnd / docEnd). */
1318
+ readonly endnotes?: NoteAreaRecord;
1319
+ /**
1320
+ * Ownership of note-only overflow sheets. Absent on ordinary body pages.
1321
+ * Layout pagination sets this so endnote hosting does not treat footnote drain
1322
+ * pages as free body space.
1323
+ */
1324
+ readonly noteStream?: PageNoteStream;
1325
+ /**
1326
+ * Section-local PAGE/SECTIONPAGES inputs for finalize. Absent → physical page index and
1327
+ * document-wide section count (empty `w:pgNumType` behaviour).
1328
+ */
1329
+ readonly pageFieldSource?: {
1330
+ readonly pageNumber: number;
1331
+ readonly sectionPageCount: number;
1332
+ readonly format?: string;
1333
+ };
1334
+ /**
1335
+ * Content-control boundaries whose geometry intersects this page.
1336
+ *
1337
+ * Carried on the page so a consumer that only holds a page record still sees current
1338
+ * metadata; identity reuse of an unchanged page keeps the same array only when the
1339
+ * control-context token matches.
1340
+ */
1341
+ readonly contentControls?: readonly ContentControlBoundaryRecord[];
1342
+ }
1343
+ /**
1344
+ * A complete layout pass: every page, plus the document-wide indexes derived alongside them.
1345
+ *
1346
+ * Stamped with the store `revision` it was laid out from, so anything holding geometry can tell
1347
+ * whether the document has moved underneath it — which is how stale pointer gestures and
1348
+ * overlays are refused rather than applied to coordinates that no longer describe anything.
1349
+ */
1350
+ interface SemanticLayout {
1351
+ /** The store revision these records were laid out from. */
1352
+ readonly revision: number;
1353
+ readonly pages: readonly PageRecord[];
1354
+ /**
1355
+ * Every content-control boundary in document order, including multi-page fragment lists.
1356
+ *
1357
+ * Always recomputed when a document layout pass finishes, so incremental page-identity
1358
+ * reuse cannot publish stale alias/tag/lock metadata.
1359
+ */
1360
+ readonly contentControls?: readonly ContentControlBoundaryRecord[];
1361
+ /**
1362
+ * Fingerprint of wrapper-only control metadata (alias, tag, lock, type, placeholder, binding).
1363
+ *
1364
+ * Folded into the layout producer / session context so a metadata-only edit invalidates
1365
+ * reuse paths that would otherwise return previous pages by identity with stale boundaries.
1366
+ */
1367
+ readonly controlContextToken?: string;
1368
+ }
1369
+ /** Page geometry, in points. */
1370
+ interface PageGeometry {
1371
+ readonly width: number;
1372
+ readonly height: number;
1373
+ readonly margin: {
1374
+ readonly top: number;
1375
+ readonly right: number;
1376
+ readonly bottom: number;
1377
+ readonly left: number;
1378
+ };
1379
+ /** `w:pgMar/@header` — sheet edge to header top, in points. Defaults to 36 (720 twips). */
1380
+ readonly headerDistance?: number;
1381
+ /** `w:pgMar/@footer` — sheet edge to footer bottom, in points. Defaults to 36. */
1382
+ readonly footerDistance?: number;
1383
+ }
1384
+ /** US Letter with one-inch margins, in points. */
1385
+ declare const DEFAULT_PAGE_GEOMETRY: PageGeometry;
1386
+ /**
1387
+ * Text measurement, injected.
1388
+ *
1389
+ * A real implementation shapes with the resolved font; the tests supply a deterministic one.
1390
+ * Layout never reads the DOM, so this is the only way width and height enter it.
1391
+ */
1392
+ interface TextMeasurer {
1393
+ /** Advance width of `text` in the resolved style. */
1394
+ measure(text: string, style: ResolvedRunStyle): number;
1395
+ /** Line height and baseline for the resolved style. */
1396
+ lineMetrics(style: ResolvedRunStyle): {
1397
+ height: number;
1398
+ baseline: number;
1399
+ };
1400
+ }
1401
+ /**
1402
+ * Depth-first paragraph fragments of one page, in reading order.
1403
+ *
1404
+ * Table interiors flatten through rows and cells; header-repeat rows are skipped unless
1405
+ * asked for, so interaction sees each caret stop exactly once while paint sees everything.
1406
+ */
1407
+ declare function paragraphFragmentsOf(page: PageRecord, includeHeaderRepeats?: boolean): ParagraphFragmentRecord[];
1408
+ /**
1409
+ * Depth-first paragraph fragments of one block list, in reading order.
1410
+ *
1411
+ * The same walk as {@link paragraphFragmentsOf} for fragment lists that do not sit on the
1412
+ * page directly — a header/footer story's fragments, a note story's.
1413
+ */
1414
+ declare function paragraphFragmentsOfBlocks(blocks: readonly BlockFragmentRecord[], includeHeaderRepeats?: boolean): ParagraphFragmentRecord[];
1415
+ /** Every line in a layout, in reading order — the order caret navigation walks. */
1416
+ declare function linesOf(layout: SemanticLayout): LineRecord[];
1417
+ /** Every fragment belonging to one paragraph, in order, across page boundaries. */
1418
+ declare function fragmentsOfParagraph(layout: SemanticLayout, paragraphId: string): ParagraphFragmentRecord[];
1419
+ /** The line containing a model position, or null when the position is not laid out. */
1420
+ declare function lineAtPosition(layout: SemanticLayout, paragraphId: string, offset: number): LineRecord | null;
1421
+ /** Every content-control boundary on a layout, preferring the layout-level list. */
1422
+ declare function contentControlsOfLayout(layout: SemanticLayout): readonly ContentControlBoundaryRecord[];
1423
+ /**
1424
+ * Axis-aligned union of boxes, or null when the list is empty.
1425
+ *
1426
+ * Used when a control's content spans several fragments or spans on one page.
1427
+ */
1428
+ declare function unionLayoutBoxes(boxes: readonly LayoutBox[]): LayoutBox | null;
1429
+ /** Collapse raw + ancestor locks into one `ST_Lock` vocabulary value. */
1430
+ declare function effectiveContentControlLock(locks: readonly ContentControlLock[]): ContentControlLock;
1431
+
1432
+ export { type ParagraphIndent as $, type AnchoredDrawingRecord as A, type BlockFragmentRecord as B, type ContentControlBoundaryRecord as C, type DrawingPoint as D, EMPTY_TAB_STOPS as E, type FieldPageContext as F, type HeaderFooterStoryRecord as G, type HyperlinkProjector as H, type InlineDrawingRecord as I, type LineSpacingRule as J, type ListMarkerRecord as K, type LineRecord as L, type ModelRange as M, MAX_BORDER_SPACE_PT as N, MAX_BORDER_WIDTH_PT as O, type PageGeometry as P, MAX_PARAGRAPH_SPACING_PT as Q, type ResolvedRunStyle as R, type SemanticLayout as S, type TextMeasurer as T, MAX_TABLE_BORDER_STROKES as U, MAX_TAB_POSITION_TWIPS as V, MAX_TAB_STOPS as W, PARAGRAPH_BORDER_SIDES as X, type ParagraphBorderSide as Y, type ParagraphBorderStrokeRecord as Z, type ParagraphBottomBorderRecord as _, type StyleSpanRecord as a, type ResolvedCellBorders as a0, type ResolvedTableBorderEdge as a1, type ResolvedTableBorderEdgeSegment as a2, type ResolvedUnderline as a3, SINGLE_LINE_SPACING as a4, type SourceRange as a5, type SpanLinkRecord as a6, TAB_LEADER_GLYPH as a7, type TabAlignment as a8, type TabDestination as a9, paragraphBorderStrokeWidthPt as aA, paragraphBorders as aB, paragraphBordersFingerprint as aC, paragraphBreaksBefore as aD, paragraphContextualSpacing as aE, paragraphFragmentsOf as aF, paragraphFragmentsOfBlocks as aG, paragraphLineSpacing as aH, paragraphSpacing as aI, paragraphTabStops as aJ, readBorderSide as aK, readCellBorders as aL, readTableBorders as aM, resolveBorderConflict as aN, resolveRunStyle as aO, resolveTableCellBorderGrid as aP, runStylesEqual as aQ, tabAdvanceWidth as aR, tabStopsFingerprint as aS, unionLayoutBoxes as aT, withDefaultTabInterval as aU, type TabLeader as aa, type TabStop as ab, type TableBorderSideName as ac, type TableBorderStrokeRecord as ad, type TableFragmentRecord as ae, type VerticalAlign as af, appliedSpaceBefore as ag, applyLineSpacing as ah, baselineShiftPtOf as ai, borderExtentPt as aj, borderWeight as ak, bottomBorderExtentPt as al, cascadedParagraphBorders as am, cascadedTabStops as an, collapsedSpaceBefore as ao, contentControlsOfLayout as ap, defaultTabIntervalFromSettings as aq, displayText as ar, effectiveContentControlLock as as, formatPageNumber as at, fragmentsOfParagraph as au, lineAtPosition as av, linesOf as aw, measureDisplayText as ax, nextTabDestination as ay, paragraphBorderExtentPt as az, type TableRowFragmentRecord as b, type TableCellFragmentRecord as c, type LayoutBox as d, type DrawingInsets as e, type TableBorderSide as f, type ThemeFonts as g, type ParagraphSpacing as h, type ParagraphLineSpacing as i, type ParagraphBorderEdge as j, type ParagraphBorders as k, type ResolvedTabStops as l, type PageRecord as m, type ParagraphFragmentRecord as n, type InlineDrawingLayoutContext as o, type CellBorderBox as p, type TableBorderBox as q, type BorderGridGeometry as r, type ContentControlGeometryFragment as s, type ContentControlLevel as t, type ContentControlLock as u, type ContentControlMappedType as v, DEFAULT_PAGE_GEOMETRY as w, DEFAULT_RUN_STYLE as x, DEFAULT_TAB_INTERVAL_PT as y, DEFAULT_TAB_INTERVAL_TWIPS as z };