wordinweb 0.1.22 → 0.2.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.
@@ -0,0 +1,4314 @@
1
+ import * as react from 'react';
2
+
3
+ /**
4
+ * Minimal, fast XML parser producing a lightweight element tree.
5
+ *
6
+ * OOXML parts are machine-generated, namespace-prefixed, well-formed XML with
7
+ * no DTDs. This parser handles exactly that subset: elements, attributes,
8
+ * text, CDATA, comments, processing instructions, and the built-in + numeric
9
+ * character entities. Namespace prefixes are kept verbatim (`w:p`), and
10
+ * lookups match on either the full name or the local name, which is robust
11
+ * for every mainstream producer (Word, LibreOffice, Google Docs export).
12
+ */
13
+ interface XmlElement {
14
+ name: string;
15
+ attrs: Record<string, string>;
16
+ children: XmlElement[];
17
+ /** Concatenated character data directly inside this element. */
18
+ text: string;
19
+ /** Editor placeholder retained in memory but omitted from XML while empty. */
20
+ omitWhenEmpty?: boolean;
21
+ }
22
+
23
+ /**
24
+ * Typed document model produced by the parser and consumed by the layout
25
+ * engine. All lengths are CSS px (see units.ts) unless a field name says
26
+ * otherwise. Property bags are Partial-style: `undefined` means "not set at
27
+ * this level" so the style-inheritance merge can distinguish absence from an
28
+ * explicit value.
29
+ *
30
+ * Model nodes keep `src` references to the XML elements they were parsed
31
+ * from: the XML tree is the source of truth for editing — commands mutate it
32
+ * and the model is re-derived, which preserves round-trip fidelity for
33
+ * everything untouched.
34
+ */
35
+
36
+ type BorderStyle = "none" | "single" | "double" | "dotted" | "dashed" | "thick" | "wave" | "dotDash" | "dotDotDash" | "thinThickSmallGap" | "triple";
37
+ interface Border {
38
+ style: BorderStyle;
39
+ /** Stroke width in px. */
40
+ width: number;
41
+ /** CSS color. */
42
+ color: string;
43
+ /** Gap between border and content in px (w:space, points in OOXML). */
44
+ space: number;
45
+ /** Declared stroke width in px BEFORE the 0.75px paint floor (w:sz/8 pt).
46
+ * Word's LAYOUT math (table row border share) uses this true width — a
47
+ * sz-4 rule contributes 0.5pt, not the floored 0.5625pt (phase23 p66:
48
+ * 45 rows drift 2.5px down without it). */
49
+ rawWidth?: number;
50
+ }
51
+ interface ParagraphBorders {
52
+ top?: Border;
53
+ bottom?: Border;
54
+ left?: Border;
55
+ right?: Border;
56
+ /** Drawn between consecutive paragraphs that both specify it. */
57
+ between?: Border;
58
+ }
59
+ type LineSpacingRule = "auto" | "atLeast" | "exact";
60
+ interface LineSpacing {
61
+ rule: LineSpacingRule;
62
+ /**
63
+ * For "auto": multiple of single line height (240ths in OOXML, stored here
64
+ * as the multiplier, e.g. 1.15). For atLeast/exact: px.
65
+ */
66
+ value: number;
67
+ }
68
+ type Alignment = "left" | "center" | "right" | "justify";
69
+ interface TabStop {
70
+ /** px from the left text edge */
71
+ pos: number;
72
+ align: "left" | "center" | "right" | "decimal" | "bar";
73
+ leader: "none" | "dot" | "hyphen" | "underscore" | "middleDot";
74
+ /** w:val="clear": removes the inherited stop at this pos when tab lists
75
+ * merge down the style chain (never a live stop itself). */
76
+ clear?: boolean;
77
+ }
78
+ interface RunProps {
79
+ bold?: boolean;
80
+ italic?: boolean;
81
+ underline?: string;
82
+ strike?: boolean;
83
+ doubleStrike?: boolean;
84
+ /** Resolved primary font family name (ascii/hAnsi, theme-resolved). */
85
+ font?: string;
86
+ /** East Asian font (rFonts w:eastAsia): used for CJK codepoints. */
87
+ fontEastAsia?: string;
88
+ /** hAnsi font (rFonts w:hAnsi, theme-resolved): Word uses this channel for
89
+ * non-ASCII, non-CJK, non-complex characters (curly quotes, ≤, dashes). A
90
+ * run that declares only w:ascii leaves it inherited from the style chain. */
91
+ fontHAnsi?: string;
92
+ /** Complex-script font (rFonts w:cs): used for RTL/complex runs. */
93
+ fontComplex?: string;
94
+ /** w:rtl — this run's text is right-to-left (Arabic/Hebrew). */
95
+ rtl?: boolean;
96
+ /** Font size in px. */
97
+ size?: number;
98
+ /** CSS color; "auto" resolved to inherit/black by renderer. */
99
+ color?: string;
100
+ highlight?: string;
101
+ /** Character shading fill (w:shd). */
102
+ shading?: string;
103
+ /** Run border (w:bdr): a box painted around the run. Word merges adjacent
104
+ * runs with identical borders into one box; a run wrapping across lines
105
+ * closes the box on every line segment. */
106
+ border?: Border;
107
+ verticalAlign?: "baseline" | "superscript" | "subscript";
108
+ caps?: boolean;
109
+ smallCaps?: boolean;
110
+ /** Letter spacing px (w:spacing, twips in OOXML). */
111
+ letterSpacing?: number;
112
+ /** Minimum font size in px at which w:kern enables pair kerning; null is
113
+ * the explicit w:kern=0 override. */
114
+ kerningThreshold?: number | null;
115
+ /** Paint-only letter spacing when layout advances intentionally differ. */
116
+ paintLetterSpacing?: number;
117
+ /** w:w horizontal character scaling as a fraction (1.5 = 150%). */
118
+ textScale?: number;
119
+ /** w:fitText target width in px: the run's advances scale so its text
120
+ * occupies exactly this width (resolved to textScale at layout time). */
121
+ fitText?: number;
122
+ /** w:position baseline shift in px, positive = raised. */
123
+ raise?: number;
124
+ /** w:outline — hollow stroked glyphs. */
125
+ outline?: boolean;
126
+ /** w:emboss / w:imprint — Word triple-draws offset copies. */
127
+ emboss?: boolean;
128
+ imprint?: boolean;
129
+ vanish?: boolean;
130
+ /** Character style id (w:rStyle). */
131
+ styleId?: string;
132
+ lang?: string;
133
+ }
134
+ interface NumberingRef {
135
+ numId: number;
136
+ ilvl: number;
137
+ }
138
+ interface ParaProps {
139
+ styleId?: string;
140
+ alignment?: Alignment;
141
+ /** The exact OOXML w:jc value when it is a "justify" flavor whose behavior
142
+ * differs from plain "both": `distribute` also stretches the LAST line, and
143
+ * the Arabic `*Kashida` levels elongate glyphs (kashida) so lines pack fewer
144
+ * words and — at medium/high — wrap to more lines. Absent for plain "both".
145
+ * `alignment` stays "justify" for all of these. */
146
+ justifyKind?: "distribute" | "lowKashida" | "mediumKashida" | "highKashida";
147
+ /** w:bidi — right-to-left paragraph: lines assemble RTL and default
148
+ * alignment flips to the right edge. */
149
+ bidi?: boolean;
150
+ /** px */
151
+ indentLeft?: number;
152
+ indentRight?: number;
153
+ /** Positive px; mutually exclusive with hanging in effect. */
154
+ indentFirstLine?: number;
155
+ /** Positive px hanging indent. */
156
+ indentHanging?: number;
157
+ spacingBefore?: number;
158
+ spacingAfter?: number;
159
+ /** w:beforeAutospacing/afterAutospacing: HTML-style automatic paragraph
160
+ * spacing — Word ignores the literal before/after and inserts one blank
161
+ * line's worth of space. */
162
+ beforeAutospacing?: boolean;
163
+ afterAutospacing?: boolean;
164
+ /** Word's default when absent is auto/1.0 via docDefaults. */
165
+ lineSpacing?: LineSpacing;
166
+ /** w:snapToGrid: whether this paragraph's lines use the section docGrid. */
167
+ snapToGrid?: boolean;
168
+ contextualSpacing?: boolean;
169
+ keepNext?: boolean;
170
+ /** w:framePr w:dropCap: the paragraph is a drop-cap letter frame. */
171
+ dropCap?: {
172
+ mode: "drop" | "margin";
173
+ lines: number;
174
+ hSpace: number;
175
+ pageAnchored?: boolean;
176
+ };
177
+ /** w:framePr positioned text frame (absolute-positioned paragraph the body
178
+ * text wraps around). Geometry in px. Individual attributes are optional so a
179
+ * framePr merges attribute-by-attribute across the style cascade (a direct
180
+ * framePr that sets only h/x/y keeps the style framePr's width/anchor — IEEE
181
+ * authors); the engine normalizes defaults at layout time. */
182
+ frame?: {
183
+ w?: number;
184
+ h?: number;
185
+ hRule?: "auto" | "atLeast" | "exact";
186
+ x?: number;
187
+ y?: number;
188
+ hAnchor?: "page" | "margin" | "text" | "column";
189
+ vAnchor?: "page" | "margin" | "text" | "paragraph";
190
+ xAlign?: "left" | "center" | "right" | "inside" | "outside";
191
+ yAlign?: "top" | "center" | "bottom" | "inside" | "outside" | "inline";
192
+ wrap?: "around" | "auto" | "notBeside" | "through" | "tight" | "none";
193
+ /** w:hSpace / w:vSpace: horizontal/vertical margin around the frame (px). */
194
+ hSpace?: number;
195
+ vSpace?: number;
196
+ };
197
+ keepLines?: boolean;
198
+ pageBreakBefore?: boolean;
199
+ widowControl?: boolean;
200
+ borders?: ParagraphBorders;
201
+ /** Paragraph shading fill as CSS color. */
202
+ shading?: string;
203
+ numbering?: NumberingRef | null;
204
+ /** A numPr that carries ilvl but no numId (Heading3 basedOn Heading2)
205
+ * overrides only the list LEVEL, keeping the inherited numId; the style
206
+ * chain applies it to `numbering` on merge. */
207
+ numberingLevelOverride?: number;
208
+ tabs?: TabStop[];
209
+ outlineLevel?: number;
210
+ /** Run props declared on pPr/rPr — apply to the paragraph mark & numbering label. */
211
+ markRunProps?: RunProps;
212
+ /**
213
+ * styleId of the enclosing table (set on cell paragraphs at parse time).
214
+ * A table style's own pPr layers between docDefaults and the paragraph
215
+ * style, e.g. TableGrid's `spacing after=0 line=240` overrides docDefaults'
216
+ * `after=200 line=276` so list cells lay out compactly.
217
+ */
218
+ tableStyleId?: string;
219
+ /**
220
+ * Grid position of the enclosing cell plus the table's tblLook (set on cell
221
+ * paragraphs at parse time, like tableStyleId). Lets run/paragraph
222
+ * resolution layer the applicable w:tblStylePr conditional formats (e.g.
223
+ * firstRow bold + white) between the table style's own rPr and the
224
+ * paragraph style chain.
225
+ */
226
+ tableCellCond?: {
227
+ look?: TableLook;
228
+ rowIdx: number;
229
+ nRows: number;
230
+ colStart: number;
231
+ colSpan: number;
232
+ nCols: number;
233
+ };
234
+ }
235
+ interface TextContent {
236
+ kind: "text";
237
+ text: string;
238
+ /** Source w:t element inside the run, when this text came verbatim from one. */
239
+ srcT?: XmlElement;
240
+ /** When this text is the glyph of a modern checkbox content control, the
241
+ * source `w14:checkbox` element (in the enclosing w:sdt's sdtPr). Its
242
+ * presence makes the glyph a click-to-toggle target rather than plain text. */
243
+ checkbox?: XmlElement;
244
+ }
245
+ interface BreakContent {
246
+ kind: "break";
247
+ breakType: "line" | "page" | "column";
248
+ }
249
+ interface TabContent {
250
+ kind: "tab";
251
+ }
252
+ interface PTabContent {
253
+ kind: "ptab";
254
+ /** Absolute-position tab: jump to left/center/right of the base. */
255
+ alignment: "left" | "center" | "right";
256
+ relativeTo: "margin" | "indent";
257
+ }
258
+ interface ImageContent {
259
+ kind: "image";
260
+ /** Part path of the image inside the package. */
261
+ part: string;
262
+ width: number;
263
+ height: number;
264
+ /** True when the drawing is floating (wp:anchor); v1 lays it out inline. */
265
+ anchored?: boolean;
266
+ /** a:srcRect crop, fractions of the source bitmap. */
267
+ crop?: {
268
+ l: number;
269
+ t: number;
270
+ r: number;
271
+ b: number;
272
+ };
273
+ /** a:xfrm rotation, degrees clockwise. */
274
+ rotation?: number;
275
+ /** a:ln picture outline (Word draws it just outside the image). */
276
+ border?: {
277
+ color: string;
278
+ width: number;
279
+ };
280
+ /** Source w:drawing (or pict) element, for resize/move editing. */
281
+ srcDrawing?: XmlElement;
282
+ /** Native Office 3D model shown through its saved poster raster. */
283
+ model3D?: Model3DReference;
284
+ /** Word online-video metadata shown through its saved poster raster. */
285
+ webVideo?: WebVideoReference;
286
+ /** Embedded OLE package activated as a safe download in the browser. */
287
+ embeddedObject?: EmbeddedObjectReference;
288
+ }
289
+ interface Model3DReference {
290
+ part: string;
291
+ posterPart: string;
292
+ /** Native Office model orientation, in degrees around the X/Y/Z axes. */
293
+ rotation?: {
294
+ x: number;
295
+ y: number;
296
+ z: number;
297
+ };
298
+ }
299
+ interface WebVideoReference {
300
+ url: string;
301
+ embeddedHtml: string;
302
+ width: number;
303
+ height: number;
304
+ }
305
+ interface EmbeddedObjectReference {
306
+ part: string;
307
+ filename: string;
308
+ progId: string;
309
+ }
310
+ /** OMML equation node (subset: runs, scripts, fractions, radicals). */
311
+ type MathNode = {
312
+ t: "run";
313
+ text: string;
314
+ normal?: boolean;
315
+ /** m:func function name (cos, log, …): Word kerns a thin space on
316
+ * both sides of the name. */
317
+ fname?: boolean;
318
+ } | {
319
+ t: "sup" | "sub";
320
+ base: MathNode[];
321
+ script: MathNode[];
322
+ } | {
323
+ t: "frac";
324
+ num: MathNode[];
325
+ den: MathNode[];
326
+ bar?: boolean;
327
+ }
328
+ /** Radical; deg is the optional m:deg index (∛), absent when m:degHide. */
329
+ | {
330
+ t: "rad";
331
+ e: MathNode[];
332
+ deg?: MathNode[];
333
+ }
334
+ /** n-ary operator (sum/integral); chr defaults to the integral sign. */
335
+ | {
336
+ t: "nary";
337
+ chr: string;
338
+ sub: MathNode[];
339
+ sup: MathNode[];
340
+ e: MathNode[];
341
+ }
342
+ /** Delimiters grown to the content height; beg/end default to parens.
343
+ * An empty beg/end string means that side has no delimiter (cases "{"). */
344
+ | {
345
+ t: "dlm";
346
+ beg: string;
347
+ end: string;
348
+ e: MathNode[][];
349
+ }
350
+ /** Matrix: rows x cells. */
351
+ | {
352
+ t: "mat";
353
+ rows: MathNode[][][];
354
+ }
355
+ /** m:eqArr equation array (cases/piecewise rows). */
356
+ | {
357
+ t: "eqarr";
358
+ rows: MathNode[][];
359
+ }
360
+ /** m:acc combining accent (hat/bar/vector) over the base. */
361
+ | {
362
+ t: "acc";
363
+ chr: string;
364
+ e: MathNode[];
365
+ }
366
+ /** m:groupChr horizontal group character (over/under brace). */
367
+ | {
368
+ t: "grp";
369
+ chr: string;
370
+ pos: "top" | "bot";
371
+ vertJc: "top" | "bot";
372
+ e: MathNode[];
373
+ }
374
+ /** m:limLow / m:limUpp limit stacked under/over a text operator. */
375
+ | {
376
+ t: "lim";
377
+ pos: "low" | "upp";
378
+ e: MathNode[];
379
+ lim: MathNode[];
380
+ };
381
+ interface MathContent {
382
+ kind: "math";
383
+ nodes: MathNode[];
384
+ /** Source m:oMath element (math editing). */
385
+ src?: XmlElement;
386
+ /** Display equation (wrapped in m:oMathPara): centered on its own line with
387
+ * display-style layout - larger n-ary operators with limits stacked
388
+ * above/below, and full-size fraction numerators/denominators. */
389
+ display?: boolean;
390
+ /** Display group with several direct equations. Word leaves a small
391
+ * advance after script clusters in these grouped displays. */
392
+ multiEquationDisplay?: boolean;
393
+ /** m:oMathParaPr/m:jc - display-equation justification. Absent: the
394
+ * document default (settings m:mathPr/m:defJc, itself "centerGroup" when
395
+ * unset). */
396
+ jc?: "left" | "right" | "center" | "centerGroup";
397
+ }
398
+ interface FieldContent {
399
+ kind: "field";
400
+ /** Raw field instruction, e.g. ` PAGE \\* MERGEFORMAT `. */
401
+ instruction: string;
402
+ /** Last cached result text from the file, used for unsupported fields. */
403
+ cachedResult: string;
404
+ /** When this field is a legacy FORMCHECKBOX, the source `w:checkBox` element
405
+ * (in the fldChar begin's w:ffData). Its presence makes the rendered ballot
406
+ * box a click-to-toggle target; the glyph derives from its w:checked child. */
407
+ checkbox?: XmlElement;
408
+ }
409
+ /** What an anchored shape's coordinates are measured from. "char" (relH=
410
+ * "character") measures from the anchor run's pen position; "line" (relV=
411
+ * "line") from the top of the line holding the anchor — both resolved from
412
+ * the anchor paragraph's first-pass layout. */
413
+ type AnchorRel = "page" | "margin" | "text" | "column" | "char" | "line";
414
+ interface ShapeLine {
415
+ type: "line";
416
+ x1: number;
417
+ y1: number;
418
+ x2: number;
419
+ y2: number;
420
+ color: string;
421
+ /** Stroke weight px. */
422
+ weight: number;
423
+ hRel: AnchorRel;
424
+ vRel: AnchorRel;
425
+ }
426
+ interface ShapeTextbox {
427
+ type: "textbox";
428
+ x: number;
429
+ y: number;
430
+ width: number;
431
+ height: number;
432
+ hRel: AnchorRel;
433
+ vRel: AnchorRel;
434
+ blocks: Block[];
435
+ /** Background fill (CSS color) painted behind the text. */
436
+ fill?: string;
437
+ /** Outline. */
438
+ stroke?: {
439
+ color: string;
440
+ weight: number;
441
+ };
442
+ /** Alignment-based positioning (mso-position-horizontal/vertical). */
443
+ hAlign?: "left" | "center" | "right";
444
+ vAlign?: "top" | "center" | "bottom";
445
+ /** Percent-of-base geometry, 0..1 (mso-*-percent / wp14 pct offsets).
446
+ * Bases: pctWidthRel/pctHeightRel say page vs margin. */
447
+ pctX?: number;
448
+ pctY?: number;
449
+ pctWidth?: number;
450
+ pctHeight?: number;
451
+ pctWidthRel?: "page" | "margin";
452
+ pctHeightRel?: "page" | "margin";
453
+ /** Vertical anchoring of the text INSIDE the box (v-text-anchor). */
454
+ textAnchor?: "top" | "middle" | "bottom";
455
+ /** How body text flows around a DrawingML textbox (wp:wrap*). */
456
+ wrap?: WrapMode;
457
+ /** behindDoc: paint under the body text, never displace it. */
458
+ behind?: boolean;
459
+ /** wp:anchor @allowOverlap="0": Word shifts this shape clear of earlier
460
+ * overlapping floats rather than letting them overlap. Default true. */
461
+ allowOverlap?: boolean;
462
+ /** Wrap distances px (wp:anchor distT/B/L/R). */
463
+ dist?: {
464
+ t: number;
465
+ b: number;
466
+ l: number;
467
+ r: number;
468
+ };
469
+ /** a:xfrm rotation, degrees clockwise (rotates the whole box). */
470
+ rotation?: number;
471
+ /** Text insets px (bodyPr lIns/tIns/rIns/bIns); default 9.6/4.8. For
472
+ * non-rect preset geometries these INCLUDE the geometry's text-rectangle
473
+ * insets (ellipse: inscribed rect; diamond: middle-half rect). */
474
+ insets?: {
475
+ l: number;
476
+ t: number;
477
+ r: number;
478
+ b: number;
479
+ };
480
+ /** Non-rect preset geometry: paint this outline (in a viewW x viewH space
481
+ * scaled to the shape box) instead of a rectangle. */
482
+ geom?: {
483
+ d: string;
484
+ viewW: number;
485
+ viewH: number;
486
+ };
487
+ /** bodyPr a:noAutofit: the box does NOT grow with its text — Word hides
488
+ * whole lines that stick out past the shape bottom (phase23 p12 ovals:
489
+ * "Was 0 / B" visible, the wrapped tail rows hidden). */
490
+ clipText?: boolean;
491
+ /** bodyPr a:spAutoFit: the box grows in height to exactly fit its laid text
492
+ * plus the top/bottom insets (the stored cy is only Word's last cached
493
+ * value). Width is fixed. */
494
+ autofitHeight?: boolean;
495
+ /** Linked text-box chain id (wps:txbx/@id or wps:linkedTxbx/@id): boxes with
496
+ * the same id form one story. The seq-0 box (wps:txbx) holds the content;
497
+ * later boxes (wps:linkedTxbx, chainSeq>0) are empty sinks that continue the
498
+ * overflow. */
499
+ chainId?: string;
500
+ /** Position in the linked chain (0 = the content box). */
501
+ chainSeq?: number;
502
+ /** Source w:drawing element, attached at parse time so the shape's fill can
503
+ * be an interactive hit target (select the shape instead of the body text
504
+ * behind it). */
505
+ srcDrawing?: XmlElement;
506
+ /** Header/footer-owned textbox whose text is edited as an independent story. */
507
+ textboxStory?: boolean;
508
+ /** DrawingML WordArt, including the identity textNoShape preset. */
509
+ wordArt?: boolean;
510
+ /** bodyPr a:prstTxWarp preset name (textArchUp, textWave1, textChevron,
511
+ * textCirclePour, …): the shape's text is bent onto the preset's envelope
512
+ * rather than flowed as ordinary lines. "textNoShape" (no warp) is dropped. */
513
+ warp?: string;
514
+ }
515
+ /** WordArt (VML v:textpath, e.g. a "CONFIDENTIAL" watermark): text scaled to
516
+ * fill a box, optionally rotated, painted semi-transparent behind the body. */
517
+ interface ShapeWordArt {
518
+ type: "wordart";
519
+ text: string;
520
+ fontFamily: string;
521
+ bold?: boolean;
522
+ italic?: boolean;
523
+ /** CSS fill color. */
524
+ fill: string;
525
+ /** 0..1 alpha. */
526
+ opacity: number;
527
+ x: number;
528
+ y: number;
529
+ width: number;
530
+ height: number;
531
+ hRel: AnchorRel;
532
+ vRel: AnchorRel;
533
+ hAlign?: "left" | "center" | "right";
534
+ vAlign?: "top" | "center" | "bottom";
535
+ /** Clockwise degrees. */
536
+ rotation: number;
537
+ behind?: boolean;
538
+ /** The source v:textpath font-size (px). */
539
+ fontSize?: number;
540
+ /** The referenced v:shapetype has a malformed text-guide path (missing
541
+ * coordinates), so Word collapses its horizontal glyph outlines into a thin
542
+ * vertical band instead of painting a box-filling watermark. */
543
+ noFit?: boolean;
544
+ /** Source VML shape element (v:shape / v:rect) for interactive editing:
545
+ * the v:textpath string, fill/opacity, and rotation live under it. */
546
+ src?: XmlElement;
547
+ }
548
+ interface ShapeArt {
549
+ type: "art";
550
+ srcDrawing?: XmlElement;
551
+ /** A freehand stroke, selectable by the Draw ribbon's stroke eraser. */
552
+ ink?: boolean;
553
+ x: number;
554
+ y: number;
555
+ /** Percent-of-page offsets (wp14:pctPos*Offset), 0..1. */
556
+ pctX?: number;
557
+ pctY?: number;
558
+ width: number;
559
+ height: number;
560
+ hRel: AnchorRel;
561
+ vRel: AnchorRel;
562
+ hAlign?: "left" | "center" | "right";
563
+ vAlign?: "top" | "center" | "bottom";
564
+ behind?: boolean;
565
+ /** a:xfrm rotation, degrees clockwise. */
566
+ rotation?: number;
567
+ lines: DrawingLine[];
568
+ images: DrawingImage[];
569
+ paths: DrawingPath[];
570
+ /** Positioned text bodies inside the group (wps textboxes, dsp:sp txBody). */
571
+ texts?: DrawingTextShape[];
572
+ }
573
+ /** How text interacts with a floating image. */
574
+ type WrapMode = "square" | "topAndBottom" | "none";
575
+ interface ShapeImage {
576
+ type: "image";
577
+ part: string;
578
+ /** Offset from the anchor origin, px. */
579
+ x: number;
580
+ y: number;
581
+ width: number;
582
+ height: number;
583
+ hRel: AnchorRel;
584
+ vRel: AnchorRel;
585
+ /** Horizontal alignment when the file uses wp:align instead of an offset. */
586
+ hAlign?: "left" | "center" | "right";
587
+ /** Vertical alignment keyword (VML mso-position-vertical; watermarks center). */
588
+ vAlign?: "top" | "center" | "bottom";
589
+ wrap: WrapMode;
590
+ /** behindDoc anchors render under the text and never displace it. */
591
+ behind?: boolean;
592
+ /** Wrap distances px (wp:anchor distT/B/L/R); text clears the image by these. */
593
+ dist?: {
594
+ t: number;
595
+ b: number;
596
+ l: number;
597
+ r: number;
598
+ };
599
+ crop?: {
600
+ l: number;
601
+ t: number;
602
+ r: number;
603
+ b: number;
604
+ };
605
+ rotation?: number;
606
+ /** Watermark "washout" recolor (VML v:imagedata gain/blacklevel, 0..1). */
607
+ washout?: {
608
+ gain: number;
609
+ blacklevel: number;
610
+ };
611
+ /** Source w:drawing element (editing). */
612
+ srcDrawing?: XmlElement;
613
+ model3D?: Model3DReference;
614
+ webVideo?: WebVideoReference;
615
+ embeddedObject?: EmbeddedObjectReference;
616
+ }
617
+ type Shape = (ShapeLine | ShapeTextbox | ShapeImage | ShapeArt | ShapeWordArt) & {
618
+ /** wp:anchor relativeHeight, used for object stacking order. */
619
+ z?: number;
620
+ };
621
+ /**
622
+ * Floating/anchored object: does not occupy inline space; positioned against
623
+ * page/margin/paragraph during layout. (How classic pleading paper draws its
624
+ * margin line numbers and vertical rules.)
625
+ */
626
+ interface AnchorContent {
627
+ kind: "anchor";
628
+ shape: Shape;
629
+ }
630
+ /** A stroked segment inside a composite drawing, px relative to its box. */
631
+ interface DrawingLine {
632
+ x1: number;
633
+ y1: number;
634
+ x2: number;
635
+ y2: number;
636
+ color: string;
637
+ weight: number;
638
+ style?: "single" | "dotted" | "dashed";
639
+ }
640
+ interface DrawingImage {
641
+ part: string;
642
+ x: number;
643
+ y: number;
644
+ width: number;
645
+ height: number;
646
+ crop?: {
647
+ l: number;
648
+ t: number;
649
+ r: number;
650
+ b: number;
651
+ };
652
+ rotation?: number;
653
+ /** a:ln picture outline (Word draws it just outside the image). */
654
+ border?: {
655
+ color: string;
656
+ width: number;
657
+ };
658
+ }
659
+ /**
660
+ * Composite inline drawing (DrawingML groups): vector lines + placed images
661
+ * inside a width×height box that flows like an image.
662
+ */
663
+ interface DrawingContent {
664
+ kind: "drawing";
665
+ width: number;
666
+ height: number;
667
+ lines: DrawingLine[];
668
+ images: DrawingImage[];
669
+ /** Native ChartML chart data resolved from the related chart part. */
670
+ chart?: ChartData;
671
+ /** Native SmartArt diagram data resolved from its diagram parts. */
672
+ smartArt?: SmartArtData;
673
+ /** Freeform vector shapes (a:custGeom), as SVG path data. */
674
+ paths?: DrawingPath[];
675
+ /** An INLINE wps text box (wp:inline wps:txbx): a fixed-extent box that
676
+ * flows in the text (occupying its width x height like an inline image) and
677
+ * carries a fill/border + its own block content. Distinct from the floating
678
+ * ShapeTextbox, which is absolutely placed via a wp:anchor. */
679
+ textbox?: {
680
+ blocks: Block[];
681
+ fill?: string;
682
+ stroke?: {
683
+ color: string;
684
+ weight: number;
685
+ };
686
+ insets?: {
687
+ l: number;
688
+ t: number;
689
+ r: number;
690
+ b: number;
691
+ };
692
+ textAnchor?: "top" | "middle" | "bottom";
693
+ };
694
+ /** Source w:drawing element (select/move as a group). */
695
+ srcDrawing?: XmlElement;
696
+ /** Positioned text bodies inside the drawing (multi-textbox groups and
697
+ * SmartArt cached-drawing shapes). Painted above lines/paths. */
698
+ texts?: DrawingTextShape[];
699
+ }
700
+ type ChartType = "column" | "bar" | "line" | "pie";
701
+ interface ChartSeries {
702
+ name: string;
703
+ values: number[];
704
+ }
705
+ interface ChartData {
706
+ type: ChartType;
707
+ title?: string;
708
+ categories: string[];
709
+ series: ChartSeries[];
710
+ }
711
+ type SmartArtLayout = "process" | "cycle" | "hierarchy" | "list";
712
+ interface SmartArtData {
713
+ layout: SmartArtLayout;
714
+ items: string[];
715
+ }
716
+ /** A text body positioned inside a composite drawing (a wps textbox in a
717
+ * group, or a SmartArt dsp:sp txBody), px relative to the drawing's box. */
718
+ interface DrawingTextShape {
719
+ x: number;
720
+ y: number;
721
+ width: number;
722
+ height: number;
723
+ blocks: Block[];
724
+ /** Background fill painted behind the text box. */
725
+ fill?: string;
726
+ /** Outline. */
727
+ stroke?: {
728
+ color: string;
729
+ weight: number;
730
+ };
731
+ /** Text insets px (bodyPr lIns/tIns/rIns/bIns). */
732
+ insets: {
733
+ l: number;
734
+ t: number;
735
+ r: number;
736
+ b: number;
737
+ };
738
+ /** Vertical anchoring of the text inside the box (bodyPr anchor). */
739
+ textAnchor?: "top" | "middle" | "bottom";
740
+ /** DrawingML cached text paint-only baseline adjustment in px. */
741
+ paintOffsetY?: number;
742
+ }
743
+ interface DrawingPath {
744
+ /** Position/size inside the drawing, px. */
745
+ x: number;
746
+ y: number;
747
+ width: number;
748
+ height: number;
749
+ /** SVG path in the `viewW x viewH` source coordinate space. */
750
+ d: string;
751
+ viewW: number;
752
+ viewH: number;
753
+ fill?: string;
754
+ stroke?: {
755
+ color: string;
756
+ width: number;
757
+ opacity?: number;
758
+ };
759
+ /** Zero-based visible SmartArt node this path paints. */
760
+ smartArtNodeIndex?: number;
761
+ }
762
+ /**
763
+ * Footnote/endnote reference mark. In body text (w:footnoteReference /
764
+ * w:endnoteReference) `id` points into the notes part. Inside a note body,
765
+ * w:footnoteRef / w:endnoteRef render the note's own mark: `self` is true and
766
+ * `id` is meaningless.
767
+ */
768
+ interface NoteRefContent {
769
+ kind: "noteRef";
770
+ noteType: "footnote" | "endnote";
771
+ id: number;
772
+ self?: boolean;
773
+ /** The document supplies the visible mark in the following run content. */
774
+ customMarkFollows?: boolean;
775
+ }
776
+ /** w:ruby — an East-Asian ruby (furigana) cluster: an annotation run (w:rt)
777
+ * riding above a base run (w:rubyBase). Both sides render as their own runs;
778
+ * the annotation is centered over the base and raised above its baseline. */
779
+ interface RubyContent {
780
+ kind: "ruby";
781
+ base: Run;
782
+ rt: Run;
783
+ /** w:rubyPr/w:hpsRaise — annotation baseline raise above the base baseline,
784
+ * half-points. */
785
+ hpsRaise?: number;
786
+ /** w:rubyPr/w:rubyAlign — how the annotation distributes over the base. */
787
+ align?: "center" | "distributeLetter" | "distributeSpace" | "left" | "right" | "rightVertical";
788
+ }
789
+ type RunContent = MathContent | TextContent | BreakContent | TabContent | PTabContent | ImageContent | FieldContent | AnchorContent | DrawingContent | NoteRefContent | RubyContent;
790
+ interface Run {
791
+ type: "run";
792
+ props: RunProps;
793
+ content: RunContent[];
794
+ /** Source w:r element. */
795
+ src?: XmlElement;
796
+ /** Element whose children array contains src (w:p, w:hyperlink, …). */
797
+ srcParent?: XmlElement;
798
+ }
799
+ interface Hyperlink {
800
+ type: "hyperlink";
801
+ href?: string;
802
+ anchor?: string;
803
+ runs: Run[];
804
+ }
805
+ type ParaChild = Run | Hyperlink;
806
+ interface Paragraph {
807
+ type: "paragraph";
808
+ props: ParaProps;
809
+ children: ParaChild[];
810
+ /** Section break attached to this paragraph's pPr (ends a section). */
811
+ sectionBreak?: SectionProps;
812
+ /** w:bookmarkStart names in this paragraph (PAGEREF targets). */
813
+ bookmarks?: string[];
814
+ /** Final revision view: the paragraph mark AND all content are tracked
815
+ * deletions — the paragraph does not exist (no line, no numbering). */
816
+ revisionHidden?: boolean;
817
+ /** Source w:p element. */
818
+ src?: XmlElement;
819
+ }
820
+ interface TableCellProps {
821
+ /** Preferred width px (from tcW when dxa). */
822
+ width?: number;
823
+ /** The source explicitly declares tcW type="auto" (distinct from no tcW). */
824
+ widthAuto?: boolean;
825
+ gridSpan: number;
826
+ vMerge?: "restart" | "continue";
827
+ borders?: {
828
+ top?: Border;
829
+ bottom?: Border;
830
+ left?: Border;
831
+ right?: Border;
832
+ tl2br?: Border;
833
+ tr2bl?: Border;
834
+ };
835
+ shading?: string;
836
+ /** Cell margins px. */
837
+ margins?: {
838
+ top?: number;
839
+ right?: number;
840
+ bottom?: number;
841
+ left?: number;
842
+ };
843
+ verticalAlign?: "top" | "center" | "bottom";
844
+ textDirection?: "btLr" | "tbRl";
845
+ }
846
+ interface TableCell {
847
+ props: TableCellProps;
848
+ blocks: Block[];
849
+ }
850
+ interface TableRowProps {
851
+ /** px; undefined = auto */
852
+ height?: number;
853
+ heightRule?: "auto" | "atLeast" | "exact";
854
+ cantSplit?: boolean;
855
+ /** Repeat as header row on each page. */
856
+ tblHeader?: boolean;
857
+ }
858
+ interface TableRow {
859
+ props: TableRowProps;
860
+ cells: TableCell[];
861
+ }
862
+ interface TableProps {
863
+ styleId?: string;
864
+ /** px table indent from left margin. */
865
+ indent?: number;
866
+ alignment?: Alignment;
867
+ /** w:bidiVisual — RTL table: column order reverses and the table hugs the
868
+ * right margin. */
869
+ bidiVisual?: boolean;
870
+ borders?: {
871
+ top?: Border;
872
+ bottom?: Border;
873
+ left?: Border;
874
+ right?: Border;
875
+ insideH?: Border;
876
+ insideV?: Border;
877
+ };
878
+ /** Default cell margins px. */
879
+ cellMargins?: {
880
+ top?: number;
881
+ right?: number;
882
+ bottom?: number;
883
+ left?: number;
884
+ };
885
+ /** w:tblCellSpacing px — old-style separated cell borders: every cell gets
886
+ * its own border box inset by this much from the grid slot on all sides
887
+ * (adjacent cells end up 2x apart), and the table outline is a separate box
888
+ * around everything. */
889
+ cellSpacing?: number;
890
+ /** w:tblpPr — floating table: absolutely positioned against page/margin/
891
+ * column, painted over an opaque white sheet, with body text (including
892
+ * text EARLIER in the flow on the same page) wrapping square around it. */
893
+ floating?: {
894
+ hAnchor: "page" | "margin" | "text";
895
+ vAnchor: "page" | "margin" | "text";
896
+ /** tblpX/tblpY px (absent when an alignment keyword is used). */
897
+ x?: number;
898
+ y?: number;
899
+ xAlign?: "left" | "center" | "right";
900
+ yAlign?: "top" | "center" | "bottom";
901
+ /** Wrap distances px (leftFromText, …). */
902
+ dist: {
903
+ l: number;
904
+ r: number;
905
+ t: number;
906
+ b: number;
907
+ };
908
+ /** w:tblOverlap "never": shift clear of earlier floating tables. */
909
+ allowOverlap: boolean;
910
+ };
911
+ /** Preferred total width px (tblW dxa) or undefined for auto. */
912
+ width?: number;
913
+ /** Preferred total width as a fraction of available width (tblW pct). */
914
+ widthPct?: number;
915
+ layout?: "fixed" | "autofit";
916
+ /** w:tblLook flags controlling which conditional formats apply. */
917
+ tblLook?: TableLook;
918
+ }
919
+ /** w:tblLook flags controlling which conditional table-style formats apply. */
920
+ interface TableLook {
921
+ firstRow: boolean;
922
+ lastRow: boolean;
923
+ firstColumn: boolean;
924
+ lastColumn: boolean;
925
+ noHBand: boolean;
926
+ noVBand: boolean;
927
+ }
928
+ interface Table {
929
+ type: "table";
930
+ props: TableProps;
931
+ /** Column grid widths px (tblGrid). */
932
+ grid: number[];
933
+ rows: TableRow[];
934
+ /** Source w:tbl element. */
935
+ src?: XmlElement;
936
+ }
937
+ type Block = Paragraph | Table;
938
+ interface HeaderFooterRefs {
939
+ default?: string;
940
+ first?: string;
941
+ even?: string;
942
+ }
943
+ interface ColumnSpec {
944
+ count: number;
945
+ /** Space between columns px. */
946
+ space: number;
947
+ /** Explicit widths px when equalWidth=false. */
948
+ widths?: number[];
949
+ /** Per-column trailing space px (w:col w:space), when explicit w:col
950
+ * entries are present. spaces[i] separates column i from column i+1. */
951
+ spaces?: number[];
952
+ /** w:cols w:sep: paint a vertical rule centered in each inter-column gap. */
953
+ sep?: boolean;
954
+ }
955
+ interface SectionProps {
956
+ pageWidth: number;
957
+ pageHeight: number;
958
+ marginTop: number;
959
+ marginRight: number;
960
+ marginBottom: number;
961
+ marginLeft: number;
962
+ /** Distance of header top from page top, px (w:headerReference distance). */
963
+ headerDistance: number;
964
+ footerDistance: number;
965
+ gutter: number;
966
+ headerRefs: HeaderFooterRefs;
967
+ footerRefs: HeaderFooterRefs;
968
+ /** Different first-page header/footer enabled. */
969
+ titlePage: boolean;
970
+ /** Page numbering restart value, if set. */
971
+ pageNumberStart?: number;
972
+ pageNumberFormat?: string;
973
+ columns: ColumnSpec;
974
+ /** w:docGrid type=lines/linesAndChars: minimum single-line height (px) that
975
+ * the line-spacing multiplier is applied over. Word snaps every line's font
976
+ * height up to this grid pitch (CJK documents). */
977
+ docGridLinePitch?: number;
978
+ /** Present w:docGrid type; omitted w:type means "default". */
979
+ docGridType?: "default" | "lines" | "linesAndChars" | "snapToChars";
980
+ /** w:docGrid type="charsAndLines": a combined char+line grid that (in compat
981
+ * 15) keeps natural East-Asian line pitch. Drives the CJK line-height
982
+ * correction during measurement. */
983
+ docGridCharGrid?: boolean;
984
+ type?: "nextPage" | "continuous" | "evenPage" | "oddPage" | "nextColumn";
985
+ /** Vertical alignment of page content. */
986
+ vAlign?: "top" | "center" | "both" | "bottom";
987
+ /** w:pgBorders. Offsets (border.space, px) measure from text or page edge. */
988
+ pageBorders?: {
989
+ top?: Border;
990
+ bottom?: Border;
991
+ left?: Border;
992
+ right?: Border;
993
+ offsetFrom: "text" | "page";
994
+ };
995
+ /** w:lnNumType: margin line numbering. distance px from the text edge. */
996
+ lineNumbering?: {
997
+ countBy: number;
998
+ start: number;
999
+ distance: number;
1000
+ restart: "continuous" | "newPage" | "newSection";
1001
+ };
1002
+ /** Footnote/endnote mark numbering (w:footnotePr / w:endnotePr). */
1003
+ footnoteNumFmt?: string;
1004
+ footnoteNumStart?: number;
1005
+ endnoteNumFmt?: string;
1006
+ endnoteNumStart?: number;
1007
+ /** w:textDirection tbRl: the whole section flows as East-Asian vertical
1008
+ * writing — lines run top-to-bottom, progressing right-to-left. */
1009
+ textDirection?: "tbRl";
1010
+ }
1011
+ interface Section {
1012
+ props: SectionProps;
1013
+ blocks: Block[];
1014
+ }
1015
+ /** A review comment from word/comments.xml. */
1016
+ interface DocComment {
1017
+ id: string;
1018
+ author: string;
1019
+ initials?: string;
1020
+ /** ISO timestamp from w:date, verbatim. */
1021
+ date?: string;
1022
+ /** Plain text of the comment body (paragraphs joined with newlines). */
1023
+ text: string;
1024
+ /** w14:paraId of the comment's last body paragraph (threading key). */
1025
+ paraId?: string;
1026
+ /** Parent comment id when this comment is a reply (commentsExtended). */
1027
+ parentId?: string;
1028
+ }
1029
+ interface HeaderFooter {
1030
+ blocks: Block[];
1031
+ }
1032
+ /** One w:tblStylePr conditional-formatting block (firstRow, band1Horz, …). */
1033
+ interface TableCondFormat {
1034
+ /** Cell shading fill as CSS color. */
1035
+ shd?: string;
1036
+ /** Conditional cell borders. */
1037
+ borders?: {
1038
+ top?: Border;
1039
+ bottom?: Border;
1040
+ left?: Border;
1041
+ right?: Border;
1042
+ insideH?: Border;
1043
+ insideV?: Border;
1044
+ };
1045
+ /** Conditional run bold (firstRow/firstCol headers). */
1046
+ bold?: boolean;
1047
+ /** Full conditional run props (w:tblStylePr > w:rPr). */
1048
+ rPr?: RunProps;
1049
+ }
1050
+ type TableCondType = "wholeTable" | "band1Vert" | "band2Vert" | "band1Horz" | "band2Horz" | "firstRow" | "lastRow" | "firstCol" | "lastCol" | "nwCell" | "neCell" | "swCell" | "seCell";
1051
+ interface Style {
1052
+ id: string;
1053
+ type: "paragraph" | "character" | "table" | "numbering";
1054
+ name?: string;
1055
+ basedOn?: string;
1056
+ isDefault?: boolean;
1057
+ pPr?: ParaProps;
1058
+ rPr?: RunProps;
1059
+ tblPr?: TableProps;
1060
+ /** w:tblStylePr conditional formats, by type (table styles only). */
1061
+ condFormats?: Map<TableCondType, TableCondFormat>;
1062
+ /** w:tblStyleRowBandSize / ColBandSize (default 1). */
1063
+ rowBandSize?: number;
1064
+ colBandSize?: number;
1065
+ }
1066
+ interface Styles {
1067
+ byId: Map<string, Style>;
1068
+ defaultParagraphStyle?: string;
1069
+ defaultRPr: RunProps;
1070
+ defaultPPr: ParaProps;
1071
+ }
1072
+ interface NumberingLevel {
1073
+ ilvl: number;
1074
+ start: number;
1075
+ format: string;
1076
+ /** Template like "%1." */
1077
+ text: string;
1078
+ alignment: Alignment;
1079
+ /** Paragraph props contributed by the level (indents). */
1080
+ pPr?: ParaProps;
1081
+ rPr?: RunProps;
1082
+ suffix: "tab" | "space" | "nothing";
1083
+ restartAfter?: number;
1084
+ }
1085
+ interface AbstractNum {
1086
+ id: number;
1087
+ levels: Map<number, NumberingLevel>;
1088
+ numStyleLink?: string;
1089
+ }
1090
+ interface NumInstance {
1091
+ numId: number;
1092
+ abstractNumId: number;
1093
+ overrides: Map<number, {
1094
+ startOverride?: number;
1095
+ level?: NumberingLevel;
1096
+ }>;
1097
+ }
1098
+ interface Numbering {
1099
+ abstract: Map<number, AbstractNum>;
1100
+ instances: Map<number, NumInstance>;
1101
+ }
1102
+ interface Theme {
1103
+ majorFont: string;
1104
+ minorFont: string;
1105
+ majorBidiFont?: string;
1106
+ minorBidiFont?: string;
1107
+ /** East Asian faces from theme <a:ea> (fontScheme major/minorFont). Word's
1108
+ * eastAsiaTheme="minor/majorEastAsia" resolves through these, NOT the Latin
1109
+ * minor/major font — routing a CJK run's theme reference to the Latin face
1110
+ * (Calibri) leaves East Asian text in a glyphless Latin font. */
1111
+ majorEastAsiaFont?: string;
1112
+ minorEastAsiaFont?: string;
1113
+ colors: Map<string, string>;
1114
+ }
1115
+
1116
+ /** A read-only view over the OPC (zip) package inside a .docx file. */
1117
+ declare class Package {
1118
+ private files;
1119
+ constructor(data: Uint8Array);
1120
+ static from(data: ArrayBuffer | Uint8Array): Package;
1121
+ has(name: string): boolean;
1122
+ binary(name: string): Uint8Array | undefined;
1123
+ text(name: string): string | undefined;
1124
+ names(): string[];
1125
+ /** Raw entry map (shared, do not mutate) — used for write-back. */
1126
+ raw(): Record<string, Uint8Array>;
1127
+ /** OPC part names never start with '/' inside the zip; tolerate both. */
1128
+ private normalize;
1129
+ }
1130
+
1131
+ /**
1132
+ * Stable node identity for replicated editing.
1133
+ *
1134
+ * Positions on the wire cannot be object references, and numeric child paths
1135
+ * shift under concurrent structural edits — so blocks (w:p, w:tbl) and runs
1136
+ * (w:r) get stable numeric ids held in an in-memory side table. The table is
1137
+ * identity-keyed and NEVER serialized into the XML: writing ids as attributes
1138
+ * would dirty every part on open and break the byte-identical round-trip
1139
+ * guarantee for untouched parts.
1140
+ *
1141
+ * Assignment is deterministic: a document-order walk over the given roots,
1142
+ * so two replicas that parse the same document derive the same table. Nodes
1143
+ * created later by edits get ids explicitly (locally via `assign`, or from a
1144
+ * replicated edit that carries the originating client's allocation) — a
1145
+ * re-walk after edits only fills gaps and never renumbers survivors, because
1146
+ * the table is keyed by element identity and in-place XML mutation preserves
1147
+ * identity across `refresh()`.
1148
+ */
1149
+ type StableId$1 = number;
1150
+ interface EncodedCaret {
1151
+ blockId: StableId$1;
1152
+ runId: StableId$1;
1153
+ /** Offset in the run's WIRE space: cumulative through the run's inline
1154
+ * content in document order, where each w:t contributes its text length
1155
+ * and each inline separator (w:tab / w:br / w:cr) contributes ONE unit.
1156
+ * Counting separators makes encoding ONE-TO-ONE: a caret at the end of
1157
+ * the w:t before a tab and one at the start of the w:t after it are
1158
+ * different physical positions and get different wire offsets — without
1159
+ * the separator unit both collapsed to the same number and the decoder
1160
+ * had to guess (review bug: boundary intents applied into the wrong w:t
1161
+ * on remote replicas, and boundary-starting ranges always rejected). */
1162
+ offset: number;
1163
+ }
1164
+ declare class StableIds {
1165
+ private byEl;
1166
+ private byId;
1167
+ private next;
1168
+ /** Document-order assignment over the given roots (typically
1169
+ * `doc.editableRoots()`). Idempotent: elements that already have ids keep
1170
+ * them; untracked ids are assigned in walk order. Call after load and
1171
+ * after any edit that created tracked nodes without explicit ids. */
1172
+ assignFromRoots(roots: XmlElement[]): void;
1173
+ /**
1174
+ * Assign ids inside the given subtrees only — the scoped counterpart of
1175
+ * assignFromRoots, for an edit that is known to have created nodes in one
1176
+ * or two paragraphs (dirty-scoped reconciliation, perf B9/B10).
1177
+ *
1178
+ * EQUIVALENCE (the property the scoped hot path rests on): this produces
1179
+ * the same table a full assignFromRoots would, PROVIDED every tracked node
1180
+ * outside the subtrees already has an id. Assignment is order-dependent —
1181
+ * fresh nodes take sequential numbers from `next` in walk order — and a
1182
+ * subtree walk visits exactly a contiguous slice of the document-order
1183
+ * walk, in the same order. So the only way the two can disagree is an
1184
+ * un-id'd node elsewhere that the full walk would have numbered first.
1185
+ * That invariant holds inductively: load/importSidecar ids everything, and
1186
+ * every apply either ids the nodes it creates (carried ids), reports
1187
+ * document scope (full refresh + full assign), or creates nodes only
1188
+ * inside the subtrees passed here. Callers that cannot establish it must
1189
+ * fall back to assignFromRoots.
1190
+ */
1191
+ assignFromSubtrees(subtrees: XmlElement[]): void;
1192
+ private walk;
1193
+ /** Explicitly assign an id to a newly created node. Local edits allocate
1194
+ * (`assign(el)`); applying a replicated edit installs the carried value
1195
+ * (`assign(el, carriedId)`). Carried ids must not collide. */
1196
+ assign(el: XmlElement, id?: StableId$1): StableId$1;
1197
+ /** Force `el` to carry `id`, replacing any id it already has (e.g. one
1198
+ * auto-assigned by a refresh() before a carried id could be installed).
1199
+ * The previous id is retired. Throws if `id` is held by a different element. */
1200
+ reassign(el: XmlElement, id: StableId$1): void;
1201
+ private install;
1202
+ idOf(el: XmlElement): StableId$1 | undefined;
1203
+ elOf(id: StableId$1): XmlElement | undefined;
1204
+ /** Drop mappings for elements no longer reachable from the given roots
1205
+ * (deleted content). Keeps the table from growing across long sessions.
1206
+ * Ids of dropped elements are retired, never reused. */
1207
+ prune(roots: XmlElement[]): void;
1208
+ size(): number;
1209
+ /** Snapshot the table as [id, element] pairs against a parallel clone of
1210
+ * the tree: `pairs` maps originals→clones (as produced by walking original
1211
+ * and cloned roots in step). Used by the reconciliation snapshot (plan
1212
+ * doc 03) so a restore re-keys the table to the restored elements. */
1213
+ captureForClone(originalToClone: Map<XmlElement, XmlElement>): Map<StableId$1, XmlElement>;
1214
+ /** Replace the table wholesale from a captured mapping (restore path). */
1215
+ restore(fromCapture: Map<StableId$1, XmlElement>, nextId: StableId$1): void;
1216
+ nextId(): StableId$1;
1217
+ /**
1218
+ * Export the table as `(id, path)` pairs, where a path is the structural
1219
+ * location of the id'd element within the given roots: `[rootIndex,
1220
+ * childIndex, childIndex, …]`. This is the checkpoint ID sidecar (plan doc
1221
+ * 03): it lets a replica that reloads a snapshot reproduce the exact id
1222
+ * table — parse-order re-derivation cannot, because split-created nodes
1223
+ * carry non-sequential ids. Also carries `next` so id allocation resumes
1224
+ * consistently.
1225
+ */
1226
+ exportSidecar(roots: XmlElement[]): {
1227
+ next: StableId$1;
1228
+ entries: [StableId$1, number[]][];
1229
+ };
1230
+ /** Rebuild the table from a sidecar against freshly parsed roots (the
1231
+ * snapshot the sidecar was exported from). Replaces any current mappings. */
1232
+ importSidecar(roots: XmlElement[], sidecar: {
1233
+ next: StableId$1;
1234
+ entries: [StableId$1, number[]][];
1235
+ }): void;
1236
+ /** Encode a caret as wire-stable addresses. `t` is the w:t (or other text
1237
+ * holder) the caret sits in; the run is its nearest tracked ancestor run,
1238
+ * the block the nearest tracked block above that. Returns null when the
1239
+ * position isn't inside id-tracked content (e.g. math internals) — such
1240
+ * positions are not yet addressable on the wire. */
1241
+ encodeCaret(t: XmlElement, offset: number, parentOf: (el: XmlElement) => XmlElement | null): EncodedCaret | null;
1242
+ /** Resolve an encoded caret back to a concrete (w:t, offset) in THIS
1243
+ * table's tree — the inverse of encodeCaret, for restoring a caret across
1244
+ * a reconciliation reload (the ids survive via the sidecar; the node
1245
+ * references do not). When the exact run is gone (deleted or replaced by
1246
+ * a concurrent edit) the caret falls back to the block's first text; null
1247
+ * when nothing addressable remains. */
1248
+ decodeCaret(pos: EncodedCaret): {
1249
+ t: XmlElement;
1250
+ offset: number;
1251
+ } | null;
1252
+ }
1253
+
1254
+ interface Relationship {
1255
+ id: string;
1256
+ type: string;
1257
+ target: string;
1258
+ external: boolean;
1259
+ }
1260
+ type Relationships = Map<string, Relationship>;
1261
+
1262
+ /**
1263
+ * A fully parsed .docx: sections of blocks, styles, numbering, theme, and
1264
+ * header/footer parts, with helpers to resolve effective formatting.
1265
+ */
1266
+ declare class DocxDocument {
1267
+ /** Changes whenever refresh() rebuilds the parsed model. Plain in-place text
1268
+ * edits can keep this stable so incremental layout reuses model-only caches. */
1269
+ private _modelVersion;
1270
+ private _packageResourceVersion;
1271
+ get modelVersion(): number;
1272
+ /** Invalidate layout derived from related parts such as ChartML. */
1273
+ markPackageResourceChanged(): void;
1274
+ readonly pkg: Package;
1275
+ readonly theme: Theme;
1276
+ styles: Styles;
1277
+ numbering: Numbering;
1278
+ sections: Section[];
1279
+ /** Header/footer parts keyed by relationship id from document.xml.rels. */
1280
+ readonly headers: Map<string, HeaderFooter>;
1281
+ readonly footers: Map<string, HeaderFooter>;
1282
+ /** Note content by note id (render-only; sources stripped). */
1283
+ readonly footnotes: Map<number, Block[]>;
1284
+ readonly endnotes: Map<number, Block[]>;
1285
+ /** The separator paragraph controls the gap between its rule and the first
1286
+ * footnote. */
1287
+ readonly footnoteSeparator: Block[];
1288
+ /** `_Ref` cross-reference bookmark ranges (name → captured runs). REF
1289
+ * fields re-render the referenced text from these — Word recomputes REF on
1290
+ * open, so the cached field result in the file is stale. */
1291
+ refBookmarks: Map<string, Run[]>;
1292
+ readonly documentRels: Relationships;
1293
+ /** settings.xml w:evenAndOddHeaders — enables the "even" header/footer variants. */
1294
+ readonly evenAndOddHeaders: boolean;
1295
+ /** settings.xml w:mirrorMargins — facing-page (book fold) margins: even
1296
+ * (verso) pages swap the left/right margins and place the gutter on the
1297
+ * inside (right) edge so the binding margin stays on the inner side of
1298
+ * each spread. */
1299
+ readonly mirrorMargins: boolean;
1300
+ /** settings.xml w:defaultTabStop in px (Word default 0.5"). */
1301
+ readonly defaultTabStop: number;
1302
+ /** settings.xml w:compat compatibilityMode (12=Word2007, 14=Word2010,
1303
+ * 15=Word2013+). Word 2013 (mode 15) introduced suppressing a paragraph's
1304
+ * space-before when it lands at the top of a page; mode 14 and earlier keep
1305
+ * it (nccih: a Heading1/2 after a page break sits at margin + its before).
1306
+ * Absent → treated as current (15). */
1307
+ readonly compatibilityMode: number;
1308
+ /** settings.xml m:mathPr/m:defJc — default justification for display
1309
+ * equations whose m:oMathParaPr carries no explicit m:jc (Word default:
1310
+ * centerGroup — the rows of a broken equation left-align to each other and
1311
+ * the group is centered in the column). */
1312
+ readonly mathDefJc: "left" | "right" | "center" | "centerGroup";
1313
+ /** settings.xml m:mathPr/m:wrapIndent in px (Word default 1440tw = 1"):
1314
+ * indent of auto-wrapped display-equation continuation rows from the
1315
+ * equation group's left edge (dense p13: the "+Dc(...)" continuations sit
1316
+ * exactly 72pt right of the explicit rows). */
1317
+ readonly mathWrapIndent: number;
1318
+ /** Review comments from word/comments.xml (empty when the part is absent).
1319
+ * Re-derived from the retained comments XML on every refresh(). */
1320
+ comments: DocComment[];
1321
+ /** Retained comments.xml tree (editing + save round-trip), when present. */
1322
+ private commentsPart;
1323
+ private commentsRoot;
1324
+ /** Retained commentsExtended.xml tree (comment threading), when present. */
1325
+ private commentsExtPart;
1326
+ private commentsExtRoot;
1327
+ private commentsExtDirty;
1328
+ /** Conditional table formats per table style id, keyed by the Styles object
1329
+ * so re-parsing styles.xml (edits) naturally invalidates the cache. */
1330
+ private tableCondCache;
1331
+ /** Retained styles.xml tree (built-in style injection + save). */
1332
+ private stylesPart;
1333
+ private stylesRoot;
1334
+ /** Retained numbering.xml tree (list creation + save round-trip). */
1335
+ private numberingPart;
1336
+ private numberingRoot;
1337
+ private numberingDirty;
1338
+ /** Retained footnotes.xml tree (footnote insertion + save round-trip). */
1339
+ private footnotesPart;
1340
+ private footnotesRoot;
1341
+ private footnotesDirty;
1342
+ private footnotesRels;
1343
+ /** Serialize retained optional parts only once actually mutated, keeping
1344
+ * untouched parts byte-identical through save(). */
1345
+ private stylesDirty;
1346
+ private commentsDirty;
1347
+ /** Retained XML roots — source of truth for editing and save(). */
1348
+ private readonly docPart;
1349
+ /** Retained settings.xml tree. A synthetic empty root keeps history root
1350
+ * indices stable for documents that did not originally contain the part. */
1351
+ private readonly settingsPart;
1352
+ private readonly settingsRoot;
1353
+ private settingsDirty;
1354
+ /** Parsed document.xml root (read-only outside the class; the layout engine
1355
+ * scans it for incremental-reuse eligibility, tests walk it). */
1356
+ readonly docRoot: XmlElement;
1357
+ private readonly hfParts;
1358
+ private readonly ctxBase;
1359
+ /** Tracked-changes display mode; refresh() re-derives after changes. */
1360
+ revisionView: "final" | "markup";
1361
+ private readonly relsPath;
1362
+ private relsRoot;
1363
+ private contentTypesRoot;
1364
+ /** Canonical XML as first parsed from each always-modeled package part.
1365
+ * If the retained tree still matches on save, keep the part's original
1366
+ * bytes instead of replacing producer formatting such as CRLF line ends. */
1367
+ private readonly originalModeledXml;
1368
+ private docPrIdCounter;
1369
+ /** Transient layout state: set by the engine while laying out a docGrid
1370
+ * type="charsAndLines" section so line measurement can give East-Asian
1371
+ * glyphs their true (uninflated) grid line height. Off outside such a
1372
+ * section. Safe as document-scoped mutable state because layout is
1373
+ * single-threaded and sequential per section. */
1374
+ charGridEa: boolean;
1375
+ private constructor();
1376
+ /** Resolve content controls mapped to standard package core properties.
1377
+ * Word refreshes these bindings on open, so the serialized sdtContent can
1378
+ * be stale even though the visible value comes from docProps/core.xml. */
1379
+ private hydrateCorePropertyControls;
1380
+ /** Repair only objects emitted by older WordInWeb builds that Word rejects. */
1381
+ private repairLegacyWordInWebObjects;
1382
+ /**
1383
+ * Re-derive the document model from the retained XML trees. Called after
1384
+ * edit commands mutate the XML.
1385
+ */
1386
+ /** Switch tracked-changes display and re-derive the model. */
1387
+ setRevisionView(view: "final" | "markup"): void;
1388
+ /** Invalidated on refresh; see layoutGlobalSig. */
1389
+ private _layoutGlobalSig;
1390
+ /** Signature of everything OUTSIDE a paragraph's own XML that affects how it
1391
+ * breaks into lines: style + numbering definitions, doc-level layout scalars,
1392
+ * and the tracked-changes view mode. The line-break cache (layout/inline.ts)
1393
+ * combines this with a paragraph's own content signature so a style/numbering/
1394
+ * settings edit invalidates cached breaks even though the paragraph XML is
1395
+ * unchanged. Memoized until the next refresh() (styles/numbering trees are
1396
+ * stable across a plain text edit). */
1397
+ layoutGlobalSig(): string;
1398
+ refresh(): void;
1399
+ /** Stable node-id side table for replicated editing. Null (and zero cost)
1400
+ * for local-only documents; call `enableStableIds()` to populate and
1401
+ * maintain it. Kept in memory only — never serialized into the XML. */
1402
+ stableIds: StableIds | null;
1403
+ /** Populate the stable-id table from current content and keep it updated
1404
+ * on every subsequent refresh(). Idempotent. */
1405
+ enableStableIds(): StableIds;
1406
+ /** Reparse the two sibling body-story paragraphs created by Enter without
1407
+ * rebuilding the complete document model. Paragraphs nested in table cells
1408
+ * are included because legal documents spend most of their body inside
1409
+ * tables; revisions, bookmarks, fields, and section breaks use refresh(). */
1410
+ reparseDirectBodyParagraphSplit(beforeSource: XmlElement, afterSource: XmlElement): {
1411
+ before: Paragraph;
1412
+ after: Paragraph;
1413
+ } | null;
1414
+ /**
1415
+ * The parsed body-story block list holding the paragraph parsed from
1416
+ * `source`, and its index in it — searching sections and, recursively,
1417
+ * table cells. Null when the model doesn't hold that paragraph (it lives in
1418
+ * a header/footer/footnote part, or the model predates its creation), which
1419
+ * is every targeted-reparse helper's signal to fall back to a full
1420
+ * refresh(). Shared by those helpers and by paragraphBySource.
1421
+ */
1422
+ private locateParagraph;
1423
+ private _paraLoc;
1424
+ /** The parsed Paragraph for a retained w:p in the body story (table cells
1425
+ * included), or null when the model doesn't hold it. Lets a caller that
1426
+ * already knows which paragraph an edit addresses reach that paragraph's
1427
+ * runs without walking the whole model. */
1428
+ paragraphBySource(source: XmlElement): Paragraph | null;
1429
+ /** Insert a new paragraph immediately before a retained body paragraph
1430
+ * without rebuilding the complete document model. Used by Enter at the
1431
+ * exact paragraph start, where the existing paragraph itself is unchanged. */
1432
+ insertDirectBodyParagraphBefore(referenceSource: XmlElement, insertedSource: XmlElement): Paragraph | null;
1433
+ /** Reparse a body paragraph plus several new siblings created by
1434
+ * click-and-type without rebuilding the complete document model. */
1435
+ reparseDirectBodyParagraphSplits(beforeSource: XmlElement, afterSources: XmlElement[]): Paragraph[] | null;
1436
+ /** Reparse one retained body-story paragraph after a structural edit that
1437
+ * leaves its surrounding block list unchanged, such as inserting ink. */
1438
+ reparseBodyParagraph(source: XmlElement): Paragraph | null;
1439
+ /** Reparse two sibling body-story paragraphs after Backspace/Delete merged
1440
+ * their XML into one. Keeps the parsed model generation stable so long
1441
+ * documents can use incremental layout instead of repaginating in full. */
1442
+ reparseDirectBodyParagraphMerge(beforeSource: XmlElement, afterSource: XmlElement, survivorSource: XmlElement): Paragraph | null;
1443
+ private deriveComments;
1444
+ /** Retained comments tree for edit commands (null when the doc has none). */
1445
+ /**
1446
+ * Retained comments tree. With create=true, a missing comments.xml part is
1447
+ * created and registered (content type + document relationship) so newly
1448
+ * added comments serialize and round-trip through Word.
1449
+ */
1450
+ commentsTree(create?: boolean): XmlElement | null;
1451
+ /**
1452
+ * Make sure a paragraph style is usable: Word ships built-in definitions
1453
+ * for Heading 1-6/Title even when a file doesn't declare them, so applying
1454
+ * one to such a file must inject a standard definition (otherwise the
1455
+ * paragraph would reference an undefined style and render as Normal).
1456
+ */
1457
+ ensureParagraphStyle(styleId: string): boolean;
1458
+ /**
1459
+ * Retained numbering tree. With create=true, a missing numbering.xml part
1460
+ * is created and registered (content type + document relationship) so list
1461
+ * definitions added by editing serialize and round-trip.
1462
+ */
1463
+ numberingTree(create?: boolean): XmlElement | null;
1464
+ markNumberingChanged(): void;
1465
+ /**
1466
+ * Retained footnotes tree. With create=true, a missing footnotes.xml part
1467
+ * is created and registered (with Word's required separator footnotes) so
1468
+ * inserted footnotes serialize and round-trip.
1469
+ */
1470
+ footnotesTree(create?: boolean): XmlElement | null;
1471
+ /**
1472
+ * Create an empty header/footer part (with a default-type reference in
1473
+ * every sectPr) when the document has none - Word does this implicitly the
1474
+ * first time you edit the header area. Returns the part's root.
1475
+ */
1476
+ /** Whether the document declares any section properties at all. A blank
1477
+ * minimal document has none; see ensureHfPart. */
1478
+ private hasSectPr;
1479
+ /** Whether a header (or footer) part already exists — the precondition
1480
+ * `ensureHfPart` tests internally, exposed so a caller can tell a real
1481
+ * creation from a no-op before mutating (the collab apply needs to know
1482
+ * whether an ensureHeaderFooter intent has anything to do). */
1483
+ hasHfPart(kind: "header" | "footer"): boolean;
1484
+ ensureHfPart(kind: "header" | "footer"): XmlElement;
1485
+ markFootnotesChanged(): void;
1486
+ /** Called by comment edit commands after mutating the comments tree. */
1487
+ markCommentsChanged(): void;
1488
+ /**
1489
+ * Retained commentsExtended tree (threading). With create=true, a missing
1490
+ * part is created and registered (content type + document relationship) so
1491
+ * Word picks up reply threading.
1492
+ */
1493
+ commentsExtendedTree(create?: boolean): XmlElement | null;
1494
+ markCommentsExtendedChanged(): void;
1495
+ /**
1496
+ * The w:t elements covered by each comment's range, in document order.
1497
+ * Point comments (a bare commentReference with no range) anchor to the
1498
+ * nearest preceding w:t.
1499
+ */
1500
+ commentAnchors(): Map<string, XmlElement[]>;
1501
+ /** Flag the footnotes part dirty when `t` lives inside it, so save()
1502
+ * re-serializes footnotes.xml. Called by the editor after a text edit; a
1503
+ * no-op for body/header/footer targets. */
1504
+ markDirtyIfFootnote(t: XmlElement): void;
1505
+ /** The mutable XML roots (document body, related modeled parts, settings).
1506
+ * settingsRoot is always second and always present so its history snapshot
1507
+ * index stays stable even when optional related roots are created later. */
1508
+ editableRoots(): XmlElement[];
1509
+ /** Toggle the document-global facing-page margin mode in settings.xml. */
1510
+ setMirrorMargins(enabled: boolean): void;
1511
+ /**
1512
+ * Find the parent element of `target` in any modeled XML tree (document
1513
+ * body, headers, footers). Linear scan — documents are small and this only
1514
+ * runs on structural edits (Enter, paragraph merge).
1515
+ */
1516
+ /** XML roots that can carry tracked changes: body, headers/footers, footnotes. */
1517
+ revisionRoots(): XmlElement[];
1518
+ /**
1519
+ * Memoized child→parent links. Nothing invalidates this map — the tree is
1520
+ * spliced constantly — so every answer is RE-DERIVED before it is returned:
1521
+ * the memoized parent must still list the target among its children, and
1522
+ * must still hang off a root (see memoIsLive). A stale entry can then only
1523
+ * cost a cache miss, never a wrong answer.
1524
+ *
1525
+ * Both halves are needed. Containment alone is not proof of a live parent:
1526
+ * a run split for formatting is spliced OUT of its paragraph while its own
1527
+ * `children` array still lists the elements that moved into the replacement
1528
+ * runs, so a containment-only check would hand back the detached run where a
1529
+ * walk from the roots finds the new one.
1530
+ */
1531
+ private _parentMemo;
1532
+ private memoIsLive;
1533
+ findParentOf(target: XmlElement): XmlElement | undefined;
1534
+ /** Record a parent link the caller just created (a splice), so the next
1535
+ * lookup skips the full-document walk. Advisory only — findParentOf
1536
+ * verifies every memo against the live tree before trusting it. */
1537
+ noteParent(child: XmlElement, parent: XmlElement): void;
1538
+ /**
1539
+ * Serialize the (possibly edited) document back to .docx bytes. Only the
1540
+ * XML parts we model are re-serialized; every other part round-trips
1541
+ * byte-for-byte.
1542
+ */
1543
+ private rememberOriginalXml;
1544
+ private writeModeledXml;
1545
+ /**
1546
+ * Canonicalize producer shorthand that Google Docs otherwise interprets as
1547
+ * a fixed, few-twip table. Word treats `tblW="100%"` plus a placeholder
1548
+ * grid as autofit; Google needs the standard pct value and a usable cached
1549
+ * grid. The cached widths follow the same content-dominant shape and do not
1550
+ * change Word's autofit result.
1551
+ */
1552
+ /** Undo closures recorded by save-time fixups while `saveJournal` is
1553
+ * active, so `save()` can revert every live-tree mutation and remain
1554
+ * side-effect-free. Collaborative checkpoints re-serialize the
1555
+ * authoritative document repeatedly; a save that mutated the tree would
1556
+ * change its hash outside the intent stream and desync the fleet. */
1557
+ private saveJournal;
1558
+ private journalSetAttr;
1559
+ private normalizePercentageTableGrids;
1560
+ save(): Uint8Array;
1561
+ saveAsync(): Promise<Uint8Array>;
1562
+ private buildPackage;
1563
+ private buildPackageFiles;
1564
+ /** Fresh unique docPr id for inserted drawings. Seeded once past the
1565
+ * highest id already present in any editable root (floor 1000, matching
1566
+ * the historical seed for documents with no drawings) so a document that
1567
+ * already carries drawings never gets a colliding id. */
1568
+ nextDrawingId(): number;
1569
+ /** Next unused revision id (w:id on w:ins/w:del). Seeded once past the
1570
+ * highest id already present in any editable root so a document that
1571
+ * already has tracked changes never collides. */
1572
+ private revIdCounter;
1573
+ nextRevisionId(): number;
1574
+ /**
1575
+ * Add image bytes as a new media part + relationship (+ content-type
1576
+ * default). Returns the relationship id for use in a w:drawing.
1577
+ */
1578
+ private ensureRelsRoot;
1579
+ /** Register an external hyperlink relationship and return its rId. */
1580
+ addHyperlinkRel(url: string): string;
1581
+ /** Retarget an existing external relationship (hyperlink href edit). */
1582
+ setRelTarget(relId: string, url: string): boolean;
1583
+ addImageResource(bytes: Uint8Array, ext: string): string;
1584
+ /** Add a GLB model part and its Office 2019 model3d relationship. */
1585
+ addModel3DResource(bytes: Uint8Array): {
1586
+ relId: string;
1587
+ part: string;
1588
+ };
1589
+ /** Add an OLE package part used by a Word w:object. */
1590
+ addEmbeddedObjectResource(bytes: Uint8Array): {
1591
+ relId: string;
1592
+ part: string;
1593
+ };
1594
+ /** Add a DOCX package embedded as an activatable Word.Document.12 object. */
1595
+ addEmbeddedWordDocumentResource(bytes: Uint8Array): {
1596
+ relId: string;
1597
+ part: string;
1598
+ };
1599
+ /** Add a native ChartML part and its embedded editable workbook. */
1600
+ addChartResource(chartXml: string, workbook: Uint8Array): {
1601
+ relId: string;
1602
+ part: string;
1603
+ };
1604
+ /** Add the SmartArt data/layout/style/color parts and its cached diagram drawing. */
1605
+ addSmartArtResources(layoutXml: string, styleXml: string, colorsXml: string, drawingXml: string, dataXml: (drawingRelId: string) => string): {
1606
+ dataRelId: string;
1607
+ layoutRelId: string;
1608
+ styleRelId: string;
1609
+ colorsRelId: string;
1610
+ drawingRelId: string;
1611
+ };
1612
+ static load(data: ArrayBuffer | Uint8Array): DocxDocument;
1613
+ media(part: string): Uint8Array | undefined;
1614
+ /**
1615
+ * Pending-media registry (plan doc 05 change 1 / doc 16 §6): parts whose
1616
+ * XML registration (rels + content-type + extent geometry) exists but
1617
+ * whose BYTES have not arrived yet — the out-of-band media model. Layout
1618
+ * needs no bytes (extents live in the XML), so a doc with pending parts
1619
+ * lays out pixel-identically; the renderer shows a placeholder until
1620
+ * `installMedia`. Keyed by part name; the value is the doc-16 §5.3
1621
+ * metadata (declared sha + optional E2EE iv/epoch for re-supply).
1622
+ */
1623
+ readonly pendingMedia: Map<string, {
1624
+ sha: string;
1625
+ iv?: string;
1626
+ genesisId?: string;
1627
+ }>;
1628
+ /** DISPLAY-ONLY transfer state for a pending part (doc 16 §5.2 step 4), so
1629
+ * the skeleton can say "fetching" versus "nobody online has this yet".
1630
+ * Written by the media transfer layer, read by the renderer; never part of
1631
+ * the document's identity and never serialized. */
1632
+ readonly mediaTransferState: Map<string, "unavailable" | "fetching" | "waiting">;
1633
+ /** Per-part media metadata that PERSISTS after install (doc 16 §5.3):
1634
+ * holder duty needs the sha (and E2EE iv/epoch) of READY parts to answer
1635
+ * re-supply requests; pendingMedia above only tracks not-yet-arrived. */
1636
+ readonly mediaMeta: Map<string, {
1637
+ sha: string;
1638
+ iv?: string;
1639
+ genesisId?: string;
1640
+ }>;
1641
+ /** "ready" = bytes present; "pending" = registered, bytes absent (doc 05).
1642
+ * Unregistered parts report "pending" too — a rel pointing at a missing
1643
+ * zip entry after a save/parse round-trip IS the pending state (a hole is
1644
+ * detectable, never corrupt — doc 16 §6 round-trip rule). */
1645
+ mediaStatus(part: string): "ready" | "pending";
1646
+ /**
1647
+ * Register an image part + relationship WITHOUT bytes (doc 16 §2: the
1648
+ * intent carries the sha; bytes travel out of band). Same deterministic
1649
+ * naming/rId scan as addImageResource so every replica applying the same
1650
+ * canonical intent derives identical registration.
1651
+ */
1652
+ registerPendingImage(sha: string, ext: string, meta?: {
1653
+ iv?: string;
1654
+ genesisId?: string;
1655
+ }): string;
1656
+ /** Install fetched bytes into a pending part (doc 05). The caller has
1657
+ * ALREADY verified the sha against the intent's declaration (the
1658
+ * reservation is the hash commitment — doc 16 §1.1); this installs and
1659
+ * clears the pending record. Returns false for unknown parts. */
1660
+ installMedia(part: string, bytes: Uint8Array): boolean;
1661
+ /** Effective paragraph properties: docDefaults → table style → style chain → direct. */
1662
+ effectiveParaProps(para: Paragraph): ParaProps;
1663
+ /**
1664
+ * Run props contributed by the enclosing table style's conditional
1665
+ * w:tblStylePr blocks for this paragraph's cell (undefined when the
1666
+ * paragraph isn't in a styled table cell or nothing applies).
1667
+ */
1668
+ private tableCondRPr;
1669
+ /** Effective run properties for a run inside a paragraph. */
1670
+ effectiveRunProps(para: Paragraph, runProps: RunProps): RunProps;
1671
+ numberingLevel(numId: number, ilvl: number): NumberingLevel | undefined;
1672
+ numberingInstance(numId: number): NumInstance | undefined;
1673
+ private findDocumentPart;
1674
+ private readXmlOptional;
1675
+ }
1676
+
1677
+ /** Maps a rendered text item back to its source XML for editing. */
1678
+ interface TextSource {
1679
+ run: Run;
1680
+ /** Source w:t; null when the text is synthetic (fields, symbols). */
1681
+ t: XmlElement | null;
1682
+ /** Char offset of this item's first character within t's text. */
1683
+ offset: number;
1684
+ }
1685
+ /**
1686
+ * Layout output: pages of absolutely positioned primitives (px, page-relative).
1687
+ * The renderer maps these 1:1 to DOM/canvas/SVG — no further layout happens
1688
+ * downstream, which is what guarantees pagination fidelity.
1689
+ */
1690
+ interface FontSpec {
1691
+ family: string;
1692
+ size: number;
1693
+ bold: boolean;
1694
+ italic: boolean;
1695
+ /** Pair kerning is active because the run met its w:kern size threshold. */
1696
+ kerning?: boolean;
1697
+ /** Optional PAINT-only family tried before `family` in the CSS stack, without
1698
+ * affecting metrics or width lookups (keyed by `family`). Used for CJK: the
1699
+ * line-pitch profile stays keyed by the macOS substitute face while the real
1700
+ * Windows glyphs (MS Mincho, Microsoft JhengHei, …) paint when available. */
1701
+ paintFamily?: string;
1702
+ /** PAINT-only vertical nudge (CSS px, positive = down) applied to the glyphs
1703
+ * without touching layout metrics or advances. Used when a substitute face
1704
+ * (Latha for Word's Tamil Vijaya) sits on a different baseline than the face
1705
+ * Word rasterized; the shift lands the glyphs on Word's baseline. */
1706
+ paintDY?: number;
1707
+ }
1708
+ interface PathItem {
1709
+ kind: "path";
1710
+ x: number;
1711
+ y: number;
1712
+ width: number;
1713
+ height: number;
1714
+ /** SVG path data in a `viewW x viewH` coordinate space. */
1715
+ d: string;
1716
+ viewW: number;
1717
+ viewH: number;
1718
+ fill?: string;
1719
+ stroke?: {
1720
+ color: string;
1721
+ width: number;
1722
+ opacity?: number;
1723
+ };
1724
+ rotate?: {
1725
+ deg: number;
1726
+ ox: number;
1727
+ oy: number;
1728
+ };
1729
+ /** Paint under / over the body text (anchored-shape layering). */
1730
+ behind?: boolean;
1731
+ front?: boolean;
1732
+ z?: number;
1733
+ }
1734
+ interface TextItem {
1735
+ kind: "text";
1736
+ x: number;
1737
+ /** Baseline y. */
1738
+ baseline: number;
1739
+ width: number;
1740
+ text: string;
1741
+ props: RunProps;
1742
+ font: FontSpec;
1743
+ /** Footnote/endnote id referenced by this run (registration happens when
1744
+ * the item lands on a real page - split table rows carry it across). */
1745
+ noteId?: number;
1746
+ /** Vertical stretch for tall delimiter glyphs (Word's glyph variants). */
1747
+ mathScaleY?: number;
1748
+ /** Horizontal stretch for wide brace glyphs (Word's over/under brace
1749
+ * variants), applied about the glyph box's horizontal center. */
1750
+ mathScaleX?: number;
1751
+ /** Stretch anchor above the baseline, px. */
1752
+ mathScaleAnchor?: number;
1753
+ /** Line box for selection/highlight backgrounds. */
1754
+ lineTop: number;
1755
+ lineHeight: number;
1756
+ /** Exact glyph box for baseline-shifted runs (superscript/subscript):
1757
+ * the renderer anchors these instead of bottoming on the line box. */
1758
+ glyphTop?: number;
1759
+ glyphBoxH?: number;
1760
+ /** Small-caps reduced segment: the base run font that must supply the
1761
+ * strut so the painted baseline matches neighboring full-size spans (the
1762
+ * renderer sizes the outer span with this font and shrinks the text via
1763
+ * a baseline-aligned inner span). */
1764
+ strutFont?: FontSpec;
1765
+ /** PAGEREF bookmark name: the final pass rewrites this item's text with
1766
+ * the bookmark's page number (Word recomputes PAGEREF on open; the docx
1767
+ * cached result is stale in real TOCs). */
1768
+ pageRef?: string;
1769
+ /** DOCX bookmarks that begin on this text item. */
1770
+ bookmarks?: string[];
1771
+ /** Source m:oMath element when this text is a piece of an equation. */
1772
+ mathSrc?: XmlElement;
1773
+ href?: string;
1774
+ /** Present for editable text (absent on numbering labels etc.). */
1775
+ src?: TextSource;
1776
+ /** Source drawing when this text belongs to an independently editable
1777
+ * text-box story. */
1778
+ textboxStory?: XmlElement;
1779
+ /** Rotate about a point (px, relative to this item's top-left). */
1780
+ rotate?: {
1781
+ deg: number;
1782
+ ox: number;
1783
+ oy: number;
1784
+ };
1785
+ /** Paint under the body text (behindDoc textbox content). */
1786
+ behind?: boolean;
1787
+ /** Paint over the body text (non-behindDoc anchored shape content: Word
1788
+ * layers in-front shapes above the text layer). */
1789
+ front?: boolean;
1790
+ z?: number;
1791
+ /** Right-to-left run: renderer sets direction:rtl so the browser shapes and
1792
+ * orders the (Arabic/Hebrew) glyphs within the span box. */
1793
+ rtl?: boolean;
1794
+ /** Ordinal of the source paragraph within its cell/frame. Set only for
1795
+ * widow-controlled paragraphs; row splitting uses it to scope Word's
1796
+ * widow/orphan rules to the paragraph straddling the cut. */
1797
+ paraSeq?: number;
1798
+ /** Editor-only: maximum caret x for this item — a trailing hanging space
1799
+ * confined to its table cell pins the caret at the cell's content edge
1800
+ * (Word behavior) while the span keeps its true hanging layout x. */
1801
+ caretClampX?: number;
1802
+ }
1803
+ interface RectItem {
1804
+ kind: "rect";
1805
+ x: number;
1806
+ y: number;
1807
+ width: number;
1808
+ height: number;
1809
+ fill: string;
1810
+ /** Semantic paint provenance used by render-layer comparison. */
1811
+ role?: "table-fill";
1812
+ /** Rotate about a point (px, relative to this item's top-left). */
1813
+ rotate?: {
1814
+ deg: number;
1815
+ ox: number;
1816
+ oy: number;
1817
+ };
1818
+ behind?: boolean;
1819
+ /** Paint over the body text (non-behindDoc anchored shape fill). */
1820
+ front?: boolean;
1821
+ z?: number;
1822
+ }
1823
+ interface LineEdgeItem {
1824
+ kind: "edge";
1825
+ x1: number;
1826
+ y1: number;
1827
+ x2: number;
1828
+ y2: number;
1829
+ border: Border;
1830
+ /** Semantic paint provenance used by render-layer comparison. */
1831
+ role?: "table-rule";
1832
+ /** Rotate about a point (px, relative to this item's top-left). */
1833
+ rotate?: {
1834
+ deg: number;
1835
+ ox: number;
1836
+ oy: number;
1837
+ };
1838
+ /** Paint over the body text (non-behindDoc anchored shape border). */
1839
+ front?: boolean;
1840
+ /** Paint below the body text (behindDoc anchored shape border). */
1841
+ behind?: boolean;
1842
+ z?: number;
1843
+ }
1844
+ interface ImageItem {
1845
+ kind: "image";
1846
+ x: number;
1847
+ y: number;
1848
+ width: number;
1849
+ height: number;
1850
+ /** Package part path; renderer resolves bytes via DocxDocument.media(). */
1851
+ part: string;
1852
+ /** a:srcRect crop (fractions) and a:xfrm rotation (degrees). */
1853
+ crop?: {
1854
+ l: number;
1855
+ t: number;
1856
+ r: number;
1857
+ b: number;
1858
+ };
1859
+ rotation?: number;
1860
+ /** Whole-group rotation about an external origin. */
1861
+ rotate?: {
1862
+ deg: number;
1863
+ ox: number;
1864
+ oy: number;
1865
+ };
1866
+ /** Picture-watermark "washout" (VML v:imagedata gain/blacklevel, 0..1
1867
+ * fractions). Per-channel linear recolor, measured against Word's PDF of
1868
+ * probe2-picture-watermark (gain 0.3, blacklevel 0.35: source 32 -> 215,
1869
+ * 74 -> 227, 135 -> 246, 210 -> clamped 255):
1870
+ * out = in * gain + 255 * (blacklevel * (1 + gain) + (1 - gain) / 2) */
1871
+ washout?: {
1872
+ gain: number;
1873
+ blacklevel: number;
1874
+ };
1875
+ /** a:ln picture outline, drawn just outside the image (Word hairline). */
1876
+ border?: {
1877
+ color: string;
1878
+ width: number;
1879
+ };
1880
+ /** behindDoc: paint under the text layer. */
1881
+ behind?: boolean;
1882
+ /** wrapNone + !behindDoc ("in front of text"): paint ABOVE the text layer
1883
+ * (Word's z-order; without this, later-emitted text spans cover the image
1884
+ * and it neither shows in front nor receives clicks/drags). */
1885
+ front?: boolean;
1886
+ z?: number;
1887
+ /** Source w:drawing element (for interactive resize/move). */
1888
+ src?: XmlElement;
1889
+ model3D?: Model3DReference;
1890
+ webVideo?: WebVideoReference;
1891
+ embeddedObject?: EmbeddedObjectReference;
1892
+ }
1893
+ interface ChartItem {
1894
+ kind: "chart";
1895
+ x: number;
1896
+ y: number;
1897
+ width: number;
1898
+ height: number;
1899
+ data: ChartData;
1900
+ }
1901
+ /** Interactive resize zone over a table boundary (column or row). */
1902
+ interface DrawingHitItem {
1903
+ kind: "drawingHit";
1904
+ x: number;
1905
+ y: number;
1906
+ width: number;
1907
+ height: number;
1908
+ /** Source w:drawing element (select/move the whole drawing). */
1909
+ src: XmlElement;
1910
+ /** Anchored drawings drag by offset; inline ones re-anchor into text. */
1911
+ anchored: boolean;
1912
+ /** Freehand stroke that may be removed with the Draw ribbon's eraser. */
1913
+ ink?: boolean;
1914
+ /** Native SmartArt diagram selection target. */
1915
+ smartArt?: boolean;
1916
+ /** Node hit boxes inside the SmartArt drawing, relative to this item. */
1917
+ smartArtNodes?: Array<{
1918
+ index: number;
1919
+ x: number;
1920
+ y: number;
1921
+ width: number;
1922
+ height: number;
1923
+ }>;
1924
+ chartData?: ChartData;
1925
+ smartArtData?: SmartArtData;
1926
+ /** A shape fill hit that sits UNDER the shape's own text spans (same z): a
1927
+ * click on bare fill selects or drags the shape, while clicks on its text
1928
+ * glyphs still reach the text editor. */
1929
+ belowText?: boolean;
1930
+ /** Keep a behindDoc drawing target under the body text. */
1931
+ behind?: boolean;
1932
+ /** The drawing owns an independently editable text-box story. */
1933
+ textboxStory?: boolean;
1934
+ rotate?: {
1935
+ deg: number;
1936
+ ox: number;
1937
+ oy: number;
1938
+ };
1939
+ z?: number;
1940
+ }
1941
+ interface GripItem {
1942
+ kind: "grip";
1943
+ /** "col": vertical zone at x spanning y1..y2. "row": horizontal zone at y1
1944
+ * spanning x..x2. "move": table bounds x..x2, y1..y2. */
1945
+ axis: "col" | "row" | "move";
1946
+ x: number;
1947
+ y1: number;
1948
+ y2: number;
1949
+ /** Right edge for row grips. */
1950
+ x2?: number;
1951
+ /** Source w:tbl element. */
1952
+ tbl: XmlElement;
1953
+ /** col: boundary 1..n (n = right edge). row: row index above the boundary. */
1954
+ boundary: number;
1955
+ /** Laid-out height of the row above (row grips), px. */
1956
+ rowHeightPx?: number;
1957
+ /** Rendered column widths px (col grips) — resize works in this space. */
1958
+ renderedWidths?: number[];
1959
+ }
1960
+ /** WordArt/watermark text scaled to fill a box, rotated about its center. */
1961
+ interface WordArtItem {
1962
+ kind: "wordart";
1963
+ /** Box top-left, px. */
1964
+ x: number;
1965
+ y: number;
1966
+ width: number;
1967
+ height: number;
1968
+ text: string;
1969
+ fontFamily: string;
1970
+ bold?: boolean;
1971
+ italic?: boolean;
1972
+ fill: string;
1973
+ opacity: number;
1974
+ /** Clockwise degrees. */
1975
+ rotation: number;
1976
+ behind?: boolean;
1977
+ /** Source v:textpath font-size (px). */
1978
+ fontSize?: number;
1979
+ /** Malformed shapetype guide path: Word collapses the fitted glyph outlines
1980
+ * into a thin vertical band. */
1981
+ noFit?: boolean;
1982
+ /** Source VML shape element (v:shape / v:rect), for interactive select/edit. */
1983
+ src?: XmlElement;
1984
+ }
1985
+ /** DrawingML a:prstTxWarp text: the shape's single text line bent onto a
1986
+ * preset envelope (arch/wave/chevron/circle-pour), filling the shape box. */
1987
+ interface WarpTextItem {
1988
+ kind: "warptext";
1989
+ /** Shape box top-left, px. */
1990
+ x: number;
1991
+ y: number;
1992
+ width: number;
1993
+ height: number;
1994
+ text: string;
1995
+ fontFamily: string;
1996
+ /** Run font size px (the text's natural size; presets that don't fill the
1997
+ * box — textArchUp — ride the text at this size). */
1998
+ fontSize: number;
1999
+ bold?: boolean;
2000
+ italic?: boolean;
2001
+ /** CSS text color. */
2002
+ fill: string;
2003
+ /** Preset name (textArchUp, textWave1, textChevron, textCirclePour, …). */
2004
+ warp: string;
2005
+ rotate?: {
2006
+ deg: number;
2007
+ ox: number;
2008
+ oy: number;
2009
+ };
2010
+ behind?: boolean;
2011
+ front?: boolean;
2012
+ z?: number;
2013
+ }
2014
+ type PageItem = TextItem | PathItem | RectItem | LineEdgeItem | ImageItem | ChartItem | DrawingHitItem | GripItem | WordArtItem | WarpTextItem;
2015
+ interface LaidOutPage {
2016
+ width: number;
2017
+ height: number;
2018
+ /** 1-based physical index. */
2019
+ index: number;
2020
+ /** Display page number after pgNumType.start is applied. */
2021
+ number: number;
2022
+ items: PageItem[];
2023
+ /** Body box (for header/footer editing chrome). */
2024
+ bodyTop: number;
2025
+ bodyBottom: number;
2026
+ /** Items from this index on belong to the header/footer parts. */
2027
+ hfStart: number;
2028
+ /** Column geometry for editor hit-testing. A page can contain more than one
2029
+ * band when a continuous section changes the column layout mid-page. */
2030
+ columnBands: Array<{
2031
+ top: number;
2032
+ colXs: number[];
2033
+ colWidths: number[];
2034
+ }>;
2035
+ }
2036
+ interface LayoutResult {
2037
+ pages: LaidOutPage[];
2038
+ totalPages: number;
2039
+ /** Opaque incremental-reuse state (engine-internal); pass a previous result
2040
+ * back as LayoutOptions.prev to enable incremental relayout. Not part of the
2041
+ * rendered output and ignored by the equivalence harness. */
2042
+ _incr?: unknown;
2043
+ /** Internal marker: unchanged pages in this result retain exact object
2044
+ * identity from the previous layout, so the DOM diff can use identity
2045
+ * instead of structurally walking a dense changed page. */
2046
+ _incremental?: boolean;
2047
+ /** Exclusive global page index through which an incremental render may run
2048
+ * pageEq before the page containing the exact dirty text source. */
2049
+ _incrementalStructuralPrefixEnd?: number;
2050
+ /** Opaque page state used to refresh headers/footers without repaginating
2051
+ * the body when their measured geometry is unchanged. */
2052
+ _hf?: unknown;
2053
+ }
2054
+
2055
+ interface FontMetrics {
2056
+ /** px above baseline (raw font ascent - anchors CSS glyph boxes). */
2057
+ ascent: number;
2058
+ /** px below baseline, raw (positive - anchors CSS glyph boxes). */
2059
+ descent: number;
2060
+ /** Natural single-spaced line height px (what Word calls single spacing). */
2061
+ lineHeight: number;
2062
+ /** Word's below-baseline share of lineHeight (quantized); use for
2063
+ * baseline placement. Falls back to `descent` when absent. */
2064
+ lineDescent?: number;
2065
+ }
2066
+ interface TextMeasurer {
2067
+ width(text: string, font: FontSpec, letterSpacing?: number): number;
2068
+ metrics(font: FontSpec): FontMetrics;
2069
+ /** The browser's own font box (fontBoundingBoxAscent/Descent) for the
2070
+ * resolved first face of this font's CSS stack, when the host can know it.
2071
+ * Used to anchor paint-routed CJK spans: the DOM renderer centers glyphs
2072
+ * by the browser strut, which differs from the engine's calibrated line
2073
+ * profile (Hiragino/PingFang) whenever the real Windows face paints. */
2074
+ paintBox?(font: FontSpec): {
2075
+ ascent: number;
2076
+ descent: number;
2077
+ } | undefined;
2078
+ /** Ink extents (actualBoundingBox*) of `text` in the RESOLVED paint face,
2079
+ * px above/below the baseline. The radical join needs the √ glyph's real
2080
+ * ink — which face paints it (real Cambria Math vs the STIX substitute)
2081
+ * changes the extents, so a baked table can't cover both. */
2082
+ inkBox?(text: string, font: FontSpec): {
2083
+ ascent: number;
2084
+ descent: number;
2085
+ } | undefined;
2086
+ }
2087
+
2088
+ interface LayoutOptions {
2089
+ measurer?: TextMeasurer;
2090
+ /** Previous layout result (from an earlier layoutDocument call on the same
2091
+ * document). Enables incremental relayout: pages whose input blocks and
2092
+ * lead-in state are unchanged are reused instead of re-laid. The engine falls
2093
+ * back to a full layout whenever it cannot prove reuse is byte-identical. */
2094
+ prev?: LayoutResult;
2095
+ /** The top-level block XML element (w:p / w:tbl) the editor mutated IN PLACE
2096
+ * since `prev` — the paragraph the caret sits in for a single-character
2097
+ * type/delete. Lets the incremental scan skip re-hashing every block: it
2098
+ * re-hashes only the hinted block and its two neighbours and reuses prev's
2099
+ * per-block signatures for the rest. Purely an optimisation — the fast path
2100
+ * is gated on block identity/count and neighbour-signature checks, so a stale
2101
+ * or wrong hint falls through to the full block scan. */
2102
+ dirtyHint?: XmlElement;
2103
+ /** Exact retained text element changed by a local edit. Used only to bound
2104
+ * structural page comparison before the first actually dirty page. */
2105
+ dirtySource?: XmlElement;
2106
+ }
2107
+ declare function layoutDocument(doc: DocxDocument, options?: LayoutOptions): LayoutResult;
2108
+
2109
+ interface RenderOptions {
2110
+ /** Zoom factor (1 = 100%). */
2111
+ zoom?: number;
2112
+ /** Gap between pages, px. */
2113
+ pageGap?: number;
2114
+ /** Page drop shadow / chrome. */
2115
+ pageShadow?: boolean;
2116
+ /** Materialize interactive affordances (table resize grips). */
2117
+ interactive?: boolean;
2118
+ /** Keep only nearby page contents mounted. Page shells always remain so
2119
+ * scroll geometry is unchanged. Intended for long interactive documents. */
2120
+ virtualize?: boolean;
2121
+ /** Called after the mounted page window changes so editor chrome can be
2122
+ * restored on newly-mounted pages. */
2123
+ onViewportChange?: () => void;
2124
+ /** Show review comments (highlight + margin balloons). Default true. */
2125
+ comments?: boolean;
2126
+ /** Called when the user deletes a comment from its balloon. The balloon
2127
+ * shows a delete button only when this is provided. */
2128
+ onDeleteComment?: (id: string) => void;
2129
+ /** Called when the user submits a reply from a balloon's reply box. The
2130
+ * reply box only renders when this is provided. */
2131
+ onReplyComment?: (id: string, text: string) => void;
2132
+ }
2133
+ interface TextBinding {
2134
+ el: HTMLElement;
2135
+ item: TextItem;
2136
+ }
2137
+ interface GripBinding {
2138
+ el: HTMLElement;
2139
+ item: GripItem;
2140
+ }
2141
+ interface ImageBinding {
2142
+ el: HTMLElement;
2143
+ item: ImageItem;
2144
+ }
2145
+ interface DrawingBinding {
2146
+ el: HTMLElement;
2147
+ item: DrawingHitItem;
2148
+ }
2149
+ interface WordArtBinding {
2150
+ el: HTMLElement;
2151
+ item: WordArtItem;
2152
+ }
2153
+ interface RenderHandle {
2154
+ /** Root element containing all pages. */
2155
+ root: HTMLElement;
2156
+ /** Rendered text elements in paint order, for selection mapping. */
2157
+ bindings: TextBinding[];
2158
+ /** Mounted text bindings indexed by their retained source w:t element. */
2159
+ bindingsByText: Map<XmlElement, TextBinding[]>;
2160
+ /** Table resize grips (only when options.interactive). */
2161
+ grips: GripBinding[];
2162
+ /** Rendered images, for interactive select/resize/move. */
2163
+ images: ImageBinding[];
2164
+ /** Transparent hit targets over vector drawings/icons (select/move). */
2165
+ drawings: DrawingBinding[];
2166
+ /** WordArt / text watermarks, for interactive select/edit. */
2167
+ wordarts: WordArtBinding[];
2168
+ /** Revoke object URLs etc. */
2169
+ destroy: () => void;
2170
+ /** Temporarily mount every page, returning a function that restores the
2171
+ * viewport window. Used by the synchronous print-clone path. */
2172
+ materializeAll?: () => () => void;
2173
+ /** Recompute the mounted page window after an external scroll/resize. */
2174
+ updateViewport?: () => void;
2175
+ /** Per-page render records, retained so the next render can reuse the DOM of
2176
+ * pages whose layout is unchanged (see renderToDom's `prev` parameter). */
2177
+ _pages?: PageRender[];
2178
+ /** Whether this render drew the comments overlay. Comment highlights live
2179
+ * INSIDE page surfaces, so the next render must sweep them out of any page
2180
+ * DOM it adopts before re-running the overlay. */
2181
+ _hadComments?: boolean;
2182
+ /** Zoom this render painted at — adoption is only valid at the same zoom. */
2183
+ _zoom?: number;
2184
+ /** Whether this handle keeps only a viewport-sized page window mounted. */
2185
+ _virtualized?: boolean;
2186
+ /** Parsed-model generation whose source bindings this handle owns. */
2187
+ _modelVersion?: number;
2188
+ /** Remove viewport listeners before this handle is replaced. */
2189
+ _stopVirtualizer?: () => void;
2190
+ }
2191
+ /** One page's DOM element plus the editor bindings it owns and the object URLs
2192
+ * it created. Retained on the handle so an incremental re-render can adopt an
2193
+ * unchanged page wholesale instead of tearing it down and rebuilding it. */
2194
+ interface PageRender {
2195
+ el: HTMLElement;
2196
+ page: LaidOutPage;
2197
+ bindings: TextBinding[];
2198
+ grips: GripBinding[];
2199
+ images: ImageBinding[];
2200
+ drawings: DrawingBinding[];
2201
+ wordarts: WordArtBinding[];
2202
+ urls: string[];
2203
+ mounted: boolean;
2204
+ }
2205
+ declare function renderToDom(doc: DocxDocument, layout: LayoutResult, container: HTMLElement, options?: RenderOptions, prev?: RenderHandle): RenderHandle;
2206
+ /**
2207
+ * Print the rendered pages (browser print -> paper or PDF): clones the page
2208
+ * DOM into a hidden same-origin iframe sized to the document's page, strips
2209
+ * screen chrome (shadows, gaps), and invokes the print dialog.
2210
+ */
2211
+ declare function printPages(root: HTMLElement, pageWidthPx: number, pageHeightPx: number): void;
2212
+
2213
+ interface RunFormatPatch {
2214
+ bold?: boolean;
2215
+ italic?: boolean;
2216
+ underline?: boolean;
2217
+ strike?: boolean;
2218
+ /** "#RRGGBB"; null removes the direct color. */
2219
+ color?: string | null;
2220
+ /** Word highlight name ("yellow", "cyan", …); null removes. */
2221
+ highlight?: string | null;
2222
+ /** Font size in points. */
2223
+ fontSizePt?: number;
2224
+ fontFamily?: string;
2225
+ /** Superscript/subscript; null returns the run to the baseline. */
2226
+ verticalAlign?: "superscript" | "subscript" | null;
2227
+ /** Remove all direct character formatting (and character style). */
2228
+ clear?: boolean;
2229
+ }
2230
+ interface SelectionFormat {
2231
+ bold: boolean;
2232
+ italic: boolean;
2233
+ underline: boolean;
2234
+ strike: boolean;
2235
+ /** Common font size in points, if uniform. */
2236
+ fontSizePt?: number;
2237
+ color?: string;
2238
+ highlight?: string;
2239
+ fontFamily?: string;
2240
+ verticalAlign?: "superscript" | "subscript";
2241
+ }
2242
+
2243
+ type ShapePreset = "line" | "verticalLine" | "rectangle" | "roundedRectangle" | "ellipse" | "diamond" | "textBox";
2244
+ type WordArtPreset = "plain" | "archUp" | "archDown" | "wave" | "chevron";
2245
+ type DrawingLineDash = "solid" | "dashed" | "dotted";
2246
+ type DrawingTool = {
2247
+ kind?: "pen";
2248
+ color: string;
2249
+ width: number;
2250
+ } | {
2251
+ kind: "highlighter";
2252
+ color: string;
2253
+ width: number;
2254
+ } | {
2255
+ kind: "eraser";
2256
+ size: number;
2257
+ } | {
2258
+ kind: "lasso";
2259
+ };
2260
+
2261
+ interface SmartArtTextFormat {
2262
+ fontFamily: string;
2263
+ fontSizePt: number;
2264
+ color: string;
2265
+ bold: boolean;
2266
+ italic: boolean;
2267
+ alignment: "left" | "center" | "right";
2268
+ }
2269
+
2270
+ type ObjectArrangeAction = "alignLeft" | "alignCenter" | "alignRight" | "alignTop" | "alignMiddle" | "alignBottom" | "rotateLeft" | "rotateRight" | "bringToFront" | "sendToBack";
2271
+ type SelectedObjectKind = "shape" | "line" | "smartArt" | "chart" | "image" | "model3d";
2272
+ type SelectedObjectCommand = "altText" | "editText" | "fill" | "outline" | "lineStyle" | "rotate" | "size" | "position" | "wrapInline" | "wrapSquare" | "wrapTopAndBottom" | "wrapFront" | "wrapBehind" | "bringForward" | "sendBackward" | "reset3d" | "delete";
2273
+ /** A local edit expressed as a replicable intent, emitted by the editor for
2274
+ * the collab layer. Positions are stable ids; the connection adds wire
2275
+ * bookkeeping (clientId/clientSeq/base). Mirrors the @wordinweb/collab intent
2276
+ * shapes without a dependency on that package. */
2277
+ type EditorIntent = {
2278
+ kind: "insertText";
2279
+ at: {
2280
+ blockId: number;
2281
+ runId: number;
2282
+ offset: number;
2283
+ };
2284
+ text: string;
2285
+ suggest?: {
2286
+ author: string;
2287
+ date: string;
2288
+ };
2289
+ } | {
2290
+ kind: "deleteText";
2291
+ blockId: number;
2292
+ runId: number;
2293
+ start: number;
2294
+ end: number;
2295
+ } | {
2296
+ kind: "splitParagraph";
2297
+ at: {
2298
+ blockId: number;
2299
+ runId: number;
2300
+ offset: number;
2301
+ };
2302
+ newBlockId: number;
2303
+ newRunId: number;
2304
+ } | {
2305
+ kind: "formatRun";
2306
+ blockId: number;
2307
+ runId: number;
2308
+ patch: Record<string, unknown>;
2309
+ } | {
2310
+ kind: "formatRange";
2311
+ blockId: number;
2312
+ runId: number;
2313
+ start: number;
2314
+ end: number;
2315
+ patch: Record<string, unknown>;
2316
+ beforeId?: number;
2317
+ middleId: number;
2318
+ afterId?: number;
2319
+ } | {
2320
+ kind: "formatParagraph";
2321
+ blockId: number;
2322
+ align?: "left" | "center" | "right" | "justify";
2323
+ styleId?: string | null;
2324
+ } | {
2325
+ kind: "setListType";
2326
+ blockId: number;
2327
+ listKind: "bullet" | "number" | null;
2328
+ } | {
2329
+ kind: "mergeParagraph";
2330
+ blockId: number;
2331
+ } | {
2332
+ kind: "suggestRevision";
2333
+ ranges?: {
2334
+ blockId: number;
2335
+ runId: number;
2336
+ start: number;
2337
+ end: number;
2338
+ }[];
2339
+ marks?: {
2340
+ blockId: number;
2341
+ glyph: "ins" | "del";
2342
+ }[];
2343
+ suggest: {
2344
+ author: string;
2345
+ date: string;
2346
+ };
2347
+ } | {
2348
+ kind: "setImageWrap";
2349
+ runId: number;
2350
+ mode: "inline" | "square" | "topAndBottom" | "none" | "behind";
2351
+ } | {
2352
+ kind: "setFloatingPagePosition";
2353
+ runId: number;
2354
+ xPx: number;
2355
+ yPx: number;
2356
+ } | {
2357
+ kind: "resizeDrawing";
2358
+ runId: number;
2359
+ widthPx: number;
2360
+ heightPx: number;
2361
+ } | {
2362
+ kind: "setDrawingRotation";
2363
+ runId: number;
2364
+ degrees: number;
2365
+ } | {
2366
+ kind: "setDrawingOrder";
2367
+ runId: number;
2368
+ order: "front" | "back";
2369
+ } | {
2370
+ kind: "setDrawingLineStyle";
2371
+ runId: number;
2372
+ color: string;
2373
+ widthPx: number;
2374
+ dash: "solid" | "dashed" | "dotted";
2375
+ } | {
2376
+ kind: "setImageAltText";
2377
+ runId: number;
2378
+ alt: string;
2379
+ } | {
2380
+ kind: "setDrawingFill";
2381
+ runId: number;
2382
+ color: string | null;
2383
+ } | {
2384
+ kind: "setSmartArtFill";
2385
+ runId: number;
2386
+ color: string | null;
2387
+ nodeIndex?: number;
2388
+ } | {
2389
+ kind: "setSmartArtNodeText";
2390
+ runId: number;
2391
+ index: number;
2392
+ text: string;
2393
+ } | {
2394
+ kind: "setDrawingWordArtText";
2395
+ runId: number;
2396
+ text: string;
2397
+ } | {
2398
+ kind: "insertBreak";
2399
+ runId: number;
2400
+ breakKind: "page" | "column";
2401
+ nodeIds: number[];
2402
+ } | {
2403
+ kind: "resizeTableColumn";
2404
+ cellParagraphId: number;
2405
+ boundary: number;
2406
+ deltaPx: number;
2407
+ renderedWidths?: number[];
2408
+ } | {
2409
+ kind: "resizeTableRow";
2410
+ cellParagraphId: number;
2411
+ rowIdx: number;
2412
+ heightPx: number;
2413
+ } | {
2414
+ kind: "moveTable";
2415
+ cellParagraphId: number;
2416
+ xPx: number;
2417
+ yPx: number;
2418
+ preservePageStart: boolean;
2419
+ pageDelta: number;
2420
+ } | {
2421
+ kind: "tableOp";
2422
+ cellParagraphId: number;
2423
+ op: {
2424
+ kind: "textWrapping";
2425
+ wrapping: "none" | "around";
2426
+ xPx: number;
2427
+ yPx: number;
2428
+ };
2429
+ } | {
2430
+ kind: "removeDrawing";
2431
+ runId: number;
2432
+ } | {
2433
+ kind: "setMathLinear";
2434
+ blockId: number;
2435
+ mathText: string;
2436
+ } | {
2437
+ kind: "deleteMath";
2438
+ blockId: number;
2439
+ } | {
2440
+ kind: "moveMath";
2441
+ blockId: number;
2442
+ at: {
2443
+ blockId: number;
2444
+ runId: number;
2445
+ offset: number;
2446
+ };
2447
+ nodeIds: number[];
2448
+ } | {
2449
+ kind: "ensureHeaderFooter";
2450
+ hfKind: "header" | "footer";
2451
+ nodeIds: number[];
2452
+ };
2453
+
2454
+ type ParagraphAlignment = "left" | "center" | "right" | "justify";
2455
+ interface PageLayoutPatch {
2456
+ /** Margins in inches. */
2457
+ margins?: {
2458
+ top?: number;
2459
+ right?: number;
2460
+ bottom?: number;
2461
+ left?: number;
2462
+ };
2463
+ /** Document-global facing-page mode (settings.xml w:mirrorMargins). */
2464
+ mirrorMargins?: boolean;
2465
+ /** Page size in inches (before orientation). */
2466
+ size?: {
2467
+ width: number;
2468
+ height: number;
2469
+ };
2470
+ orientation?: "portrait" | "landscape";
2471
+ /** Equal-width text columns (1 removes w:cols). */
2472
+ columns?: number;
2473
+ /** Draw a vertical rule between text columns. Used with columns > 1. */
2474
+ columnSeparator?: boolean;
2475
+ /** Box page border (null removes). sz in eighth-points, color hex. */
2476
+ pageBorders?: {
2477
+ sz?: number;
2478
+ color?: string;
2479
+ offsetFrom?: "text" | "page";
2480
+ } | null;
2481
+ }
2482
+
2483
+ /**
2484
+ * Table manipulation: add/remove rows and columns relative to the cell
2485
+ * containing the caret. Mutates source XML; callers checkpoint + relayout.
2486
+ *
2487
+ * v1 scope: column operations refuse tables using gridSpan (merged cells)
2488
+ * rather than corrupt them.
2489
+ */
2490
+ type TableOp = "rowAbove" | "rowBelow" | "deleteRow" | "colLeft" | "colRight" | "deleteCol" | "deleteTable" | "mergeRight" | "mergeDown" | "splitCell" | {
2491
+ kind: "cellShading";
2492
+ fill: string | null;
2493
+ } | {
2494
+ kind: "cellVAlign";
2495
+ v: "top" | "center" | "bottom";
2496
+ } | {
2497
+ kind: "textWrapping";
2498
+ wrapping: "none" | "around";
2499
+ xPx: number;
2500
+ yPx: number;
2501
+ };
2502
+
2503
+ type ParagraphDividerStyle = "single" | "double" | "dotted" | "dashed" | "thinThickSmallGap";
2504
+ interface ParagraphDivider {
2505
+ style: ParagraphDividerStyle;
2506
+ color: string;
2507
+ widthPt: number;
2508
+ spacePt: number;
2509
+ }
2510
+
2511
+ interface CoverPageContent {
2512
+ title: string;
2513
+ subtitle?: string;
2514
+ author?: string;
2515
+ }
2516
+ interface LineNumberingPatch {
2517
+ /** Turn margin line numbering on/off for the target section(s). */
2518
+ enabled: boolean;
2519
+ /** Number every Nth line (1/5/10). Default 1 when first enabled. */
2520
+ countBy?: number;
2521
+ /** When the count resets. Default newPage when first enabled. */
2522
+ restart?: "continuous" | "newPage" | "newSection";
2523
+ /** First line number. Default 1. */
2524
+ start?: number;
2525
+ }
2526
+
2527
+ interface MissingFont {
2528
+ /** The face the document asked for (first name in the item's stack). */
2529
+ family: string;
2530
+ /** A short sample of text that will render in a substitute. */
2531
+ sample: string;
2532
+ }
2533
+
2534
+ /**
2535
+ * Wire intents for replicated editing.
2536
+ *
2537
+ * Positions address content by stable id (plan doc 02): `{ blockId, runId,
2538
+ * offset }`, where offset is a character offset within the run's text. Ids
2539
+ * come from the core StableIds side table; the server validates every
2540
+ * client-supplied id before applying (validate-then-install, doc 11) — the
2541
+ * types here are the transport shape, not a trust boundary.
2542
+ *
2543
+ * `base` is the last server sequence number the originating client had
2544
+ * applied when it produced the intent; the server transforms the intent
2545
+ * against everything sequenced in `(base, seq)` before applying and
2546
+ * broadcasting the canonical form (doc 03).
2547
+ */
2548
+ type StableId = number;
2549
+ /** A caret/endpoint position within a run. */
2550
+ interface Position {
2551
+ blockId: StableId;
2552
+ runId: StableId;
2553
+ offset: number;
2554
+ }
2555
+ interface IntentBase {
2556
+ /** Originating client (authenticated identity; the server never trusts a
2557
+ * claimed id — see doc 11). */
2558
+ clientId: string;
2559
+ /** Per-client monotonic counter; `(clientId, clientSeq)` is the idempotency
2560
+ * key that dedups re-sends after reconnect (doc 03). */
2561
+ clientSeq: number;
2562
+ /** Last applied server seq at production time. */
2563
+ base: number;
2564
+ }
2565
+ /**
2566
+ * Tracked-change marks (plan doc 14 §3 L2): strike text ranges as w:del
2567
+ * revisions and/or mark paragraph glyphs (split "ins" marks, merge "del"
2568
+ * suggestions) — the SUGGESTING counterparts of deleteText/mergeParagraph.
2569
+ * Nothing is removed: Word keeps both sides until accept/reject (the
2570
+ * already-collab-wired acceptRevision/rejectRevision/acceptAllRevisions).
2571
+ * Author/date travel IN the intent (doc 05 rule a: nondeterministic values
2572
+ * are generated once by the originator); revision w:ids are scan-derived
2573
+ * from identical tree state on every replica. Positional offsets were
2574
+ * client-resolved (rule b — no Intl re-derivation at apply).
2575
+ */
2576
+ interface SuggestRevisionIntent extends IntentBase {
2577
+ kind: "suggestRevision";
2578
+ ranges?: {
2579
+ blockId: StableId;
2580
+ runId: StableId;
2581
+ start: number;
2582
+ end: number;
2583
+ }[];
2584
+ marks?: {
2585
+ blockId: StableId;
2586
+ glyph: "ins" | "del";
2587
+ }[];
2588
+ suggest: {
2589
+ author: string;
2590
+ date: string;
2591
+ };
2592
+ }
2593
+ /** Insert `text` at a position. With `suggest`, the insertion is recorded as
2594
+ * a tracked change (w:ins) carrying the author + date (revision tracking /
2595
+ * suggesting mode), rather than a plain insert. */
2596
+ interface InsertTextIntent extends IntentBase {
2597
+ kind: "insertText";
2598
+ at: Position;
2599
+ text: string;
2600
+ /** Tracked-change (suggesting) metadata; omit for a plain insert. */
2601
+ suggest?: {
2602
+ author: string;
2603
+ date: string;
2604
+ };
2605
+ }
2606
+ /** Delete `[start, end)` characters within a single run. */
2607
+ interface DeleteTextIntent extends IntentBase {
2608
+ kind: "deleteText";
2609
+ blockId: StableId;
2610
+ runId: StableId;
2611
+ start: number;
2612
+ end: number;
2613
+ }
2614
+ /**
2615
+ * Split a paragraph at a position. Text after `at.offset` in run `at.runId`
2616
+ * (and following runs) moves to a new paragraph. The originating client
2617
+ * allocates the ids for the new paragraph and the new run holding the moved
2618
+ * tail; every replica installs these recorded values (doc 03), and they also
2619
+ * let the transform remap concurrent positions that fall in the moved tail.
2620
+ */
2621
+ interface SplitParagraphIntent extends IntentBase {
2622
+ kind: "splitParagraph";
2623
+ at: Position;
2624
+ newBlockId: StableId;
2625
+ newRunId: StableId;
2626
+ }
2627
+ /**
2628
+ * Character-format an entire run (bold/italic/underline/…). Whole-run only:
2629
+ * it mutates the run's w:rPr in place, so no run splits, no new ids, and the
2630
+ * run id is preserved — which makes its transform identity (formatting moves
2631
+ * no text). Sub-range formatting splits the run into up to three, needing the
2632
+ * run-split id-inheritance + position remapping (plan doc 03 F3); that is a
2633
+ * documented extension, not implemented here.
2634
+ */
2635
+ interface FormatRunIntent extends IntentBase {
2636
+ kind: "formatRun";
2637
+ blockId: StableId;
2638
+ runId: StableId;
2639
+ /** RunFormatPatch (bold/italic/underline/strike/color/…). Structural shape
2640
+ * mirrors @wordinweb/core's RunFormatPatch; carried verbatim. */
2641
+ patch: Record<string, unknown>;
2642
+ }
2643
+ /**
2644
+ * Paragraph-level formatting: alignment and/or paragraph style. Block-level —
2645
+ * it mutates the paragraph's w:pPr, creates no tracked nodes, and moves no
2646
+ * text, so the block id is preserved and its transform is identity.
2647
+ */
2648
+ interface FormatParagraphIntent extends IntentBase {
2649
+ kind: "formatParagraph";
2650
+ blockId: StableId;
2651
+ align?: "left" | "center" | "right" | "justify";
2652
+ /** Paragraph style id; null clears to Normal. Omit to leave unchanged. */
2653
+ styleId?: string | null;
2654
+ }
2655
+ /**
2656
+ * Turn a paragraph into a bullet/numbered list item, or clear its list
2657
+ * formatting (kind null). Block-level: mutates w:pPr/numbering, preserves the
2658
+ * block id, moves no text — transform identity.
2659
+ */
2660
+ interface SetListTypeIntent extends IntentBase {
2661
+ kind: "setListType";
2662
+ blockId: StableId;
2663
+ listKind: "bullet" | "number" | null;
2664
+ }
2665
+ /**
2666
+ * Format a character sub-range of a single run (plan doc 03 F3). The run is
2667
+ * split into up to three pieces — before [0,start), middle [start,end)
2668
+ * (formatted), after [end,len) — replacing the original run. The originating
2669
+ * client allocates ids for the pieces that exist and carries them so every
2670
+ * replica addresses the pieces identically; the transform remaps any
2671
+ * concurrent position in the old run into the correct piece.
2672
+ */
2673
+ interface FormatRangeIntent extends IntentBase {
2674
+ kind: "formatRange";
2675
+ blockId: StableId;
2676
+ runId: StableId;
2677
+ start: number;
2678
+ end: number;
2679
+ patch: Record<string, unknown>;
2680
+ /** Piece ids, present iff the piece exists: before when start>0, after when
2681
+ * end<runLen. middle always. */
2682
+ beforeId?: StableId;
2683
+ middleId: StableId;
2684
+ afterId?: StableId;
2685
+ }
2686
+ /**
2687
+ * A table operation that does NOT create new tracked nodes (so no carried ids
2688
+ * are needed and the transform is identity): delete row/column/table, cell
2689
+ * shading, cell vertical align. The target cell is addressed by the stable id
2690
+ * of a paragraph inside it. Row/column INSERTION (which creates cells with new
2691
+ * paragraphs and runs needing carried ids) is a documented harder extension.
2692
+ */
2693
+ interface TableOpIntent extends IntentBase {
2694
+ /** Stable id of a paragraph inside the target cell. */
2695
+ cellParagraphId: StableId;
2696
+ kind: "tableOp";
2697
+ op: "deleteRow" | "deleteCol" | "deleteTable" | "rowAbove" | "rowBelow" | "colLeft" | "colRight" | {
2698
+ kind: "cellShading";
2699
+ fill: string | null;
2700
+ } | {
2701
+ kind: "cellVAlign";
2702
+ v: "top" | "center" | "bottom";
2703
+ } | {
2704
+ kind: "textWrapping";
2705
+ wrapping: "none" | "around";
2706
+ xPx: number;
2707
+ yPx: number;
2708
+ };
2709
+ /** For INSERT ops (rowAbove/rowBelow/colLeft/colRight): carried ids for the
2710
+ * new tracked nodes (p / r) in document order, so replicas address them
2711
+ * alike. */
2712
+ nodeIds?: StableId[];
2713
+ }
2714
+ /**
2715
+ * Merge a paragraph into its predecessor (Backspace at paragraph start). The
2716
+ * paragraph's runs MOVE into the previous paragraph (element identity
2717
+ * preserved), so run-addressed positions survive unchanged — an identity
2718
+ * transform. The merged paragraph's block id is retired.
2719
+ */
2720
+ interface MergeParagraphIntent extends IntentBase {
2721
+ kind: "mergeParagraph";
2722
+ /** The paragraph to merge into the one before it. */
2723
+ blockId: StableId;
2724
+ }
2725
+ /**
2726
+ * Add a review comment anchored to an entire run. Comment markers
2727
+ * (commentRangeStart/End + a commentReference run) are inserted as run
2728
+ * siblings and the comment body goes into comments.xml — no commented run's
2729
+ * text moves, so the transform is identity. The nondeterministic values
2730
+ * (w14:paraId, w:date) are generated once by the originating client and
2731
+ * carried here (plan doc 05) so every replica writes identical XML. Sub-range
2732
+ * comments (splitting the run) are the documented harder extension.
2733
+ */
2734
+ interface CommentRunIntent extends IntentBase {
2735
+ kind: "commentRun";
2736
+ runId: StableId;
2737
+ text: string;
2738
+ author: string;
2739
+ initials?: string;
2740
+ /** Carried provenance for deterministic XML across replicas. */
2741
+ date: string;
2742
+ paraId: string;
2743
+ }
2744
+ /**
2745
+ * Rich paste: splice validated OOXML paragraph blocks after a target
2746
+ * paragraph. The pasting client converts clipboard HTML to OOXML locally
2747
+ * (browser-only, engine-dependent — doc 02 M4) and carries the serialized
2748
+ * blocks; the server VALIDATES them against the positive allowlist before
2749
+ * applying (doc 11 gate 2). `nodeIds` are carried ids for the new tracked
2750
+ * nodes (p / r) in document order, so every replica addresses them alike.
2751
+ * Inserting separate blocks shifts no existing run's offsets — identity
2752
+ * transform.
2753
+ */
2754
+ interface PasteBlocksIntent extends IntentBase {
2755
+ kind: "pasteBlocks";
2756
+ /** Paragraph after which to insert the pasted blocks. */
2757
+ afterBlockId: StableId;
2758
+ /** Serialized OOXML block list (w:p elements), validated at apply. */
2759
+ blocksXml: string;
2760
+ /** Carried ids for the new p/r nodes, in document order. */
2761
+ nodeIds: StableId[];
2762
+ }
2763
+ /**
2764
+ * Insert an image at a run (as a sibling drawing run — no text split, identity
2765
+ * transform). The plan's media design carries bytes out-of-band via presigned
2766
+ * upload with the intent carrying (part, extents, sha) — modeled here with the
2767
+ * bytes inline (base64), which is correct for small images and keeps the whole
2768
+ * flow testable headlessly; the out-of-band path is a transport optimization
2769
+ * over the same convergence. Client-measured dimensions (widthPx/heightPx) are
2770
+ * carried so layout reserves space deterministically. `nodeIds` are carried
2771
+ * ids for the new drawing run(s).
2772
+ */
2773
+ interface InsertImageIntent extends IntentBase {
2774
+ kind: "insertImage";
2775
+ runId: StableId;
2776
+ /**
2777
+ * sha256 hex of the BLOB (plan doc 16 §2): plaintext bytes in plaintext
2778
+ * mode, ciphertext in E2EE mode. This is the reservation's hash
2779
+ * COMMITMENT — every replica registers the part pending and verifies any
2780
+ * later-arriving bytes against it (the swap-proofing trust chain, doc 16
2781
+ * §1.1). Bytes NEVER ride the intent (doc 06: sequencer bandwidth is
2782
+ * proportional to typing, not content).
2783
+ */
2784
+ blobSha: string;
2785
+ /** Blob length in bytes — validate.ts bounds it (doc 13 blocker 3). */
2786
+ bytesLen: number;
2787
+ /** File extension without dot (png/jpg/gif…) — drives the content type. */
2788
+ ext: string;
2789
+ /** E2EE only: base64 12-byte GCM IV, recorded so any holder can re-seal
2790
+ * byte-identically for re-supply (doc 16 §5.3). */
2791
+ iv?: string;
2792
+ widthPx: number;
2793
+ heightPx: number;
2794
+ nodeIds: StableId[];
2795
+ }
2796
+ /**
2797
+ * Insert a page/column break at the END of a run (inserts sibling break + tail
2798
+ * runs — no text split, identity transform). Mid-run breaks (which split the
2799
+ * run) follow the carried-id-split pattern (formatRange) and are the
2800
+ * documented extension. `nodeIds` are carried ids for the new runs.
2801
+ */
2802
+ interface InsertBreakIntent extends IntentBase {
2803
+ kind: "insertBreak";
2804
+ runId: StableId;
2805
+ breakKind: "page" | "column";
2806
+ nodeIds: StableId[];
2807
+ }
2808
+ /**
2809
+ * Insert a math (OMML) formula from a linear expression at the end of a run
2810
+ * (sibling insertion — identity transform). `nodeIds` for any new tracked
2811
+ * nodes.
2812
+ */
2813
+ interface InsertMathIntent extends IntentBase {
2814
+ kind: "insertMath";
2815
+ runId: StableId;
2816
+ mathText: string;
2817
+ nodeIds: StableId[];
2818
+ }
2819
+ /** Insert a shape/textbox drawing at the end of a run (sibling — identity). */
2820
+ interface InsertShapeIntent extends IntentBase {
2821
+ kind: "insertShape";
2822
+ runId: StableId;
2823
+ preset: "line" | "verticalLine" | "rectangle" | "roundedRectangle" | "ellipse" | "diamond" | "textBox";
2824
+ text?: string;
2825
+ nodeIds: StableId[];
2826
+ }
2827
+ /**
2828
+ * Reply to an existing comment (threading). Addressed by the parent comment's
2829
+ * id (a deterministic string). Carries provenance (w14:paraId candidates in
2830
+ * consumption order + the w:date) so every replica writes identical XML. No
2831
+ * document run's text moves — identity transform.
2832
+ */
2833
+ interface ReplyCommentIntent extends IntentBase {
2834
+ kind: "replyComment";
2835
+ parentId: string;
2836
+ text: string;
2837
+ author: string;
2838
+ initials?: string;
2839
+ date: string;
2840
+ /** paraId candidates in the order replyToComment consumes them: the reply's
2841
+ * paraId, preceded by the parent's if the parent lacks one. */
2842
+ paraIds: string[];
2843
+ }
2844
+ /** Adjust a paragraph's indent (Tab/Shift-Tab). Block-level, identity. */
2845
+ interface AdjustIndentIntent extends IntentBase {
2846
+ kind: "adjustIndent";
2847
+ blockId: StableId;
2848
+ direction: 1 | -1;
2849
+ }
2850
+ /** Set paragraph line/before/after spacing. Block-level, identity. */
2851
+ interface SetSpacingIntent extends IntentBase {
2852
+ kind: "setSpacing";
2853
+ blockId: StableId;
2854
+ /** ParagraphSpacingPatch (before/after/line/lineRule) — carried verbatim. */
2855
+ patch: Record<string, unknown>;
2856
+ }
2857
+ /** Insert a page-number field at the end of a run (sibling insertion,
2858
+ * identity). Page fields are deterministic (no clock). */
2859
+ interface InsertPageFieldIntent extends IntentBase {
2860
+ kind: "insertPageField";
2861
+ runId: StableId;
2862
+ fieldKind: "page" | "pageOfTotal";
2863
+ nodeIds: StableId[];
2864
+ }
2865
+ /** Wrap a run in a hyperlink to `url`. The url is scheme-validated at apply
2866
+ * (rejects javascript:/data: etc. — doc 11 gate 1). Run-level, identity. */
2867
+ interface SetLinkIntent extends IntentBase {
2868
+ kind: "setLink";
2869
+ runId: StableId;
2870
+ url: string;
2871
+ nodeIds: StableId[];
2872
+ }
2873
+ /** Insert a footnote reference at the end of a run + its footnote text. */
2874
+ interface InsertFootnoteIntent extends IntentBase {
2875
+ kind: "insertFootnote";
2876
+ runId: StableId;
2877
+ text: string;
2878
+ nodeIds: StableId[];
2879
+ }
2880
+ /** Set/clear a drop cap on a paragraph. Block-level. */
2881
+ interface SetDropCapIntent extends IntentBase {
2882
+ kind: "setDropCap";
2883
+ blockId: StableId;
2884
+ mode: "drop" | "margin" | null;
2885
+ nodeIds: StableId[];
2886
+ }
2887
+ /** Set/clear a paragraph divider (horizontal rule). Block-level, identity. */
2888
+ interface SetDividerIntent extends IntentBase {
2889
+ kind: "setDivider";
2890
+ blockId: StableId;
2891
+ divider: Record<string, unknown> | null;
2892
+ }
2893
+ /** Insert a bookmark anchor at the end of a run. */
2894
+ interface InsertBookmarkIntent extends IntentBase {
2895
+ kind: "insertBookmark";
2896
+ runId: StableId;
2897
+ name: string;
2898
+ }
2899
+ type Intent = InsertTextIntent | DeleteTextIntent | SplitParagraphIntent | FormatRunIntent | FormatParagraphIntent | SetListTypeIntent | FormatRangeIntent | TableOpIntent | MergeParagraphIntent | CommentRunIntent | PasteBlocksIntent | InsertImageIntent | SuggestRevisionIntent | InsertBreakIntent | InsertMathIntent | InsertShapeIntent | ReplyCommentIntent | AdjustIndentIntent | SetSpacingIntent | InsertPageFieldIntent | SetLinkIntent | InsertFootnoteIntent | SetDropCapIntent | SetDividerIntent | InsertBookmarkIntent | InsertBlankPageIntent | InsertSectionBreakIntent | InsertCrossRefIntent | InsertCoverPageIntent | SetPageLayoutIntent | SetListLevelIntent | InsertWordArtIntent | InsertChartIntent | InsertSmartArtIntent | SetLineNumberingIntent | InsertDateTimeFieldIntent | InsertFieldIntent | SetDrawingRotationIntent | SetDrawingFillIntent | SetChartDataIntent | SetSmartArtNodeTextIntent | SetDrawingWordArtTextIntent | SetDrawingLineStyleIntent | SetImageAltTextIntent | RemoveLinkIntent | SetImageWrapIntent | SetDrawingOrderIntent | SetSmartArtDataIntent | SetSmartArtFillIntent | SetSmartArtTextFormatIntent | SetFloatingPagePositionIntent | ResizeDrawingIntent | ResizeTableColumnIntent | ResizeTableRowIntent | MoveTableIntent | RemoveDrawingIntent | SetMathLinearIntent | DeleteMathIntent | MoveMathIntent | EnsureHeaderFooterIntent | DeleteCommentIntent | InsertBookmarkRangeIntent | ToggleCheckboxIntent | AcceptRevisionIntent | RejectRevisionIntent | AcceptAllRevisionsIntent | InsertTableIntent;
2900
+ /** Insert a blank page at the end of a run. */
2901
+ interface InsertBlankPageIntent extends IntentBase {
2902
+ kind: "insertBlankPage";
2903
+ runId: StableId;
2904
+ nodeIds: StableId[];
2905
+ }
2906
+ /** Insert a section break at the end of a run. */
2907
+ interface InsertSectionBreakIntent extends IntentBase {
2908
+ kind: "insertSectionBreak";
2909
+ runId: StableId;
2910
+ breakType: "nextPage" | "continuous";
2911
+ nodeIds: StableId[];
2912
+ }
2913
+ /** Insert a cross-reference to a bookmark at the end of a run. */
2914
+ interface InsertCrossRefIntent extends IntentBase {
2915
+ kind: "insertCrossRef";
2916
+ runId: StableId;
2917
+ bookmark: string;
2918
+ refKind: "text" | "page";
2919
+ nodeIds: StableId[];
2920
+ }
2921
+ /** Insert a cover page (document-level). */
2922
+ interface InsertCoverPageIntent extends IntentBase {
2923
+ kind: "insertCoverPage";
2924
+ /** CoverPageContent ({title, subtitle?, author?, …}) — carried verbatim. */
2925
+ content: Record<string, unknown>;
2926
+ nodeIds: StableId[];
2927
+ }
2928
+ /** Update page setup: margins/size/orientation/columns/borders. When target is
2929
+ * omitted it applies to every section; document-level, identity transform. */
2930
+ interface SetPageLayoutIntent extends IntentBase {
2931
+ kind: "setPageLayout";
2932
+ /** PageLayoutPatch ({margins?, size?, orientation?, columns?, …}) — carried
2933
+ * verbatim; validated against a shape+range allowlist. */
2934
+ patch: Record<string, unknown>;
2935
+ }
2936
+ /** Indent (+1) or outdent (-1) a paragraph's list nesting level. Block-level. */
2937
+ interface SetListLevelIntent extends IntentBase {
2938
+ kind: "setListLevel";
2939
+ blockId: StableId;
2940
+ delta: 1 | -1;
2941
+ }
2942
+ /** Insert decorative WordArt (a text drawing) at the end of a run. */
2943
+ interface InsertWordArtIntent extends IntentBase {
2944
+ kind: "insertWordArt";
2945
+ runId: StableId;
2946
+ text: string;
2947
+ preset: "plain" | "archUp" | "archDown" | "wave" | "chevron";
2948
+ nodeIds: StableId[];
2949
+ }
2950
+ /** Insert a data chart (column/bar/line/pie) at the end of a run. The chart
2951
+ * data is carried verbatim; the workbook part is generated deterministically. */
2952
+ interface InsertChartIntent extends IntentBase {
2953
+ kind: "insertChart";
2954
+ runId: StableId;
2955
+ chart: {
2956
+ type: "column" | "bar" | "line" | "pie";
2957
+ title?: string;
2958
+ categories: string[];
2959
+ series: {
2960
+ name: string;
2961
+ values: number[];
2962
+ }[];
2963
+ };
2964
+ nodeIds: StableId[];
2965
+ }
2966
+ /** Insert a SmartArt diagram (process/cycle/hierarchy/list) at end of a run. */
2967
+ interface InsertSmartArtIntent extends IntentBase {
2968
+ kind: "insertSmartArt";
2969
+ runId: StableId;
2970
+ smartArt: {
2971
+ layout: "process" | "cycle" | "hierarchy" | "list";
2972
+ items: string[];
2973
+ };
2974
+ nodeIds: StableId[];
2975
+ }
2976
+ /** Toggle/configure margin line numbering for the section(s). Document-level. */
2977
+ interface SetLineNumberingIntent extends IntentBase {
2978
+ kind: "setLineNumbering";
2979
+ patch: {
2980
+ enabled: boolean;
2981
+ countBy?: number;
2982
+ restart?: "continuous" | "newPage" | "newSection";
2983
+ start?: number;
2984
+ };
2985
+ }
2986
+ /** Insert an auto-updating DATE or TIME field at the end of a run. The picture
2987
+ * (format string) is carried so every replica renders identical field XML. */
2988
+ interface InsertDateTimeFieldIntent extends IntentBase {
2989
+ kind: "insertDateTimeField";
2990
+ runId: StableId;
2991
+ dtKind: "date" | "time";
2992
+ picture: string;
2993
+ nodeIds: StableId[];
2994
+ }
2995
+ /** Insert a Word field (e.g. PAGE, NUMPAGES, REF) at the end of a run. The
2996
+ * instruction is restricted to a safe positive allowlist (no INCLUDETEXT/DDE/
2997
+ * LINK external-content fields). */
2998
+ interface InsertFieldIntent extends IntentBase {
2999
+ kind: "insertField";
3000
+ runId: StableId;
3001
+ instruction: string;
3002
+ cachedResult?: string;
3003
+ nodeIds: StableId[];
3004
+ }
3005
+ /** Rotate the drawing carried by a run. Identity transform (run-addressed). */
3006
+ interface SetDrawingRotationIntent extends IntentBase {
3007
+ kind: "setDrawingRotation";
3008
+ runId: StableId;
3009
+ degrees: number;
3010
+ }
3011
+ /** Set/clear the solid fill of the drawing carried by a run. `color` is a
3012
+ * 6-hex-digit RGB (no #) or null to clear. */
3013
+ interface SetDrawingFillIntent extends IntentBase {
3014
+ kind: "setDrawingFill";
3015
+ runId: StableId;
3016
+ color: string | null;
3017
+ }
3018
+ /** Replace the data of the chart carried by a run (same shape as insertChart's
3019
+ * chart). Edits an existing chart in place. */
3020
+ interface SetChartDataIntent extends IntentBase {
3021
+ kind: "setChartData";
3022
+ runId: StableId;
3023
+ chart: {
3024
+ type: "column" | "bar" | "line" | "pie";
3025
+ title?: string;
3026
+ categories: string[];
3027
+ series: {
3028
+ name: string;
3029
+ values: number[];
3030
+ }[];
3031
+ };
3032
+ }
3033
+ /** Set the text of one node of the SmartArt diagram carried by a run. */
3034
+ interface SetSmartArtNodeTextIntent extends IntentBase {
3035
+ kind: "setSmartArtNodeText";
3036
+ runId: StableId;
3037
+ index: number;
3038
+ text: string;
3039
+ }
3040
+ /** Replace the text of the WordArt drawing carried by a run. */
3041
+ interface SetDrawingWordArtTextIntent extends IntentBase {
3042
+ kind: "setDrawingWordArtText";
3043
+ runId: StableId;
3044
+ text: string;
3045
+ }
3046
+ /** Set the outline (line) style of the drawing carried by a run. */
3047
+ interface SetDrawingLineStyleIntent extends IntentBase {
3048
+ kind: "setDrawingLineStyle";
3049
+ runId: StableId;
3050
+ color: string;
3051
+ widthPx: number;
3052
+ dash: "solid" | "dashed" | "dotted";
3053
+ }
3054
+ /** Set/clear the alt text (accessibility description) of the image/drawing
3055
+ * carried by a run. Empty string clears it. */
3056
+ interface SetImageAltTextIntent extends IntentBase {
3057
+ kind: "setImageAltText";
3058
+ runId: StableId;
3059
+ alt: string;
3060
+ }
3061
+ /** Remove the hyperlink wrapping a run's text (inverse of setLink). */
3062
+ interface RemoveLinkIntent extends IntentBase {
3063
+ kind: "removeLink";
3064
+ runId: StableId;
3065
+ }
3066
+ /** Set the text-wrap mode of the drawing carried by a run. A floating mode
3067
+ * converts an inline drawing to an anchor. */
3068
+ interface SetImageWrapIntent extends IntentBase {
3069
+ kind: "setImageWrap";
3070
+ runId: StableId;
3071
+ mode: "inline" | "square" | "topAndBottom" | "none" | "behind";
3072
+ }
3073
+ /** Bring the (floating) drawing carried by a run to front/back. */
3074
+ interface SetDrawingOrderIntent extends IntentBase {
3075
+ kind: "setDrawingOrder";
3076
+ runId: StableId;
3077
+ order: "front" | "back";
3078
+ }
3079
+ /** Replace the whole SmartArt diagram (layout + items) carried by a run. */
3080
+ interface SetSmartArtDataIntent extends IntentBase {
3081
+ kind: "setSmartArtData";
3082
+ runId: StableId;
3083
+ smartArt: {
3084
+ layout: "process" | "cycle" | "hierarchy" | "list";
3085
+ items: string[];
3086
+ };
3087
+ }
3088
+ /** Set/clear the fill of a SmartArt node (or all nodes when nodeIndex omitted).
3089
+ * `color` is 6-hex RGB (no #) or null to clear. */
3090
+ interface SetSmartArtFillIntent extends IntentBase {
3091
+ kind: "setSmartArtFill";
3092
+ runId: StableId;
3093
+ color: string | null;
3094
+ nodeIndex?: number;
3095
+ }
3096
+ /** Set the text format of a SmartArt node (or all nodes). */
3097
+ interface SetSmartArtTextFormatIntent extends IntentBase {
3098
+ kind: "setSmartArtTextFormat";
3099
+ runId: StableId;
3100
+ format: {
3101
+ fontFamily: string;
3102
+ fontSizePt: number;
3103
+ color: string;
3104
+ bold: boolean;
3105
+ italic: boolean;
3106
+ alignment: "left" | "center" | "right";
3107
+ };
3108
+ nodeIndex?: number;
3109
+ }
3110
+ /** Position the (floating) drawing carried by a run relative to the page. */
3111
+ interface SetFloatingPagePositionIntent extends IntentBase {
3112
+ kind: "setFloatingPagePosition";
3113
+ runId: StableId;
3114
+ xPx: number;
3115
+ yPx: number;
3116
+ }
3117
+ /** Resize the drawing carried by a run (extent in px; the mutation clamps to
3118
+ * a 1px-EMU floor and keeps line geometry degenerate on its minor axis). */
3119
+ interface ResizeDrawingIntent extends IntentBase {
3120
+ kind: "resizeDrawing";
3121
+ runId: StableId;
3122
+ widthPx: number;
3123
+ heightPx: number;
3124
+ }
3125
+ /** Drag-resize a table column boundary. Addressed like tableOp: a paragraph
3126
+ * inside the table. `renderedWidths` (the dragger's measured column widths)
3127
+ * rides as DATA so every replica applies the identical mutation. */
3128
+ interface ResizeTableColumnIntent extends IntentBase {
3129
+ kind: "resizeTableColumn";
3130
+ cellParagraphId: StableId;
3131
+ boundary: number;
3132
+ deltaPx: number;
3133
+ renderedWidths?: number[];
3134
+ }
3135
+ /** Drag-resize a table row's height (trHeight atLeast). */
3136
+ interface ResizeTableRowIntent extends IntentBase {
3137
+ kind: "resizeTableRow";
3138
+ cellParagraphId: StableId;
3139
+ rowIdx: number;
3140
+ heightPx: number;
3141
+ }
3142
+ /** Position a table at page coordinates (converts inline to float). */
3143
+ interface MoveTableIntent extends IntentBase {
3144
+ kind: "moveTable";
3145
+ cellParagraphId: StableId;
3146
+ xPx: number;
3147
+ yPx: number;
3148
+ preservePageStart: boolean;
3149
+ pageDelta: number;
3150
+ }
3151
+ /** Delete the drawing carried by a run (removes the carrier run). */
3152
+ interface RemoveDrawingIntent extends IntentBase {
3153
+ kind: "removeDrawing";
3154
+ runId: StableId;
3155
+ }
3156
+ /** Replace the equation of the math object in a paragraph (linear syntax).
3157
+ * Math (m:oMath) lives at paragraph level, so this is block-addressed. */
3158
+ interface SetMathLinearIntent extends IntentBase {
3159
+ kind: "setMathLinear";
3160
+ blockId: StableId;
3161
+ mathText: string;
3162
+ }
3163
+ /** Delete the math object in a paragraph. */
3164
+ interface DeleteMathIntent extends IntentBase {
3165
+ kind: "deleteMath";
3166
+ blockId: StableId;
3167
+ }
3168
+ /** Drag-move the equation out of `blockId` to the text position `at`. The
3169
+ * equation is addressed like the other math intents (block + firstMathIn); the
3170
+ * destination is an ordinary wire caret. Dropping mid-text splits the
3171
+ * destination run, so the split-off tail run takes a carried id. */
3172
+ interface MoveMathIntent extends IntentBase {
3173
+ kind: "moveMath";
3174
+ blockId: StableId;
3175
+ at: Position;
3176
+ nodeIds: StableId[];
3177
+ }
3178
+ /** Create the document's header (or footer) part the way Word does on first
3179
+ * entry into the margin band: a new part + relationship + content-type
3180
+ * override, referenced from every sectPr (one is materialized when a minimal
3181
+ * document has none). Applies as a clean no-op when the part already exists,
3182
+ * so it is safe for two participants to open the header at the same moment.
3183
+ * The created paragraph and run take carried ids — every later edit in the
3184
+ * header addresses them. */
3185
+ interface EnsureHeaderFooterIntent extends IntentBase {
3186
+ kind: "ensureHeaderFooter";
3187
+ hfKind: "header" | "footer";
3188
+ nodeIds: StableId[];
3189
+ }
3190
+ /** Delete a comment (and its reply thread) by id. */
3191
+ interface DeleteCommentIntent extends IntentBase {
3192
+ kind: "deleteComment";
3193
+ commentId: string;
3194
+ }
3195
+ /** Wrap a sub-range of a run's text in a named bookmark. */
3196
+ interface InsertBookmarkRangeIntent extends IntentBase {
3197
+ kind: "insertBookmarkRange";
3198
+ runId: StableId;
3199
+ name: string;
3200
+ start: number;
3201
+ end: number;
3202
+ }
3203
+ /** Toggle the checkbox content control carried by a run. */
3204
+ interface ToggleCheckboxIntent extends IntentBase {
3205
+ kind: "toggleCheckbox";
3206
+ runId: StableId;
3207
+ }
3208
+ /** Accept one tracked change, addressed by its position in document order
3209
+ * (collectRevisions order — deterministic across replicas). */
3210
+ interface AcceptRevisionIntent extends IntentBase {
3211
+ kind: "acceptRevision";
3212
+ index: number;
3213
+ }
3214
+ /** Reject one tracked change, addressed by document-order index. */
3215
+ interface RejectRevisionIntent extends IntentBase {
3216
+ kind: "rejectRevision";
3217
+ index: number;
3218
+ }
3219
+ /** Accept every tracked change in the document. */
3220
+ interface AcceptAllRevisionsIntent extends IntentBase {
3221
+ kind: "acceptAllRevisions";
3222
+ }
3223
+ /** Insert a rows×cols table after the paragraph containing the anchor run. */
3224
+ interface InsertTableIntent extends IntentBase {
3225
+ kind: "insertTable";
3226
+ runId: StableId;
3227
+ rows: number;
3228
+ cols: number;
3229
+ nodeIds: StableId[];
3230
+ }
3231
+ /** A sequenced log entry: an applied intent with its assigned seq, or a
3232
+ * rejection no-op (doc 03) that still occupies a position in the total order
3233
+ * so every replica agrees where a drop took effect. */
3234
+ type LogEntry = {
3235
+ seq: number;
3236
+ kind: "applied";
3237
+ intent: Intent;
3238
+ } | {
3239
+ seq: number;
3240
+ kind: "rejected";
3241
+ clientId: string;
3242
+ clientSeq: number;
3243
+ reason: string;
3244
+ };
3245
+
3246
+ /** The ID sidecar carried in a checkpoint bundle (plan doc 03). */
3247
+ type IdSidecar = ReturnType<StableIds["exportSidecar"]>;
3248
+ /**
3249
+ * What the top of a client's undo stack offers. The three cases are
3250
+ * deliberately distinct because the UI says something different for each:
3251
+ * nothing to undo (button disabled), the last action can't be reversed yet
3252
+ * (button disabled WITH a reason), or here is the inverse to submit.
3253
+ */
3254
+ /**
3255
+ * What a collaborative undo attempt did, as the UI needs to distinguish it:
3256
+ *
3257
+ * undone the inverse was submitted and painted optimistically
3258
+ * cannot-undo the last action has no inverse yet — a HARD STOP; undo does
3259
+ * not reach past it (the cure is more inverses, not skipping)
3260
+ * changed-since the target is already gone in canonical state, so the undo
3261
+ * could never land. The entry IS consumed: what the user
3262
+ * asked for has effectively happened, so their next press
3263
+ * moving to the previous action cannot surprise them
3264
+ * nothing-to-undo the stack is empty
3265
+ * unavailable this connection has no collaborative undo (plaintext rooms,
3266
+ * whose authority is the server)
3267
+ */
3268
+ type UndoOutcome = "undone" | "cannot-undo" | "changed-since" | "nothing-to-undo" | "unavailable";
3269
+
3270
+ /**
3271
+ * E2EE primitives for the blind-sequencer mode (plan doc 13). Pure WebCrypto
3272
+ * (available in browsers and Node ≥16 globals) — no dependencies, usable
3273
+ * from both the client and tests.
3274
+ *
3275
+ * Key model (doc 13 §1): a per-DOCUMENT master key `K_doc` minted at
3276
+ * go-live, carried only in the share link's URL fragment. Per-epoch subkeys
3277
+ * are DERIVED — `K_epoch = HKDF(K_doc, genesisId [+ share code])` — then
3278
+ * domain-separated into K_content (intents/checkpoints) and K_media
3279
+ * (blobs). Epoch derivation kills cross-epoch replay (round-4 F5a) and
3280
+ * resets the GCM IV budget per epoch (F21); nobody ever redistributes keys,
3281
+ * because anyone with the link + the (public) epoch id derives the same
3282
+ * values.
3283
+ *
3284
+ * AAD discipline (round-4 F5): every ciphertext is bound to its exact
3285
+ * position in the protocol — intents to (docId, genesisId, clientId,
3286
+ * clientSeq, base); checkpoints to (docId, genesisId, seq). `base` is
3287
+ * authenticated because it is a TRANSFORM INPUT: an unauthenticated base
3288
+ * would let a keyless server shift where an edit applies.
3289
+ */
3290
+ /** The plaintext bookkeeping + opaque body the blind sequencer handles. */
3291
+ interface IntentEnvelope {
3292
+ clientId: string;
3293
+ clientSeq: number;
3294
+ base: number;
3295
+ /** base64 12-byte GCM IV (fresh per envelope). */
3296
+ iv: string;
3297
+ /** base64 AES-256-GCM ciphertext (+tag) of the JSON-serialized intent. */
3298
+ ciphertext: string;
3299
+ }
3300
+ interface EpochKeys {
3301
+ kContent: CryptoKey;
3302
+ kMedia: CryptoKey;
3303
+ /**
3304
+ * Presence (carets/selections). A THIRD domain-separated key rather than
3305
+ * kContent, because presence is high-frequency, low-value material with a
3306
+ * completely different exposure profile: it is sealed and opened on every
3307
+ * caret move, so it burns IVs fastest and is the likeliest place for a
3308
+ * future mistake. Key separation costs one HKDF call at join and means a
3309
+ * presence-side compromise cannot touch document content.
3310
+ */
3311
+ kPresence: CryptoKey;
3312
+ }
3313
+ declare function bytesToB64(bytes: Uint8Array): string;
3314
+ /** Mint a fresh 256-bit document master key (go-live, doc 13 §1). Returned
3315
+ * base64url — the exact string that rides the URL fragment (`#k=`). */
3316
+ declare function mintDocKey(): string;
3317
+ /** Parse `#k=<key>` out of a URL fragment; null if absent. The PRESENCE of
3318
+ * this is what fixes the doc's mode client-side (doc 13 §6: `#k` present ⇒
3319
+ * encrypted, always — a welcome claiming otherwise is hard-refused). */
3320
+ declare function docKeyFromFragment(fragment: string): string | null;
3321
+ /**
3322
+ * Derive this epoch's working keys. Share code (doc 13 §7) mixes in when
3323
+ * set: a leaked link without the code cannot decrypt anything. The code is
3324
+ * stretched by the CALLER via stretchShareCode (PBKDF2) — this function
3325
+ * just mixes; keeping stretching separate lets the UI do it once.
3326
+ */
3327
+ declare function deriveEpochKeys(docKeyB64url: string, genesisId: string, stretchedCode?: Uint8Array): Promise<EpochKeys>;
3328
+ /** PBKDF2-SHA256 stretch of the share code (doc 13 §7): 600k iterations,
3329
+ * WebCrypto-native (no WASM dep), salt bound to the docId so a precomputed
3330
+ * table for one doc is useless for another. Deliberately NOT epoch-salted:
3331
+ * the hello must carry the proof BEFORE the welcome reveals the epoch id
3332
+ * (chicken-and-egg otherwise); epoch-binding of the KEYS happens in
3333
+ * deriveEpochKeys' HKDF salt instead, which is where it matters. */
3334
+ declare function stretchShareCode(code: string, docId: string): Promise<Uint8Array>;
3335
+ /** Seal a checkpoint bundle (doc 13 §3): AAD binds (docId, genesisId, seq)
3336
+ * so a stored checkpoint cannot be replayed at another position (F5c). The
3337
+ * body carries the docx + sidecar + the canonical docHash for joiner
3338
+ * cross-checking (blocker-2 verification). */
3339
+ /**
3340
+ * The sealed checkpoint's plaintext body (doc 13 §3). `mediaMeta` is the
3341
+ * doc-16 §6 late-join map: the declared sha (+ IV) of every registered media
3342
+ * part, so a joiner whose snapshot contains the REGISTRATIONS can also fetch
3343
+ * the bytes — the addresses live only in already-folded intents otherwise,
3344
+ * which is what made late-join media unfetchable.
3345
+ *
3346
+ * It rides INSIDE the sealed body on purpose: in an encrypted room the server
3347
+ * must not learn the document's part structure. It learns which shas exist
3348
+ * only when someone PUTs or GETs one, and nothing more.
3349
+ *
3350
+ * OPTIONAL and tolerantly read — a checkpoint sealed by an older build simply
3351
+ * has no map, and such a joiner degrades to reserving the box without being
3352
+ * able to fill it (the prior behavior). Additive, so no engine bump.
3353
+ */
3354
+ interface CheckpointBody {
3355
+ docx: string;
3356
+ sidecar: unknown;
3357
+ docHash: string;
3358
+ mediaMeta?: {
3359
+ part: string;
3360
+ sha: string;
3361
+ iv?: string;
3362
+ }[];
3363
+ }
3364
+ declare function sealCheckpoint(kContent: CryptoKey, docId: string, genesisId: string, seq: number, body: CheckpointBody): Promise<{
3365
+ iv: string;
3366
+ ciphertext: string;
3367
+ }>;
3368
+ /** A presence payload as it crosses a BLIND relay: an opaque blob. The
3369
+ * server sees two base64 strings and the sender's id — never a coordinate. */
3370
+ interface SealedPresence {
3371
+ iv: string;
3372
+ ciphertext: string;
3373
+ }
3374
+
3375
+ /** A sequenced envelope in an encrypted room's log (doc 13 §2): the
3376
+ * server-assigned seq plus the opaque envelope. The server can read only
3377
+ * the bookkeeping; clients derive the canonical form themselves. */
3378
+ type EnvelopeEntry = IntentEnvelope & {
3379
+ seq: number;
3380
+ };
3381
+ /**
3382
+ * One head in a document's LINEAGE chain (doc 15 §1): the identity of the
3383
+ * confirmed state a session epoch ended at. Ancestry is decided by hash
3384
+ * membership — never by dates (clocks are self-asserted). A rejoiner whose
3385
+ * own head appears in the seed's lineage is a strict ancestor and can
3386
+ * FAST-FORWARD silently; anything else is true divergence (draft/fork).
3387
+ */
3388
+ interface LineageHead {
3389
+ genesisId: string;
3390
+ seq: number;
3391
+ /** sha256 hex of the confirmed docx bytes at (genesisId, seq). */
3392
+ docHash: string;
3393
+ }
3394
+ /** A sealed checkpoint blob as the wire carries it (doc 13 §3). */
3395
+ interface SealedCheckpoint {
3396
+ seq: number;
3397
+ iv: string;
3398
+ ciphertext: string;
3399
+ }
3400
+ /**
3401
+ * Wire protocol between a collab client and the server host. Transport-
3402
+ * agnostic: the hub speaks these messages; a WebSocket (or in-memory test)
3403
+ * adapter serializes them. Kept deliberately small (plan doc 06).
3404
+ */
3405
+ /** One highlighted stretch of a remote participant's selection: a half-open
3406
+ * `[start, end)` inside ONE run, in the same WIRE offset basis carets and
3407
+ * suggestRevision use (cumulative within the run, inline separators counting
3408
+ * one unit each — see core `EncodedCaret`). The sender emits one entry per
3409
+ * selection segment, and a segment never straddles a w:t, so a range stays
3410
+ * inside a single run by construction. */
3411
+ interface PresenceRange {
3412
+ blockId: number;
3413
+ runId: number;
3414
+ start: number;
3415
+ end: number;
3416
+ }
3417
+ /** An ephemeral cursor/selection position on the presence channel: stable-id
3418
+ * addresses (plan doc 03). Never logged, never persisted. */
3419
+ interface PresencePosition {
3420
+ anchor: {
3421
+ blockId: number;
3422
+ runId: number;
3423
+ offset: number;
3424
+ };
3425
+ focus?: {
3426
+ blockId: number;
3427
+ runId: number;
3428
+ offset: number;
3429
+ };
3430
+ /** Selection highlight (Google-Docs style): the stretches the participant
3431
+ * has selected, drawn in their presence color under the remote caret.
3432
+ * OPTIONAL and additive — a payload from an older client carries only the
3433
+ * anchor and still renders a caret. Absent/empty ⇒ no selection. */
3434
+ ranges?: PresenceRange[];
3435
+ }
3436
+ /**
3437
+ * A participant's self-asserted display profile (plan doc 14 §2). No
3438
+ * accounts: anyone with the link can claim any name — stated in the UI
3439
+ * ("names are chosen by participants") — but the F4 clientId binding
3440
+ * guarantees CONTINUITY: one name maps to one actor per session and nobody
3441
+ * can impersonate an existing participant mid-session. The server sanitizes
3442
+ * (length, control chars, color shape) and clients still render text-node-
3443
+ * only with palette-validated colors (doc 11 XSS vector 7 — defense in
3444
+ * depth, since a hostile server or E2EE mode moves sanitization client-side).
3445
+ */
3446
+ interface ParticipantProfile {
3447
+ /** 1–40 chars after server sanitization. */
3448
+ name: string;
3449
+ /** Hex color `#rrggbb`; invalid values are replaced server-side by a
3450
+ * palette color derived from the clientId hash. */
3451
+ color: string;
3452
+ }
3453
+ /** One roster row (doc 14 §2): keyed by the BOUND clientId — the same
3454
+ * identity intents carry, so attribution, presence, and roster share one
3455
+ * keyspace. Disconnected participants stay listed (greyed) for the session's
3456
+ * lifetime; a reconnect under the same clientId resumes the entry. */
3457
+ /**
3458
+ * Whether a participant's edits will be SEQUENCED, and if not, why.
3459
+ *
3460
+ * A POSITIVE signal, which is the entire point. Until this existed a client
3461
+ * could only discover it was blocked by making an edit and being refused —
3462
+ * which cost the user their first keystrokes every time (optimistic local
3463
+ * mutation, refusal, heal: a burst of text that appears and vanishes), and
3464
+ * left them stuck in viewer mode after a lock was LIFTED, because nothing
3465
+ * announced the good news either.
3466
+ *
3467
+ * The three blocked states arrive as one `read-only` refusal on the wire, so
3468
+ * they are separated here — the difference is what a UI can honestly say:
3469
+ * - `owner-lock` the owner paused editing for the whole room; it may lift
3470
+ * - `demoted` the owner made THIS participant a viewer
3471
+ * - `viewer-role` the access token grants read access only; nothing the
3472
+ * owner does in-session changes it
3473
+ */
3474
+ type WriteStatus = "allowed" | "owner-lock" | "demoted" | "viewer-role";
3475
+ /** One roster row (doc 14 §2): keyed by the BOUND clientId — the same
3476
+ * identity intents carry, so attribution, presence, and roster share one
3477
+ * keyspace. Disconnected participants stay listed (greyed) for the session's
3478
+ * lifetime; a reconnect under the same clientId resumes the entry. */
3479
+ interface RosterEntry {
3480
+ clientId: string;
3481
+ profile: ParticipantProfile;
3482
+ connected: boolean;
3483
+ /**
3484
+ * Whether this participant may write, re-fanned on every transition.
3485
+ *
3486
+ * OPTIONAL because it is additive: an older server omits it. A client that
3487
+ * sees `undefined` must NOT assume "allowed" — it should fall back to the
3488
+ * refusal-driven behaviour it had before, exactly as the published media
3489
+ * limit's `null` means "skip the pre-check". Inventing a permissive default
3490
+ * would put the user back in the first-edit-lost loop this removes.
3491
+ *
3492
+ * PER-CLIENT, not a room flag: an owner keeps writing through their own
3493
+ * room-wide lock, so a single broadcast value would be wrong for them.
3494
+ */
3495
+ write?: WriteStatus;
3496
+ }
3497
+ /** Client → server. */
3498
+ type ClientMessage = {
3499
+ t: "hello";
3500
+ protocolVersion: number;
3501
+ docId: string;
3502
+ /**
3503
+ * The identity every subsequent submit/presence message is bound to.
3504
+ * The hub registers it for this socket at hello and REFUSES any submit
3505
+ * whose intent.clientId differs (plan doc 11 decision 8 / round-4 F4):
3506
+ * `(clientId, clientSeq)` is the idempotency key, so an unbound claimed
3507
+ * id would let one client poison another's dedup space (silent edit
3508
+ * loss) and forge attribution. Registration-on-first-hello is
3509
+ * sufficient — the id needs continuity, not global meaning.
3510
+ */
3511
+ clientId: string;
3512
+ /**
3513
+ * Single-live-connection rule (plan doc 12 §7): a second hello for the
3514
+ * same (docId, clientId) is refused `already-open` — same-profile tabs
3515
+ * share localStorage identity and would collide clientSeq counters.
3516
+ * With `takeover: true` the NEW connection wins instead: the hub kicks
3517
+ * the old socket (it may be a zombie tab) and admits this one.
3518
+ */
3519
+ takeover?: boolean;
3520
+ token?: string;
3521
+ sinceSeq: number;
3522
+ /** The epoch id from the client's stored bundle, when resuming (plan
3523
+ * doc 12 §5). Informational to the server today — epoch comparison is
3524
+ * client-side (welcome.genesisId vs the bundle's) — but carried so the
3525
+ * server can log resume-vs-fresh joins and future-proof case handling. */
3526
+ genesisId?: string;
3527
+ /** Display profile (doc 14 §2) — optional; omitted = anonymous default
3528
+ * derived server-side. In E2EE mode this becomes an opaque encrypted
3529
+ * blob (doc 13/14) — the shape change rides that protocol bump. */
3530
+ profile?: ParticipantProfile;
3531
+ /** Engine (transform/apply semantics) version — REQUIRED for encrypted
3532
+ * rooms, where the fence prevents mixed-semantics divergence. */
3533
+ engineVersion?: string;
3534
+ /**
3535
+ * Share-code proof (doc 13 §7): the PBKDF2-stretched code, base64.
3536
+ * The server compares against the verifier registered at seed time and
3537
+ * enforces an attempt budget — 10^6 combinations is plenty against 5
3538
+ * online tries, and in E2EE mode the code ALSO mixes into key
3539
+ * derivation, so even a server bypass yields undecryptable content.
3540
+ */
3541
+ codeProof?: string;
3542
+ /**
3543
+ * Owner-capability proof (doc 14 §2.5): the token minted at seed and
3544
+ * returned ONLY to the seeder — never part of the shared link. A
3545
+ * matching token flags this connection as the epoch's owner
3546
+ * (read-only bypass + admin channel). Roles are capabilities, not
3547
+ * accounts: lose the bundle, lose the crown; re-seed to reclaim.
3548
+ */
3549
+ ownerToken?: string;
3550
+ }
3551
+ /** Update this connection's profile mid-session (rename / recolor). */
3552
+ | {
3553
+ t: "profile";
3554
+ profile: ParticipantProfile;
3555
+ }
3556
+ /**
3557
+ * Owner admin channel (doc 14 §2.5) — only honored from a connection
3558
+ * whose hello proved the owner token. All enforceable by a BLIND server
3559
+ * (identity/role are plaintext bookkeeping). Honest limits: kick/demote
3560
+ * key by clientId (a determined user can mint a fresh id — a nudge, not
3561
+ * a wall, without accounts), and in E2EE mode an owner controls WRITES,
3562
+ * never reads (re-key is the only read fence).
3563
+ */
3564
+ | {
3565
+ t: "admin";
3566
+ action: {
3567
+ op: "kick";
3568
+ clientId: string;
3569
+ } | {
3570
+ op: "readOnly";
3571
+ on: boolean;
3572
+ } | {
3573
+ op: "setRole";
3574
+ clientId: string;
3575
+ role: "editor" | "viewer";
3576
+ };
3577
+ } | {
3578
+ t: "submit";
3579
+ intent: Intent;
3580
+ }
3581
+ /** Encrypted-mode submit (doc 13 §2): opaque body, plaintext bookkeeping.
3582
+ * The hub refuses this on plaintext rooms and refuses plaintext `submit`
3583
+ * on encrypted rooms — no mixed-mode documents, enforced both ways. */
3584
+ | {
3585
+ t: "submit-enc";
3586
+ envelope: IntentEnvelope;
3587
+ }
3588
+ /** Upload a sealed checkpoint (doc 13 §3) — accepted ONLY from the
3589
+ * connection the server currently designates as checkpointer. */
3590
+ | {
3591
+ t: "checkpoint";
3592
+ checkpoint: SealedCheckpoint;
3593
+ }
3594
+ /** Media re-supply (doc 16 §3): "I need blob <sha>" (relay miss). */
3595
+ | {
3596
+ t: "media-need";
3597
+ sha: string;
3598
+ }
3599
+ /** "I hold these blobs" — reply to media-request, or unsolicited right
3600
+ * after a welcome whose mediaNeeded intersects local holdings (§5.4). */
3601
+ | {
3602
+ t: "media-have";
3603
+ shas: string[];
3604
+ }
3605
+ /** Encrypted hash gossip (doc 13 §2): an OPAQUE sealed {seq, hash} blob
3606
+ * the server relays without reading — divergence detection must not leak
3607
+ * even document hashes to a blind server (a hash is a stable content
3608
+ * fingerprint: confirmable by anyone holding a guess). */
3609
+ | {
3610
+ t: "gossip";
3611
+ iv: string;
3612
+ ciphertext: string;
3613
+ }
3614
+ /** Presence. In an ENCRYPTED room this is a `SealedPresence` blob and the
3615
+ * hub relays it without looking (#20): the server learns who is pointing,
3616
+ * never where. Plaintext rooms keep the structured position, which the hub
3617
+ * still clamps at the relay. */
3618
+ | {
3619
+ t: "presence";
3620
+ position: PresencePosition | SealedPresence | null;
3621
+ }
3622
+ /**
3623
+ * APPLICATION-LEVEL LIVENESS PROBE (the reconnect arc). The server answers
3624
+ * every one with {@link ServerMessage} `pong` carrying the same nonce.
3625
+ *
3626
+ * WHY THIS EXISTS AT ALL, given TCP and the WebSocket close handshake: a
3627
+ * phone that sleeps mid-session leaves a HALF-OPEN connection. The peer is
3628
+ * gone, but nothing tells this end — `onclose` never fires, `onerror` never
3629
+ * fires, `readyState` still reads OPEN, and every `send()` succeeds into a
3630
+ * socket whose bytes will never arrive. The only way to learn the truth is
3631
+ * to ask for a reply and notice it did not come.
3632
+ *
3633
+ * `nonce` is an opaque round-trip token, echoed back unmodified. It exists
3634
+ * so a pong can be matched to the ping it answers — without it a stale pong
3635
+ * from before a reconnect could silently satisfy the CURRENT probe and mask
3636
+ * a genuinely dead socket. It carries no meaning and is never persisted.
3637
+ *
3638
+ * DELIBERATELY NOT ACTIVITY. The hub answers this WITHOUT calling
3639
+ * `noteActivity`, so a heartbeat cannot hold an abandoned room open — the
3640
+ * exact hazard the idle-timeout notes call out ("an activity-based clock is
3641
+ * what a keepalive script defeats"). A ping proves the socket is alive; it
3642
+ * proves nothing about a human being present, and those are different
3643
+ * questions with different consequences.
3644
+ */
3645
+ | {
3646
+ t: "ping";
3647
+ nonce: number;
3648
+ };
3649
+ /** Server → client. */
3650
+ type ServerMessage = {
3651
+ t: "welcome";
3652
+ docId: string;
3653
+ seq: number;
3654
+ snapshot: string;
3655
+ /**
3656
+ * The stable-id sidecar for the snapshot (plan doc 12 §2, round-4 F10).
3657
+ * A snapshot NEVER travels without it: the joiner cannot re-derive the
3658
+ * id table from parse order once history contains split-created carried
3659
+ * ids (round-2 F1) — it would silently mis-address every later intent.
3660
+ */
3661
+ sidecar: IdSidecar;
3662
+ tail: LogEntry[];
3663
+ /**
3664
+ * The session EPOCH id (plan doc 12): minted fresh every time a doc is
3665
+ * seeded/re-seeded. A resuming client compares it against its bundle's
3666
+ * stored genesisId — same ⇒ seamless rejoin (case 1); different ⇒
3667
+ * someone re-seeded while it was away (case 2: take server state, keep
3668
+ * the local copy per doc 15 lineage — NEVER silently merge epochs).
3669
+ */
3670
+ genesisId: string;
3671
+ /** Session encryption mode (plan doc 13 §6). Clients derive the truth
3672
+ * from their link (#k present ⇒ encrypted) and HARD-REFUSE a welcome
3673
+ * that contradicts it — the wire value must never downgrade a client. */
3674
+ mode: "plaintext" | "encrypted";
3675
+ /** The seed's lineage chain (doc 15): lets a rejoining holder decide
3676
+ * fast-forward vs fork CLIENT-side. Absent for provider-created and
3677
+ * legacy rooms (⇒ every epoch mismatch is a fork, the safe default). */
3678
+ lineage?: LineageHead[];
3679
+ /** Shas with waiters / outstanding unavailability (doc 16 §3): a
3680
+ * joining holder intersects with its local media and volunteers —
3681
+ * the mechanism behind "reappears when a holder rejoins". */
3682
+ mediaNeeded?: string[];
3683
+ /** Per-part media ADDRESSES (doc 16 §6 late-join): the declared sha
3684
+ * (+ E2EE iv) for every registered media part, so a joiner can fetch
3685
+ * pixels its snapshot references but does not contain — without this
3686
+ * the address lives only in the already-folded insertImage intent and
3687
+ * late-join media is unfetchable. Plaintext rooms only; encrypted
3688
+ * rooms carry the same map INSIDE the sealed checkpoint body (the
3689
+ * server must not learn part structure beyond what PUT addresses
3690
+ * already reveal). Parse-derived holes with unknown sha are omitted. */
3691
+ media?: {
3692
+ part: string;
3693
+ sha: string;
3694
+ iv?: string;
3695
+ }[];
3696
+ /**
3697
+ * The relay's configured per-blob size limit, in bytes, so a client can
3698
+ * CHECK A FILE BEFORE UPLOADING IT rather than discovering the limit
3699
+ * from a 413 afterwards.
3700
+ *
3701
+ * The number flows FORWARD — server to client, once per session — which
3702
+ * is why it lives here rather than being threaded back through the
3703
+ * upload's return type. A refusal has to travel up through every layer
3704
+ * between the relay and the file picker, and each of those layers has
3705
+ * its own idea of what failure looks like; a value published at join
3706
+ * time has no such path to get lost on.
3707
+ *
3708
+ * NOT A SECRET, and nothing is gated on it: the same number already
3709
+ * appears in a 413 body, and anyone can discover it by uploading
3710
+ * something large. It is per-SERVER-CONFIG, not per-room, so it carries
3711
+ * nothing about the room's contents or participants.
3712
+ *
3713
+ * Optional and additive: an older server omits it, and a client that
3714
+ * gets nothing should skip the pre-check rather than invent a limit —
3715
+ * the server still enforces the real one.
3716
+ */
3717
+ mediaMaxBlobBytes?: number;
3718
+ } | {
3719
+ t: "broadcast";
3720
+ entries: LogEntry[];
3721
+ }
3722
+ /**
3723
+ * Encrypted-mode welcome (doc 13 §3): the seed checkpoint (sealed by the
3724
+ * epoch's seeder) + the epoch's envelope log after it. BASE-COMPLETE by
3725
+ * construction (round-4 blocker 1): the demo server retains the WHOLE
3726
+ * epoch log in RAM (doc 12 — rooms are session-scoped), so every entry a
3727
+ * joiner needs to re-derive canonical forms is present; client-produced
3728
+ * mid-session checkpoints (doc 13 item 6) are a RAM optimization on top,
3729
+ * not a correctness requirement, and land with the retention rule.
3730
+ */
3731
+ | {
3732
+ t: "welcome-enc";
3733
+ docId: string;
3734
+ genesisId: string;
3735
+ checkpoint: SealedCheckpoint;
3736
+ tail: EnvelopeEntry[];
3737
+ mode: "encrypted";
3738
+ mediaNeeded?: string[];
3739
+ /** Relay per-blob size limit — see the `welcome` field of the same
3740
+ * name. Published identically in encrypted rooms: it is server config,
3741
+ * not room content, so a blind sequencer can state it in the clear. */
3742
+ mediaMaxBlobBytes?: number;
3743
+ }
3744
+ /** Encrypted-mode broadcast: sequenced opaque envelopes. */
3745
+ | {
3746
+ t: "broadcast-enc";
3747
+ entries: EnvelopeEntry[];
3748
+ }
3749
+ /** Relayed gossip blob; `from` is the sender's BOUND clientId. */
3750
+ | {
3751
+ t: "gossip";
3752
+ from: string;
3753
+ iv: string;
3754
+ ciphertext: string;
3755
+ }
3756
+ /** Media re-supply control (doc 16 §4). request = broadcast "who has
3757
+ * it"; upload = to ONE chosen holder; ready/unavailable = to waiters. */
3758
+ | {
3759
+ t: "media-request";
3760
+ sha: string;
3761
+ } | {
3762
+ t: "media-upload";
3763
+ sha: string;
3764
+ } | {
3765
+ t: "media-ready";
3766
+ sha: string;
3767
+ } | {
3768
+ t: "media-unavailable";
3769
+ sha: string;
3770
+ }
3771
+ /** Checkpointer designation (doc 13 §3): the SERVER assigns the role —
3772
+ * the v1 lowest-clientId election was riggable (round-4 blocker 2); an
3773
+ * assigned role can only be held by an authenticated connection the
3774
+ * server picked. Rotated on disconnect. */
3775
+ | {
3776
+ t: "checkpointer";
3777
+ active: boolean;
3778
+ }
3779
+ /** `participant` is the sender's bound clientId (round-4 F14) — the same
3780
+ * identifier intents carry — so presence joins the roster/attribution
3781
+ * keyspace and survives the sender reconnecting on a new socket. */
3782
+ | {
3783
+ t: "presence";
3784
+ participant: string;
3785
+ position: PresencePosition | SealedPresence | null;
3786
+ }
3787
+ /** Full roster snapshot, fanned out on every join/leave/profile change
3788
+ * (rooms are small; a snapshot beats delta bookkeeping). Ephemeral like
3789
+ * presence: never logged, never persisted, dies with the room. */
3790
+ | {
3791
+ t: "roster";
3792
+ roster: RosterEntry[];
3793
+ }
3794
+ /**
3795
+ * The session is going to END, and this is the grace period before it does
3796
+ * (server lifecycle arc). Sent once per armed deadline to every connection
3797
+ * in the room so the UI can run a countdown — a session that vanishes
3798
+ * without warning is indistinguishable from a crash, and the whole point of
3799
+ * these timeouts is that they are POLICY, visibly applied.
3800
+ *
3801
+ * - `idle` nobody has done anything for the idle window. CANCELLABLE:
3802
+ * any qualifying activity (a submit, an admin action, a media
3803
+ * transfer, someone joining — never presence) resets the clock
3804
+ * and produces `session-warning-cleared`.
3805
+ * - `lifetime` the room has reached its absolute age cap. NOT cancellable
3806
+ * by anything; the deadline was fixed when the room was
3807
+ * created. Ending it does not end the document — any holder
3808
+ * re-seeds from their bundle into a NEW epoch.
3809
+ *
3810
+ * `inMs` is the ACTUAL remaining time when the frame was built, not the
3811
+ * configured lead time: warnings are emitted by a periodic sweep, so the
3812
+ * real remainder is up to one sweep interval less than the configured
3813
+ * value, and a client counting down from the constant would lie.
3814
+ */
3815
+ | {
3816
+ t: "session-warning";
3817
+ reason: "idle" | "lifetime";
3818
+ inMs: number;
3819
+ }
3820
+ /**
3821
+ * A previously warned deadline is no longer approaching — the countdown
3822
+ * should disappear. Only ever `idle`: the lifetime cap cannot be cancelled,
3823
+ * and narrowing the type here means a consumer cannot write a branch the
3824
+ * server is incapable of producing.
3825
+ */
3826
+ | {
3827
+ t: "session-warning-cleared";
3828
+ reason: "idle";
3829
+ }
3830
+ /**
3831
+ * The answer to a client `ping`, echoing its nonce. Proof that the socket is
3832
+ * end-to-end alive RIGHT NOW — not merely that the local OS still believes
3833
+ * in it, which is all `readyState === OPEN` ever establishes.
3834
+ *
3835
+ * Emitted unconditionally and statelessly: no room lookup, no auth check, no
3836
+ * activity bookkeeping. A liveness answer that could itself be refused would
3837
+ * be indistinguishable from the death it is supposed to detect.
3838
+ */
3839
+ | {
3840
+ t: "pong";
3841
+ nonce: number;
3842
+ } | {
3843
+ t: "refused";
3844
+ reason: string;
3845
+ };
3846
+
3847
+ interface ToolbarMenuSelectOption {
3848
+ value: string;
3849
+ label: React.ReactNode;
3850
+ group?: string;
3851
+ disabled?: boolean;
3852
+ fontFamily?: string;
3853
+ }
3854
+ interface ToolbarMenuSelectProps {
3855
+ value: string;
3856
+ options: ToolbarMenuSelectOption[];
3857
+ onChange: (value: string) => void;
3858
+ placeholder?: React.ReactNode;
3859
+ title?: string;
3860
+ ariaLabel?: string;
3861
+ triggerAriaLabel?: string;
3862
+ width?: number | string;
3863
+ menuWidth?: number;
3864
+ className?: string;
3865
+ style?: React.CSSProperties;
3866
+ }
3867
+ /** CSS-overridable replacement for visible native selects. An inert,
3868
+ * transparent select is kept as an event bridge for existing integrations;
3869
+ * every user-facing part is a button/listbox rendered by us. */
3870
+ declare function ToolbarMenuSelect({ value, options, onChange, placeholder, title, ariaLabel, triggerAriaLabel, width, menuWidth, className, style, }: ToolbarMenuSelectProps): react.JSX.Element;
3871
+ /** Toolbar control groups a host can disable via the `features` prop. */
3872
+ type ToolbarFeature = "history" | "styles" | "font" | "size" | "format" | "color" | "highlight" | "alignment" | "indent" | "spacing" | "link" | "lists" | "table" | "image" | "icon" | "screenshot" | "model3D" | "media" | "object" | "chart" | "smartArt" | "comment" | "footnote" | "bookmark" | "crossReference" | "dateTime" | "field" | "equation" | "symbol" | "shape" | "divider" | "textBox" | "wordArt" | "drawing" | "arrange" | "dropCap" | "headerFooter" | "coverPage" | "pageNumber" | "break" | "layout" | "help" | "download";
3873
+ /**
3874
+ * Every `DocxViewApi` command that INSERTS content at the caret, paired with
3875
+ * the toolbar feature group that gates its control.
3876
+ *
3877
+ * A single source of truth in the same spirit as core's
3878
+ * `SELECTED_OBJECT_COMMANDS`: the collab audit (react/test/insert-commands
3879
+ * INVARIANT C) reads this list and the real `COLLAB_TOOLBAR_DEFAULTS` gate
3880
+ * rather than restating either, so "what the toolbar offers in a room" and
3881
+ * "what the audit checks" cannot drift apart.
3882
+ *
3883
+ * THE RULE a new entry signs up to: in collab mode an insert command must
3884
+ * EMIT an intent, or its feature must be gated off so no control exists to
3885
+ * press. An absent button is honest; a present button that mutates the local
3886
+ * document without emitting forks the room silently — the exact shape that
3887
+ * shipped in `insertImage` and stayed green through the whole capability
3888
+ * matrix, because the matrix's collab mount withholds `submitOp` and so
3889
+ * never exercises an insert command's collab path.
3890
+ */
3891
+ interface InsertCommandSpec {
3892
+ /** The `DocxViewApi` method name. */
3893
+ command: string;
3894
+ /** Toolbar group whose gate decides whether a control is offered. */
3895
+ feature: ToolbarFeature;
3896
+ }
3897
+ declare const INSERT_COMMANDS: readonly InsertCommandSpec[];
3898
+ type ToolbarMode = "simple" | "advanced";
3899
+ interface DocxToolbarProps {
3900
+ api: DocxViewApi | null;
3901
+ onSave?: (bytes: Uint8Array) => void;
3902
+ /** Simple shows basic Home editing; advanced adds the Insert, Draw, and Layout ribbons. */
3903
+ mode?: ToolbarMode;
3904
+ /** Per-group overrides; every group defaults to enabled. */
3905
+ features?: Partial<Record<ToolbarFeature, boolean>>;
3906
+ /** Extra class on the toolbar root (e.g. a scope for CSS-variable overrides). */
3907
+ className?: string;
3908
+ /** Inline overrides merged onto the toolbar root; wins over the defaults. */
3909
+ style?: React.CSSProperties;
3910
+ }
3911
+ declare function DocxToolbar({ api, onSave, mode, features, className, style, }: DocxToolbarProps): react.JSX.Element;
3912
+
3913
+ interface DocxViewApi {
3914
+ /** Apply character formatting to the current browser selection. */
3915
+ applyFormat(patch: RunFormatPatch): void;
3916
+ /** Create a review comment on the current selection. False if no selection. */
3917
+ addComment(text: string): boolean;
3918
+ /** Insert a footnote at the caret. False without a caret. */
3919
+ addFootnote(text: string): boolean;
3920
+ /** Insert a dynamic page-number field at the caret (body, header or footer). */
3921
+ insertPageNumber(kind?: "page" | "pageOfTotal"): boolean;
3922
+ /** Insert any Word field instruction supported by the renderer. */
3923
+ insertField(instruction: string, cachedResult?: string): boolean;
3924
+ /** Insert a live DATE or TIME field with an optional Word date picture. */
3925
+ insertDateTime(kind: "date" | "time", picture?: string): boolean;
3926
+ /** Named bookmarks in document order. */
3927
+ listBookmarks(): string[];
3928
+ /** Add a bookmark around the selection, or a zero-length bookmark at the caret. */
3929
+ addBookmark(name: string): boolean;
3930
+ /** Insert a live text or page reference to a bookmark. */
3931
+ insertCrossReference(bookmark: string, kind: "text" | "page"): boolean;
3932
+ /** Insert an editable inline equation from WordInWeb's linear math syntax. */
3933
+ insertEquation(linear: string): boolean;
3934
+ /** Insert a Unicode symbol through the normal undo/suggestion-aware typing path. */
3935
+ insertSymbol(symbol: string): boolean;
3936
+ /** Insert a floating editable DrawingML shape at the caret. */
3937
+ insertShape(preset: ShapePreset, text?: string, lineStyle?: {
3938
+ color: string;
3939
+ width: number;
3940
+ dash: DrawingLineDash;
3941
+ }): boolean;
3942
+ /** Insert editable DrawingML WordArt at the caret. */
3943
+ insertWordArt(text: string, preset?: WordArtPreset): boolean;
3944
+ /** Insert a native editable ChartML chart at the caret. */
3945
+ insertChart(data: ChartData): boolean;
3946
+ /** Replace the selected native chart's type and data. */
3947
+ updateSelectedChart(data: ChartData): boolean;
3948
+ /** Data for the selected native chart, or null when another object is selected. */
3949
+ getSelectedChart(): ChartData | null;
3950
+ /** Insert a native editable SmartArt diagram at the caret. */
3951
+ insertSmartArt(data: SmartArtData): boolean;
3952
+ /** Replace the selected native SmartArt diagram's layout and node text. */
3953
+ updateSelectedSmartArt(data: SmartArtData): boolean;
3954
+ /** Data for the selected native SmartArt diagram, or null when another object is selected. */
3955
+ getSelectedSmartArt(): SmartArtData | null;
3956
+ /** Text formatting for the selected SmartArt node, or the first node when the group is selected. */
3957
+ getSelectedSmartArtTextFormat(): SmartArtTextFormat | null;
3958
+ /** Format the selected SmartArt node, or every node when the group is selected. */
3959
+ setSelectedSmartArtTextFormat(patch: Partial<SmartArtTextFormat>): boolean;
3960
+ /** Insert a native Office 3D model with an optional custom poster image. */
3961
+ insertModel3D(file: Blob, poster?: Blob): Promise<boolean>;
3962
+ /** Insert Word online-video metadata with a browser-safe poster. */
3963
+ insertOnlineVideo(url: string): Promise<boolean>;
3964
+ /** Embed an arbitrary file as a native OLE Package object. */
3965
+ insertEmbeddedObject(file: Blob, filename?: string): Promise<boolean>;
3966
+ /** Activate a freehand pen, or return to selection mode with null. */
3967
+ setDrawingTool(tool: DrawingTool | null): void;
3968
+ /** Current freehand pen, or null while in selection mode. */
3969
+ getDrawingTool(): DrawingTool | null;
3970
+ /** Align, rotate, or reorder the selected image, shape, or ink group. */
3971
+ arrangeObject(action: ObjectArrangeAction): boolean;
3972
+ /** True while an image, shape, or ink group is selected. */
3973
+ hasSelectedObject(): boolean;
3974
+ /** Kind of the selected object, used by contextual formatting controls. */
3975
+ getSelectedObjectContext(): {
3976
+ kind: SelectedObjectKind;
3977
+ canEditText: boolean;
3978
+ smartArtNodeSelected?: boolean;
3979
+ smartArtNodeIndex?: number;
3980
+ } | null;
3981
+ /** Run a formatting command against the current selected object. */
3982
+ runSelectedObjectCommand(command: SelectedObjectCommand): boolean;
3983
+ undo(): void;
3984
+ redo(): void;
3985
+ canUndo(): boolean;
3986
+ canRedo(): boolean;
3987
+ /** Insert a rows×cols table at the caret's paragraph. */
3988
+ insertTable(rows: number, cols: number): void;
3989
+ /** Row/column/table operations on the table containing the caret. */
3990
+ tableOp(op: TableOp): void;
3991
+ /** Current table-cell fill, undefined when the caret is outside a table. */
3992
+ getTableCellFill(): string | null | undefined;
3993
+ /** Insert an image file at the caret (inline, natural size clamped to column).
3994
+ * The result is REPORTED rather than swallowed: a picker that accepts a file
3995
+ * and then does nothing is indistinguishable from a broken button. */
3996
+ insertImage(file: Blob): Promise<ImageInsertResult>;
3997
+ /** What this document's image picker should ADVERTISE, as a file-input
3998
+ * `accept` string — narrower in a shared document, where the format has to
3999
+ * survive the wire. Derived from the same list the insert guard consults, so
4000
+ * the picker cannot offer something the insert will refuse. */
4001
+ imageAccept(): string;
4002
+ /** The relay's published per-image byte limit, or null when there is none to
4003
+ * show (a local document, or a server that publishes no limit). Null means
4004
+ * "say nothing about size" — never substitute a default. */
4005
+ imageMaxBytes(): number | null;
4006
+ /** Capture a screen, window, or browser tab and insert the current frame as a PNG picture. */
4007
+ insertScreenshot(): Promise<ScreenshotInsertResult>;
4008
+ /** Align the paragraph(s) under the caret or selection. */
4009
+ setAlignment(align: ParagraphAlignment): void;
4010
+ /** Apply a named paragraph style (null clears back to Normal). */
4011
+ setParagraphStyle(styleId: string | null): void;
4012
+ /** Toggle bulleted/numbered list on the paragraph(s) under the selection. */
4013
+ toggleList(kind: "bullet" | "number"): void;
4014
+ /** Current list kind at the caret ("bullet" | "number" | null). */
4015
+ getListType(): "bullet" | "number" | null;
4016
+ /** Link the selection to a URL; null removes the link at the caret. */
4017
+ setLink(url: string | null): void;
4018
+ /** URL of the hyperlink at the caret/selection, or null. */
4019
+ getLinkAt(): string | null;
4020
+ /** Step paragraph indent by half an inch (Word's indent buttons). */
4021
+ adjustIndent(direction: 1 | -1): void;
4022
+ /** Line spacing multiple or exact point height, and/or space before/after (points). */
4023
+ setParagraphSpacing(patch: {
4024
+ lineMultiple?: number;
4025
+ exactLinePt?: number;
4026
+ beforePt?: number | null;
4027
+ afterPt?: number | null;
4028
+ }): void;
4029
+ /** Create, customize, or remove the bottom-border divider on the selected paragraph(s). */
4030
+ setParagraphDivider(divider: ParagraphDivider | null): boolean;
4031
+ /** Direct bottom-border divider on the caret paragraph. */
4032
+ getParagraphDivider(): ParagraphDivider | null;
4033
+ /** Apply or remove a native Word drop cap on the caret paragraph. */
4034
+ setDropCap(mode: "drop" | "margin" | null, lines?: number): boolean;
4035
+ /** Remove direct character formatting from the selection. */
4036
+ clearFormatting(): void;
4037
+ /** Change the selection's case. */
4038
+ changeCase(mode: "upper" | "lower" | "title"): void;
4039
+ /** Find matches for a query; selects the first and returns the count. */
4040
+ find(query: string, opts?: {
4041
+ matchCase?: boolean;
4042
+ }): number;
4043
+ /** Select the next/previous match; returns 1-based index or 0. */
4044
+ findStep(delta: 1 | -1): number;
4045
+ /** Replace the current match; returns remaining match count. */
4046
+ replaceCurrent(replacement: string): number;
4047
+ /** Replace every match; returns how many were replaced. */
4048
+ replaceAll(query: string, replacement: string): number;
4049
+ /** Paragraph styles for the style menu (declared + Word built-ins). */
4050
+ listParagraphStyles(): {
4051
+ id: string;
4052
+ name: string;
4053
+ }[];
4054
+ /** pStyle id of the caret paragraph (null = Normal). */
4055
+ getParagraphStyleId(): string | null;
4056
+ /** Change margins / page size / orientation (inches). */
4057
+ setPageLayout(patch: PageLayoutPatch, scope?: "document" | "section"): void;
4058
+ /** One-based logical section containing the caret or selection. */
4059
+ getSectionContext(): {
4060
+ index: number;
4061
+ count: number;
4062
+ } | null;
4063
+ /** Insert a page/column break or a section break at the caret. */
4064
+ insertBreak(kind: "page" | "column" | "sectionNextPage" | "sectionContinuous"): boolean;
4065
+ /** Insert a full blank page at the caret (two consecutive page breaks). */
4066
+ insertBlankPage(): boolean;
4067
+ /** Insert an editable cover page before the current document. */
4068
+ insertCoverPage(content: CoverPageContent): boolean;
4069
+ /** Toggle/configure margin line numbers (Word's Layout > Line Numbers).
4070
+ * scope "section" targets the caret's section; "document" every section. */
4071
+ setLineNumbering(patch: LineNumberingPatch, scope?: "document" | "section"): void;
4072
+ /** Current line-numbering settings for the caret's section, or null (off). */
4073
+ getLineNumbering(): {
4074
+ countBy: number;
4075
+ restart: "continuous" | "newPage" | "newSection";
4076
+ start: number;
4077
+ } | null;
4078
+ /** Leave header/footer editing mode. */
4079
+ closeHeaderFooter(): void;
4080
+ /** Enter and, if needed, create the header or footer on the caret's page. */
4081
+ openHeaderFooter(kind: "header" | "footer"): boolean;
4082
+ /** Effective formatting of the current selection (toolbar state), or null. */
4083
+ getSelectionFormat(): SelectionFormat | null;
4084
+ /** Print the rendered pages (browser print dialog / save as PDF). */
4085
+ print(): void;
4086
+ /** Serialize the (edited) document back to .docx bytes. */
4087
+ save(): Uint8Array;
4088
+ /** Page count after the latest layout. */
4089
+ pageCount(): number;
4090
+ /**
4091
+ * Suggesting mode: when on, edits record as OOXML tracked changes (w:ins /
4092
+ * w:del) instead of mutating text directly, and the view switches to markup
4093
+ * so the suggestion shows live. `author` stamps the revision (defaults to the
4094
+ * commentAuthor prop). Turning it off restores the prior revision view.
4095
+ */
4096
+ setSuggesting(on: boolean, author?: string): void;
4097
+ isSuggesting(): boolean;
4098
+ /** Accept the tracked change at the caret (keep insertion / apply deletion).
4099
+ * Refuses (false, no mutation) in a live collab session — resolving a
4100
+ * suggestion emits no intent yet, so it would fork the room. */
4101
+ acceptRevisionAtCaret(): boolean;
4102
+ /** Reject the tracked change at the caret (drop insertion / restore
4103
+ * deletion). Refuses in a live collab session, like accept. */
4104
+ rejectRevisionAtCaret(): boolean;
4105
+ /** How many tracked changes (suggestions) the document currently holds. */
4106
+ revisionCount(): number;
4107
+ /** Accept every tracked change (one undo step). Returns how many applied.
4108
+ * Returns 0 without mutating in a live collab session, like accept. */
4109
+ acceptAllRevisions(): number;
4110
+ /** Reject every tracked change (one undo step). Returns how many applied.
4111
+ * Returns 0 without mutating in a live collab session, like accept. */
4112
+ rejectAllRevisions(): number;
4113
+ /** Current caret as stable-id addresses (collab), or null. The encoding
4114
+ * survives a reconciliation reload, so it can be captured from a view
4115
+ * about to remount and restored into its replacement. */
4116
+ getEncodedCaret(): EncodedCaret | null;
4117
+ /** Restore a caret captured by getEncodedCaret. False when the position no
4118
+ * longer resolves (or outside collab mode). */
4119
+ setCaretFromEncoded(pos: EncodedCaret): boolean;
4120
+ /**
4121
+ * Scroll the view so `participant`'s live presence caret is on screen —
4122
+ * "jump to that person". A pure VIEW operation: it never relayouts, marks
4123
+ * nothing dirty, and mutates no document state. The jump is instant, never
4124
+ * smooth: the target can be hundreds of virtualized pages away, and a
4125
+ * smooth scroll across that distance forces continuous page mounting;
4126
+ * instant also satisfies prefers-reduced-motion by construction.
4127
+ *
4128
+ * - "revealed" scrolled to the caret (or, when the exact run was
4129
+ * deleted since it was reported, to the start of its
4130
+ * paragraph — the same fallback caret restore uses).
4131
+ * - "no-position" that participant has no broadcast cursor right now
4132
+ * (never placed one, cleared it, or left).
4133
+ * - "unresolved" the position no longer maps to content in this replica.
4134
+ */
4135
+ revealPresence(participant: string): "revealed" | "no-position" | "unresolved";
4136
+ document: DocxDocument;
4137
+ }
4138
+ type ScreenshotInsertResult = "inserted" | "unsupported" | "cancelled" | "error" | "no-caret";
4139
+ /**
4140
+ * Image formats a SHARED document accepts, and the single source of truth for
4141
+ * both halves of that promise: the guard that declines an intent and the
4142
+ * `accept` attribute of the file picker that offers one.
4143
+ *
4144
+ * They were separately written constants once, and drifted — the picker
4145
+ * advertised SVG while the guard refused it, so choosing an SVG in a shared
4146
+ * document did nothing at all, with no skeleton, no message and no log. The
4147
+ * user's report was "it just didn't work and then disappeared". Deriving the
4148
+ * offer from the acceptance is what makes that drift unrepresentable.
4149
+ *
4150
+ * Raster-only because the wire allowlist is (collab/src/validate.ts): widening
4151
+ * it is an intent-shape change with an ENGINE_VERSION bump, not a UI edit.
4152
+ */
4153
+ declare const COLLAB_IMAGE_EXTS: readonly ["png", "jpg", "jpeg", "gif", "bmp", "webp"];
4154
+ /**
4155
+ * Why an image insert did not happen, so the caller can SAY so. Every one of
4156
+ * these was a bare `return` until a user hit the `unsupported-format` path and
4157
+ * had no way to tell a rejected file from a broken button.
4158
+ */
4159
+ type ImageInsertResult = "inserted" | "no-caret"
4160
+ /** Not a format a shared document can carry (SVG today). */
4161
+ | "unsupported-format"
4162
+ /** Bigger than the relay's published per-blob limit. Refused BEFORE any
4163
+ * bytes were read, sealed, hashed or sent — see `imageMaxBytes()` for the
4164
+ * number to show the user. */
4165
+ | "too-large"
4166
+ /** Collab is wired but no media relay is configured — nothing can be
4167
+ * uploaded, and inserting locally would fork the room. */
4168
+ | "no-relay"
4169
+ /** The relay refused the bytes; nothing was reserved, nothing forked. */
4170
+ | "upload-failed"
4171
+ /** The bytes did not decode as an image at all. */
4172
+ | "error";
4173
+ interface DocxViewProps {
4174
+ /** The document: raw bytes, a File/Blob, or a URL to fetch. */
4175
+ source: ArrayBuffer | Uint8Array | Blob | string;
4176
+ /** Zoom factor, 1 = 100%. */
4177
+ zoom?: number;
4178
+ /**
4179
+ * Fit-to-width (Google-Docs mobile behavior): when the page is wider than the
4180
+ * viewport, auto-scale down so it fits with a small gutter and never scrolls
4181
+ * horizontally. The computed scale drives the real `zoom` (crisp text, not a
4182
+ * blurry transform) and is capped at `zoom`, so a wide desktop viewport is
4183
+ * unchanged. Recomputed on container resize. Default true.
4184
+ */
4185
+ fitWidth?: boolean;
4186
+ /**
4187
+ * Container width (px) at or below which the chrome switches to its compact
4188
+ * phone/tablet treatment — the comment rail collapses to tap-to-open cards so
4189
+ * balloons never force horizontal scroll. Default 820.
4190
+ */
4191
+ narrowWidth?: number;
4192
+ /**
4193
+ * Enable editing commands (selection-based formatting, save-back).
4194
+ * Default false: pure render-only viewer.
4195
+ */
4196
+ editable?: boolean;
4197
+ className?: string;
4198
+ style?: React.CSSProperties;
4199
+ onLoad?: (info: {
4200
+ pageCount: number;
4201
+ document: DocxDocument;
4202
+ }) => void;
4203
+ /** Fires whenever editing changes the rendered page count. */
4204
+ onPageCountChange?: (pageCount: number) => void;
4205
+ /** Fires when the document is ready; the api is only usable while mounted. */
4206
+ onReady?: (api: DocxViewApi) => void;
4207
+ onError?: (error: Error) => void;
4208
+ /**
4209
+ * Optional collaborative session (from `wordinweb/collab`'s `useCollab`).
4210
+ * When provided, the editor forwards each local edit as an intent via
4211
+ * `collab.submit`. Typed structurally so the main `wordinweb` bundle carries
4212
+ * no runtime dependency on the collab engine (plan doc 07 tree-shaking) —
4213
+ * the app imports the session from the separate `wordinweb/collab` entry and
4214
+ * injects it here.
4215
+ */
4216
+ collab?: {
4217
+ /** clientId → display name for caret flags (doc 14 §2); text-node rendered. */
4218
+ participantNames?: Record<string, string>;
4219
+ submit: (intent: EditorIntent) => void;
4220
+ /** Remote participants' cursor/selection positions, drawn as colored
4221
+ * carets over the page (see presence-cursors). */
4222
+ presence?: Record<string, PresencePosition | null>;
4223
+ /** Allocate `n` fresh carried node ids (for sub-range format / split /
4224
+ * insert intents). Injected from the collab connection. */
4225
+ allocIds?: (n: number) => number[];
4226
+ /** The live reconciled document object to render DIRECTLY (skip the
4227
+ * bytes → parse round-trip). The collab replica mutates this same instance
4228
+ * in place on each broadcast; DocxView repaints it when `renderSignal`
4229
+ * bumps, so a remote edit costs one repaint — no re-serialize, no re-parse,
4230
+ * no caret reset. When present, `source` is only a placeholder. */
4231
+ doc?: DocxDocument;
4232
+ /** Monotonic counter that bumps whenever `doc` was mutated in place; a
4233
+ * change triggers an in-place repaint of `doc`. */
4234
+ renderSignal?: number;
4235
+ /** Drain the union of the dirty scopes behind `renderSignal` since the
4236
+ * last take. A narrow scope lets the repaint relayout one paragraph
4237
+ * incrementally (the same path local typing takes) instead of the whole
4238
+ * document; `doc` scope (or an absent method) keeps the whole-document
4239
+ * repaint; null means nothing is dirty and the repaint is skipped.
4240
+ * Consumed at the repaint so a coalesced repaint covers every batched
4241
+ * remote intent. */
4242
+ takeRenderScope?: () => {
4243
+ kind: "doc";
4244
+ } | {
4245
+ kind: "block";
4246
+ blocks: XmlElement[];
4247
+ } | {
4248
+ kind: "split";
4249
+ before: XmlElement;
4250
+ after: XmlElement;
4251
+ } | null;
4252
+ /** Broadcast the local caret so remote participants draw this user's
4253
+ * cursor. Called with the caret's stable-id address on every caret move
4254
+ * (null when the caret leaves id-tracked content). */
4255
+ setPresence?: (pos: PresencePosition | null) => void;
4256
+ /** Submit a toolbar/API operation NOT yet applied locally. The connection
4257
+ * applies it optimistically through the same canonical code the server
4258
+ * runs, so the local result is byte-identical to every replica. When set,
4259
+ * DocxView routes its imperative commands (insert chart/table/equation,
4260
+ * set link/page layout, comments, ...) through this instead of mutating
4261
+ * the document itself. */
4262
+ submitOp?: (intent: {
4263
+ kind: string;
4264
+ } & Record<string, unknown>) => void;
4265
+ /** Upload image bytes to the media relay and return the address fields
4266
+ * the insertImage intent must carry (plan doc 16 §5.1). Null means the
4267
+ * relay REFUSED — the caller must then not reserve anything, or the room
4268
+ * gets a skeleton nobody can ever fill. Absent when the app supplied no
4269
+ * relay origin, in which case images stay a local-only feature. */
4270
+ uploadMedia?: (bytes: Uint8Array) => Promise<{
4271
+ blobSha: string;
4272
+ bytesLen: number;
4273
+ iv?: string;
4274
+ } | null>;
4275
+ /**
4276
+ * Largest single upload the RELAY will accept, in bytes, as published in
4277
+ * the welcome. Lets the insert refuse an oversized file locally instead of
4278
+ * discovering it after sealing, hashing and a full upload.
4279
+ *
4280
+ * NULL MEANS SKIP THE CHECK — not "no limit" and not "use a default". An
4281
+ * older server publishes nothing, and a client that invents a number
4282
+ * either blocks uploads the server would have taken or promises the user
4283
+ * one it will refuse. The server enforces the real limit either way, so
4284
+ * skipping is safe and guessing is not.
4285
+ */
4286
+ mediaMaxBlobBytes?: number | null;
4287
+ /** Reverse this user's last SEQUENCED action (plan doc 03 Phase 8). The
4288
+ * editor routes Cmd+Z here in a room, because replaying the LOCAL history
4289
+ * stack would edit this replica with nothing on the wire. Absent ⇒ undo
4290
+ * declines rather than mutating. */
4291
+ undoLast?: () => void;
4292
+ };
4293
+ /** Author name stamped on comment replies (default "You"). */
4294
+ commentAuthor?: string;
4295
+ /** Render review comments (range highlights + margin balloons). Default true. */
4296
+ showComments?: boolean;
4297
+ /** Tracked-changes display: "final" (default) or "markup". */
4298
+ revisions?: "final" | "markup";
4299
+ /** Fires after render with document-requested font faces the browser cannot
4300
+ * render (unavailable, or lacking the document's script) — the page is
4301
+ * silently substituting and may differ from Word. Empty array = all good. */
4302
+ onMissingFonts?: (missing: MissingFont[]) => void;
4303
+ }
4304
+ /**
4305
+ * High-fidelity paginated DOCX viewer (and, with `editable`, editor).
4306
+ *
4307
+ * ```tsx
4308
+ * <DocxView source="/report.docx" /> // render-only
4309
+ * <DocxView source="/report.docx" editable onReady={setApi} /> // editing
4310
+ * ```
4311
+ */
4312
+ declare function DocxView({ source, zoom, fitWidth, narrowWidth, editable, className, style, onLoad, onPageCountChange, onReady, onError, commentAuthor, showComments, revisions, onMissingFonts, collab, }: DocxViewProps): react.JSX.Element;
4313
+
4314
+ export { type ShapePreset as A, ToolbarMenuSelect as B, type ClientMessage as C, DocxDocument as D, type ToolbarMenuSelectOption as E, type ToolbarMenuSelectProps as F, layoutDocument as G, printPages as H, type Intent as I, renderToDom as J, type LineageHead as L, type PresencePosition as P, type RosterEntry as R, type ServerMessage as S, type ToolbarFeature as T, type UndoOutcome as U, type WriteStatus as W, type XmlElement as X, type ParticipantProfile as a, type IdSidecar as b, type ToolbarMode as c, type DocxViewApi as d, bytesToB64 as e, deriveEpochKeys as f, docKeyFromFragment as g, stretchShareCode as h, COLLAB_IMAGE_EXTS as i, type CoverPageContent as j, DocxToolbar as k, type DocxToolbarProps as l, mintDocKey as m, DocxView as n, type DocxViewProps as o, type DrawingTool as p, INSERT_COMMANDS as q, type ImageInsertResult as r, sealCheckpoint as s, type InsertCommandSpec as t, type LineNumberingPatch as u, type PageLayoutPatch as v, type ParagraphAlignment as w, type RunFormatPatch as x, type ScreenshotInsertResult as y, type SelectionFormat as z };