@docx-editor.dev/core 2.27.0 → 2.29.0

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 (124) hide show
  1. package/dist/automation.cjs +1 -1
  2. package/dist/automation.d.ts +76 -16
  3. package/dist/automation.js +1 -1
  4. package/dist/binding.cjs +1 -1
  5. package/dist/binding.d.ts +31 -12
  6. package/dist/binding.js +1 -1
  7. package/dist/{index-CkQVRJtJ.d.ts → chrome-print-CAs51cpq.d.ts} +248 -115
  8. package/dist/chunk-236O4YQW.js +2 -0
  9. package/dist/chunk-4HENIAFG.js +1 -0
  10. package/dist/chunk-6JPL6ZAS.cjs +5 -0
  11. package/dist/chunk-6ZQP4HQ7.js +3 -0
  12. package/dist/{chunk-2GK7YQB2.cjs → chunk-7WQ7AAHW.cjs} +1 -1
  13. package/dist/chunk-BCXH4BXB.js +5 -0
  14. package/dist/chunk-BY2ACZZ6.cjs +1 -0
  15. package/dist/chunk-CAJEXXVG.cjs +4 -0
  16. package/dist/chunk-CPWLFPWC.js +30 -0
  17. package/dist/chunk-D7ZMDMPX.cjs +3 -0
  18. package/dist/chunk-DOHF6FA7.cjs +30 -0
  19. package/dist/chunk-DVX32P2P.js +27 -0
  20. package/dist/chunk-E4LOMDNL.js +1 -0
  21. package/dist/chunk-EKLM7NWB.cjs +7 -0
  22. package/dist/chunk-G3K63G7P.cjs +27 -0
  23. package/dist/chunk-G4HOFIKJ.js +1 -0
  24. package/dist/chunk-JLVBDYVQ.cjs +2 -0
  25. package/dist/chunk-LQX36Z5P.js +7 -0
  26. package/dist/chunk-LSZ6KPKK.js +1 -0
  27. package/dist/chunk-MM5QBMTE.cjs +1 -0
  28. package/dist/chunk-OMCG63ZF.js +1 -0
  29. package/dist/chunk-OSFOJTHU.cjs +18 -0
  30. package/dist/chunk-RCLZXP3B.js +2 -0
  31. package/dist/chunk-T6K2QYNZ.cjs +2 -0
  32. package/dist/chunk-TRGAVV2Y.js +4 -0
  33. package/dist/chunk-U4JZVSOE.js +2 -0
  34. package/dist/chunk-UHHYFIDT.cjs +1 -0
  35. package/dist/chunk-V3N4SOAK.cjs +1 -0
  36. package/dist/chunk-W35WTWAO.cjs +1 -0
  37. package/dist/chunk-YQ5G6LC7.js +18 -0
  38. package/dist/collaboration/replication.cjs +1 -1
  39. package/dist/collaboration/replication.d.ts +42 -6
  40. package/dist/collaboration/replication.js +1 -1
  41. package/dist/collaboration.cjs +1 -1
  42. package/dist/collaboration.d.ts +99 -7
  43. package/dist/collaboration.js +1 -1
  44. package/dist/contracts/document.d.ts +18 -3
  45. package/dist/contracts/editor.d.ts +19 -10
  46. package/dist/contracts/interaction.d.ts +31 -2
  47. package/dist/contracts/modules.cjs +1 -1
  48. package/dist/contracts/modules.d.ts +56 -8
  49. package/dist/contracts/modules.js +1 -1
  50. package/dist/contracts/types.d.ts +11 -2
  51. package/dist/{custom-node-writes-Vzpq9I8_.d.ts → custom-node-writes-Bn6uciLb.d.ts} +2 -2
  52. package/dist/diagnostic-context-B8M79cfK.d.ts +30 -0
  53. package/dist/diagnostic-hints-BeVjfNOv.d.ts +113 -0
  54. package/dist/{document-protection-9zpIHWTE.d.ts → document-protection-CUb55pl_.d.ts} +2 -2
  55. package/dist/{editor-B3OBYNrF.d.ts → editor-CZQZmTIU.d.ts} +770 -141
  56. package/dist/{editor-scope-OHPwVQ5l.d.ts → editor-scope-7o9xweiJ.d.ts} +1 -1
  57. package/dist/editor.cjs +1 -1
  58. package/dist/editor.css +5 -5
  59. package/dist/editor.d.ts +439 -33
  60. package/dist/editor.js +1 -1
  61. package/dist/export.cjs +2 -2
  62. package/dist/export.d.ts +23 -14
  63. package/dist/export.js +2 -2
  64. package/dist/failure-NS2lsya4.d.ts +115 -0
  65. package/dist/{font-resolver-DN0xNOf_.d.ts → font-resolver-lSBLirLR.d.ts} +3 -3
  66. package/dist/{headless-document-view-ICYqNkQ1.d.ts → headless-document-view-pp3q7wIh.d.ts} +2 -2
  67. package/dist/{image-resources-BDFxcu7W.d.ts → image-resources-tXLQShoC.d.ts} +10 -10
  68. package/dist/index.cjs +1 -1
  69. package/dist/index.d.ts +92 -20
  70. package/dist/index.js +1 -1
  71. package/dist/{layout-shaping-BvZD_VKP.d.ts → layout-shaping-BD-dF_rn.d.ts} +1 -1
  72. package/dist/layout.cjs +1 -1
  73. package/dist/layout.d.ts +168 -79
  74. package/dist/layout.js +1 -1
  75. package/dist/{note-properties-CkAk7g-n.d.ts → note-properties-qQUjO1GQ.d.ts} +3 -3
  76. package/dist/{omml-equation-DuczgG5o.d.ts → omml-equation-BUbU0utM.d.ts} +1 -1
  77. package/dist/output.cjs +1 -1
  78. package/dist/output.d.ts +18 -9
  79. package/dist/output.js +1 -1
  80. package/dist/{protocol-aKYr8r3u.d.ts → protocol-DYzzz2ej.d.ts} +44 -4
  81. package/dist/{review-paragraph-order-Ddj5d6rp.d.ts → review-paragraph-order-CN_0l2Cb.d.ts} +5 -2
  82. package/dist/{semantic-cell-selection-DoqYCz_F.d.ts → semantic-cell-selection-eYag3P47.d.ts} +8 -213
  83. package/dist/{semantic-interaction-G0bjmmmk.d.ts → semantic-interaction-kR_QB845.d.ts} +25 -34
  84. package/dist/{semantic-paint-CsMzF8-O.d.ts → semantic-paint-ZayFF3tD.d.ts} +17 -5
  85. package/dist/{semantic-record-queries-1du6lQqS.d.ts → semantic-record-queries-Bsgth6lv.d.ts} +1 -1
  86. package/dist/{semantic-records-B9OolWGN.d.ts → semantic-records-DQmumPgL.d.ts} +1367 -884
  87. package/dist/store.cjs +1 -1
  88. package/dist/store.d.ts +1611 -1586
  89. package/dist/store.js +1 -1
  90. package/dist/{tree-package-fragment-DYZIk1rY.d.ts → tree-package-store-CovjNARF.d.ts} +157 -137
  91. package/dist/{tree-session-contract-DQVJOOkv.d.ts → tree-session-contract-CZVAGl51.d.ts} +8 -6
  92. package/dist/{types-barrel-BFKR8Pzp.d.ts → types-barrel-DuC5QBNC.d.ts} +352 -231
  93. package/package.json +3 -3
  94. package/dist/chunk-4KZ6L5YF.cjs +0 -15
  95. package/dist/chunk-55AT3MOU.cjs +0 -25
  96. package/dist/chunk-6ZQMII6V.js +0 -1
  97. package/dist/chunk-7IGCWG4H.js +0 -2
  98. package/dist/chunk-7MYITTAH.js +0 -5
  99. package/dist/chunk-AVKVRU7Z.cjs +0 -5
  100. package/dist/chunk-CWRBFNK5.cjs +0 -5
  101. package/dist/chunk-DGPA3TST.cjs +0 -27
  102. package/dist/chunk-DNXVNQEO.js +0 -1
  103. package/dist/chunk-FQN4KSFL.cjs +0 -7
  104. package/dist/chunk-GVUOWHTO.cjs +0 -1
  105. package/dist/chunk-GWQJ5774.js +0 -5
  106. package/dist/chunk-HAQ72KD7.js +0 -1
  107. package/dist/chunk-HO5XFO32.cjs +0 -3
  108. package/dist/chunk-IGR23DV2.js +0 -1
  109. package/dist/chunk-JSIX2EOH.js +0 -3
  110. package/dist/chunk-K246T47S.cjs +0 -1
  111. package/dist/chunk-KAXOGCGA.js +0 -1
  112. package/dist/chunk-LZLTN5A3.cjs +0 -1
  113. package/dist/chunk-NBCVVZOM.cjs +0 -1
  114. package/dist/chunk-O5INAWZQ.js +0 -1
  115. package/dist/chunk-OHAXVTWG.cjs +0 -1
  116. package/dist/chunk-S74GRBNE.js +0 -7
  117. package/dist/chunk-UBLUVNZR.js +0 -27
  118. package/dist/chunk-UMRSBLO7.js +0 -15
  119. package/dist/chunk-VIPMGGUT.js +0 -25
  120. package/dist/chunk-WG4KMSX2.cjs +0 -2
  121. package/dist/chunk-WRLKVNNR.cjs +0 -1
  122. package/dist/chunk-ZU7IVN6G.js +0 -2
  123. package/dist/failure-BIPVQbRm.d.ts +0 -65
  124. package/dist/revision-projection-CSNFudZY.d.ts +0 -242
@@ -1,991 +1,1474 @@
1
- import { o as RevisionAttribution, b as RevisionDisplayMode } from './revision-projection-CSNFudZY.js';
2
- import { X as OoxmlProperty, Z as OoxmlElement, N as OoxmlNode, O as OoxmlPart, aG as TableBorderStyle, aH as DrawingAccessibility, aI as DrawingImageEffects, a8 as SourceCrop, aJ as DrawingTransform, aK as VectorShapeProjection, aj as DrawingHorizontalReferenceFrame, ak as DrawingVerticalReferenceFrame, aa as ImageWrapTarget, a0 as DrawingProjection, aL as OoxmlDrawingNode } from './types-barrel-BFKR8Pzp.js';
3
- import { b as ImageResourceState } from './image-resources-BDFxcu7W.js';
4
- import { O as OmmlEquationProjection } from './omml-equation-DuczgG5o.js';
1
+ import { N as OoxmlNode, ah as OoxmlProperty, O as OoxmlPart, ak as OoxmlElement, aM as TableBorderStyle, aN as DrawingAccessibility, aO as DrawingImageEffects, aI as SourceCrop, aP as DrawingTransform, aQ as VectorShapeProjection, X as DrawingHorizontalReferenceFrame, $ as DrawingVerticalReferenceFrame, a1 as ImageWrapTarget, an as DrawingProjection, aR as OoxmlDrawingNode } from './types-barrel-DuC5QBNC.js';
2
+ import { I as ImageResourceState } from './image-resources-tXLQShoC.js';
3
+ import { O as OmmlEquationProjection } from './omml-equation-BUbU0utM.js';
5
4
 
6
- /** A strikeout stroke at the drawn size, in points. `offsetPt` is the stroke TOP. @public */
7
- interface StrikeoutStrokePt {
8
- /** Distance from the baseline UP to the top of the stroke. */
9
- readonly offsetPt: number;
10
- /** Stroke height. Always positive. */
11
- readonly thicknessPt: number;
5
+ /** Named revision colors. Identifiers are stable and never contain CSS. @public */
6
+ declare const REVISION_MARKUP_COLORS: readonly ['black', 'blue', 'turquoise', 'green', 'pink', 'red', 'yellow', 'white', 'darkBlue', 'teal', 'darkGreen', 'violet', 'darkRed', 'darkYellow', 'gray50', 'gray25', 'lightBlue', 'lightYellow', 'lightOrange', 'lightPurple', 'lightGreen', 'gray'];
7
+ /** @public */
8
+ type RevisionMarkupNamedColor = (typeof REVISION_MARKUP_COLORS)[number];
9
+ /** @public */
10
+ type RevisionMarkupColor = RevisionMarkupNamedColor | 'byAuthor' | 'auto';
11
+ /** @public */
12
+ type RevisionMarkupMark = 'none' | 'colorOnly' | 'bold' | 'italic' | 'underline' | 'doubleUnderline' | 'strikethrough' | 'doubleStrikethrough';
13
+ /** @public */
14
+ type RevisionDeletionMark = RevisionMarkupMark | 'hidden' | 'caret' | 'pound';
15
+ /** @public */
16
+ type RevisionChangedLinesMark = 'none' | 'leftBorder' | 'rightBorder' | 'outsideBorder';
17
+ /** @public */
18
+ interface RevisionMarkupStyle<Mark extends string = RevisionMarkupMark> {
19
+ readonly mark: Mark;
20
+ readonly color: RevisionMarkupColor;
21
+ }
22
+ /** Viewer-only text styling. Document highlighting remains unchanged. @public */
23
+ interface RevisionMarkupTextStyle<Mark extends string = RevisionMarkupMark> extends RevisionMarkupStyle<Mark> {
24
+ /** Background color behind revision text. Defaults to `none`. */
25
+ readonly background: RevisionMarkupNamedColor | 'byAuthor' | 'none';
26
+ }
27
+ /** Fully resolved, immutable viewer preferences. @public */
28
+ interface ResolvedRevisionMarkup {
29
+ readonly insertions: RevisionMarkupTextStyle;
30
+ readonly deletions: RevisionMarkupTextStyle<RevisionDeletionMark>;
31
+ readonly movedFrom: RevisionMarkupTextStyle<RevisionDeletionMark>;
32
+ readonly movedTo: RevisionMarkupTextStyle;
33
+ readonly formatting: RevisionMarkupTextStyle;
34
+ readonly changedLines: RevisionMarkupStyle<RevisionChangedLinesMark>;
35
+ readonly cells: {
36
+ readonly inserted: RevisionMarkupNamedColor | 'byAuthor' | 'none';
37
+ readonly deleted: RevisionMarkupNamedColor | 'byAuthor' | 'none';
38
+ readonly merged: RevisionMarkupNamedColor | 'byAuthor' | 'none';
39
+ readonly split: RevisionMarkupNamedColor | 'byAuthor' | 'none';
40
+ };
41
+ readonly trackMoves: boolean;
42
+ readonly trackFormatting: boolean;
43
+ }
44
+ /** Partial viewer preferences. Omitted values keep the previous value. @public */
45
+ type RevisionMarkupOptions = {
46
+ readonly [K in keyof ResolvedRevisionMarkup]?: ResolvedRevisionMarkup[K] extends boolean ? boolean : Partial<ResolvedRevisionMarkup[K]>;
47
+ };
48
+ /** Initial viewer preferences. @public */
49
+ declare const DEFAULT_REVISION_MARKUP: ResolvedRevisionMarkup;
50
+ /** Validate and merge preferences without modifying the input. @public */
51
+ declare function resolveRevisionMarkup(input?: RevisionMarkupOptions, previous?: ResolvedRevisionMarkup): ResolvedRevisionMarkup;
52
+ /** A local settings draft owned by the editor. @public */
53
+ interface RevisionMarkupDialogSession {
54
+ /** Aborts on close, replacement, load, detach, destroy, or renderer disposal. */
55
+ readonly signal: AbortSignal;
56
+ /** Read the immutable draft. */
57
+ get(): ResolvedRevisionMarkup;
58
+ /** Merge validated options into the draft without committing preferences. */
59
+ set(options: RevisionMarkupOptions): void;
60
+ /** Restore default preferences in the draft only. */
61
+ reset(): void;
62
+ /** Observe draft changes, including effective external preference changes. */
63
+ subscribe(listener: () => void): () => void;
64
+ canApply(): boolean;
65
+ /** Commit the draft and close. Returns false after cancellation. */
66
+ apply(): boolean;
67
+ /** Close without committing the draft. */
68
+ cancel(): void;
69
+ }
70
+ /** Host rendering for the Track changes options dialog. @public */
71
+ interface RevisionMarkupChromeHandlers {
72
+ readonly onRequest?: (session: RevisionMarkupDialogSession) => void;
12
73
  }
13
74
 
14
75
  /**
15
- * `w:tblpPr/@w:horzAnchor` (17.4.58) and `@w:vertAnchor` (17.4.66): the box a floated
16
- * table's offsets are measured from. Absent means `text` for both.
76
+ * What a revision wrapper asserts about the content inside it.
77
+ *
78
+ * `moveFrom` / `moveTo` are deliberately distinct from `delete` / `insert`: a move is one
79
+ * decision with two halves, and presenting it as an unrelated deletion and insertion invites
80
+ * resolving one without the other, which duplicates or loses the content.
17
81
  */
18
- type TableFloatAnchor = 'text' | 'margin' | 'page';
19
- /** `w:tblpPr/@w:tblpXSpec` (17.4.63, ST_XAlign). */
20
- type TableFloatXSpec = 'left' | 'center' | 'right' | 'inside' | 'outside';
21
- /** `w:tblpPr/@w:tblpYSpec` (17.4.65, ST_YAlign). */
22
- type TableFloatYSpec = 'inline' | 'top' | 'center' | 'bottom' | 'inside' | 'outside';
82
+ type RevisionKind = 'insert' | 'delete' | 'moveFrom' | 'moveTo' | 'format';
23
83
  /**
24
- * `w:tblPr/w:tblpPr` (17.4.57) — a table positioned against an anchor box rather than at
25
- * the point in the text where it was authored.
84
+ * One revision wrapper's provenance, as authored.
26
85
  *
27
- * A spec (`tblpXSpec`/`tblpYSpec`) supersedes the matching offset when both are present:
28
- * 17.4.57 states the alignment outright, and the offset only answers "how far from the
29
- * anchor" for the case where no alignment was stated.
86
+ * `id` is the verbatim `@w:id` string rather than a number: `ST_DecimalNumber` restricts
87
+ * `xsd:integer` with no bounds, so a file may carry a value outside the safe integer range, and
88
+ * parsing it to a number would silently merge two distinct revisions.
89
+ *
90
+ * `date` is absent when the file omits it. `@w:date` is optional on `CT_TrackChange`, and
91
+ * inventing one is a silent content change.
30
92
  */
31
- interface TableFloatPosition {
32
- readonly horzAnchor: TableFloatAnchor;
33
- readonly vertAnchor: TableFloatAnchor;
34
- readonly xSpec?: TableFloatXSpec;
35
- /** `w:tblpX` in points; signed, so a table can be pulled into the margin. */
36
- readonly xPt: number;
37
- readonly ySpec?: TableFloatYSpec;
38
- /** `w:tblpY` in points; signed. */
39
- readonly yPt: number;
40
- readonly distances?: {
41
- readonly top: number;
42
- readonly right: number;
43
- readonly bottom: number;
44
- readonly left: number;
45
- };
93
+ interface RevisionAttribution {
94
+ readonly kind: RevisionKind;
95
+ readonly id: string;
96
+ readonly author: string;
97
+ readonly date?: string;
98
+ /** The wrapper's node id, so a surface can address this exact site. */
99
+ readonly nodeId: string;
46
100
  }
47
-
48
101
  /**
49
- * One `w:numPicBullet`: the image a level draws instead of its `w:lvlText`.
102
+ * Which revisions layout resolves before producing pages.
50
103
  *
51
- * `relationshipId` is unresolved on purpose — it names a relationship of the numbering part,
52
- * and resolving it is the image-resource lookup's job, under its own validation and caps.
53
- * @public
104
+ * - `all-markup` shows both halves of every change.
105
+ * - `proposed` shows what the document becomes if every change is accepted.
106
+ * - `original` shows what it was before any of them.
107
+ *
108
+ * The last two are specified as equal to accept-all and reject-all OUTPUT, which is what makes
109
+ * them testable, without either applying an op.
54
110
  */
55
- interface NumberingPictureBullet {
56
- /** `w:numPicBulletId` — what a level's `w:lvlPicBulletId` names. */
57
- readonly picBulletId: string;
58
- /** `v:imagedata/@r:id`, relative to the NUMBERING part's relationships. */
59
- readonly relationshipId: string;
60
- /** Authored `v:shape` width in points. */
61
- readonly width: number;
62
- /** Authored `v:shape` height in points. */
63
- readonly height: number;
64
- }
111
+ type RevisionDisplayMode = 'all-markup' | 'proposed' | 'original';
65
112
  /**
66
- * A level's picture bullet with the size it is actually DRAWN at.
113
+ * The views the review chrome offers, in Word's terms: the three layout projections plus
114
+ * Simple Markup, which reads as the proposed result with a red change bar beside every line
115
+ * a change touched — the document as accepting everything would leave it, and where.
116
+ */
117
+ type ReviewDisplayMode = RevisionDisplayMode | 'simple-markup';
118
+ /**
119
+ * How a document renders tracked changes when nothing says otherwise.
67
120
  *
68
- * Separate from {@link NumberingPictureBullet} because the authored `v:shape` extent is not
69
- * the painted extent: Word scales it by the marker run's font size. Layout, hit testing and
70
- * both paint sinks read `width` / `height`; `authored` is kept so the scale stays inspectable.
71
- * @public
121
+ * `all-markup` matches Word's own default: a reader who opens a document with pending changes
122
+ * sees them, rather than a clean-looking document hiding edits nobody has accepted.
72
123
  */
73
- interface ResolvedPictureBullet {
74
- /** `v:imagedata/@r:id`, relative to the NUMBERING part's relationships. */
75
- readonly relationshipId: string;
76
- /** Painted width in points, after the marker font scale. */
77
- readonly width: number;
78
- /** Painted height in points, after the marker font scale. */
79
- readonly height: number;
80
- /** The `v:shape` extent exactly as the file authored it. */
81
- readonly authored: NumberingPictureBullet;
82
- }
83
-
84
- /** Soft ceiling on abstractNum / num entries read from one part. */
85
- declare const MAX_NUMBERING_DEFINITIONS = 512;
86
- /** Soft ceiling on override entries per `w:num`. */
87
- declare const MAX_LVL_OVERRIDES = 9;
88
- /** `w:suff` — what separates a list marker from the text after it. */
89
- type ListSuffix = 'tab' | 'space' | 'nothing';
90
- /** `w:lvlJc` — how a list marker aligns within its own indent. */
91
- type ListMarkerAlign = 'left' | 'center' | 'right';
124
+ declare const DEFAULT_REVISION_DISPLAY_MODE: RevisionDisplayMode;
92
125
  /**
93
- * One level's indent, plus which parts the level actually AUTHORED.
126
+ * A view-time tracked-change filter. Revisions rejected by `includes` use the filter's accepted
127
+ * or rejected projection, while included revisions keep the display mode's normal projection.
94
128
  *
95
- * The provenance matters: `w:ind` cascades per-attribute, so a level that authored only `left`
96
- * must not overwrite an inherited `hanging` with a synthesized zero.
129
+ * `cacheKey` is a canonical, content-based identity for layout caches. The set itself is kept
130
+ * because author names are attacker-controlled strings and must never be parsed back from a
131
+ * delimiter-based key.
97
132
  */
98
- interface NumberingLevelIndent {
99
- /**
100
- * The sides as the level spells them, in points. `left` and `start` both name the leading
101
- * indent, and `right` and `end` the trailing one; `left`/`right` below hold those two.
102
- */
103
- readonly authored?: {
104
- readonly left?: number;
105
- readonly right?: number;
106
- readonly start?: number;
107
- readonly end?: number;
108
- };
109
- readonly left: number;
110
- readonly right: number;
111
- readonly hanging: number;
112
- readonly firstLine: number;
133
+ interface RevisionFilter {
134
+ readonly revisionMarkup?: ResolvedRevisionMarkup;
135
+ readonly hiddenAuthors: ReadonlySet<string>;
136
+ readonly includes?: (revision: RevisionAttribution) => boolean;
137
+ readonly includesNode?: (nodeId: string, author: string) => boolean;
138
+ /** Accepted/original projection for a revision excluded by `includesNode`. */
139
+ readonly excludedNodeMode?: (nodeId: string, author: string) => 'proposed' | 'original';
113
140
  /**
114
- * Which of these the LEVEL actually authored.
115
- *
116
- * A level's `w:pPr/w:ind` sits between the formatting it outranks and the formatting that
117
- * outranks it (`mergeListIndent`), so the merge has to tell "the level says left = 0" from
118
- * "the level says nothing about left and the style's value stands". Absent (the default) reads as "says nothing", which is what a
119
- * hand-built level in a test means.
141
+ * In a resolved view, content the view keeps paints as ORDINARY text: no author ink, no
142
+ * decoration, no change bar of its own. This is Word's No Markup, Original and Simple
143
+ * Markup, which the review chrome asks for. Without it the accepted projection keeps the
144
+ * attribution on kept insertions, which is what the engine paints when no review module
145
+ * is registered — colour by author, deletions resolved — and what its roster reads.
120
146
  */
121
- readonly stated?: {
122
- readonly left: boolean;
123
- readonly right: boolean;
124
- /** `w:hanging`/`w:firstLine` are one mutually exclusive slot (§17.3.1.10, §17.3.1.12). */
125
- readonly firstLineOffset: boolean;
126
- };
147
+ readonly resolvedMarkup?: 'plain';
148
+ readonly cacheKey: string;
127
149
  }
128
- /** One `w:lvl`: how this depth numbers, what its marker looks like, and how it indents. */
129
- interface NumberingLevel {
130
- readonly ilvl: number;
131
- readonly start: number;
132
- readonly numFmt: string;
133
- readonly lvlText: string;
134
- readonly lvlJc: ListMarkerAlign;
135
- readonly suff: ListSuffix;
136
- readonly indent: NumberingLevelIndent;
137
- /**
138
- * `w:lvlRestart` one-based trigger level, or `0` when the level never restarts.
139
- * Omitted in XML → restart when the previous level (or any earlier level) is used.
140
- */
141
- readonly lvlRestart?: number;
142
- /**
143
- * `w:isLgl` (§17.9.9): render EVERY level referenced by this level's `w:lvlText` in
144
- * decimal, whatever number format those levels declare for themselves.
145
- */
146
- readonly isLgl: boolean;
147
- /**
148
- * `w:lvlPicBulletId` (§17.9.12): this level's marker is the `w:numPicBullet` of that id,
149
- * drawn INSTEAD of `w:lvlText`. Absent on every ordinary level.
150
- */
151
- readonly picBulletId?: string;
152
- /** Level `w:rPr` as flat properties (for marker face / vanish). */
153
- readonly runProperties: readonly OoxmlProperty[];
154
- /** True when level run props request vanish — marker must not paint. */
155
- readonly vanish: boolean;
150
+ /** @deprecated Use {@link RevisionFilter}. */
151
+ interface RevisionAuthorFilter extends RevisionFilter {
156
152
  }
153
+ /** Build a canonical reviewer filter. An empty input returns `undefined` for the fast path. */
154
+ declare function revisionAuthorFilter(hiddenAuthors: Iterable<string>): RevisionAuthorFilter | undefined;
157
155
  /**
158
- * One `w:lvlOverride` on a `w:num`: a restart value, a replacement level, or both.
156
+ * Whether content under this stack of revisions is laid out in the given mode.
159
157
  *
160
- * How two lists share an abstract definition while numbering independently.
158
+ * Containment governs, so a single enclosing wrapper the mode resolves away suppresses
159
+ * everything inside it regardless of what the inner wrappers say. An insertion inside a
160
+ * deletion does not survive the proposed result: the deletion it sits in was accepted.
161
161
  */
162
- interface LevelOverride {
163
- readonly startOverride?: number;
164
- /** Full level replacement when `w:lvl` is present under the override. */
165
- readonly level?: NumberingLevel;
166
- }
162
+ declare function revisionsVisible(revisions: readonly RevisionAttribution[], mode: RevisionDisplayMode, authorFilter?: RevisionAuthorFilter): boolean;
167
163
  /**
168
- * One `w:abstractNum` — the shape of a list, without being a list.
164
+ * Every revision on a paragraph's own MARK, from `w:pPr/w:rPr/w:ins|w:del|w:moveFrom|w:moveTo`.
169
165
  *
170
- * Never referenced by a paragraph directly. Paragraphs name a {@link NumDefinition}, which names
171
- * one of these, so several lists can share a definition and still count separately.
166
+ * All four members of `EG_ParaRPrTrackChanges`. A moved paragraph carries `w:moveFrom` on the
167
+ * mark of the copy it left and `w:moveTo` on the mark of the copy it arrived at, so a move
168
+ * that spans whole paragraphs is recorded here and nowhere else.
169
+ *
170
+ * `EG_ParaRPrTrackChanges` records that the pilcrow itself was inserted or deleted, which is how
171
+ * Word writes a paragraph split or merge. It is not content — there is no text to decorate — so
172
+ * a surface shows it as a mark of its own beside the paragraph, the way Word draws a struck-
173
+ * through ¶.
174
+ *
175
+ * A LIST, because the group is `ins? del? moveFrom? moveTo?` and the first two can both be
176
+ * there: that pair is what Word writes when a second author proposes removing a mark the first
177
+ * proposed adding, and it is what this engine's own writer emits (`tree-op-tracked.ts`).
178
+ * Answering with the first one hid the second author's decision from the PAGE. The review pane
179
+ * walks the tree itself and always listed both, which is the worse shape of the two: a card
180
+ * offering a decision the reader could see no sign of.
181
+ *
182
+ * Ordered as the file orders them, which is the order the group declares.
183
+ *
184
+ * Property-position `w:ins`/`w:del` stay `generic` in the tree deliberately, so this reads them
185
+ * by name rather than by kind.
172
186
  */
173
- interface AbstractNumDefinition {
174
- readonly abstractNumId: string;
175
- readonly levels: ReadonlyMap<number, NumberingLevel>;
176
- /**
177
- * `w:numStyleLink` (§17.9.21): this definition carries no levels of its own — the numbering
178
- * lives on the named style. Word follows the link; resolve it with
179
- * {@link resolveNumberingStyleLinks} before reading levels, or every paragraph on this
180
- * abstract renders with NO marker at all.
181
- */
182
- readonly numStyleLink?: string;
183
- /**
184
- * `w:styleLink` (§17.9.23): the definition side of the same pair — this abstract IS the
185
- * numbering of the named style. Kept so a link can be verified rather than assumed.
186
- */
187
- readonly styleLink?: string;
188
- }
187
+ declare function paragraphMarkRevisionsOf(paragraph: OoxmlNode): readonly RevisionAttribution[];
189
188
  /**
190
- * One `w:num` — the thing a paragraph's `w:numId` actually names.
189
+ * The tracked FORMAT change on a paragraph's own mark, from `w:pPr/w:rPr/w:rPrChange`.
191
190
  *
192
- * Points at an {@link AbstractNumDefinition} and may override any of its levels.
191
+ * `CT_ParaRPr` ends with it (§17.13.5.32), and Word writes it when a user changes the mark's
192
+ * own run properties with tracking on. It reaches the fragment rather than a span because it
193
+ * decorates no characters, and it is not in `props`: a fragment's `props` carry the mark's
194
+ * `w:rPr` by NAME only, with none of its children.
195
+ *
196
+ * Note that `w:rPrChange/w:rPr` is `CT_ParaRPrOriginal`, which may carry its own revision
197
+ * marks. Those describe the mark as it WAS and must not be read as live ones, which is why
198
+ * this reads the change element's own attributes and never descends into it.
193
199
  */
194
- interface NumDefinition {
195
- readonly numId: string;
196
- readonly abstractNumId: string;
197
- readonly overrides: ReadonlyMap<number, LevelOverride>;
198
- }
200
+ declare function paragraphMarkFormatRevisionOf(paragraph: OoxmlNode): RevisionAttribution | null;
199
201
  /**
200
- * The bounded projection of `numbering.xml` that list layout resolves against.
202
+ * The single decision a one-field reader sees.
201
203
  *
202
- * Projection ONLY — never a mutation or serialization authority. Hostile values are dropped or
203
- * clamped, and a missing definition resolves to "no list" rather than a guess.
204
+ * A DELETION wins when a mark carries both, for the reason paint draws it that way: a break
205
+ * proposed and then unproposed ends up removed, so that is the decision a reader who can only
206
+ * see one must see. One function decides it, so the deprecated field, the deprecated function
207
+ * and the painted glyph cannot answer differently.
204
208
  */
205
- interface NumberingIndex {
206
- readonly abstractNums: ReadonlyMap<string, AbstractNumDefinition>;
207
- readonly nums: ReadonlyMap<string, NumDefinition>;
208
- /**
209
- * `w:numPicBullet` declarations keyed by `w:numPicBulletId` (§17.9.20).
210
- *
211
- * Optional so a hand-built index stays valid; absent reads as "this part declares none",
212
- * which is what a level's `w:lvlPicBulletId` then falls back from.
213
- */
214
- readonly pictureBullets?: ReadonlyMap<string, NumberingPictureBullet>;
215
- }
209
+ declare function shownMarkRevision(revisions: readonly RevisionAttribution[]): RevisionAttribution | undefined;
216
210
  /**
217
- * Build a numbering index from the root of a numbering part (`w:numbering`).
211
+ * Does this decision, taken, remove the paragraph mark?
218
212
  *
219
- * Empty / missing roots yield an empty index. Duplicate ids keep the first definition.
213
+ * A deletion does, and so does a `moveFrom`: the copy the paragraph moved OUT of goes away
214
+ * when the move is accepted. `insert` and `moveTo` are the other half of each pair, and they
215
+ * keep the break. Paint, the change bar and the resolved views all ask this one question, so
216
+ * a `moveFrom` cannot end up struck through in the margin and blue on the glyph.
220
217
  */
218
+ declare function markRevisionRemovesMark(revision: RevisionAttribution): boolean;
221
219
  /**
222
- * Project `numbering.xml` into the bounded index.
220
+ * The one decision on a paragraph's mark that a single-field reader sees.
223
221
  *
224
- * Every ceiling here exists because the input is file-derived: definition counts, override
225
- * counts, indent magnitudes and style-link hop depth are all capped, and nothing from the file
226
- * becomes a loop bound or an allocation size.
222
+ * @deprecated Reads one of the revisions a mark can carry. Use {@link paragraphMarkRevisionsOf},
223
+ * which answers with all of them.
227
224
  */
228
- declare function buildNumberingIndex(root: OoxmlElement | null | undefined): NumberingIndex;
229
- /** Resolve the effective level for a `numId` + `ilvl`, applying overrides. */
225
+ declare function paragraphMarkRevisionOf(paragraph: OoxmlNode): RevisionAttribution | null;
230
226
  /**
231
- * The effective level for one paragraph's numbering reference, after overrides and style links.
227
+ * The tracked FORMAT change on a property list, from `w:rPrChange` or `w:pPrChange`.
232
228
  *
233
- * Answers null for a reference the index cannot resolve — a paragraph naming a definition the
234
- * file never declared is an unnumbered paragraph, not an error.
229
+ * A property change alters no characters, so it has no span of its own to strike or underline.
230
+ * Word marks the affected text and says what changed; the minimum a reader needs is to see that
231
+ * this text's formatting is itself a pending decision.
232
+ *
233
+ * Read from the flattened property list because that is what layout already carries — the
234
+ * change wrapper is a `w:rPr`/`w:pPr` child like any other.
235
235
  */
236
- declare function resolveNumberingLevel(index: NumberingIndex, numId: string, ilvl: number): {
237
- readonly abstractNumId: string;
238
- readonly level: NumberingLevel;
239
- readonly startOverride?: number;
240
- } | null;
241
- /** Empty index for tests / documents without numbering. */
242
- declare const EMPTY_NUMBERING_INDEX: NumberingIndex;
236
+ declare function formatRevisionOf(properties: readonly {
237
+ readonly localName: string;
238
+ readonly attributes?: Readonly<Record<string, string>>;
239
+ }[]): RevisionAttribution | null;
240
+ /** True when this stack of revisions marks its content as deleted from the live document. */
241
+ declare function revisionsAreDeletion(revisions: readonly RevisionAttribution[]): boolean;
242
+
243
+ /** A strikeout stroke at the drawn size, in points. `offsetPt` is the stroke TOP. @public */
244
+ interface StrikeoutStrokePt {
245
+ /** Distance from the baseline UP to the top of the stroke. */
246
+ readonly offsetPt: number;
247
+ /** Stroke height. Always positive. */
248
+ readonly thicknessPt: number;
249
+ }
243
250
 
244
251
  /**
245
- * A face, as something asks for it: family plus the two axes this engine admits.
252
+ * Whether a paragraph must start a new page (`w:pageBreakBefore`).
246
253
  *
247
- * Only static weights and slants. Variable-font axes are deliberately outside this vocabulary —
248
- * the shaper refuses variation axes, and a variable file admitted here would render bold at
249
- * regular weight.
254
+ * LAST WINS, like every other toggle read off the cascade (`paragraphKeeps` does the same
255
+ * for `w:keepNext`). An any-wins `.some()` cannot be switched off: a Chapter or Heading style
256
+ * carries `w:pageBreakBefore`, and Word writes `w:val="0"` on the one instance whose author
257
+ * unchecked the box — that paragraph still broke, so the document grew a blank page and every
258
+ * page number after it was wrong.
259
+ *
260
+ * An absent `w:val` is on (§17.17.4), and the off vocabulary is the whole of it — `off` is a
261
+ * spelling too, which {@link isOn} beside this already accepts.
250
262
  */
251
- interface FontRequest {
252
- readonly family: string;
253
- readonly weight: number;
254
- readonly style: 'normal' | 'italic';
255
- }
263
+ declare function paragraphBreaksBefore(props: readonly OoxmlProperty[]): boolean;
256
264
  /**
257
- * A request that was answered by a DIFFERENT face than the one asked for.
265
+ * Soft ceiling matching the spike's resolved-style limit (31_680 twips ≈ 22"). Beyond
266
+ * that an attacker-authored spacing would push pagination into pathological page counts.
267
+ */
268
+ declare const MAX_PARAGRAPH_SPACING_PT: number;
269
+ /** Soft ceiling on border width (96 eighths = 12pt). Word's UI tops out well below this. */
270
+ declare const MAX_BORDER_WIDTH_PT = 12;
271
+ /** Soft ceiling on border-to-text gap (`w:space`, already in points). */
272
+ declare const MAX_BORDER_SPACE_PT = 3168;
273
+ /**
274
+ * A paragraph's resolved space before and after, in points.
258
275
  *
259
- * Recorded rather than silently applied, because a substitution changes measurement: it is part
260
- * of the shaping fingerprint, so a cached run shaped against a substitute is never reused for the
261
- * real face.
276
+ * Already collapsed against `w:contextualSpacing`, so adjacent same-style paragraphs that suppress
277
+ * their gap arrive here with it removed rather than leaving that to whoever stacks them.
262
278
  */
263
- interface FontSubstitution {
264
- readonly requested: FontRequest;
265
- readonly resolved: FontRequest;
266
- readonly lineMetrics?: {
267
- readonly heightEm: number;
268
- readonly baselineEm: number;
269
- };
279
+ interface ParagraphSpacing {
280
+ /** `w:spacing/@before`, in points. */
281
+ readonly before: number;
282
+ /** `w:spacing/@after`, in points. */
283
+ readonly after: number;
270
284
  }
271
- declare const RESOLVED_FONT_BRAND: unique symbol;
272
285
  /**
273
- * A face that passed admission: the bytes are present, within limits, hash-verified, and parse as
274
- * a font.
286
+ * The gap Word substitutes when `w:beforeAutospacing` / `w:afterAutospacing` is on
287
+ * (ECMA-376 §17.3.1.33, the `w:spacing` clause both attributes belong to).
275
288
  *
276
- * Branded, so a `ResolvedFont` cannot be constructed by an object literal. Holding one is proof
277
- * the checks ran — which is what lets shaping skip re-validating on every call.
289
+ * The attribute means "the consumer decides", and the authored `@before` / `@after` beside it
290
+ * is IGNORED rather than used as the value. Word's answer is HTML's default `<p>` margin,
291
+ * 14pt, which is what a document round-tripped through Word's HTML filter carries — and this
292
+ * one is everywhere, because Word writes `w:before="100" w:beforeAutospacing="1"` for it. Reading
293
+ * only the literal 100 twips lays every such paragraph out 9pt tight, which moves page breaks.
278
294
  */
279
- interface ResolvedFont {
280
- readonly [RESOLVED_FONT_BRAND]: true;
281
- readonly id: string;
282
- readonly identity: string;
283
- readonly request: FontRequest;
284
- readonly family: string;
285
- /** Owned byte length, available without creating a defensive byte copy. */
286
- readonly byteLength: number;
287
- readonly bytes: Uint8Array;
288
- readonly hash: string;
289
- readonly faceIndex: number;
290
- readonly substitution: FontSubstitution | null;
291
- }
295
+ declare const AUTO_PARAGRAPH_SPACING_PT = 14;
292
296
  /**
293
- * Largest single face this engine will ever admit, whatever a caller configures.
297
+ * Where a paragraph sits, for the contexts in which Word's auto spacing resolves to 0
298
+ * instead of {@link AUTO_PARAGRAPH_SPACING_PT}.
294
299
  *
295
- * A CEILING, not a default: a host may set a smaller `maxFontBytes`, but nothing can raise it
296
- * past this. Font bytes come from files, and an unbounded face is a memory-exhaustion vector.
297
- */
298
- declare const HARD_MAX_FONT_BYTES: number;
299
- /** Most faces one snapshot may hold, whatever a caller configures. */
300
- declare const HARD_MAX_FONT_SOURCES = 256;
301
- /** Most bytes all admitted faces may total, whatever a caller configures. */
302
- declare const HARD_MAX_AGGREGATE_FONT_BYTES: number;
303
- /**
304
- * Why a face was not admitted.
300
+ * This resolves the interior list-item value. Body layout restores each outer list margin
301
+ * in `resolveListAutoSpacing`, where neighboring blocks are available. A caller that says
302
+ * nothing gets the body answer.
305
303
  *
306
- * `forbidden` and `hashMismatch` are adversarial signals rather than ordinary failures: the first
307
- * is a face the host declared off-limits, the second is bytes that are not what their source
308
- * claimed.
304
+ * A TABLE CELL is not such a context. Measured on a captured control at 10pt, where the
305
+ * line pitch is 11.52pt: the reference puts 26.16pt between two paragraphs in the body with
306
+ * auto spacing on, and the SAME 26.16pt between two paragraphs inside a cell. With an empty
307
+ * paragraph between them it is 52.32, exactly twice. Reading it as 0 in a cell lays every
308
+ * such paragraph 13.92pt tight, which is enough to pull a signature block back onto the
309
+ * previous page. See `.cache/pdf/claude-autospacing/`.
309
310
  */
310
- type FontResolutionErrorCode = 'missing' | 'forbidden' | 'overLimit' | 'malformed' | 'hashMismatch';
311
+ interface ParagraphAutoSpacingContext {
312
+ /** The paragraph participates in numbering (`w:numPr`), i.e. it is a list item. */
313
+ readonly inList?: boolean;
314
+ /**
315
+ * The paragraph lives in a table cell. Kept because callers describe position with it and
316
+ * list resolution reads it; it does NOT zero auto spacing.
317
+ */
318
+ readonly inTableCell?: boolean;
319
+ /** Section grid pitch in points; no grid uses Word's fixed 12pt line unit. */
320
+ readonly lineUnitPt?: number;
321
+ /**
322
+ * `w:doNotUseHTMLParagraphAutoSpacing` is on: automatic spacing outside a list resolves to
323
+ * the fixed 5pt before and 10pt after instead of {@link AUTO_PARAGRAPH_SPACING_PT}.
324
+ */
325
+ readonly fixedAutoSpacing?: boolean;
326
+ }
311
327
  /**
312
- * A face that could not be admitted, carrying whatever evidence the refusal produced.
328
+ * Resolved line spacing (`w:spacing/@line` + `@lineRule`, ECMA-376 17.3.1.33).
313
329
  *
314
- * RETURNED rather than thrown by `FontResourceSnapshot.resolve`, because a missing face is an
315
- * ordinary condition — layout falls back and carries on. It extends `Error` so a caller that
316
- * would rather throw can.
330
+ * `auto` is the interesting one: `@line` is 240ths of a line, so 240 is single, 360 is
331
+ * one-and-a-half, 480 is double — and Word's own Normal style since 2013 is 259, i.e.
332
+ * 1.08. A document laid out at a flat single spacing is ~8% tight on EVERY line, which
333
+ * moves every page break, so this is not a cosmetic detail.
334
+ *
335
+ * `exact` fixes the line box at `@line` twips and lets tall glyphs clip, the way Word
336
+ * does. `atLeast` uses it as a floor.
317
337
  */
318
- declare class FontResolutionError extends Error {
319
- readonly name = "FontResolutionError";
320
- readonly request: FontRequest;
321
- readonly code: FontResolutionErrorCode;
322
- readonly limit?: number;
323
- readonly actual?: number;
324
- readonly diagnostic?: string;
325
- readonly expectedHash?: string;
326
- readonly actualHash?: string;
327
- constructor(code: FontResolutionErrorCode, request: FontRequest, details?: {
328
- readonly limit?: number;
329
- readonly actual?: number;
330
- readonly diagnostic?: string;
331
- readonly expectedHash?: string;
332
- readonly actualHash?: string;
333
- });
334
- }
338
+ type LineSpacingRule = 'auto' | 'exact' | 'atLeast';
335
339
  /**
336
- * One face offered to a snapshot, before admission.
340
+ * Resolved line spacing: the rule, and the value it applies.
337
341
  *
338
- * `hash` is checked against the bytes, so a source cannot claim a face it did not supply. Setting
339
- * `availability` to `forbidden` declares a face that exists but must not be used, which resolves
340
- * as a typed refusal rather than as "missing".
342
+ * `value` means different things per rule — 240ths of a line under `auto`, points under `exact`
343
+ * and `atLeast` — which is why the two travel together and neither is useful alone.
341
344
  */
342
- interface FontResourceDefinition {
343
- readonly request: FontRequest;
344
- readonly id: string;
345
- readonly bytes: Uint8Array;
346
- readonly hash: string;
347
- readonly faceIndex: number;
348
- readonly availability?: 'available' | 'forbidden';
345
+ interface ParagraphLineSpacing {
346
+ readonly rule: LineSpacingRule;
347
+ /** `auto`: the 240ths-of-a-line multiplier numerator. Otherwise points. */
348
+ readonly value: number;
349
+ /** Legacy noExtraLineSpacing: retain the natural baseline within an exact-height box. */
350
+ readonly preserveExactBaseline?: true;
351
+ /**
352
+ * Active section line-grid pitch in points (`w:docGrid/@w:linePitch`). Present only when
353
+ * the paragraph snaps to the grid; only with `auto`. See `line-grid.ts`.
354
+ */
355
+ readonly gridPitch?: number;
349
356
  }
357
+ /** Single spacing: what a paragraph that says nothing gets. */
358
+ declare const SINGLE_LINE_SPACING: ParagraphLineSpacing;
350
359
  /**
351
- * A host-declared redirect: requests for `from` resolve to `to`.
360
+ * One resolved `w:pBdr` edge: its style, colour, thickness and gap.
352
361
  *
353
- * How a metric-compatible substitute is wired in — a document naming Calibri resolves to Carlito
354
- * without the document being rewritten.
362
+ * `widthPt` and `spacePt` are already converted and CLAMPED — both come from a file, and an
363
+ * unbounded border width becomes a layout dimension.
355
364
  */
356
- interface DeclaredFontSubstitution {
357
- readonly from: FontRequest;
358
- readonly to: FontRequest;
359
- readonly lineMetrics?: {
360
- readonly heightEm: number;
361
- readonly baselineEm: number;
362
- };
365
+ interface ParagraphBorderEdge {
366
+ /** Authored `ST_Border` value (`single`, `dashed`, …). */
367
+ readonly val: string;
368
+ /** RRGGBB, or null when auto/missing (paint defaults to black). */
369
+ readonly color: string | null;
370
+ /** Border thickness in points (`w:sz` is eighths of a point). */
371
+ readonly widthPt: number;
372
+ /** Gap from text to the rule, in points (`w:space`). */
373
+ readonly spacePt: number;
374
+ /**
375
+ * `w:shadow` — Word offsets a drop shadow behind the rule.
376
+ *
377
+ * Present only when authored true, so an edge that says nothing keeps the shape earlier
378
+ * fixtures assert. Resolved and carried; drawing it is deferred.
379
+ */
380
+ readonly shadow?: true;
363
381
  }
364
- /** Whether bytes parse as a usable font, with a diagnostic when they do not. */
365
- type FontValidationResult = {
366
- readonly valid: true;
367
- } | {
368
- readonly valid: false;
369
- readonly diagnostic: string;
370
- };
382
+ /** The six `CT_PBdr` children, in schema order (ECMA-376 §17.3.1.24). */
383
+ declare const PARAGRAPH_BORDER_SIDES: readonly ['top', 'left', 'bottom', 'right', 'between', 'bar'];
371
384
  /**
372
- * The injected check that bytes really are a font.
385
+ * Which of the six `CT_PBdr` edges.
373
386
  *
374
- * Injected because it is the shaper that knows — HarfBuzz can open a face and read its tables,
375
- * and this layer must not duplicate that judgement. Every byte reaching it is file input.
387
+ * Four are physical box edges; `between` and `bar` are group-relative, drawn only where
388
+ * consecutive paragraphs share a border definition.
376
389
  */
377
- type FontByteValidator = (bytes: Uint8Array, faceIndex: number) => FontValidationResult;
390
+ type ParagraphBorderSide = (typeof PARAGRAPH_BORDER_SIDES)[number];
378
391
  /**
379
- * An immutable set of admitted faces, and the only way to reach one.
392
+ * A paragraph's resolved `w:pBdr` (ECMA-376 §17.3.1.24).
380
393
  *
381
- * `epoch` identifies the snapshot: fonts change by REPLACING it, never by mutation, so a layout
382
- * pass holds one snapshot for its whole run and cannot observe a face appearing or vanishing
383
- * midway.
394
+ * `top`/`left`/`bottom`/`right` are the four physical edges of the box. The other two are
395
+ * group-relative: `between` draws at a boundary INSIDE a run of consecutive paragraphs whose
396
+ * border settings are identical, and `bar` is the vertical change-bar rule beside the
397
+ * paragraph, drawn whether or not the paragraph groups with its neighbours.
384
398
  */
385
- interface FontResourceSnapshot {
386
- readonly epoch: number;
387
- /** The admitted face, or a typed refusal. Never throws. */
388
- resolve(request: FontRequest): ResolvedFont | FontResolutionError;
399
+ interface ParagraphBorders {
400
+ readonly top?: ParagraphBorderEdge;
401
+ readonly left?: ParagraphBorderEdge;
402
+ readonly bottom?: ParagraphBorderEdge;
403
+ readonly right?: ParagraphBorderEdge;
404
+ readonly between?: ParagraphBorderEdge;
405
+ readonly bar?: ParagraphBorderEdge;
389
406
  }
390
407
  /**
391
- * How a snapshot is built: which faces, under what limits, validated how.
408
+ * Resolve `w:spacing` before/after from flat paragraph properties.
392
409
  *
393
- * `maxFontBytes` is clamped to {@link HARD_MAX_FONT_BYTES} — a caller may tighten the budget but
394
- * never widen it past the engine's own ceiling.
395
- */
396
- interface FontResourceSnapshotOptions {
397
- readonly epoch: number;
398
- readonly maxFontBytes: number;
399
- readonly resources: readonly FontResourceDefinition[];
400
- readonly substitutions?: readonly DeclaredFontSubstitution[];
401
- readonly validateFont: FontByteValidator;
402
- readonly instrumentation?: FontResourceInstrumentation;
403
- }
404
- /**
405
- * Optional counters for the operations font admission is expected to do RARELY.
410
+ * Line spacing (`w:line` / `w:lineRule`) is a separate concern — it changes measured line
411
+ * height, not the gap between paragraphs — and is not resolved here.
406
412
  *
407
- * Exists so tests can assert the absence of work: hashing and byte-copying a 64 MB face on every
408
- * resolve would not be visible in output, only in a stalled document, so the checks assert these
409
- * fire once rather than per call.
413
+ * `w:beforeAutospacing` / `w:afterAutospacing` REPLACE the authored measurement on their own
414
+ * side rather than adding to it; see {@link AUTO_PARAGRAPH_SPACING_PT}.
410
415
  */
411
- interface FontResourceInstrumentation {
412
- readonly onOwnedByteCopy?: () => void;
413
- readonly onHash?: () => void;
414
- readonly onTableScan?: () => void;
415
- readonly onAdmission?: () => void;
416
- }
416
+ declare function paragraphSpacing(props: readonly OoxmlProperty[], context?: ParagraphAutoSpacingContext): ParagraphSpacing;
417
417
  /**
418
- * The canonical string identifying one face request.
418
+ * Resolve `w:line` / `w:lineRule` from flat paragraph properties.
419
419
  *
420
- * Case- and whitespace-normalized, so `"Times New Roman"` and `"times new roman"` are one key —
421
- * a document may name a family either way and both must reach the same face.
420
+ * Merged per attribute for the same reason as before/after: `w:spacing` is one element
421
+ * carrying independent attributes, and a style that states only `@line` must not reset the
422
+ * rule an earlier entry in the cascade set.
422
423
  */
423
- declare const fontRequestKey: (request: FontRequest) => string;
424
- /** Bounded minimum sfnt/TTC check; Task 4 supplies full parser-backed validation. */
425
- declare const boundedStructuralFontValidator: FontByteValidator;
424
+ declare function paragraphLineSpacing(props: readonly OoxmlProperty[]): ParagraphLineSpacing;
426
425
  /**
427
- * Admit a set of faces and return the immutable snapshot layout resolves against.
426
+ * Apply resolved line spacing to a line's natural (glyph-derived) box.
428
427
  *
429
- * Admission is the trust boundary for font bytes: counts and sizes are checked against the hard
430
- * ceilings, each face's hash is re-derived and compared, and the bytes are handed to the injected
431
- * validator. A face failing any of these is recorded as a typed refusal rather than dropped, so
432
- * `resolve` can explain itself instead of answering "missing".
433
- */
434
- declare const createFontResourceSnapshot: (options: FontResourceSnapshotOptions) => FontResourceSnapshot;
435
-
436
- declare const FIXED_POINT: unique symbol;
437
- /** Fixed-point coordinates are safe integers in units declared by ShapingEnvironment.fixedPointScale. */
438
- type FixedPoint = number & {
439
- readonly [FIXED_POINT]: true;
440
- };
441
- /**
442
- * Brand a safe integer as a {@link FixedPoint} coordinate.
428
+ * `auto` places its extra BELOW the line — the last line's multiple spacing still separates
429
+ * it from the next paragraph. Putting that delta above inverted cover-page rhythm:
430
+ * `w:line="460"` on "between" opened a large gap above the word and almost none before
431
+ * "MERIDIAN".
443
432
  *
444
- * Fixed point rather than float throughout shaping so that two runs shaped identically compare
445
- * EQUAL — float accumulation would make the same text measure differently depending on how it was
446
- * split, and pagination is decided on those measurements.
433
+ * `atLeast` does the OPPOSITE: the box grows upward and the glyphs sit on its floor. A
434
+ * captured control settles it — Times New Roman 12 pt at `w:line="360" w:lineRule="atLeast"`
435
+ * puts the reference's baseline at 15.36 pt, which is `18 - descent` snapped to the device
436
+ * grid, where treating it like `auto` leaves the baseline at 11.28 and the text 4.08 pt too
437
+ * high. `w:line="240" atLeast` is below the natural line, falls through to the natural
438
+ * height, and is unaffected. See `.cache/pdf/claude-linerule/`.
447
439
  *
448
- * @throws RangeError when the value is not a safe integer.
440
+ * Exact-height boxes place their baseline at 80% of the height; the legacy
441
+ * noExtraLineSpacing switch instead preserves the face baseline within the box.
442
+ *
443
+ * An `auto` or `atLeast` line of a snapping paragraph under an active line grid takes whole
444
+ * pitches instead ({@link gridLineBox}); `trailing` then names the box below the glyphs.
449
445
  */
450
- declare const fixedPoint: (value: number) => FixedPoint;
451
- /** Which way a run reads. The parity projection of its bidi embedding level. */
452
- type TextDirection = 'ltr' | 'rtl';
453
- /** How fixed-point conversion breaks ties. Part of the shaping fingerprint. */
454
- type FixedPointRoundingMode = 'halfAwayFromZero' | 'halfToEven' | 'towardZero';
455
- /** Which Unicode normalization is applied before shaping, if any. */
456
- type NormalizationPolicy = 'none' | 'NFC' | 'NFD' | 'NFKC' | 'NFKD';
446
+ declare function applyLineSpacing(spacing: ParagraphLineSpacing, naturalHeight: number, naturalBaseline: number): {
447
+ height: number;
448
+ baseline: number;
449
+ trailing?: number;
450
+ };
457
451
  /**
458
- * The shaping library and its exact version.
459
- *
460
- * Versioned because a library upgrade can change glyph positioning, and a cached measurement
461
- * taken under the old one must not be reused under the new one.
452
+ * `w:contextualSpacing` (17.3.1.9): drop before/after between paragraphs of the SAME
453
+ * style. Word's built-in `ListParagraph` sets it, so every list authored in Word gets a
454
+ * paragraph gap between items without this.
462
455
  */
463
- interface VersionedShapingLibrary {
464
- readonly name: string;
465
- readonly version: string;
466
- }
456
+ declare function paragraphContextualSpacing(props: readonly OoxmlProperty[]): boolean;
467
457
  /**
468
- * Everything that determines how text shapes — the complete input to a
469
- * {@link ShapingEnvironment}.
458
+ * Resolve `w:pBdr` from the paragraph-properties node.
470
459
  *
471
- * Exhaustive on purpose. Any field that could change a glyph's position belongs here, because the
472
- * environment's fingerprint is what decides whether a cached shaped run may be reused.
460
+ * Nested — every edge is a child of `pBdr`, not an attribute — so this reads the typed tree
461
+ * rather than the flattened `OoxmlProperty[]` bag `propertiesOf` builds for leaf props.
473
462
  */
474
- interface ShapingEnvironmentInput {
475
- readonly font: ResolvedFont;
476
- readonly variationAxes: Readonly<Record<string, number>>;
477
- readonly shapingLibrary: VersionedShapingLibrary;
478
- readonly unicodeDataVersion: string;
479
- readonly normalization: NormalizationPolicy;
480
- readonly script: string;
481
- readonly language: string;
482
- readonly direction: TextDirection;
483
- readonly features: Readonly<Record<string, number>>;
484
- readonly fallbackOrder: readonly ResolvedFont[];
485
- readonly fixedPointScale: number;
486
- readonly roundingMode: FixedPointRoundingMode;
487
- }
463
+ declare function paragraphBorders(pPr: OoxmlNode | undefined): ParagraphBorders;
488
464
  /**
489
- * A validated {@link ShapingEnvironmentInput} — build one with `createShapingEnvironment`.
465
+ * `w:pBdr` after the style cascade: a later `w:pBdr` replaces an earlier one WHOLESALE.
490
466
  *
491
- * Structurally identical to its input, but the nominal distinction is the point: holding one
492
- * means the tags, axes and fonts inside it have already been checked.
467
+ * Word does not merge edges across the cascade. A style that states only `w:bottom` discards
468
+ * the box its `basedOn` ancestor declared, so folding edge by edge would leave a lone
469
+ * underline surrounded by a box no one authored. Absence inherits; `nil`/`none` clear.
493
470
  */
494
- interface ShapingEnvironment extends ShapingEnvironmentInput {
495
- }
496
- /** One shaping call: the text, its size, its bidi level, and the environment to shape it in. */
497
- interface ShapeInput {
498
- readonly text: string;
499
- readonly fontSizeHalfPoints: number;
500
- /** Exact UAX #9 embedding/isolate level; direction is its parity projection. */
501
- readonly bidiLevel: number;
502
- readonly environment: ShapingEnvironment;
503
- /**
504
- * Logical text around `text` that the shaper reads for contextual forms but never
505
- * shapes: HarfBuzz pre- and post-context. A word split across two formatting runs joins
506
- * across the split only when each half sees the other's letters. At most
507
- * {@link MAX_SHAPING_CONTEXT} UTF-16 units each; glyphs and clusters cover `text` alone.
508
- */
509
- readonly context?: ShapingContext;
510
- }
511
- /** Pre- and post-context for one shaping call. See {@link ShapeInput.context}. @public */
512
- interface ShapingContext {
513
- readonly before: string;
514
- readonly after: string;
515
- }
516
- /** Longest context on either side, in UTF-16 units. Joining needs only the nearest letters. @public */
517
- declare const MAX_SHAPING_CONTEXT = 16;
471
+ declare function cascadedParagraphBorders(paragraphPropertyNodes: readonly OoxmlNode[]): ParagraphBorders;
518
472
  /**
519
- * One positioned glyph.
473
+ * Visual stroke thickness layout publishes for one edge (points).
520
474
  *
521
- * `id` is a glyph index in its FACE, not a character — a ligature is one glyph spanning several
522
- * characters, and a single character may produce several glyphs. Use `cluster` to get back to
523
- * text.
475
+ * Compound `ST_Border` values (`double`, …) use the shared inflated band so a thin
476
+ * `w:sz="3"` double still occupies a visible double-line box — matching table borders.
524
477
  */
525
- interface ShapedGlyph {
526
- readonly id: number;
527
- /** Optional synthetic glyph-size factor; positions and advances already include it. */
528
- readonly drawScale?: number;
529
- /** UTF-16 text offset identifying the cluster that produced this glyph. */
530
- readonly cluster: number;
531
- /** Pen origin before this glyph's shaping offsets, in fixed-point run coordinates. */
532
- readonly originX: FixedPoint;
533
- readonly originY: FixedPoint;
534
- readonly advanceX: FixedPoint;
535
- readonly advanceY: FixedPoint;
536
- readonly offsetX: FixedPoint;
537
- readonly offsetY: FixedPoint;
538
- /** Exact monochrome outline returned by the admitted HarfBuzz face. */
539
- readonly outline: GlyphOutline;
540
- }
541
- /** A glyph's outline as SVG path data, in font design units. */
542
- interface GlyphOutline {
543
- readonly path: string;
544
- /** Design units per em — divide by this to scale the path to a point size. */
545
- readonly unitsPerEm: number;
546
- }
478
+ declare function paragraphBorderStrokeWidthPt(edge: ParagraphBorderEdge): number;
547
479
  /**
548
- * One cluster: the smallest indivisible text-to-glyph correspondence.
480
+ * Extent one border edge occupies away from the text it decorates: gap plus rule, in points.
549
481
  *
550
- * The unit the CARET moves by. A cluster may be several characters (a ligature) or several glyphs
551
- * (a decomposed mark), so neither a character index nor a glyph index is a valid caret position —
552
- * `caretEdges` is, and it includes both endpoints.
482
+ * Vertically that is flow height — a top rule pushes the first line down, a bottom rule holds
483
+ * the page open below the last one — so pagination has to see it. Horizontally it is
484
+ * publish-only: Word draws left/right paragraph rules OUTSIDE the text column and never
485
+ * re-breaks the lines, which is why adding a box to a paragraph in Word does not reflow it.
553
486
  */
554
- interface ShapedCluster {
555
- /** Half-open logical UTF-16 range in ShapedRun.text. */
556
- readonly textStart: number;
557
- readonly textEnd: number;
558
- /** Half-open visual glyph range in ShapedRun.glyphs. */
559
- readonly glyphStart: number;
560
- readonly glyphEnd: number;
561
- readonly advance: FixedPoint;
562
- /** Fixed-point edges from this cluster's visual origin, including both endpoints. */
563
- readonly caretEdges: readonly FixedPoint[];
564
- /** Index into ShapedRun.fontSpans, preserving the exact fallback choice. */
565
- readonly fontSpan: number;
566
- }
567
- /** A run's vertical metrics, from the face that shaped it. Drives line height and baseline. */
568
- interface ShapedVerticalMetrics {
569
- readonly ascent: FixedPoint;
570
- readonly descent: FixedPoint;
571
- readonly lineGap: FixedPoint;
572
- }
487
+ declare function paragraphBorderExtentPt(edge: ParagraphBorderEdge | undefined): number;
488
+ /** Vertical extent a bottom border adds below the last line (gap + rule). */
489
+ declare function bottomBorderExtentPt(edge: ParagraphBorderEdge | undefined): number;
573
490
  /**
574
- * A stretch of glyphs that came from ONE face.
491
+ * Identity of a paragraph's border set, for the `w:between` group rule.
575
492
  *
576
- * A run whose text needed fallback has several of these. Recording the exact face per span is
577
- * what lets a re-shape reproduce the same result rather than re-running fallback selection and
578
- * possibly choosing differently.
493
+ * Word treats consecutive paragraphs whose border settings are IDENTICAL as ONE bordered
494
+ * block: the top rule draws above the first, the bottom rule below the last, and each
495
+ * interior boundary gets `w:between` or nothing (§17.3.1.24). That is why applying a box to
496
+ * three selected paragraphs in Word draws one box and not three.
497
+ *
498
+ * Empty string means "no borders", which never groups with anything.
579
499
  */
580
- interface ShapedFontSpan {
581
- readonly glyphStart: number;
582
- readonly glyphEnd: number;
583
- readonly font: ResolvedFont;
584
- /** null denotes the primary font; otherwise this is the environment fallback-order index. */
585
- readonly fallbackIndex: number | null;
586
- }
500
+ declare function paragraphBordersFingerprint(borders: ParagraphBorders): string;
587
501
  /**
588
- * One shaped run: text turned into positioned glyphs, with everything needed to measure it, paint
589
- * it, and put a caret in it.
590
- *
591
- * The engine's measurement unit. Layout never measures characters — it measures these.
502
+ * Gap to insert before a paragraph once the previous paragraph's `after` is already in the
503
+ * flow cursor — Word takes the larger of the two rather than summing them.
592
504
  */
593
- interface ShapedRun {
594
- readonly text: string;
595
- readonly direction: TextDirection;
596
- readonly bidiLevel: number;
597
- readonly glyphs: readonly ShapedGlyph[];
598
- readonly clusters: readonly ShapedCluster[];
599
- readonly fontSpans: readonly ShapedFontSpan[];
600
- readonly metrics: ShapedVerticalMetrics;
601
- /**
602
- * The same horizontal advances as `glyphs[i].advanceX`, in the same fixed-point units, but
603
- * BEFORE the per-glyph rounding to the fixed-point grid. Parallel to `glyphs`.
604
- *
605
- * Measurement and line breaking read `advanceX` and must keep doing so: pagination is decided
606
- * on those numbers, and two runs shaped identically have to compare equal. A painter that
607
- * gives every glyph its own absolute position sums the advances instead, and a sum of ROUNDED
608
- * advances drifts by up to half a fixed-point unit per glyph — an error that only grows along
609
- * a line. Summing these and rounding once, at the position actually written, does not.
610
- *
611
- * Absent on runs an assembly path cannot state exactly; a reader falls back to `originX`.
612
- */
613
- readonly exactAdvancesX?: readonly number[];
614
- }
505
+ declare function collapsedSpaceBefore(before: number, previousAfter: number): number;
615
506
  /**
616
- * A {@link ShapedRun} reduced to the fields two shaping results must agree on to be considered
617
- * identical.
507
+ * Applied before-spacing for placement (Word 2013+ / compat mode 15).
618
508
  *
619
- * Fonts appear as {@link FontFingerprintInputs} rather than whole `ResolvedFont` objects, so the
620
- * comparison is over VALUES and does not depend on object identity — which is what makes it work
621
- * across a reload or a worker boundary.
509
+ * Adjacent before/after still collapse to the larger gap, but before is dropped entirely when
510
+ * the paragraph naturally moves to the top of a page mid-section. An explicit page-break-before
511
+ * paragraph, or the first paragraph of a document/section, retains before. Callers publish this
512
+ * applied value on the fragment so shading, borders,
513
+ * selection, and paint share one geometry.
622
514
  */
623
- interface ShapedRunComparatorInputs {
624
- readonly text: string;
625
- readonly direction: TextDirection;
626
- readonly script: string;
627
- readonly language: string;
628
- readonly bidiLevel: number;
629
- readonly glyphs: readonly ShapedGlyph[];
630
- readonly clusters: readonly ShapedCluster[];
631
- readonly fontSpans: readonly {
632
- readonly glyphStart: number;
633
- readonly glyphEnd: number;
634
- readonly fallbackIndex: number | null;
635
- readonly font: FontFingerprintInputs;
636
- }[];
637
- readonly metrics: ShapedVerticalMetrics;
638
- }
515
+ declare function appliedSpaceBefore(before: number, previousAfter: number, atTopOfPage: boolean, preserveAtPageStart: boolean): number;
516
+
639
517
  /**
640
- * The one thing layout needs from a shaping backend.
518
+ * The four physical sides of `CT_PageBorders`.
641
519
  *
642
- * Injected rather than imported, which is what keeps layout DOM-free and testable: a fixed-metric
643
- * shaper measures deterministically in a test, HarfBuzz measures for real in a browser, and
644
- * layout cannot tell the difference.
520
+ * Four, not the six of `w:pBdr`: `w:between` and `w:bar` are paragraph-group rules and the
521
+ * page has no analogue for either.
645
522
  */
646
- interface TextShaper {
647
- shape(input: ShapeInput): ShapedRun;
648
- }
523
+ declare const PAGE_BORDER_SIDES: readonly ['top', 'left', 'bottom', 'right'];
524
+ /** Which of the four page-frame edges. */
525
+ type PageBorderSide = (typeof PAGE_BORDER_SIDES)[number];
649
526
  /**
650
- * A font reduced to the values that identify it for fingerprinting.
527
+ * `w:offsetFrom` (`ST_PgBorderOffset`) — what `w:space` on each edge is measured from.
651
528
  *
652
- * Includes the content `hash` and `faceIndex`, so two faces with the same family name but
653
- * different bytes fingerprint differently — which is what stops a cached measurement being reused
654
- * against a substituted face.
655
- */
656
- interface FontFingerprintInputs {
657
- readonly identity: string;
658
- readonly id: string;
659
- readonly family: string;
660
- readonly request: FontRequest;
661
- readonly hash: string;
662
- readonly faceIndex: number;
663
- readonly byteLength: number;
664
- readonly substitution: FontSubstitution | null;
665
- }
529
+ * The schema default is `text`, NOT `page`. Word's own UI opens on "Edge of page", which is
530
+ * why the wrong default is easy to assume and expensive to hold: at `text` the rule sits
531
+ * `w:space` points outside the TEXT, so its distance from the sheet edge is the margin minus
532
+ * the space — a number that moves whenever the margins do.
533
+ */
534
+ type PageBorderOffsetFrom = 'page' | 'text';
535
+ /** `w:display` (`ST_PgBorderDisplay`) — which pages of the section carry the frame. */
536
+ type PageBorderDisplay = 'allPages' | 'firstPage' | 'notFirstPage';
537
+ /** `w:zOrder` (`ST_PgBorderZOrder`) — whether the frame paints over the text or under it. */
538
+ type PageBorderZOrder = 'front' | 'back';
666
539
  /**
667
- * A {@link ShapingEnvironment} reduced to comparable values.
540
+ * One section's resolved `w:pgBorders`.
668
541
  *
669
- * Records and axis maps become SORTED entry arrays, because two environments differing only in
670
- * key insertion order must fingerprint the same — otherwise a cache would miss on a difference
671
- * that changes no glyph.
542
+ * Resolved, not raw: every attribute the file omitted is filled in with the SCHEMA default,
543
+ * so a consumer never has to know which of the three were authored. An edge is present only
544
+ * when it paints — `nil` / `none` and art borders are already gone (see {@link parsePageBorders}).
672
545
  */
673
- interface ShapingEnvironmentFingerprintInputs {
674
- readonly font: FontFingerprintInputs;
675
- readonly variationAxes: readonly (readonly [string, number])[];
676
- readonly shapingLibrary: VersionedShapingLibrary;
677
- readonly unicodeDataVersion: string;
678
- readonly normalization: NormalizationPolicy;
679
- readonly script: string;
680
- readonly language: string;
681
- readonly direction: TextDirection;
682
- readonly features: readonly (readonly [string, number])[];
683
- readonly fallbackOrder: readonly FontFingerprintInputs[];
684
- readonly fixedPointScale: number;
685
- readonly roundingMode: FixedPointRoundingMode;
546
+ interface SectionPageBorders {
547
+ /** `w:offsetFrom`; absent attribute reads as `text` (§17.6.10). */
548
+ readonly offsetFrom: PageBorderOffsetFrom;
549
+ /** `w:display`; absent attribute reads as `allPages`. */
550
+ readonly display: PageBorderDisplay;
551
+ /** `w:zOrder`; absent attribute reads as `front`. */
552
+ readonly zOrder: PageBorderZOrder;
553
+ readonly top?: ParagraphBorderEdge;
554
+ readonly left?: ParagraphBorderEdge;
555
+ readonly bottom?: ParagraphBorderEdge;
556
+ readonly right?: ParagraphBorderEdge;
686
557
  }
687
558
  /**
688
- * Validate and freeze a shaping environment.
689
- *
690
- * Checks every field that could silently corrupt a measurement: OpenType tags must be four ASCII
691
- * bytes, script and language must be non-blank and control-character free, fonts must already be
692
- * validated. Throws rather than coercing — a bad tag that shapes anyway produces a document that
693
- * measures wrong everywhere and looks fine.
559
+ * Parse `w:pgBorders` off one `w:sectPr`.
694
560
  *
695
- * @throws TypeError on a malformed tag, name, or axis value.
561
+ * Undefined when the element is absent AND when it paints nothing — every edge off, or every
562
+ * edge an art border. Unlike `w:pgNumType`, an empty-but-present element is not distinguished:
563
+ * these properties are a LAYOUT input, serialization re-emits the canonical tree, and a frame
564
+ * with no edges has exactly one meaning downstream.
696
565
  */
697
- declare const createShapingEnvironment: (input: ShapingEnvironmentInput) => ShapingEnvironment;
566
+ declare function parsePageBorders(sectPr: OoxmlNode): SectionPageBorders | undefined;
698
567
  /**
699
- * Reduce an environment to its comparable form, with records sorted so key order cannot affect
700
- * the result.
568
+ * Identity of one section's page frame, for incremental-layout context keys.
569
+ *
570
+ * A `w:pgBorders` edit changes nothing the flow measures — the frame is drawn beside the text,
571
+ * never through it — so no per-paragraph key moves and every reuse path would hand back the
572
+ * previous sheets with the previous frame. This token goes in the pass context so it does not.
701
573
  */
702
- declare const shapingEnvironmentFingerprintInputs: (input: ShapingEnvironmentInput) => ShapingEnvironmentFingerprintInputs;
703
- /** Canonical serialization of every shaping variable, including byte hash and provenance. */
704
- declare const shapingEnvironmentFingerprint: (environment: ShapingEnvironmentInput) => string;
574
+ declare function pageBordersFingerprint(borders: SectionPageBorders | undefined): string;
575
+
705
576
  /**
706
- * Validate and freeze a shaped run against the environment that produced it.
577
+ * Hard ceiling on sections enumerated from a document (matches write-path
578
+ * `MAX_SECTIONS` in note/hf lifecycle). Hostile packages with unbounded `w:sectPr`
579
+ * marks fail closed here rather than amplifying layout props arrays.
580
+ */
581
+ declare const MAX_DOCUMENT_SECTIONS = 4096;
582
+ /** A section's margins in twips, including the header and footer reserve bands. */
583
+ interface SectionMargins {
584
+ readonly topTwips: number;
585
+ readonly rightTwips: number;
586
+ readonly bottomTwips: number;
587
+ readonly leftTwips: number;
588
+ readonly headerTwips: number;
589
+ readonly footerTwips: number;
590
+ readonly gutterTwips: number;
591
+ }
592
+ /** One explicit `w:col`: its width and the gap after it. */
593
+ interface SectionColumnDefinition {
594
+ readonly widthTwips: number;
595
+ /** Space after this column; zero on the final column. */
596
+ readonly gapTwips: number;
597
+ }
598
+ /**
599
+ * A section's column layout.
600
+ *
601
+ * Equal-width and explicit-width columns are one type because a file may declare `w:num` with no
602
+ * `w:col` children at all, and layout must handle both without branching at every use site.
603
+ */
604
+ interface SectionColumns {
605
+ readonly count: number;
606
+ /** Shared gap for equal-width columns and fallback gap for incomplete explicit definitions. */
607
+ readonly gapTwips: number;
608
+ readonly equalWidth?: boolean;
609
+ readonly separator?: boolean;
610
+ /** Authored `w:col` geometry when `equalWidth` is false. */
611
+ readonly definitions?: readonly SectionColumnDefinition[];
612
+ }
613
+ /**
614
+ * How this section is placed relative to the previous one (ECMA-376 17.6.22,
615
+ * `ST_SectionMark`).
616
+ *
617
+ * All five schema values are read. `nextColumn` paginates like `nextPage` for now: in a
618
+ * single-column section that IS Word's behaviour, and multi-column flow is not modelled,
619
+ * so collapsing it at the parse boundary would only hide the authored value from a
620
+ * consumer that asks.
621
+ */
622
+ type SectionBreakType = 'nextPage' | 'continuous' | 'evenPage' | 'oddPage' | 'nextColumn';
623
+ /**
624
+ * Authored `w:pgNumType` (ECMA-376 CT_PageNumber).
625
+ *
626
+ * Distinguishes three states via {@link SectionProperties.pageNumbering}:
627
+ * - element absent → `undefined` (Word defaults; no empty element to re-emit)
628
+ * - empty `<w:pgNumType/>` → `{}` (present, no authored attrs; must round-trip empty)
629
+ * - attributes set → only those keys appear (never invent schema defaults like `fmt=decimal`)
630
+ *
631
+ * `chapStyle` / `chapSep` are preserved for consumers; PAGE projection does not yet compose
632
+ * chapter numbers (heading outline resolution is out of this slice).
633
+ */
634
+ interface SectionPageNumbering {
635
+ /** Authored `@w:start` when present and in range; otherwise omitted. */
636
+ readonly start?: number;
637
+ /** Authored `@w:fmt` (ST_NumberFormat) when present; otherwise omitted. */
638
+ readonly fmt?: string;
639
+ /** Authored `@w:chapStyle` outline level when present and in range. */
640
+ readonly chapStyle?: number;
641
+ /** Authored `@w:chapSep` when present (hyphen / period / colon / emDash / enDash). */
642
+ readonly chapSep?: string;
643
+ }
644
+ /**
645
+ * One section's resolved `w:sectPr`, as layout needs it.
646
+ *
647
+ * Resolved, not raw: defaults the file omitted are filled in here (an absent `w:type` is
648
+ * `nextPage`), so layout never has to know which attributes were authored and which were
649
+ * inherited.
650
+ */
651
+ interface SectionProperties {
652
+ /** Active document grid line pitch: line-unit paragraph margins and line snapping. */
653
+ readonly gridLinePitchTwips?: number;
654
+ readonly pageSize: {
655
+ readonly widthTwips: number;
656
+ readonly heightTwips: number;
657
+ };
658
+ readonly margins: SectionMargins;
659
+ readonly columns: SectionColumns;
660
+ readonly landscape: boolean;
661
+ readonly titlePage: boolean;
662
+ /** Absent `w:type` defaults to `nextPage`. */
663
+ readonly breakType: SectionBreakType;
664
+ /**
665
+ * Authored page-number type. Absent when `w:pgNumType` is missing; empty object when the
666
+ * element is present with no attributes (comprehensive-fixture shape).
667
+ */
668
+ readonly pageNumbering?: SectionPageNumbering;
669
+ /**
670
+ * Resolved `w:pgBorders` (§17.6.10). Absent when the section declares none, or when every
671
+ * edge it declares is off or an art border — see {@link parsePageBorders}.
672
+ */
673
+ readonly pageBorders?: SectionPageBorders;
674
+ }
675
+ /**
676
+ * One section of the body story: contiguous top-level blocks plus the properties that end it.
707
677
  *
708
- * Checks the internal consistency a shaper must satisfy: direction matches the environment,
709
- * cluster ranges tile the text without gaps or overlap, glyph ranges stay in bounds, font spans
710
- * cover every glyph, and caret edges are monotonic. A shaper that violates any of these produces
711
- * a caret that lands in the wrong place, which is far harder to diagnose downstream than here.
678
+ * `blockStart` / `blockEndExclusive` index into `storyBlocks(part)`.
679
+ */
680
+ interface DocumentSection {
681
+ readonly index: number;
682
+ readonly properties: SectionProperties;
683
+ readonly blockStart: number;
684
+ readonly blockEndExclusive: number;
685
+ }
686
+ /** US Letter, portrait, one-inch margins: Word's own default when a section says nothing. */
687
+ declare const DEFAULT_SECTION_PROPERTIES: SectionProperties;
688
+ /**
689
+ * Parse authored `w:pgNumType` without inventing schema defaults.
712
690
  *
713
- * @throws TypeError when the run and environment disagree, or the run is internally inconsistent.
691
+ * Returns `undefined` when the element is absent. An empty element yields `{}` so callers
692
+ * can tell "present but unauthored" from "missing" and serialization can re-emit empty.
693
+ * Hostile / out-of-range attribute values are dropped rather than clamped into meaning.
714
694
  */
715
- declare const createShapedRun: (input: ShapedRun, environmentInput: ShapingEnvironmentInput) => ShapedRun;
695
+ declare function parsePageNumbering(sectPr: OoxmlNode): SectionPageNumbering | undefined;
696
+ /** Parse one `w:sectPr` into geometry/break properties (null reads as Word's defaults). */
697
+ declare function parseSectionProperties(sectPr: OoxmlNode | null | undefined): SectionProperties;
698
+ /** `w:sectPr` nested under a paragraph's `w:pPr`, if present. */
699
+ declare function paragraphSectionNode(paragraph: OoxmlElement): OoxmlElement | undefined;
716
700
  /**
717
- * Reduce a shaped run to its comparable form, validating it on the way through.
701
+ * The section properties a part declares, or Word's defaults where it says nothing.
718
702
  *
719
- * What the D9 determinism oracles compare: two shaping runs of the same text in the same
720
- * environment must produce byte-identical structures here.
703
+ * Returns the FINAL section (body-level `w:sectPr`, else the last paragraph-level one).
704
+ * Multi-section geometry belongs to `enumerateDocumentSections`; chrome that needs "the
705
+ * document's page" still reads the last section, which is what Word's body-level sectPr is.
721
706
  */
722
- declare const shapedRunComparatorInputs: (input: ShapedRun, environment: ShapingEnvironmentInput) => ShapedRunComparatorInputs;
723
-
707
+ declare function readSectionProperties(part: OoxmlPart): SectionProperties;
724
708
  /**
725
- * Whether a paragraph must start a new page (`w:pageBreakBefore`).
709
+ * Split the body story into sections.
726
710
  *
727
- * LAST WINS, like every other toggle read off the cascade (`paragraphKeeps` does the same
728
- * for `w:keepNext`). An any-wins `.some()` cannot be switched off: a Chapter or Heading style
729
- * carries `w:pageBreakBefore`, and Word writes `w:val="0"` on the one instance whose author
730
- * unchecked the box — that paragraph still broke, so the document grew a blank page and every
731
- * page number after it was wrong.
711
+ * A paragraph carrying `w:pPr/w:sectPr` ends the current section (that paragraph is IN the
712
+ * section being ended). The body-level `w:sectPr` ends the final section. A document with
713
+ * neither yields one section of Word defaults covering every block.
732
714
  *
733
- * An absent `w:val` is on (§17.17.4), and the off vocabulary is the whole of it — `off` is a
734
- * spelling too, which {@link isOn} beside this already accepts.
715
+ * Enumeration is capped at {@link MAX_DOCUMENT_SECTIONS}. Further paragraph-level section
716
+ * breaks are ignored and remaining blocks fold into the last accepted section (fail closed).
717
+ *
718
+ * `displayMode` MUST match the one the caller passes to `storyBlocks`. `blockStart` /
719
+ * `blockEndExclusive` are indices into that list, and the list changes shape with the mode:
720
+ * the proposed view drops a paragraph whose mark and content a revision both removed. Slicing
721
+ * a filtered list with indices counted over an unfiltered one puts body text under another
722
+ * section's page geometry — the wrong paper size, the wrong margins, the wrong header.
735
723
  */
736
- declare function paragraphBreaksBefore(props: readonly OoxmlProperty[]): boolean;
724
+ declare function enumerateDocumentSections(part: OoxmlPart, displayMode?: RevisionDisplayMode, authorFilter?: RevisionAuthorFilter): DocumentSection[];
737
725
  /**
738
- * Soft ceiling matching the spike's resolved-style limit (31_680 twips ≈ 22"). Beyond
739
- * that an attacker-authored spacing would push pagination into pathological page counts.
726
+ * Canonical section index behind each section in a revision-projected body.
727
+ *
728
+ * A removed paragraph mark can collapse an entire section. The projected section keeps the
729
+ * surviving paragraph's geometry, while package APIs such as header/footer resolution remain
730
+ * indexed over the canonical tree. This bridge prevents those two index spaces from being
731
+ * paired positionally.
740
732
  */
741
- declare const MAX_PARAGRAPH_SPACING_PT: number;
742
- /** Soft ceiling on border width (96 eighths = 12pt). Word's UI tops out well below this. */
743
- declare const MAX_BORDER_WIDTH_PT = 12;
744
- /** Soft ceiling on border-to-text gap (`w:space`, already in points). */
745
- declare const MAX_BORDER_SPACE_PT = 3168;
733
+ declare function projectedSectionSourceIndexes(part: OoxmlPart, displayMode?: RevisionDisplayMode, authorFilter?: RevisionAuthorFilter): readonly number[];
746
734
  /**
747
- * A paragraph's resolved space before and after, in points.
735
+ * Every section in a document, with a flag saying whether the list was cut short.
748
736
  *
749
- * Already collapsed against `w:contextualSpacing`, so adjacent same-style paragraphs that suppress
750
- * their gap arrive here with it removed rather than leaving that to whoever stacks them.
737
+ * `truncated` is reported rather than silent: section count comes from a file, so a crafted
738
+ * document declaring thousands of paragraph-level `w:sectPr` marks is bounded, and a reader
739
+ * should be able to tell that happened.
751
740
  */
752
- interface ParagraphSpacing {
753
- /** `w:spacing/@before`, in points. */
754
- readonly before: number;
755
- /** `w:spacing/@after`, in points. */
756
- readonly after: number;
741
+ interface DocumentSectionsEnumeration {
742
+ readonly sections: DocumentSection[];
743
+ /** True when paragraph-level sectPr marks beyond {@link MAX_DOCUMENT_SECTIONS} were dropped. */
744
+ readonly truncated: boolean;
757
745
  }
758
746
  /**
759
- * The gap Word substitutes when `w:beforeAutospacing` / `w:afterAutospacing` is on
760
- * (ECMA-376 §17.3.1.33, the `w:spacing` clause both attributes belong to).
761
- *
762
- * The attribute means "the consumer decides", and the authored `@before` / `@after` beside it
763
- * is IGNORED rather than used as the value. Word's answer is HTML's default `<p>` margin,
764
- * 14pt, which is what a document round-tripped through Word's HTML filter carries — and this
765
- * one is everywhere, because Word writes `w:before="100" w:beforeAutospacing="1"` for it. Reading
766
- * only the literal 100 twips lays every such paragraph out 9pt tight, which moves page breaks.
747
+ * Like {@link enumerateDocumentSections}, but reports whether the section bound clipped
748
+ * hostile input. Prefer the plain enumerator for normal layout; use this when a caller
749
+ * needs a named fail-closed diagnostic.
767
750
  */
768
- declare const AUTO_PARAGRAPH_SPACING_PT = 14;
751
+ declare function enumerateDocumentSectionsBounded(part: OoxmlPart, displayMode?: RevisionDisplayMode, authorFilter?: RevisionAuthorFilter): DocumentSectionsEnumeration;
769
752
  /**
770
- * Where a paragraph sits, for the contexts in which Word's auto spacing resolves to 0
771
- * instead of {@link AUTO_PARAGRAPH_SPACING_PT}.
753
+ * Section properties as the geometry layout paginates against.
772
754
  *
773
- * This resolves the interior list-item value. Body layout restores each outer list margin
774
- * in `resolveListAutoSpacing`, where neighboring blocks are available. A caller that says
775
- * nothing gets the body answer.
755
+ * The gutter is added to the LEFT margin: it is binding allowance, extra space on the inner
756
+ * edge, and folding it into the content width instead would silently narrow every line.
776
757
  *
777
- * A TABLE CELL is not such a context. Measured on a captured control at 10pt, where the
778
- * line pitch is 11.52pt: the reference puts 26.16pt between two paragraphs in the body with
779
- * auto spacing on, and the SAME 26.16pt between two paragraphs inside a cell. With an empty
780
- * paragraph between them it is 52.32, exactly twice. Reading it as 0 in a cell lays every
781
- * such paragraph 13.92pt tight, which is enough to pull a signature block back onto the
782
- * previous page. See `.cache/pdf/claude-autospacing/`.
758
+ * Top and bottom keep the authored sign of `w:pgMar`: a negative value is an exact inset that
759
+ * header and footer height never moves. Read them through `page-body-margins.ts`.
783
760
  */
784
- interface ParagraphAutoSpacingContext {
785
- /** The paragraph participates in numbering (`w:numPr`), i.e. it is a list item. */
786
- readonly inList?: boolean;
787
- /**
788
- * The paragraph lives in a table cell. Kept because callers describe position with it and
789
- * list resolution reads it; it does NOT zero auto spacing.
790
- */
791
- readonly inTableCell?: boolean;
792
- /** Section grid pitch in points; no grid uses Word's fixed 12pt line unit. */
793
- readonly lineUnitPt?: number;
794
- /**
795
- * `w:doNotUseHTMLParagraphAutoSpacing` is on: automatic spacing outside a list resolves to
796
- * the fixed 5pt before and 10pt after instead of {@link AUTO_PARAGRAPH_SPACING_PT}.
797
- */
798
- readonly fixedAutoSpacing?: boolean;
799
- }
761
+ declare function geometryOfSection(section: SectionProperties): PageGeometry;
762
+
800
763
  /**
801
- * Resolved line spacing (`w:spacing/@line` + `@lineRule`, ECMA-376 17.3.1.33).
802
- *
803
- * `auto` is the interesting one: `@line` is 240ths of a line, so 240 is single, 360 is
804
- * one-and-a-half, 480 is double — and Word's own Normal style since 2013 is 259, i.e.
805
- * 1.08. A document laid out at a flat single spacing is ~8% tight on EVERY line, which
806
- * moves every page break, so this is not a cosmetic detail.
807
- *
808
- * `exact` fixes the line box at `@line` twips and lets tall glyphs clip, the way Word
809
- * does. `atLeast` uses it as a floor.
764
+ * `w:tblpPr/@w:horzAnchor` (17.4.58) and `@w:vertAnchor` (17.4.66): the box a floated
765
+ * table's offsets are measured from. Absent means `text` for both.
810
766
  */
811
- type LineSpacingRule = 'auto' | 'exact' | 'atLeast';
767
+ type TableFloatAnchor = 'text' | 'margin' | 'page';
768
+ /** `w:tblpPr/@w:tblpXSpec` (17.4.63, ST_XAlign). */
769
+ type TableFloatXSpec = 'left' | 'center' | 'right' | 'inside' | 'outside';
770
+ /** `w:tblpPr/@w:tblpYSpec` (17.4.65, ST_YAlign). */
771
+ type TableFloatYSpec = 'inline' | 'top' | 'center' | 'bottom' | 'inside' | 'outside';
812
772
  /**
813
- * Resolved line spacing: the rule, and the value it applies.
773
+ * `w:tblPr/w:tblpPr` (17.4.57) — a table positioned against an anchor box rather than at
774
+ * the point in the text where it was authored.
814
775
  *
815
- * `value` means different things per rule — 240ths of a line under `auto`, points under `exact`
816
- * and `atLeast` — which is why the two travel together and neither is useful alone.
776
+ * A spec (`tblpXSpec`/`tblpYSpec`) supersedes the matching offset when both are present:
777
+ * 17.4.57 states the alignment outright, and the offset only answers "how far from the
778
+ * anchor" for the case where no alignment was stated.
817
779
  */
818
- interface ParagraphLineSpacing {
819
- readonly rule: LineSpacingRule;
820
- /** `auto`: the 240ths-of-a-line multiplier numerator. Otherwise points. */
821
- readonly value: number;
822
- /** Legacy noExtraLineSpacing: retain the natural baseline within an exact-height box. */
823
- readonly preserveExactBaseline?: true;
824
- /**
825
- * Active section line-grid pitch in points (`w:docGrid/@w:linePitch`). Present only when
826
- * the paragraph snaps to the grid; only with `auto`. See `line-grid.ts`.
827
- */
828
- readonly gridPitch?: number;
780
+ interface TableFloatPosition {
781
+ readonly horzAnchor: TableFloatAnchor;
782
+ readonly vertAnchor: TableFloatAnchor;
783
+ readonly xSpec?: TableFloatXSpec;
784
+ /** `w:tblpX` in points; signed, so a table can be pulled into the margin. */
785
+ readonly xPt: number;
786
+ readonly ySpec?: TableFloatYSpec;
787
+ /** `w:tblpY` in points; signed. */
788
+ readonly yPt: number;
789
+ readonly distances?: {
790
+ readonly top: number;
791
+ readonly right: number;
792
+ readonly bottom: number;
793
+ readonly left: number;
794
+ };
829
795
  }
830
- /** Single spacing: what a paragraph that says nothing gets. */
831
- declare const SINGLE_LINE_SPACING: ParagraphLineSpacing;
796
+
832
797
  /**
833
- * One resolved `w:pBdr` edge: its style, colour, thickness and gap.
798
+ * One `w:numPicBullet`: the image a level draws instead of its `w:lvlText`.
834
799
  *
835
- * `widthPt` and `spacePt` are already converted and CLAMPED — both come from a file, and an
836
- * unbounded border width becomes a layout dimension.
800
+ * `relationshipId` is unresolved on purpose — it names a relationship of the numbering part,
801
+ * and resolving it is the image-resource lookup's job, under its own validation and caps.
802
+ * @public
837
803
  */
838
- interface ParagraphBorderEdge {
839
- /** Authored `ST_Border` value (`single`, `dashed`, …). */
840
- readonly val: string;
841
- /** RRGGBB, or null when auto/missing (paint defaults to black). */
842
- readonly color: string | null;
843
- /** Border thickness in points (`w:sz` is eighths of a point). */
844
- readonly widthPt: number;
845
- /** Gap from text to the rule, in points (`w:space`). */
846
- readonly spacePt: number;
847
- /**
848
- * `w:shadow` — Word offsets a drop shadow behind the rule.
849
- *
850
- * Present only when authored true, so an edge that says nothing keeps the shape earlier
851
- * fixtures assert. Resolved and carried; drawing it is deferred.
852
- */
853
- readonly shadow?: true;
804
+ interface NumberingPictureBullet {
805
+ /** `w:numPicBulletId` — what a level's `w:lvlPicBulletId` names. */
806
+ readonly picBulletId: string;
807
+ /** `v:imagedata/@r:id`, relative to the NUMBERING part's relationships. */
808
+ readonly relationshipId: string;
809
+ /** Authored `v:shape` width in points. */
810
+ readonly width: number;
811
+ /** Authored `v:shape` height in points. */
812
+ readonly height: number;
854
813
  }
855
- /** The six `CT_PBdr` children, in schema order (ECMA-376 §17.3.1.24). */
856
- declare const PARAGRAPH_BORDER_SIDES: readonly ['top', 'left', 'bottom', 'right', 'between', 'bar'];
857
814
  /**
858
- * Which of the six `CT_PBdr` edges.
815
+ * A level's picture bullet with the size it is actually DRAWN at.
859
816
  *
860
- * Four are physical box edges; `between` and `bar` are group-relative, drawn only where
861
- * consecutive paragraphs share a border definition.
862
- */
863
- type ParagraphBorderSide = (typeof PARAGRAPH_BORDER_SIDES)[number];
817
+ * Separate from {@link NumberingPictureBullet} because the authored `v:shape` extent is not
818
+ * the painted extent: Word scales it by the marker run's font size. Layout, hit testing and
819
+ * both paint sinks read `width` / `height`; `authored` is kept so the scale stays inspectable.
820
+ * @public
821
+ */
822
+ interface ResolvedPictureBullet {
823
+ /** `v:imagedata/@r:id`, relative to the NUMBERING part's relationships. */
824
+ readonly relationshipId: string;
825
+ /** Painted width in points, after the marker font scale. */
826
+ readonly width: number;
827
+ /** Painted height in points, after the marker font scale. */
828
+ readonly height: number;
829
+ /** The `v:shape` extent exactly as the file authored it. */
830
+ readonly authored: NumberingPictureBullet;
831
+ }
832
+
833
+ /** Soft ceiling on abstractNum / num entries read from one part. */
834
+ declare const MAX_NUMBERING_DEFINITIONS = 512;
835
+ /** Soft ceiling on override entries per `w:num`. */
836
+ declare const MAX_LVL_OVERRIDES = 9;
837
+ /** `w:suff` — what separates a list marker from the text after it. */
838
+ type ListSuffix = 'tab' | 'space' | 'nothing';
839
+ /** `w:lvlJc` — how a list marker aligns within its own indent. */
840
+ type ListMarkerAlign = 'left' | 'center' | 'right';
841
+ /**
842
+ * One level's indent, plus which parts the level actually AUTHORED.
843
+ *
844
+ * The provenance matters: `w:ind` cascades per-attribute, so a level that authored only `left`
845
+ * must not overwrite an inherited `hanging` with a synthesized zero.
846
+ */
847
+ interface NumberingLevelIndent {
848
+ /**
849
+ * The sides as the level spells them, in points. `left` and `start` both name the leading
850
+ * indent, and `right` and `end` the trailing one; `left`/`right` below hold those two.
851
+ */
852
+ readonly authored?: {
853
+ readonly left?: number;
854
+ readonly right?: number;
855
+ readonly start?: number;
856
+ readonly end?: number;
857
+ };
858
+ readonly left: number;
859
+ readonly right: number;
860
+ readonly hanging: number;
861
+ readonly firstLine: number;
862
+ /**
863
+ * Which of these the LEVEL actually authored.
864
+ *
865
+ * A level's `w:pPr/w:ind` sits between the formatting it outranks and the formatting that
866
+ * outranks it (`mergeListIndent`), so the merge has to tell "the level says left = 0" from
867
+ * "the level says nothing about left and the style's value stands". Absent (the default) reads as "says nothing", which is what a
868
+ * hand-built level in a test means.
869
+ */
870
+ readonly stated?: {
871
+ readonly left: boolean;
872
+ readonly right: boolean;
873
+ /** `w:hanging`/`w:firstLine` are one mutually exclusive slot (§17.3.1.10, §17.3.1.12). */
874
+ readonly firstLineOffset: boolean;
875
+ };
876
+ }
877
+ /** One `w:lvl`: how this depth numbers, what its marker looks like, and how it indents. */
878
+ interface NumberingLevel {
879
+ readonly ilvl: number;
880
+ readonly start: number;
881
+ readonly numFmt: string;
882
+ readonly lvlText: string;
883
+ readonly lvlJc: ListMarkerAlign;
884
+ readonly suff: ListSuffix;
885
+ readonly indent: NumberingLevelIndent;
886
+ /**
887
+ * `w:lvlRestart` one-based trigger level, or `0` when the level never restarts.
888
+ * Omitted in XML → restart when the previous level (or any earlier level) is used.
889
+ */
890
+ readonly lvlRestart?: number;
891
+ /**
892
+ * `w:isLgl` (§17.9.9): render EVERY level referenced by this level's `w:lvlText` in
893
+ * decimal, whatever number format those levels declare for themselves.
894
+ */
895
+ readonly isLgl: boolean;
896
+ /**
897
+ * `w:lvlPicBulletId` (§17.9.12): this level's marker is the `w:numPicBullet` of that id,
898
+ * drawn INSTEAD of `w:lvlText`. Absent on every ordinary level.
899
+ */
900
+ readonly picBulletId?: string;
901
+ /** Level `w:rPr` as flat properties (for marker face / vanish). */
902
+ readonly runProperties: readonly OoxmlProperty[];
903
+ /** True when level run props request vanish — marker must not paint. */
904
+ readonly vanish: boolean;
905
+ }
906
+ /**
907
+ * One `w:lvlOverride` on a `w:num`: a restart value, a replacement level, or both.
908
+ *
909
+ * How two lists share an abstract definition while numbering independently.
910
+ */
911
+ interface LevelOverride {
912
+ readonly startOverride?: number;
913
+ /** Full level replacement when `w:lvl` is present under the override. */
914
+ readonly level?: NumberingLevel;
915
+ }
916
+ /**
917
+ * One `w:abstractNum` — the shape of a list, without being a list.
918
+ *
919
+ * Never referenced by a paragraph directly. Paragraphs name a {@link NumDefinition}, which names
920
+ * one of these, so several lists can share a definition and still count separately.
921
+ */
922
+ interface AbstractNumDefinition {
923
+ readonly abstractNumId: string;
924
+ readonly levels: ReadonlyMap<number, NumberingLevel>;
925
+ /**
926
+ * `w:numStyleLink` (§17.9.21): this definition carries no levels of its own — the numbering
927
+ * lives on the named style. Word follows the link; resolve it with
928
+ * {@link resolveNumberingStyleLinks} before reading levels, or every paragraph on this
929
+ * abstract renders with NO marker at all.
930
+ */
931
+ readonly numStyleLink?: string;
932
+ /**
933
+ * `w:styleLink` (§17.9.23): the definition side of the same pair — this abstract IS the
934
+ * numbering of the named style. Kept so a link can be verified rather than assumed.
935
+ */
936
+ readonly styleLink?: string;
937
+ }
938
+ /**
939
+ * One `w:num` — the thing a paragraph's `w:numId` actually names.
940
+ *
941
+ * Points at an {@link AbstractNumDefinition} and may override any of its levels.
942
+ */
943
+ interface NumDefinition {
944
+ readonly numId: string;
945
+ readonly abstractNumId: string;
946
+ readonly overrides: ReadonlyMap<number, LevelOverride>;
947
+ }
948
+ /**
949
+ * The bounded projection of `numbering.xml` that list layout resolves against.
950
+ *
951
+ * Projection ONLY — never a mutation or serialization authority. Hostile values are dropped or
952
+ * clamped, and a missing definition resolves to "no list" rather than a guess.
953
+ */
954
+ interface NumberingIndex {
955
+ readonly abstractNums: ReadonlyMap<string, AbstractNumDefinition>;
956
+ readonly nums: ReadonlyMap<string, NumDefinition>;
957
+ /**
958
+ * `w:numPicBullet` declarations keyed by `w:numPicBulletId` (§17.9.20).
959
+ *
960
+ * Optional so a hand-built index stays valid; absent reads as "this part declares none",
961
+ * which is what a level's `w:lvlPicBulletId` then falls back from.
962
+ */
963
+ readonly pictureBullets?: ReadonlyMap<string, NumberingPictureBullet>;
964
+ }
965
+ /**
966
+ * Build a numbering index from the root of a numbering part (`w:numbering`).
967
+ *
968
+ * Empty / missing roots yield an empty index. Duplicate ids keep the first definition.
969
+ */
970
+ /**
971
+ * Project `numbering.xml` into the bounded index.
972
+ *
973
+ * Every ceiling here exists because the input is file-derived: definition counts, override
974
+ * counts, indent magnitudes and style-link hop depth are all capped, and nothing from the file
975
+ * becomes a loop bound or an allocation size.
976
+ */
977
+ declare function buildNumberingIndex(root: OoxmlElement | null | undefined): NumberingIndex;
978
+ /** Resolve the effective level for a `numId` + `ilvl`, applying overrides. */
979
+ /**
980
+ * The effective level for one paragraph's numbering reference, after overrides and style links.
981
+ *
982
+ * Answers null for a reference the index cannot resolve — a paragraph naming a definition the
983
+ * file never declared is an unnumbered paragraph, not an error.
984
+ */
985
+ declare function resolveNumberingLevel(index: NumberingIndex, numId: string, ilvl: number): {
986
+ readonly abstractNumId: string;
987
+ readonly level: NumberingLevel;
988
+ readonly startOverride?: number;
989
+ } | null;
990
+ /** Empty index for tests / documents without numbering. */
991
+ declare const EMPTY_NUMBERING_INDEX: NumberingIndex;
992
+
993
+ /**
994
+ * A face, as something asks for it: family plus the two axes this engine admits.
995
+ *
996
+ * Only static weights and slants. Variable-font axes are deliberately outside this vocabulary —
997
+ * the shaper refuses variation axes, and a variable file admitted here would render bold at
998
+ * regular weight.
999
+ */
1000
+ interface FontRequest {
1001
+ readonly family: string;
1002
+ readonly weight: number;
1003
+ readonly style: 'normal' | 'italic';
1004
+ }
1005
+ /**
1006
+ * A request that was answered by a DIFFERENT face than the one asked for.
1007
+ *
1008
+ * Recorded rather than silently applied, because a substitution changes measurement: it is part
1009
+ * of the shaping fingerprint, so a cached run shaped against a substitute is never reused for the
1010
+ * real face.
1011
+ */
1012
+ interface FontSubstitution {
1013
+ readonly requested: FontRequest;
1014
+ readonly resolved: FontRequest;
1015
+ readonly lineMetrics?: {
1016
+ readonly heightEm: number;
1017
+ readonly baselineEm: number;
1018
+ };
1019
+ }
1020
+ declare const RESOLVED_FONT_BRAND: unique symbol;
1021
+ /**
1022
+ * A face that passed admission: the bytes are present, within limits, hash-verified, and parse as
1023
+ * a font.
1024
+ *
1025
+ * Branded, so a `ResolvedFont` cannot be constructed by an object literal. Holding one is proof
1026
+ * the checks ran — which is what lets shaping skip re-validating on every call.
1027
+ */
1028
+ interface ResolvedFont {
1029
+ readonly [RESOLVED_FONT_BRAND]: true;
1030
+ readonly id: string;
1031
+ readonly identity: string;
1032
+ readonly request: FontRequest;
1033
+ readonly family: string;
1034
+ /** Owned byte length, available without creating a defensive byte copy. */
1035
+ readonly byteLength: number;
1036
+ readonly bytes: Uint8Array;
1037
+ readonly hash: string;
1038
+ readonly faceIndex: number;
1039
+ readonly substitution: FontSubstitution | null;
1040
+ }
1041
+ /**
1042
+ * Largest single face this engine will ever admit, whatever a caller configures.
1043
+ *
1044
+ * A CEILING, not a default: a host may set a smaller `maxFontBytes`, but nothing can raise it
1045
+ * past this. Font bytes come from files, and an unbounded face is a memory-exhaustion vector.
1046
+ */
1047
+ declare const HARD_MAX_FONT_BYTES: number;
1048
+ /** Most faces one snapshot may hold, whatever a caller configures. */
1049
+ declare const HARD_MAX_FONT_SOURCES = 256;
1050
+ /** Most bytes all admitted faces may total, whatever a caller configures. */
1051
+ declare const HARD_MAX_AGGREGATE_FONT_BYTES: number;
1052
+ /**
1053
+ * Why a face was not admitted.
1054
+ *
1055
+ * `forbidden` and `hashMismatch` are adversarial signals rather than ordinary failures: the first
1056
+ * is a face the host declared off-limits, the second is bytes that are not what their source
1057
+ * claimed.
1058
+ */
1059
+ type FontResolutionErrorCode = 'missing' | 'forbidden' | 'overLimit' | 'malformed' | 'hashMismatch';
1060
+ /**
1061
+ * A face that could not be admitted, carrying whatever evidence the refusal produced.
1062
+ *
1063
+ * RETURNED rather than thrown by `FontResourceSnapshot.resolve`, because a missing face is an
1064
+ * ordinary condition — layout falls back and carries on. It extends `Error` so a caller that
1065
+ * would rather throw can.
1066
+ */
1067
+ declare class FontResolutionError extends Error {
1068
+ readonly name = "FontResolutionError";
1069
+ readonly request: FontRequest;
1070
+ readonly code: FontResolutionErrorCode;
1071
+ readonly limit?: number;
1072
+ readonly actual?: number;
1073
+ readonly diagnostic?: string;
1074
+ readonly expectedHash?: string;
1075
+ readonly actualHash?: string;
1076
+ constructor(code: FontResolutionErrorCode, request: FontRequest, details?: {
1077
+ readonly limit?: number;
1078
+ readonly actual?: number;
1079
+ readonly diagnostic?: string;
1080
+ readonly expectedHash?: string;
1081
+ readonly actualHash?: string;
1082
+ });
1083
+ }
1084
+ /**
1085
+ * One face offered to a snapshot, before admission.
1086
+ *
1087
+ * `hash` is checked against the bytes, so a source cannot claim a face it did not supply. Setting
1088
+ * `availability` to `forbidden` declares a face that exists but must not be used, which resolves
1089
+ * as a typed refusal rather than as "missing".
1090
+ */
1091
+ interface FontResourceDefinition {
1092
+ readonly request: FontRequest;
1093
+ readonly id: string;
1094
+ readonly bytes: Uint8Array;
1095
+ readonly hash: string;
1096
+ readonly faceIndex: number;
1097
+ readonly availability?: 'available' | 'forbidden';
1098
+ }
1099
+ /**
1100
+ * A host-declared redirect: requests for `from` resolve to `to`.
1101
+ *
1102
+ * How a metric-compatible substitute is wired in — a document naming Calibri resolves to Carlito
1103
+ * without the document being rewritten.
1104
+ */
1105
+ interface DeclaredFontSubstitution {
1106
+ readonly from: FontRequest;
1107
+ readonly to: FontRequest;
1108
+ readonly lineMetrics?: {
1109
+ readonly heightEm: number;
1110
+ readonly baselineEm: number;
1111
+ };
1112
+ }
1113
+ /** Whether bytes parse as a usable font, with a diagnostic when they do not. */
1114
+ type FontValidationResult = {
1115
+ readonly valid: true;
1116
+ } | {
1117
+ readonly valid: false;
1118
+ readonly diagnostic: string;
1119
+ };
1120
+ /**
1121
+ * The injected check that bytes really are a font.
1122
+ *
1123
+ * Injected because it is the shaper that knows — HarfBuzz can open a face and read its tables,
1124
+ * and this layer must not duplicate that judgement. Every byte reaching it is file input.
1125
+ */
1126
+ type FontByteValidator = (bytes: Uint8Array, faceIndex: number) => FontValidationResult;
1127
+ /**
1128
+ * An immutable set of admitted faces, and the only way to reach one.
1129
+ *
1130
+ * `epoch` identifies the snapshot: fonts change by REPLACING it, never by mutation, so a layout
1131
+ * pass holds one snapshot for its whole run and cannot observe a face appearing or vanishing
1132
+ * midway.
1133
+ */
1134
+ interface FontResourceSnapshot {
1135
+ readonly epoch: number;
1136
+ /** The admitted face, or a typed refusal. Never throws. */
1137
+ resolve(request: FontRequest): ResolvedFont | FontResolutionError;
1138
+ }
864
1139
  /**
865
- * A paragraph's resolved `w:pBdr` (ECMA-376 §17.3.1.24).
1140
+ * How a snapshot is built: which faces, under what limits, validated how.
866
1141
  *
867
- * `top`/`left`/`bottom`/`right` are the four physical edges of the box. The other two are
868
- * group-relative: `between` draws at a boundary INSIDE a run of consecutive paragraphs whose
869
- * border settings are identical, and `bar` is the vertical change-bar rule beside the
870
- * paragraph, drawn whether or not the paragraph groups with its neighbours.
1142
+ * `maxFontBytes` is clamped to {@link HARD_MAX_FONT_BYTES} — a caller may tighten the budget but
1143
+ * never widen it past the engine's own ceiling.
871
1144
  */
872
- interface ParagraphBorders {
873
- readonly top?: ParagraphBorderEdge;
874
- readonly left?: ParagraphBorderEdge;
875
- readonly bottom?: ParagraphBorderEdge;
876
- readonly right?: ParagraphBorderEdge;
877
- readonly between?: ParagraphBorderEdge;
878
- readonly bar?: ParagraphBorderEdge;
1145
+ interface FontResourceSnapshotOptions {
1146
+ readonly epoch: number;
1147
+ readonly maxFontBytes: number;
1148
+ readonly resources: readonly FontResourceDefinition[];
1149
+ readonly substitutions?: readonly DeclaredFontSubstitution[];
1150
+ readonly validateFont: FontByteValidator;
1151
+ readonly instrumentation?: FontResourceInstrumentation;
879
1152
  }
880
1153
  /**
881
- * Resolve `w:spacing` before/after from flat paragraph properties.
1154
+ * Optional counters for the operations font admission is expected to do RARELY.
882
1155
  *
883
- * Line spacing (`w:line` / `w:lineRule`) is a separate concern — it changes measured line
884
- * height, not the gap between paragraphs — and is not resolved here.
1156
+ * Exists so tests can assert the absence of work: hashing and byte-copying a 64 MB face on every
1157
+ * resolve would not be visible in output, only in a stalled document, so the checks assert these
1158
+ * fire once rather than per call.
1159
+ */
1160
+ interface FontResourceInstrumentation {
1161
+ readonly onOwnedByteCopy?: () => void;
1162
+ readonly onHash?: () => void;
1163
+ readonly onTableScan?: () => void;
1164
+ readonly onAdmission?: () => void;
1165
+ }
1166
+ /**
1167
+ * The canonical string identifying one face request.
885
1168
  *
886
- * `w:beforeAutospacing` / `w:afterAutospacing` REPLACE the authored measurement on their own
887
- * side rather than adding to it; see {@link AUTO_PARAGRAPH_SPACING_PT}.
1169
+ * Case- and whitespace-normalized, so `"Times New Roman"` and `"times new roman"` are one key —
1170
+ * a document may name a family either way and both must reach the same face.
888
1171
  */
889
- declare function paragraphSpacing(props: readonly OoxmlProperty[], context?: ParagraphAutoSpacingContext): ParagraphSpacing;
1172
+ declare const fontRequestKey: (request: FontRequest) => string;
1173
+ /** Bounded minimum sfnt/TTC check; Task 4 supplies full parser-backed validation. */
1174
+ declare const boundedStructuralFontValidator: FontByteValidator;
890
1175
  /**
891
- * Resolve `w:line` / `w:lineRule` from flat paragraph properties.
1176
+ * Admit a set of faces and return the immutable snapshot layout resolves against.
892
1177
  *
893
- * Merged per attribute for the same reason as before/after: `w:spacing` is one element
894
- * carrying independent attributes, and a style that states only `@line` must not reset the
895
- * rule an earlier entry in the cascade set.
1178
+ * Admission is the trust boundary for font bytes: counts and sizes are checked against the hard
1179
+ * ceilings, each face's hash is re-derived and compared, and the bytes are handed to the injected
1180
+ * validator. A face failing any of these is recorded as a typed refusal rather than dropped, so
1181
+ * `resolve` can explain itself instead of answering "missing".
896
1182
  */
897
- declare function paragraphLineSpacing(props: readonly OoxmlProperty[]): ParagraphLineSpacing;
1183
+ declare const createFontResourceSnapshot: (options: FontResourceSnapshotOptions) => FontResourceSnapshot;
1184
+
1185
+ declare const FIXED_POINT: unique symbol;
1186
+ /** Fixed-point coordinates are safe integers in units declared by ShapingEnvironment.fixedPointScale. */
1187
+ type FixedPoint = number & {
1188
+ readonly [FIXED_POINT]: true;
1189
+ };
898
1190
  /**
899
- * Apply resolved line spacing to a line's natural (glyph-derived) box.
1191
+ * Brand a safe integer as a {@link FixedPoint} coordinate.
900
1192
  *
901
- * `auto` places its extra BELOW the line — the last line's multiple spacing still separates
902
- * it from the next paragraph. Putting that delta above inverted cover-page rhythm:
903
- * `w:line="460"` on "between" opened a large gap above the word and almost none before
904
- * "MERIDIAN".
1193
+ * Fixed point rather than float throughout shaping so that two runs shaped identically compare
1194
+ * EQUAL — float accumulation would make the same text measure differently depending on how it was
1195
+ * split, and pagination is decided on those measurements.
905
1196
  *
906
- * `atLeast` does the OPPOSITE: the box grows upward and the glyphs sit on its floor. A
907
- * captured control settles it — Times New Roman 12 pt at `w:line="360" w:lineRule="atLeast"`
908
- * puts the reference's baseline at 15.36 pt, which is `18 - descent` snapped to the device
909
- * grid, where treating it like `auto` leaves the baseline at 11.28 and the text 4.08 pt too
910
- * high. `w:line="240" atLeast` is below the natural line, falls through to the natural
911
- * height, and is unaffected. See `.cache/pdf/claude-linerule/`.
1197
+ * @throws RangeError when the value is not a safe integer.
1198
+ */
1199
+ declare const fixedPoint: (value: number) => FixedPoint;
1200
+ /** Which way a run reads. The parity projection of its bidi embedding level. */
1201
+ type TextDirection = 'ltr' | 'rtl';
1202
+ /** How fixed-point conversion breaks ties. Part of the shaping fingerprint. */
1203
+ type FixedPointRoundingMode = 'halfAwayFromZero' | 'halfToEven' | 'towardZero';
1204
+ /** Which Unicode normalization is applied before shaping, if any. */
1205
+ type NormalizationPolicy = 'none' | 'NFC' | 'NFD' | 'NFKC' | 'NFKD';
1206
+ /**
1207
+ * The shaping library and its exact version.
912
1208
  *
913
- * Exact-height boxes place their baseline at 80% of the height; the legacy
914
- * noExtraLineSpacing switch instead preserves the face baseline within the box.
1209
+ * Versioned because a library upgrade can change glyph positioning, and a cached measurement
1210
+ * taken under the old one must not be reused under the new one.
1211
+ */
1212
+ interface VersionedShapingLibrary {
1213
+ readonly name: string;
1214
+ readonly version: string;
1215
+ }
1216
+ /**
1217
+ * Everything that determines how text shapes — the complete input to a
1218
+ * {@link ShapingEnvironment}.
915
1219
  *
916
- * An `auto` line of a snapping paragraph under an active line grid takes whole pitches
917
- * instead ({@link gridLineBox}); `trailing` then names the extra below the grid line.
1220
+ * Exhaustive on purpose. Any field that could change a glyph's position belongs here, because the
1221
+ * environment's fingerprint is what decides whether a cached shaped run may be reused.
918
1222
  */
919
- declare function applyLineSpacing(spacing: ParagraphLineSpacing, naturalHeight: number, naturalBaseline: number): {
920
- height: number;
921
- baseline: number;
922
- trailing?: number;
923
- };
1223
+ interface ShapingEnvironmentInput {
1224
+ readonly font: ResolvedFont;
1225
+ readonly variationAxes: Readonly<Record<string, number>>;
1226
+ readonly shapingLibrary: VersionedShapingLibrary;
1227
+ readonly unicodeDataVersion: string;
1228
+ readonly normalization: NormalizationPolicy;
1229
+ readonly script: string;
1230
+ readonly language: string;
1231
+ readonly direction: TextDirection;
1232
+ readonly features: Readonly<Record<string, number>>;
1233
+ readonly fallbackOrder: readonly ResolvedFont[];
1234
+ readonly fixedPointScale: number;
1235
+ readonly roundingMode: FixedPointRoundingMode;
1236
+ }
924
1237
  /**
925
- * `w:contextualSpacing` (17.3.1.9): drop before/after between paragraphs of the SAME
926
- * style. Word's built-in `ListParagraph` sets it, so every list authored in Word gets a
927
- * paragraph gap between items without this.
1238
+ * A validated {@link ShapingEnvironmentInput} — build one with `createShapingEnvironment`.
1239
+ *
1240
+ * Structurally identical to its input, but the nominal distinction is the point: holding one
1241
+ * means the tags, axes and fonts inside it have already been checked.
928
1242
  */
929
- declare function paragraphContextualSpacing(props: readonly OoxmlProperty[]): boolean;
1243
+ interface ShapingEnvironment extends ShapingEnvironmentInput {
1244
+ }
1245
+ /** One shaping call: the text, its size, its bidi level, and the environment to shape it in. */
1246
+ interface ShapeInput {
1247
+ readonly text: string;
1248
+ readonly fontSizeHalfPoints: number;
1249
+ /** Exact UAX #9 embedding/isolate level; direction is its parity projection. */
1250
+ readonly bidiLevel: number;
1251
+ readonly environment: ShapingEnvironment;
1252
+ /**
1253
+ * Logical text around `text` that the shaper reads for contextual forms but never
1254
+ * shapes: HarfBuzz pre- and post-context. A word split across two formatting runs joins
1255
+ * across the split only when each half sees the other's letters. At most
1256
+ * {@link MAX_SHAPING_CONTEXT} UTF-16 units each; glyphs and clusters cover `text` alone.
1257
+ */
1258
+ readonly context?: ShapingContext;
1259
+ }
1260
+ /** Pre- and post-context for one shaping call. See {@link ShapeInput.context}. @public */
1261
+ interface ShapingContext {
1262
+ readonly before: string;
1263
+ readonly after: string;
1264
+ }
1265
+ /** Longest context on either side, in UTF-16 units. Joining needs only the nearest letters. @public */
1266
+ declare const MAX_SHAPING_CONTEXT = 16;
1267
+ /**
1268
+ * One positioned glyph.
1269
+ *
1270
+ * `id` is a glyph index in its FACE, not a character — a ligature is one glyph spanning several
1271
+ * characters, and a single character may produce several glyphs. Use `cluster` to get back to
1272
+ * text.
1273
+ */
1274
+ interface ShapedGlyph {
1275
+ readonly id: number;
1276
+ /** Optional synthetic glyph-size factor; positions and advances already include it. */
1277
+ readonly drawScale?: number;
1278
+ /** UTF-16 text offset identifying the cluster that produced this glyph. */
1279
+ readonly cluster: number;
1280
+ /** Pen origin before this glyph's shaping offsets, in fixed-point run coordinates. */
1281
+ readonly originX: FixedPoint;
1282
+ readonly originY: FixedPoint;
1283
+ readonly advanceX: FixedPoint;
1284
+ readonly advanceY: FixedPoint;
1285
+ readonly offsetX: FixedPoint;
1286
+ readonly offsetY: FixedPoint;
1287
+ /** Exact monochrome outline returned by the admitted HarfBuzz face. */
1288
+ readonly outline: GlyphOutline;
1289
+ }
1290
+ /** A glyph's outline as SVG path data, in font design units. */
1291
+ interface GlyphOutline {
1292
+ readonly path: string;
1293
+ /** Design units per em — divide by this to scale the path to a point size. */
1294
+ readonly unitsPerEm: number;
1295
+ }
1296
+ /**
1297
+ * One cluster: the smallest indivisible text-to-glyph correspondence.
1298
+ *
1299
+ * The unit the CARET moves by. A cluster may be several characters (a ligature) or several glyphs
1300
+ * (a decomposed mark), so neither a character index nor a glyph index is a valid caret position —
1301
+ * `caretEdges` is, and it includes both endpoints.
1302
+ */
1303
+ interface ShapedCluster {
1304
+ /** Half-open logical UTF-16 range in ShapedRun.text. */
1305
+ readonly textStart: number;
1306
+ readonly textEnd: number;
1307
+ /** Half-open visual glyph range in ShapedRun.glyphs. */
1308
+ readonly glyphStart: number;
1309
+ readonly glyphEnd: number;
1310
+ readonly advance: FixedPoint;
1311
+ /** Fixed-point edges from this cluster's visual origin, including both endpoints. */
1312
+ readonly caretEdges: readonly FixedPoint[];
1313
+ /** Index into ShapedRun.fontSpans, preserving the exact fallback choice. */
1314
+ readonly fontSpan: number;
1315
+ }
1316
+ /** A run's vertical metrics, from the face that shaped it. Drives line height and baseline. */
1317
+ interface ShapedVerticalMetrics {
1318
+ readonly ascent: FixedPoint;
1319
+ readonly descent: FixedPoint;
1320
+ readonly lineGap: FixedPoint;
1321
+ }
1322
+ /**
1323
+ * A stretch of glyphs that came from ONE face.
1324
+ *
1325
+ * A run whose text needed fallback has several of these. Recording the exact face per span is
1326
+ * what lets a re-shape reproduce the same result rather than re-running fallback selection and
1327
+ * possibly choosing differently.
1328
+ */
1329
+ interface ShapedFontSpan {
1330
+ readonly glyphStart: number;
1331
+ readonly glyphEnd: number;
1332
+ readonly font: ResolvedFont;
1333
+ /** null denotes the primary font; otherwise this is the environment fallback-order index. */
1334
+ readonly fallbackIndex: number | null;
1335
+ }
1336
+ /**
1337
+ * One shaped run: text turned into positioned glyphs, with everything needed to measure it, paint
1338
+ * it, and put a caret in it.
1339
+ *
1340
+ * The engine's measurement unit. Layout never measures characters — it measures these.
1341
+ */
1342
+ interface ShapedRun {
1343
+ readonly text: string;
1344
+ readonly direction: TextDirection;
1345
+ readonly bidiLevel: number;
1346
+ readonly glyphs: readonly ShapedGlyph[];
1347
+ readonly clusters: readonly ShapedCluster[];
1348
+ readonly fontSpans: readonly ShapedFontSpan[];
1349
+ readonly metrics: ShapedVerticalMetrics;
1350
+ /**
1351
+ * The same horizontal advances as `glyphs[i].advanceX`, in the same fixed-point units, but
1352
+ * BEFORE the per-glyph rounding to the fixed-point grid. Parallel to `glyphs`.
1353
+ *
1354
+ * Measurement and line breaking read `advanceX` and must keep doing so: pagination is decided
1355
+ * on those numbers, and two runs shaped identically have to compare equal. A painter that
1356
+ * gives every glyph its own absolute position sums the advances instead, and a sum of ROUNDED
1357
+ * advances drifts by up to half a fixed-point unit per glyph — an error that only grows along
1358
+ * a line. Summing these and rounding once, at the position actually written, does not.
1359
+ *
1360
+ * Absent on runs an assembly path cannot state exactly; a reader falls back to `originX`.
1361
+ */
1362
+ readonly exactAdvancesX?: readonly number[];
1363
+ }
930
1364
  /**
931
- * Resolve `w:pBdr` from the paragraph-properties node.
1365
+ * A {@link ShapedRun} reduced to the fields two shaping results must agree on to be considered
1366
+ * identical.
932
1367
  *
933
- * Nested — every edge is a child of `pBdr`, not an attribute — so this reads the typed tree
934
- * rather than the flattened `OoxmlProperty[]` bag `propertiesOf` builds for leaf props.
1368
+ * Fonts appear as {@link FontFingerprintInputs} rather than whole `ResolvedFont` objects, so the
1369
+ * comparison is over VALUES and does not depend on object identity — which is what makes it work
1370
+ * across a reload or a worker boundary.
935
1371
  */
936
- declare function paragraphBorders(pPr: OoxmlNode | undefined): ParagraphBorders;
1372
+ interface ShapedRunComparatorInputs {
1373
+ readonly text: string;
1374
+ readonly direction: TextDirection;
1375
+ readonly script: string;
1376
+ readonly language: string;
1377
+ readonly bidiLevel: number;
1378
+ readonly glyphs: readonly ShapedGlyph[];
1379
+ readonly clusters: readonly ShapedCluster[];
1380
+ readonly fontSpans: readonly {
1381
+ readonly glyphStart: number;
1382
+ readonly glyphEnd: number;
1383
+ readonly fallbackIndex: number | null;
1384
+ readonly font: FontFingerprintInputs;
1385
+ }[];
1386
+ readonly metrics: ShapedVerticalMetrics;
1387
+ }
937
1388
  /**
938
- * `w:pBdr` after the style cascade: a later `w:pBdr` replaces an earlier one WHOLESALE.
1389
+ * The one thing layout needs from a shaping backend.
939
1390
  *
940
- * Word does not merge edges across the cascade. A style that states only `w:bottom` discards
941
- * the box its `basedOn` ancestor declared, so folding edge by edge would leave a lone
942
- * underline surrounded by a box no one authored. Absence inherits; `nil`/`none` clear.
1391
+ * Injected rather than imported, which is what keeps layout DOM-free and testable: a fixed-metric
1392
+ * shaper measures deterministically in a test, HarfBuzz measures for real in a browser, and
1393
+ * layout cannot tell the difference.
943
1394
  */
944
- declare function cascadedParagraphBorders(paragraphPropertyNodes: readonly OoxmlNode[]): ParagraphBorders;
1395
+ interface TextShaper {
1396
+ shape(input: ShapeInput): ShapedRun;
1397
+ }
945
1398
  /**
946
- * Visual stroke thickness layout publishes for one edge (points).
1399
+ * A font reduced to the values that identify it for fingerprinting.
947
1400
  *
948
- * Compound `ST_Border` values (`double`, …) use the shared inflated band so a thin
949
- * `w:sz="3"` double still occupies a visible double-line box — matching table borders.
1401
+ * Includes the content `hash` and `faceIndex`, so two faces with the same family name but
1402
+ * different bytes fingerprint differently — which is what stops a cached measurement being reused
1403
+ * against a substituted face.
950
1404
  */
951
- declare function paragraphBorderStrokeWidthPt(edge: ParagraphBorderEdge): number;
1405
+ interface FontFingerprintInputs {
1406
+ readonly identity: string;
1407
+ readonly id: string;
1408
+ readonly family: string;
1409
+ readonly request: FontRequest;
1410
+ readonly hash: string;
1411
+ readonly faceIndex: number;
1412
+ readonly byteLength: number;
1413
+ readonly substitution: FontSubstitution | null;
1414
+ }
952
1415
  /**
953
- * Extent one border edge occupies away from the text it decorates: gap plus rule, in points.
1416
+ * A {@link ShapingEnvironment} reduced to comparable values.
954
1417
  *
955
- * Vertically that is flow height — a top rule pushes the first line down, a bottom rule holds
956
- * the page open below the last one — so pagination has to see it. Horizontally it is
957
- * publish-only: Word draws left/right paragraph rules OUTSIDE the text column and never
958
- * re-breaks the lines, which is why adding a box to a paragraph in Word does not reflow it.
1418
+ * Records and axis maps become SORTED entry arrays, because two environments differing only in
1419
+ * key insertion order must fingerprint the same — otherwise a cache would miss on a difference
1420
+ * that changes no glyph.
959
1421
  */
960
- declare function paragraphBorderExtentPt(edge: ParagraphBorderEdge | undefined): number;
961
- /** Vertical extent a bottom border adds below the last line (gap + rule). */
962
- declare function bottomBorderExtentPt(edge: ParagraphBorderEdge | undefined): number;
1422
+ interface ShapingEnvironmentFingerprintInputs {
1423
+ readonly font: FontFingerprintInputs;
1424
+ readonly variationAxes: readonly (readonly [string, number])[];
1425
+ readonly shapingLibrary: VersionedShapingLibrary;
1426
+ readonly unicodeDataVersion: string;
1427
+ readonly normalization: NormalizationPolicy;
1428
+ readonly script: string;
1429
+ readonly language: string;
1430
+ readonly direction: TextDirection;
1431
+ readonly features: readonly (readonly [string, number])[];
1432
+ readonly fallbackOrder: readonly FontFingerprintInputs[];
1433
+ readonly fixedPointScale: number;
1434
+ readonly roundingMode: FixedPointRoundingMode;
1435
+ }
963
1436
  /**
964
- * Identity of a paragraph's border set, for the `w:between` group rule.
1437
+ * Validate and freeze a shaping environment.
965
1438
  *
966
- * Word treats consecutive paragraphs whose border settings are IDENTICAL as ONE bordered
967
- * block: the top rule draws above the first, the bottom rule below the last, and each
968
- * interior boundary gets `w:between` or nothing (§17.3.1.24). That is why applying a box to
969
- * three selected paragraphs in Word draws one box and not three.
1439
+ * Checks every field that could silently corrupt a measurement: OpenType tags must be four ASCII
1440
+ * bytes, script and language must be non-blank and control-character free, fonts must already be
1441
+ * validated. Throws rather than coercing — a bad tag that shapes anyway produces a document that
1442
+ * measures wrong everywhere and looks fine.
970
1443
  *
971
- * Empty string means "no borders", which never groups with anything.
1444
+ * @throws TypeError on a malformed tag, name, or axis value.
972
1445
  */
973
- declare function paragraphBordersFingerprint(borders: ParagraphBorders): string;
1446
+ declare const createShapingEnvironment: (input: ShapingEnvironmentInput) => ShapingEnvironment;
974
1447
  /**
975
- * Gap to insert before a paragraph once the previous paragraph's `after` is already in the
976
- * flow cursor — Word takes the larger of the two rather than summing them.
1448
+ * Reduce an environment to its comparable form, with records sorted so key order cannot affect
1449
+ * the result.
977
1450
  */
978
- declare function collapsedSpaceBefore(before: number, previousAfter: number): number;
1451
+ declare const shapingEnvironmentFingerprintInputs: (input: ShapingEnvironmentInput) => ShapingEnvironmentFingerprintInputs;
1452
+ /** Canonical serialization of every shaping variable, including byte hash and provenance. */
1453
+ declare const shapingEnvironmentFingerprint: (environment: ShapingEnvironmentInput) => string;
979
1454
  /**
980
- * Applied before-spacing for placement (Word 2013+ / compat mode 15).
1455
+ * Validate and freeze a shaped run against the environment that produced it.
981
1456
  *
982
- * Adjacent before/after still collapse to the larger gap, but before is dropped entirely when
983
- * the paragraph naturally moves to the top of a page mid-section. An explicit page-break-before
984
- * paragraph, or the first paragraph of a document/section, retains before. Callers publish this
985
- * applied value on the fragment so shading, borders,
986
- * selection, and paint share one geometry.
1457
+ * Checks the internal consistency a shaper must satisfy: direction matches the environment,
1458
+ * cluster ranges tile the text without gaps or overlap, glyph ranges stay in bounds, font spans
1459
+ * cover every glyph, and caret edges are monotonic. A shaper that violates any of these produces
1460
+ * a caret that lands in the wrong place, which is far harder to diagnose downstream than here.
1461
+ *
1462
+ * @throws TypeError when the run and environment disagree, or the run is internally inconsistent.
987
1463
  */
988
- declare function appliedSpaceBefore(before: number, previousAfter: number, atTopOfPage: boolean, preserveAtPageStart: boolean): number;
1464
+ declare const createShapedRun: (input: ShapedRun, environmentInput: ShapingEnvironmentInput) => ShapedRun;
1465
+ /**
1466
+ * Reduce a shaped run to its comparable form, validating it on the way through.
1467
+ *
1468
+ * What the D9 determinism oracles compare: two shaping runs of the same text in the same
1469
+ * environment must produce byte-identical structures here.
1470
+ */
1471
+ declare const shapedRunComparatorInputs: (input: ShapedRun, environment: ShapingEnvironmentInput) => ShapedRunComparatorInputs;
989
1472
 
990
1473
  /** `w:vertAlign` — script position, which also scales the run's effective size. */
991
1474
  type VerticalAlign = 'baseline' | 'superscript' | 'subscript';
@@ -1130,6 +1613,8 @@ interface ThemeFonts {
1130
1613
  /** Language-specific theme faces, keyed by ISO 15924 script. */
1131
1614
  readonly majorSupplemental?: Readonly<Record<string, string>>;
1132
1615
  readonly minorSupplemental?: Readonly<Record<string, string>>;
1616
+ /** Lower-cased font table names that declare a Chinese character set. */
1617
+ readonly chineseFontTableFaces?: readonly string[];
1133
1618
  }
1134
1619
  /**
1135
1620
  * Resolve one run's direct formatting.
@@ -1195,6 +1680,29 @@ interface StoryPageFieldNeeds {
1195
1680
  readonly hasPageParity?: boolean;
1196
1681
  }
1197
1682
 
1683
+ interface DrawingPoint {
1684
+ readonly x: number;
1685
+ readonly y: number;
1686
+ }
1687
+ interface DrawingInsets {
1688
+ readonly top: number;
1689
+ readonly right: number;
1690
+ readonly bottom: number;
1691
+ readonly left: number;
1692
+ }
1693
+ type DrawingClipFallback = 'none' | 'unsupported-preset';
1694
+ interface DrawingGeometry {
1695
+ readonly contentBounds: LayoutBox;
1696
+ readonly paintBounds: LayoutBox;
1697
+ readonly hitBounds: LayoutBox;
1698
+ readonly transformedCorners: readonly DrawingPoint[];
1699
+ /** Unclipped image corners for affine encoding; clipping must not stretch the image. */
1700
+ readonly imageTransformCorners?: readonly DrawingPoint[];
1701
+ readonly clipPolygon: readonly DrawingPoint[] | null;
1702
+ readonly clipFallback: DrawingClipFallback;
1703
+ readonly effectInsets: DrawingInsets;
1704
+ }
1705
+
1198
1706
  /**
1199
1707
  * UAX #9 embedding levels, one per UTF-16 code unit, plus the paragraph ranges they were resolved
1200
1708
  * within.
@@ -1443,6 +1951,17 @@ interface FieldAtomMarker {
1443
1951
  * first fragment, gated per field on the calibration verdict.
1444
1952
  */
1445
1953
  readonly pageRef?: PageRefFieldProjection;
1954
+ /**
1955
+ * The first model offset of the editable saved result this piece belongs to.
1956
+ *
1957
+ * Present only when saved results are laid out as text (`fieldResults: 'editable'`). The
1958
+ * result is ordinary text then, so its pieces have their own offsets; every piece of one
1959
+ * result shares this value, which keeps a multi-run link result one anchor and lets the
1960
+ * caret find the whole result it is in.
1961
+ */
1962
+ readonly resultStart?: number;
1963
+ /** The model offset just past the same result, beside {@link resultStart}. */
1964
+ readonly resultEnd?: number;
1446
1965
  }
1447
1966
  /** A half-open model-offset range, in the paragraph's own UTF-16 offset space. */
1448
1967
  interface ModelRange {
@@ -1600,29 +2119,6 @@ interface CjkTypographySettings {
1600
2119
  readonly after: Readonly<Record<string, string>>;
1601
2120
  }
1602
2121
 
1603
- interface DrawingPoint {
1604
- readonly x: number;
1605
- readonly y: number;
1606
- }
1607
- interface DrawingInsets {
1608
- readonly top: number;
1609
- readonly right: number;
1610
- readonly bottom: number;
1611
- readonly left: number;
1612
- }
1613
- type DrawingClipFallback = 'none' | 'unsupported-preset';
1614
- interface DrawingGeometry {
1615
- readonly contentBounds: LayoutBox;
1616
- readonly paintBounds: LayoutBox;
1617
- readonly hitBounds: LayoutBox;
1618
- readonly transformedCorners: readonly DrawingPoint[];
1619
- /** Unclipped image corners for affine encoding; clipping must not stretch the image. */
1620
- readonly imageTransformCorners?: readonly DrawingPoint[];
1621
- readonly clipPolygon: readonly DrawingPoint[] | null;
1622
- readonly clipFallback: DrawingClipFallback;
1623
- readonly effectInsets: DrawingInsets;
1624
- }
1625
-
1626
2122
  /** Test-only mutable counters; omitted by production call sites. Not package-public. */
1627
2123
  interface TableBorderGridResolveWork {
1628
2124
  /** Sparse intervals emitted (not dense column slots). */
@@ -1639,9 +2135,8 @@ type TableBorderSideName = 'top' | 'right' | 'bottom' | 'left';
1639
2135
  /**
1640
2136
  * One border edge in one of three states.
1641
2137
  *
1642
- * Omitted edges inherit. Explicit no-border edges suppress inherited rules; a directly
1643
- * authored opposing edge can still win. This matches the saved Word sample's mixed and
1644
- * borderless tables, including cells authored with `w:val="none"`.
2138
+ * Omitted edges inherit. Explicit no-border edges suppress their own inherited rule.
2139
+ * An opposing edge can still win, including a rule inherited by the neighboring cell.
1645
2140
  */
1646
2141
  type TableBorderSide = {
1647
2142
  readonly state: 'omitted';
@@ -1755,6 +2250,8 @@ interface BorderGridCell {
1755
2250
  readonly borders: CellBorderBox;
1756
2251
  /** Set on restart cells that visually span into later rows. */
1757
2252
  readonly mergeRowSpan?: number;
2253
+ /** The row's `w:tblPrEx` table borders, which replace the table's own for this cell. */
2254
+ readonly rowTableBorders?: TableBorderBox;
1758
2255
  }
1759
2256
  /**
1760
2257
  * The absolute grid a table's borders are drawn on: column widths, and per-row tops and heights.
@@ -1829,8 +2326,13 @@ interface TextboxStoryLayout {
1829
2326
  x: number;
1830
2327
  y: number;
1831
2328
  }>;
1832
- /** Content box width (extent minus horizontal insets). */
2329
+ /** Content box width (extent minus horizontal insets; the widest line when unwrapped). */
1833
2330
  readonly contentWidth: number;
2331
+ /**
2332
+ * Box width (points) that unwrapped text sizes its shape to: the widest line plus the
2333
+ * insets. Placement resolves the shape at this width. Absent when the extent width holds.
2334
+ */
2335
+ readonly extentWidth?: number;
1834
2336
  /** Content box height (extent minus vertical insets). */
1835
2337
  readonly contentHeight: number;
1836
2338
  /** Solid fill of the hosting shape, painted behind the story; null for no fill. */
@@ -1855,6 +2357,21 @@ interface TextboxStoryLayout {
1855
2357
  readonly clippedResourceToken?: string;
1856
2358
  }
1857
2359
 
2360
+ /** One laid-out group member story. */
2361
+ interface GroupTextboxStoryRecord {
2362
+ /** Canonical node id of the member `wps:wsp`. */
2363
+ readonly memberNodeId: string;
2364
+ /** The member box in points, relative to the drawing extent's top-left corner. */
2365
+ readonly box: LayoutBox;
2366
+ /** The member's story, in content-box coordinates inside {@link box}. */
2367
+ readonly story: TextboxStoryLayout;
2368
+ /**
2369
+ * Clockwise turn of the painted member about the center of {@link box}, in degrees. The
2370
+ * story lays out upright; paint and export turn it. Hit testing never maps into it.
2371
+ */
2372
+ readonly rotationDegrees: number;
2373
+ }
2374
+
1858
2375
  /** Clip `box` to a horizontal content band; preserves authored size, zeroes clipped axes. */
1859
2376
  /** How a drawing record holds the picture member of a shape group. */
1860
2377
  interface DrawingGroupPictureRecord {
@@ -1927,6 +2444,8 @@ interface InlineDrawingRecord {
1927
2444
  * inline text box carries one too: its story sits inside the extent the line reserves.
1928
2445
  */
1929
2446
  readonly textboxStory?: TextboxStoryLayout;
2447
+ /** Read-only stories of a group's text box members, each placed in its member box. */
2448
+ readonly groupTextboxStories?: readonly GroupTextboxStoryRecord[];
1930
2449
  }
1931
2450
  interface InlineDrawingLayoutContext {
1932
2451
  readonly ownerPartName: string;
@@ -2051,12 +2570,7 @@ declare function withNumberingStyleLinks(index: NumberingIndex, styleCascade: St
2051
2570
  * in `direct`, and the defaults and that style's bases go in `inherited`.
2052
2571
  */
2053
2572
  declare function mergeListIndent(levelIndent: NumberingLevelIndent, inherited: readonly OoxmlProperty[], direct?: readonly OoxmlProperty[]): NumberingLevelIndent;
2054
- /**
2055
- * Collect paragraphs of a block list in document order, descending into tables.
2056
- *
2057
- * Caps nesting so a hostile nested-table document cannot recurse without bound.
2058
- */
2059
- declare function walkStoryParagraphs(blocks: readonly OoxmlElement[], maxTableDepth?: number): OoxmlElement[];
2573
+
2060
2574
  /**
2061
2575
  * Resolve every list paragraph in a story to a {@link ResolvedListItem}, keyed by node id.
2062
2576
  *
@@ -2373,65 +2887,6 @@ interface EquationSpanRecord {
2373
2887
  /** Compose one bounded OMML projection into paint-ready point geometry. */
2374
2888
  declare function layoutEquation(projection: OmmlEquationProjection, measurer: TextMeasurer, style: ResolvedRunStyle): EquationSpanRecord;
2375
2889
 
2376
- /**
2377
- * The four physical sides of `CT_PageBorders`.
2378
- *
2379
- * Four, not the six of `w:pBdr`: `w:between` and `w:bar` are paragraph-group rules and the
2380
- * page has no analogue for either.
2381
- */
2382
- declare const PAGE_BORDER_SIDES: readonly ['top', 'left', 'bottom', 'right'];
2383
- /** Which of the four page-frame edges. */
2384
- type PageBorderSide = (typeof PAGE_BORDER_SIDES)[number];
2385
- /**
2386
- * `w:offsetFrom` (`ST_PgBorderOffset`) — what `w:space` on each edge is measured from.
2387
- *
2388
- * The schema default is `text`, NOT `page`. Word's own UI opens on "Edge of page", which is
2389
- * why the wrong default is easy to assume and expensive to hold: at `text` the rule sits
2390
- * `w:space` points outside the TEXT, so its distance from the sheet edge is the margin minus
2391
- * the space — a number that moves whenever the margins do.
2392
- */
2393
- type PageBorderOffsetFrom = 'page' | 'text';
2394
- /** `w:display` (`ST_PgBorderDisplay`) — which pages of the section carry the frame. */
2395
- type PageBorderDisplay = 'allPages' | 'firstPage' | 'notFirstPage';
2396
- /** `w:zOrder` (`ST_PgBorderZOrder`) — whether the frame paints over the text or under it. */
2397
- type PageBorderZOrder = 'front' | 'back';
2398
- /**
2399
- * One section's resolved `w:pgBorders`.
2400
- *
2401
- * Resolved, not raw: every attribute the file omitted is filled in with the SCHEMA default,
2402
- * so a consumer never has to know which of the three were authored. An edge is present only
2403
- * when it paints — `nil` / `none` and art borders are already gone (see {@link parsePageBorders}).
2404
- */
2405
- interface SectionPageBorders {
2406
- /** `w:offsetFrom`; absent attribute reads as `text` (§17.6.10). */
2407
- readonly offsetFrom: PageBorderOffsetFrom;
2408
- /** `w:display`; absent attribute reads as `allPages`. */
2409
- readonly display: PageBorderDisplay;
2410
- /** `w:zOrder`; absent attribute reads as `front`. */
2411
- readonly zOrder: PageBorderZOrder;
2412
- readonly top?: ParagraphBorderEdge;
2413
- readonly left?: ParagraphBorderEdge;
2414
- readonly bottom?: ParagraphBorderEdge;
2415
- readonly right?: ParagraphBorderEdge;
2416
- }
2417
- /**
2418
- * Parse `w:pgBorders` off one `w:sectPr`.
2419
- *
2420
- * Undefined when the element is absent AND when it paints nothing — every edge off, or every
2421
- * edge an art border. Unlike `w:pgNumType`, an empty-but-present element is not distinguished:
2422
- * these properties are a LAYOUT input, serialization re-emits the canonical tree, and a frame
2423
- * with no edges has exactly one meaning downstream.
2424
- */
2425
- declare function parsePageBorders(sectPr: OoxmlNode): SectionPageBorders | undefined;
2426
- /**
2427
- * Identity of one section's page frame, for incremental-layout context keys.
2428
- *
2429
- * A `w:pgBorders` edit changes nothing the flow measures — the frame is drawn beside the text,
2430
- * never through it — so no per-paragraph key moves and every reuse path would hand back the
2431
- * previous sheets with the previous frame. This token goes in the pass context so it does not.
2432
- */
2433
- declare function pageBordersFingerprint(borders: SectionPageBorders | undefined): string;
2434
-
2435
2890
  /** Published root story containing an artifact occurrence. @public */
2436
2891
  type SemanticArtifactRootStoryKind = 'body' | 'header' | 'footer' | 'footnote' | 'endnote' | 'note-separator';
2437
2892
  /** Exact story in which a review artifact's source anchor was laid out. @public */
@@ -2519,6 +2974,24 @@ interface SemanticCommentArtifactRecord {
2519
2974
  /** Exporter-neutral review artifact normalized by core. @public */
2520
2975
  type SemanticReviewArtifactRecord = SemanticTrackedChangeArtifactRecord | SemanticCommentArtifactRecord;
2521
2976
 
2977
+ /** Page geometry, in points. */
2978
+ interface PageGeometry {
2979
+ readonly width: number;
2980
+ readonly height: number;
2981
+ readonly margin: {
2982
+ readonly top: number;
2983
+ readonly right: number;
2984
+ readonly bottom: number;
2985
+ readonly left: number;
2986
+ };
2987
+ /** `w:pgMar/@header` — sheet edge to header top, in points. Defaults to 36 (720 twips). */
2988
+ readonly headerDistance?: number;
2989
+ /** `w:pgMar/@footer` — sheet edge to footer bottom, in points. Defaults to 36. */
2990
+ readonly footerDistance?: number;
2991
+ }
2992
+ /** US Letter with one-inch margins, in points. */
2993
+ declare const DEFAULT_PAGE_GEOMETRY: PageGeometry;
2994
+
2522
2995
  /** A half-open UTF-16 range inside one paragraph, addressed by its canonical node id. */
2523
2996
  interface SourceRange {
2524
2997
  readonly paragraphId: string;
@@ -2758,6 +3231,8 @@ interface StyleSpanRecord {
2758
3231
  interface LineRecord {
2759
3232
  /** An authored manual line break ends this line; optional display furniture can show ↵. */
2760
3233
  readonly manualBreakAfter?: true;
3234
+ /** An authored page or column break ends this line; optional furniture can label it. */
3235
+ readonly breakAfter?: 'page' | 'column';
2761
3236
  readonly id: string;
2762
3237
  readonly range: SourceRange;
2763
3238
  readonly spans: readonly StyleSpanRecord[];
@@ -2798,7 +3273,8 @@ interface LineRecord {
2798
3273
  * nothing, which is why the caret, paint's `padding-bottom` and the content-control
2799
3274
  * boundary all need this published rather than recovered from the box.
2800
3275
  *
2801
- * Zero under exact and atLeast, and on lines holding drawings (the box is authored).
3276
+ * Zero under exact and atLeast. On a line holding drawings, only an auto multiple above
3277
+ * single spacing sets it: the multiple's extra text band below the drawings.
2802
3278
  * Absent on lines published before this was measured; treat as zero.
2803
3279
  */
2804
3280
  readonly trailingSpacing?: number;
@@ -3455,6 +3931,13 @@ interface SemanticLayout {
3455
3931
  * reuse paths that would otherwise return previous pages by identity with stale boundaries.
3456
3932
  */
3457
3933
  readonly controlContextToken?: string;
3934
+ /** Body section breaks in document order, each named by the section that FOLLOWS it. */
3935
+ readonly sectionBreaks?: readonly SectionBreakMarkRecord[];
3936
+ }
3937
+ /** The paragraph whose `w:sectPr` ends a section, and how the next section starts. */
3938
+ interface SectionBreakMarkRecord {
3939
+ readonly paragraphId: string;
3940
+ readonly kind: SectionBreakType;
3458
3941
  }
3459
3942
 
3460
3943
  /**
@@ -3496,5 +3979,5 @@ interface TextMeasurer {
3496
3979
  };
3497
3980
  }
3498
3981
 
3499
- export { PARAGRAPH_BORDER_SIDES as a$, AUTO_PARAGRAPH_SPACING_PT as a4, HARD_MAX_AGGREGATE_FONT_BYTES as aF, HARD_MAX_FONT_BYTES as aG, HARD_MAX_FONT_SOURCES as aH, MAX_BORDER_SPACE_PT as aN, MAX_BORDER_WIDTH_PT as aO, MAX_LVL_OVERRIDES as aP, MAX_NUMBERING_DEFINITIONS as aQ, MAX_PARAGRAPH_SPACING_PT as aR, MAX_SHAPING_CONTEXT as aS, MAX_STYLE_DEFINITIONS as aT, MAX_TABLE_BORDER_STROKES as aU, MAX_TAB_POSITION_TWIPS as aV, MAX_TAB_STOPS as aW, PAGE_BORDER_SIDES as a_, DEFAULT_RUN_STYLE as ae, DEFAULT_TAB_INTERVAL_PT as af, DEFAULT_TAB_INTERVAL_TWIPS as ag, EMPTY_NUMBERING_INDEX as al, EMPTY_TAB_STOPS as am, FontResolutionError as ax, boundedStructuralFontValidator as b$, TAB_LEADER_GLYPH as bI, UnsupportedScriptError as bT, appliedSpaceBefore as bV, applyLineSpacing as bW, baselineShiftPtOf as bX, borderExtentPt as bY, borderWeight as bZ, bottomBorderExtentPt as b_, SINGLE_LINE_SPACING as bo, buildNumberingIndex as c0, buildStyleCascadeTable as c1, cascadeParagraphFormatting as c2, cascadeRunProperties as c3, cascadedBottomBorder as c4, cascadedParagraphBorders as c5, cascadedTabStops as c6, collapsedSpaceBefore as c7, createFontResourceSnapshot as c8, createShapedRun as c9, readNumPr as cA, readTableBorders as cB, resolveBorderConflict as cC, resolveNumberingLevel as cD, resolveParagraphLayoutInputs as cE, resolveRunStyle as cF, resolveStoryListItems as cG, resolveStoryRefFields as cH, resolveTableCellBorderGrid as cI, runStylesEqual as cJ, shapedRunComparatorInputs as cK, shapingEnvironmentFingerprint as cL, shapingEnvironmentFingerprintInputs as cM, styleForFontSlot as cN, tabAdvanceWidth as cO, tabStopsFingerprint as cP, walkStoryParagraphs as cQ, withDefaultTabInterval as cR, withNumberingStyleLinks as cS, withResolvedListItems as cT, createShapingEnvironment as ca, defaultTabIntervalFromSettings as cb, displayText as cc, fixedPoint as cd, fontRequestKey as ce, glyphSizeFactorOf as cf, isValidStyleId as cg, itemizeScriptFontSlots as ch, layoutEquation as ci, measureDisplayText as cj, mergeListIndent as ck, nextTabDestination as cl, pageBordersFingerprint as cm, paragraphBorderExtentPt as cn, paragraphBorderStrokeWidthPt as co, paragraphBorders as cp, paragraphBordersFingerprint as cq, paragraphBreaksBefore as cr, paragraphContextualSpacing as cs, paragraphLineSpacing as ct, paragraphSpacing as cu, paragraphTabStops as cv, parsePageBorders as cw, parseRefInstruction as cx, readBorderSide as cy, readCellBorders as cz };
3500
- export type { HyperlinkFieldSpec as $, AnchoredDrawingRecord as A, BlockFragmentRecord as B, ContentControlBoundaryRecord as C, DrawingPoint as D, InlineDrawingLayoutContext as E, FontResourceSnapshot as F, CellBorderBox as G, HeaderFooterStoryRecord as H, InlineDrawingRecord as I, TableCellStyleFormatting as J, TableFloatPosition as K, LineRecord as L, ModelRange as M, NoteAreaRecord as N, TableBorderBox as O, ParagraphFragmentRecord as P, StyleCascadeTable as Q, ResolvedRunStyle as R, SemanticLayout as S, TextMeasurer as T, FixedPoint as U, VersionedShapingLibrary as V, FontByteValidator as W, SourceRange as X, HyperlinkProjector as Y, FieldLinkProjector as Z, StoryPageFieldNeeds as _, PageRecord as a, SpanLinkRecord as a0, PageBorderFrameRecord as a1, ParagraphBorderStrokeRecord as a2, StrikeoutStrokePt as a3, AbstractNumDefinition as a5, AnchoredDrawingLayoutFallback as a6, BidiEmbeddingLevels as a7, BorderGridGeometry as a8, CascadedParagraphFormatting as a9, FontResourceInstrumentation as aA, FontResourceSnapshotOptions as aB, FontSlot as aC, FontValidationResult as aD, GlyphOutline as aE, LevelOverride as aI, LineSpacingRule as aJ, ListMarkerAlign as aK, ListMarkerRecord as aL, ListSuffix as aM, NumDefinition as aX, NumberingLevelIndent as aY, NumberingPictureBullet as aZ, CjkTypographySettings as aa, ContentControlGeometryFragment as ab, ContentControlLevel as ac, ContentControlMappedType as ad, DeclaredFontSubstitution as ah, DrawingClipFallback as ai, DrawingGeometry as aj, DrawingGroupPictureRecord as ak, EquationFractionGeometry as an, EquationGeometry as ao, EquationNaryGeometry as ap, EquationRadicalGeometry as aq, EquationRowGeometry as ar, EquationScriptGeometry as as, EquationSpanRecord as at, EquationTextGeometry as au, FieldAtomMarker as av, FontFingerprintInputs as aw, FontResolutionErrorCode as ay, FontResourceDefinition as az, LayoutBox as b, PageBorderDisplay as b0, PageBorderOffsetFrom as b1, PageBorderSide as b2, PageBorderStrokeRecord as b3, PageBorderZOrder as b4, PageRefCalibrationCell as b5, PageRefFieldProjection as b6, ParagraphAutoSpacingContext as b7, ParagraphBorderEdge as b8, ParagraphBorderSide as b9, ShapedFontSpan as bA, ShapedGlyph as bB, ShapedRunComparatorInputs as bC, ShapedVerticalMetrics as bD, ShapingContext as bE, ShapingEnvironment as bF, ShapingEnvironmentFingerprintInputs as bG, StyleDefinition as bH, TabAlignment as bJ, TabDestination as bK, TabLeader as bL, TabStop as bM, TableBorderSideName as bN, TableBorderStrokeRecord as bO, TableFragmentRecord as bP, TextDirection as bQ, TextboxStoryFallbackReason as bR, TextboxStoryLayout as bS, VerticalAlign as bU, ParagraphBorders as ba, ParagraphBottomBorderRecord as bb, ParagraphIndent as bc, ParagraphLayoutInputs as bd, ParagraphLineSpacing as be, ParagraphSpacing as bf, RefFieldSpec as bg, RefNoteParts as bh, ResolvedCellBorders as bi, ResolvedPictureBullet as bj, ResolvedTabStops as bk, ResolvedTableBorderEdge as bl, ResolvedTableBorderEdgeSegment as bm, ResolvedUnderline as bn, ScriptItem as bp, SemanticArtifactRootStoryKind as bq, SemanticArtifactStoryKind as br, SemanticCommentArtifactRecord as bs, SemanticReviewArtifactOccurrence as bt, SemanticReviewArtifactOccurrenceGeometry as bu, SemanticReviewArtifactPosition as bv, SemanticReviewArtifactSource as bw, SemanticTrackedChangeArtifactRecord as bx, ShapeInput as by, ShapedCluster as bz, NoteStoryRecord as c, ContentControlLock as d, StyleSpanRecord as e, SectionPageBorders as f, TableRowFragmentRecord as g, TableCellFragmentRecord as h, ShapingEnvironmentInput as i, TextShaper as j, ResolvedFont as k, NormalizationPolicy as l, FixedPointRoundingMode as m, FontRequest as n, SemanticReviewArtifactRecord as o, ListMarkerPictureRecord as p, FontSubstitution as q, ShapedRun as r, NumberingLevel as s, NumberingIndex as t, DrawingInsets as u, TableBorderSide as v, TableFloatXSpec as w, TableFloatYSpec as x, ResolvedListItem as y, RefFieldContext as z };
3982
+ export { DEFAULT_REVISION_MARKUP as D, resolveRevisionMarkup as Q, DEFAULT_REVISION_DISPLAY_MODE as aA, DEFAULT_RUN_STYLE as aB, DEFAULT_SECTION_PROPERTIES as aC, DEFAULT_TAB_INTERVAL_PT as aD, DEFAULT_TAB_INTERVAL_TWIPS as aE, EMPTY_NUMBERING_INDEX as aL, EMPTY_TAB_STOPS as aM, FontResolutionError as aX, AUTO_PARAGRAPH_SPACING_PT as ap, DEFAULT_PAGE_GEOMETRY as az, HARD_MAX_AGGREGATE_FONT_BYTES as b4, HARD_MAX_FONT_BYTES as b5, HARD_MAX_FONT_SOURCES as b6, SINGLE_LINE_SPACING as bS, MAX_BORDER_SPACE_PT as bc, MAX_BORDER_WIDTH_PT as bd, MAX_DOCUMENT_SECTIONS as be, MAX_LVL_OVERRIDES as bf, MAX_NUMBERING_DEFINITIONS as bg, MAX_PARAGRAPH_SPACING_PT as bh, MAX_SHAPING_CONTEXT as bi, MAX_STYLE_DEFINITIONS as bj, MAX_TABLE_BORDER_STROKES as bk, MAX_TAB_POSITION_TWIPS as bl, MAX_TAB_STOPS as bm, PAGE_BORDER_SIDES as bq, PARAGRAPH_BORDER_SIDES as br, paragraphBorderExtentPt as c$, buildStyleCascadeTable as cA, cascadeParagraphFormatting as cB, cascadeRunProperties as cC, cascadedBottomBorder as cD, cascadedParagraphBorders as cE, cascadedTabStops as cF, collapsedSpaceBefore as cG, createFontResourceSnapshot as cH, createShapedRun as cI, createShapingEnvironment as cJ, defaultTabIntervalFromSettings as cK, displayText as cL, enumerateDocumentSections as cM, enumerateDocumentSectionsBounded as cN, fixedPoint as cO, fontRequestKey as cP, formatRevisionOf as cQ, geometryOfSection as cR, glyphSizeFactorOf as cS, isValidStyleId as cT, itemizeScriptFontSlots as cU, layoutEquation as cV, markRevisionRemovesMark as cW, measureDisplayText as cX, mergeListIndent as cY, nextTabDestination as cZ, pageBordersFingerprint as c_, TAB_LEADER_GLYPH as cf, UnsupportedScriptError as cq, appliedSpaceBefore as cs, applyLineSpacing as ct, baselineShiftPtOf as cu, borderExtentPt as cv, borderWeight as cw, bottomBorderExtentPt as cx, boundedStructuralFontValidator as cy, buildNumberingIndex as cz, paragraphBorderStrokeWidthPt as d0, paragraphBorders as d1, paragraphBordersFingerprint as d2, paragraphBreaksBefore as d3, paragraphContextualSpacing as d4, paragraphLineSpacing as d5, paragraphMarkFormatRevisionOf as d6, paragraphMarkRevisionOf as d7, paragraphMarkRevisionsOf as d8, paragraphSectionNode as d9, shapingEnvironmentFingerprintInputs as dA, shownMarkRevision as dB, styleForFontSlot as dC, tabAdvanceWidth as dD, tabStopsFingerprint as dE, withDefaultTabInterval as dF, withNumberingStyleLinks as dG, withResolvedListItems as dH, paragraphSpacing as da, paragraphTabStops as db, parsePageBorders as dc, parsePageNumbering as dd, parseRefInstruction as de, parseSectionProperties as df, projectedSectionSourceIndexes as dg, readBorderSide as dh, readCellBorders as di, readNumPr as dj, readSectionProperties as dk, readTableBorders as dl, resolveBorderConflict as dm, resolveNumberingLevel as dn, resolveParagraphLayoutInputs as dp, resolveRunStyle as dq, resolveStoryListItems as dr, resolveStoryRefFields as ds, resolveTableCellBorderGrid as dt, revisionAuthorFilter as du, revisionsAreDeletion as dv, revisionsVisible as dw, runStylesEqual as dx, shapedRunComparatorInputs as dy, shapingEnvironmentFingerprint as dz, REVISION_MARKUP_COLORS as w };
3983
+ export type { TableBorderSide as $, AnchoredDrawingRecord as A, BlockFragmentRecord as B, ContentControlBoundaryRecord as C, RevisionMarkupColor as E, FontResourceSnapshot as F, RevisionMarkupMark as G, HeaderFooterStoryRecord as H, InlineDrawingRecord as I, RevisionMarkupNamedColor as J, RevisionMarkupStyle as K, LineRecord as L, RevisionMarkupTextStyle as M, NoteAreaRecord as N, SectionProperties as O, ParagraphFragmentRecord as P, RevisionMarkupDialogSession as R, SemanticLayout as S, TextMeasurer as T, NumberingLevel as U, VersionedShapingLibrary as V, NumberingIndex as W, DrawingPoint as X, DrawingInsets as Y, ModelRange as Z, RevisionAttribution as _, ReviewDisplayMode as a, FontResourceSnapshotOptions as a$, TableFloatXSpec as a0, TableFloatYSpec as a1, ResolvedListItem as a2, RefFieldContext as a3, InlineDrawingLayoutContext as a4, CellBorderBox as a5, TableBorderBox as a6, TableCellStyleFormatting as a7, TableFloatPosition as a8, StyleCascadeTable as a9, DeclaredFontSubstitution as aF, DocumentSection as aG, DocumentSectionsEnumeration as aH, DrawingClipFallback as aI, DrawingGeometry as aJ, DrawingGroupPictureRecord as aK, EquationFractionGeometry as aN, EquationGeometry as aO, EquationNaryGeometry as aP, EquationRadicalGeometry as aQ, EquationRowGeometry as aR, EquationScriptGeometry as aS, EquationSpanRecord as aT, EquationTextGeometry as aU, FieldAtomMarker as aV, FontFingerprintInputs as aW, FontResolutionErrorCode as aY, FontResourceDefinition as aZ, FontResourceInstrumentation as a_, RevisionAuthorFilter as aa, FixedPoint as ab, FontByteValidator as ac, SourceRange as ad, HyperlinkProjector as ae, FieldLinkProjector as af, StoryPageFieldNeeds as ag, HyperlinkFieldSpec as ah, SpanLinkRecord as ai, PageGeometry as aj, SectionColumns as ak, SectionPageBorders as al, PageBorderFrameRecord as am, ParagraphBorderStrokeRecord as an, StrikeoutStrokePt as ao, AbstractNumDefinition as aq, AnchoredDrawingLayoutFallback as ar, BidiEmbeddingLevels as as, BorderGridGeometry as at, CascadedParagraphFormatting as au, CjkTypographySettings as av, ContentControlGeometryFragment as aw, ContentControlLevel as ax, ContentControlMappedType as ay, RevisionDisplayMode as b, SemanticCommentArtifactRecord as b$, FontSlot as b0, FontValidationResult as b1, GlyphOutline as b2, GroupTextboxStoryRecord as b3, LevelOverride as b7, LineSpacingRule as b8, ListMarkerAlign as b9, ParagraphBorderEdge as bA, ParagraphBorderSide as bB, ParagraphBorders as bC, ParagraphBottomBorderRecord as bD, ParagraphIndent as bE, ParagraphLayoutInputs as bF, ParagraphLineSpacing as bG, ParagraphSpacing as bH, RefFieldSpec as bI, RefNoteParts as bJ, ResolvedCellBorders as bK, ResolvedPictureBullet as bL, ResolvedTabStops as bM, ResolvedTableBorderEdge as bN, ResolvedTableBorderEdgeSegment as bO, ResolvedUnderline as bP, RevisionFilter as bQ, RevisionKind as bR, ScriptItem as bT, SectionBreakMarkRecord as bU, SectionBreakType as bV, SectionColumnDefinition as bW, SectionMargins as bX, SectionPageNumbering as bY, SemanticArtifactRootStoryKind as bZ, SemanticArtifactStoryKind as b_, ListMarkerRecord as ba, ListSuffix as bb, NumDefinition as bn, NumberingLevelIndent as bo, NumberingPictureBullet as bp, PageBorderDisplay as bs, PageBorderOffsetFrom as bt, PageBorderSide as bu, PageBorderStrokeRecord as bv, PageBorderZOrder as bw, PageRefCalibrationCell as bx, PageRefFieldProjection as by, ParagraphAutoSpacingContext as bz, PageRecord as c, SemanticReviewArtifactOccurrence as c0, SemanticReviewArtifactOccurrenceGeometry as c1, SemanticReviewArtifactPosition as c2, SemanticReviewArtifactSource as c3, SemanticTrackedChangeArtifactRecord as c4, ShapeInput as c5, ShapedCluster as c6, ShapedFontSpan as c7, ShapedGlyph as c8, ShapedRunComparatorInputs as c9, ShapedVerticalMetrics as ca, ShapingContext as cb, ShapingEnvironment as cc, ShapingEnvironmentFingerprintInputs as cd, StyleDefinition as ce, TabAlignment as cg, TabDestination as ch, TabLeader as ci, TabStop as cj, TableBorderSideName as ck, TableBorderStrokeRecord as cl, TableFragmentRecord as cm, TextDirection as cn, TextboxStoryFallbackReason as co, TextboxStoryLayout as cp, VerticalAlign as cr, LayoutBox as d, NoteStoryRecord as e, ContentControlLock as f, StyleSpanRecord as g, TableRowFragmentRecord as h, TableCellFragmentRecord as i, ShapingEnvironmentInput as j, TextShaper as k, ResolvedRunStyle as l, ResolvedFont as m, NormalizationPolicy as n, FixedPointRoundingMode as o, FontRequest as p, ResolvedRevisionMarkup as q, SemanticReviewArtifactRecord as r, ListMarkerPictureRecord as s, RevisionMarkupOptions as t, FontSubstitution as u, ShapedRun as v, RevisionChangedLinesMark as x, RevisionDeletionMark as y, RevisionMarkupChromeHandlers as z };