@silurus/ooxml 0.70.1 → 0.71.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.
@@ -109,6 +109,11 @@ declare type BodyElement = {
109
109
  * (size + margins). Absent when the sectPr inherits both pgSz and pgMar
110
110
  * (the renderer then falls back to the body-level section geometry). */
111
111
  geom?: SectionGeom;
112
+ /** ECMA-376 §17.6.12 `<w:pgNumType>` — this ENDING section's page-numbering
113
+ * settings (start / fmt). Absent ⇒ numbering continues; decimal. Carried
114
+ * separately from `geom` because a section may inherit its geometry yet
115
+ * still restart / re-format its page numbers. */
116
+ pageNumType?: PageNumType | null;
112
117
  };
113
118
 
114
119
  declare interface Border {
@@ -137,6 +142,21 @@ declare interface BorderSpec {
137
142
  style: string;
138
143
  }
139
144
 
145
+ /**
146
+ * Populate a highlight overlay layer with one box per matched run-slice.
147
+ *
148
+ * @param layer the overlay div (cleared and re-sized to the canvas here).
149
+ * @param runs the page's runs (same array the page was rendered/text-layered from).
150
+ * @param matches the page's matches (run-slices + active flag).
151
+ * @param canvasCssWidth the rendered canvas's CSS width (e.g. `"700px"`).
152
+ * @param canvasCssHeight the rendered canvas's CSS height.
153
+ * @param measureForFont returns a width-measurer primed with a run's `font`
154
+ * (the viewer closes over a canvas 2d context). Kept as a
155
+ * factory so the font is set once per run, not per glyph.
156
+ * @param colors optional colour overrides.
157
+ */
158
+ declare function buildDocxHighlightLayer(layer: HTMLDivElement, runs: DocxTextRunInfo[], matches: DocxHighlightMatch[], canvasCssWidth: string, canvasCssHeight: string, measureForFont: (font: string) => (s: string) => number, colors?: DocxHighlightColors): void;
159
+
140
160
  /**
141
161
  * Build the transparent text-selection overlay for a rendered docx page: one
142
162
  * absolutely-positioned, color-transparent `<span>` per {@link DocxTextRunInfo}
@@ -144,16 +164,50 @@ declare interface BorderSpec {
144
164
  * lands on the drawn glyphs. Extracted verbatim from `DocxViewer._buildTextLayer`
145
165
  * so both the pager (DocxViewer) and the continuous-scroll viewer (DocxScrollViewer)
146
166
  * share one implementation; also public API for integrators building their own
147
- * overlay (design §10). MAIN render mode only `onTextRun` cannot cross the
148
- * worker boundary.
167
+ * overlay (design §10). IX6 usable in BOTH render modes: worker mode collects
168
+ * the same `DocxTextRunInfo[]` off-thread and ships it back beside the bitmap, so
169
+ * the overlay is built from identical geometry regardless of thread.
149
170
  *
150
- * @param layer the overlay div (position:relative parent expected).
151
- * @param runs per-run geometry from `renderPage({ onTextRun })`.
152
- * @param canvasCssWidth the rendered canvas's CSS width (e.g. `"700px"`), used
153
- * to size the overlay to match the canvas.
154
- * @param canvasCssHeight the rendered canvas's CSS height.
171
+ * @param layer the overlay div (position:relative parent expected).
172
+ * @param runs per-run geometry from `renderPage({ onTextRun })`.
173
+ * @param canvasCssWidth the rendered canvas's CSS width (e.g. `"700px"`), used
174
+ * to size the overlay to match the canvas.
175
+ * @param canvasCssHeight the rendered canvas's CSS height.
176
+ * @param onHyperlinkClick IX1 — invoked when a run carrying a resolved
177
+ * {@link HyperlinkTarget} is clicked. A hyperlink run's
178
+ * span keeps its transparent glyphs (the visible link
179
+ * colour/underline is already drawn on the canvas) but
180
+ * gains `cursor:pointer`, a `title` tooltip (the URL or
181
+ * bookmark ref) and this click handler. A plain
182
+ * `<span>` — not an `<a href>` — is used deliberately so
183
+ * the browser's own navigation can never bypass the
184
+ * caller's URL sanitisation. When omitted, link runs are
185
+ * rendered exactly like plain runs (no click affordance).
186
+ * @param measureForFont optional width-measurer factory (primed with a run's
187
+ * `font`), used ONLY to clamp a §17.3.2.10 縦中横
188
+ * (eastAsianVert) span to its drawn one-em cell (#836):
189
+ * the span composes a `scaleX(run.w / naturalWidth)` so
190
+ * its selection extent matches the compressed glyphs
191
+ * instead of the run's natural ~2× width. When omitted,
192
+ * a 縦中横 span keeps the bare rotate (no regression for
193
+ * callers that do not thread a measurer).
155
194
  */
156
- declare function buildDocxTextLayer(layer: HTMLDivElement, runs: DocxTextRunInfo[], canvasCssWidth: string, canvasCssHeight: string): void;
195
+ declare function buildDocxTextLayer(layer: HTMLDivElement, runs: DocxTextRunInfo[], canvasCssWidth: string, canvasCssHeight: string, onHyperlinkClick?: (target: HyperlinkTarget) => void, measureForFont?: (font: string) => (s: string) => number): void;
196
+
197
+ /**
198
+ * Populate a highlight overlay layer with a box per matched run-slice, grouped
199
+ * by shape frame (with the shape's rotation) so each box lands on the drawn
200
+ * glyphs.
201
+ *
202
+ * @param layer the overlay div (cleared + re-sized here).
203
+ * @param runs the slide's runs (same array the slide was rendered from).
204
+ * @param matches the slide's matches (run-slices + active flag).
205
+ * @param cssWidth rendered canvas CSS width (px, number).
206
+ * @param cssHeight rendered canvas CSS height (px, number).
207
+ * @param measureForFont returns a width-measurer primed with a run's font.
208
+ * @param colors optional colour overrides.
209
+ */
210
+ declare function buildPptxHighlightLayer(layer: HTMLDivElement, runs: PptxTextRunInfo[], matches: PptxHighlightMatch[], cssWidth: number, cssHeight: number, measureForFont: (font: string) => (s: string) => number, colors?: PptxHighlightColors): void;
157
211
 
158
212
  /**
159
213
  * Build the transparent text-selection overlay for a rendered pptx slide. Unlike
@@ -164,15 +218,25 @@ declare function buildDocxTextLayer(layer: HTMLDivElement, runs: DocxTextRunInfo
164
218
  * shape div (`inShapeX`/`inShapeY`). Extracted verbatim from
165
219
  * `PptxViewer._buildTextLayer` so the pager (PptxViewer) and the continuous-scroll
166
220
  * viewer (PptxScrollViewer, WS4) share one implementation; public API for
167
- * integrators (design §10). MAIN render mode only `onTextRun` cannot cross the
168
- * worker boundary.
221
+ * integrators (design §10). IX6 usable in BOTH render modes: worker mode
222
+ * collects the same `PptxTextRunInfo[]` off-thread and ships it back beside the
223
+ * bitmap, so the overlay is built from identical geometry regardless of thread.
224
+ *
225
+ * IX1 — when a run carries a resolved `hyperlink` (from `<a:hlinkClick>`) and an
226
+ * `onHyperlinkClick` callback is supplied, its span becomes a click target
227
+ * (`cursor:pointer`, a `title` tooltip, and a `click` handler). A plain span
228
+ * (no hyperlink) is byte-identical to before. A JS click handler is used rather
229
+ * than an `<a href>` so the URL never bypasses the viewer's sanitisation.
169
230
  *
170
231
  * @param layer the overlay div.
171
232
  * @param runs per-run + per-shape geometry from `renderSlide({ onTextRun })`.
172
233
  * @param cssWidth the rendered canvas's CSS width (px, number).
173
234
  * @param cssHeight the rendered canvas's CSS height (px, number).
235
+ * @param onHyperlinkClick called with the run's resolved {@link HyperlinkTarget}
236
+ * when a hyperlink span is clicked. Omit to leave links
237
+ * non-interactive (spans stay plain, selectable text).
174
238
  */
175
- declare function buildPptxTextLayer(layer: HTMLDivElement, runs: PptxTextRunInfo[], cssWidth: number, cssHeight: number): void;
239
+ declare function buildPptxTextLayer(layer: HTMLDivElement, runs: PptxTextRunInfo[], cssWidth: number, cssHeight: number, onHyperlinkClick?: (target: HyperlinkTarget) => void): void;
176
240
 
177
241
  declare type Bullet = {
178
242
  type: 'none';
@@ -216,14 +280,23 @@ declare interface Camera3d {
216
280
  declare interface Cell {
217
281
  col: number;
218
282
  row: number;
219
- colRef: string;
220
283
  value: CellValue;
221
- styleIndex: number;
284
+ /** Style index into the styles table. Omitted on the wire when `0` (the
285
+ * common unstyled case), so read it as `styleIndex ?? 0`. */
286
+ styleIndex?: number;
222
287
  /** Raw `<f>` formula text (ECMA-376 §18.3.1.40), when present. The renderer
223
288
  * uses this to recompute volatile functions (TODAY, NOW) at display time
224
289
  * so the cached `<v>` — frozen when the file was last saved — doesn't
225
290
  * show a stale date. */
226
291
  formula?: string;
292
+ /** Whether this cell displays its phonetic hint (furigana). The parser
293
+ * resolves it as `cell/@ph ?? row/@ph ?? false` — the per-cell `<c ph>`
294
+ * (ECMA-376 §18.3.1.4) wins when present (an explicit `ph="0"` overrides an
295
+ * enabled row), otherwise the row-level `<row ph>` (§18.3.1.73) is inherited,
296
+ * otherwise the schema default (false). Omitted on the wire when false, so
297
+ * read as `showPhonetic ?? false`. A cell whose String Item carries `<rPh>`
298
+ * runs still shows NO furigana unless the resolved value is true. */
299
+ showPhonetic?: boolean;
227
300
  }
228
301
 
229
302
  declare interface CellAddress {
@@ -294,6 +367,15 @@ declare type CellValue = {
294
367
  type: 'text';
295
368
  text: string;
296
369
  runs?: Run[];
370
+ /** ECMA-376 §18.4.6 phonetic runs (furigana) carried over from the
371
+ * resolved String Item. Present for inline strings, and populated by
372
+ * {@link resolveSharedStrings} for shared-string cells. Absent when the
373
+ * string has no furigana. */
374
+ phoneticRuns?: PhoneticRun[];
375
+ /** ECMA-376 §18.4.3 phonetic display properties (font index / char set /
376
+ * alignment) for the furigana above. Absent when the `<si>` had no
377
+ * `<phoneticPr>`. */
378
+ phoneticPr?: PhoneticProperties;
297
379
  } | {
298
380
  type: 'number';
299
381
  number: number;
@@ -303,6 +385,14 @@ declare type CellValue = {
303
385
  } | {
304
386
  type: 'error';
305
387
  error: string;
388
+ }
389
+ /** Shared-string reference into `ParsedWorkbook.sharedStrings` (ECMA-376
390
+ * §18.4.8). Resolved to `{ type: 'text', ... }` by the workbook before the
391
+ * renderer (or any other consumer) sees it, so downstream code never
392
+ * encounters this variant. */
393
+ | {
394
+ type: 'shared';
395
+ si: number;
306
396
  };
307
397
 
308
398
  declare interface CellXf {
@@ -415,6 +505,30 @@ declare interface ChartDataLabelOverride {
415
505
  fontSizeHpt?: number;
416
506
  /** `<a:defRPr b="1">` inside the per-idx rich text. */
417
507
  fontBold?: boolean;
508
+ /** Per-point callout box (`<c:dLbl><c:spPr>`, ECMA-376 §21.2.2.47/§21.2.2.197):
509
+ * overrides the series-default box for this one slice. */
510
+ labelBox?: ChartLabelBox;
511
+ /**
512
+ * Per-point label-content flags (`<c:dLbl>` §21.2.2.47 carries the same
513
+ * show-flag group as the series `<c:dLbls>` §21.2.2.49: §21.2.2.189
514
+ * `<c:showVal>`, §21.2.2.177 `<c:showCatName>`, §21.2.2.180 `<c:showSerName>`,
515
+ * §21.2.2.187 `<c:showPercent>`). When present they OVERRIDE the series-level
516
+ * defaults for that one point (e.g. sample-14 slide-7's pie sets
517
+ * `showCatName=0 showPercent=1` per slice while the series default is
518
+ * `showCatName=1`, so each label is percent only). undefined = inherit the
519
+ * series default for that flag.
520
+ */
521
+ showVal?: boolean;
522
+ showCatName?: boolean;
523
+ showSerName?: boolean;
524
+ showPercent?: boolean;
525
+ /**
526
+ * `<c:dLbl><c:delete val="1"/>` (ECMA-376 §21.2.2.43) — the point's label is
527
+ * removed. Distinguishes a genuine delete from a `<c:dLbl>` that only carries
528
+ * style / flag overrides with no `<c:tx>` (both otherwise present as
529
+ * `text === ''`). true = skip the label; undefined/absent = not deleted.
530
+ */
531
+ deleted?: boolean;
418
532
  }
419
533
 
420
534
  declare interface ChartDataPointOverride {
@@ -425,6 +539,18 @@ declare interface ChartDataPointOverride {
425
539
  markerSize?: number;
426
540
  markerFill?: string;
427
541
  markerLine?: string;
542
+ /**
543
+ * `<c:dPt><c:explosion val>` (ECMA-376 §21.2.2.61) — the amount this
544
+ * pie/doughnut slice is moved out from the center. The schema type is
545
+ * `CT_UnsignedInt` (unbounded `xsd:unsignedInt`); the spec text only says
546
+ * "the amount the data point shall be moved from the center of the pie"
547
+ * and does not itself define units or a 0–100 range. We treat it as a
548
+ * de-facto percentage of the outer radius (0–100 typical), matching
549
+ * Office's UI (the Point Explosion slider caps at 100%) rather than a
550
+ * spec-mandated bound. undefined/absent = 0 (no explosion, flush with the
551
+ * ring). Only consulted by the pie/doughnut renderer.
552
+ */
553
+ explosion?: number;
428
554
  }
429
555
 
430
556
  /**
@@ -464,6 +590,74 @@ declare interface ChartErrBars {
464
590
  dash?: string;
465
591
  }
466
592
 
593
+ /**
594
+ * One box-and-whisker series (chartEx `boxWhisker`, MS 2014 chartex ext). Each
595
+ * `<cx:series>` references its own raw sample points via `<cx:dataId>`; the
596
+ * parser groups them by category and threads the `<cx:layoutPr>` flags. The
597
+ * renderer derives the statistics.
598
+ */
599
+ declare interface ChartexBoxSeries {
600
+ /** Series display name (`<cx:tx><cx:v>`). */
601
+ name: string;
602
+ /** Fill (hex, no '#') — theme accent cycled by series index. null = fall
603
+ * back to the renderer palette. */
604
+ color?: string | null;
605
+ /** Raw sample values grouped by category (outer = category index parallel to
606
+ * {@link ChartexBoxWhisker.categories}, inner = the points in that group). */
607
+ valuesByCategory: number[][];
608
+ /** `<cx:visibility meanMarker>` — draw the mean `×`. */
609
+ meanMarker: boolean;
610
+ /** `<cx:visibility meanLine>` — draw a mean connector line across categories. */
611
+ meanLine: boolean;
612
+ /** `<cx:visibility outliers>` — draw outlier points. */
613
+ showOutliers: boolean;
614
+ /** `<cx:visibility nonoutliers>` — draw the interior (non-outlier) sample
615
+ * points as jittered dots on top of the box. Flag parsed; interior-dot
616
+ * rendering is pending a fixture that enables it (every sample-24 series
617
+ * ships `nonoutliers="0"`, so there is nothing to verify against yet). */
618
+ showNonoutliers: boolean;
619
+ /** `<cx:statistics quartileMethod>` — "exclusive" (Excel default) | "inclusive". */
620
+ quartileMethod: string;
621
+ }
622
+
623
+ /** A chartEx box-and-whisker chart: unique categories + one series per column. */
624
+ declare interface ChartexBoxWhisker {
625
+ /** Unique category labels in first-seen order. */
626
+ categories: string[];
627
+ /** One entry per `<cx:series>`. */
628
+ series: ChartexBoxSeries[];
629
+ }
630
+
631
+ /** A chartEx sunburst: the flat rows the renderer folds into a ring tree. */
632
+ declare interface ChartexSunburst {
633
+ rows: ChartexSunburstRow[];
634
+ }
635
+
636
+ /**
637
+ * One row of a chartEx `sunburst`: the branch→…→leaf label chain (empty
638
+ * trailing segments trimmed) and its size value.
639
+ */
640
+ declare interface ChartexSunburstRow {
641
+ /** Label chain root→leaf. */
642
+ path: string[];
643
+ /** `<cx:numDim type="size">` value attaching to the deepest node in `path`. */
644
+ size: number;
645
+ }
646
+
647
+ /** Callout-box style for a pie/doughnut data label — the white (or themed)
648
+ * rounded rectangle with a thin border Word draws around a `bestFit` label
649
+ * placed outside its slice. From the label's `<c:spPr>` (§21.2.2.197). All
650
+ * fields optional: absent → transparent / unbordered. Mirror of Rust
651
+ * `ChartLabelBox`. */
652
+ declare interface ChartLabelBox {
653
+ /** `<a:solidFill>` resolved hex (no `#`). Box background. */
654
+ fill?: string;
655
+ /** `<a:ln><a:solidFill>` resolved hex (no `#`). Border stroke. */
656
+ borderColor?: string;
657
+ /** `<a:ln w>` border width in EMU (12700 EMU = 1 pt). */
658
+ borderWidthEmu?: number;
659
+ }
660
+
467
661
  /**
468
662
  * `<c:manualLayout>` block. Fractions are of the chart-space rect.
469
663
  * `xMode`/`yMode`: "edge" = absolute fraction from top-left, "factor" =
@@ -594,6 +788,35 @@ declare interface ChartModel {
594
788
  valAxisTitleFontBold?: boolean | null;
595
789
  /** `<c:valAx><c:title>` run-prop color (hex without '#'). null = default. */
596
790
  valAxisTitleFontColor?: string | null;
791
+ /** `<c:catAx><c:txPr>…<a:latin typeface>` tick-label font. */
792
+ catAxisFontFace?: string | null;
793
+ /** `<c:valAx><c:txPr>…<a:latin typeface>` tick-label font. */
794
+ valAxisFontFace?: string | null;
795
+ /** `<c:catAx><c:title>…<a:latin typeface>` axis-title font. */
796
+ catAxisTitleFontFace?: string | null;
797
+ /** `<c:valAx><c:title>…<a:latin typeface>` axis-title font. */
798
+ valAxisTitleFontFace?: string | null;
799
+ /** `<c:dLbls><c:txPr>…<a:latin typeface>` data-label font. */
800
+ dataLabelFontFace?: string | null;
801
+ /** `<c:legend><c:txPr>…<a:latin typeface>` legend font. */
802
+ legendFontFace?: string | null;
803
+ /** `<c:legend><c:txPr>…<a:solidFill>` legend text color (hex without '#'). */
804
+ legendFontColor?: string | null;
805
+ /** `<c:legend><c:txPr>` legend font size (OOXML hundredths of a point). */
806
+ legendFontSizeHpt?: number | null;
807
+ /** `<c:legend><c:txPr>…defRPr@b` legend bold flag. */
808
+ legendFontBold?: boolean | null;
809
+ /**
810
+ * Theme font-scheme faces (`<a:fontScheme>`, ECMA-376 §20.1.4.2). Latin
811
+ * heading (majorFont) and body (minorFont) typefaces, used as the fallback
812
+ * for any chart text element whose own `<c:txPr>` supplies no `<a:latin>`.
813
+ * null when the theme is not threaded to the chart (then the renderer's
814
+ * built-in sans-serif remains, byte-stable). Axis titles / chart title use
815
+ * the major (heading) face; tick labels / data labels / legend use the
816
+ * minor (body) face — matching Office's default chart text styling.
817
+ */
818
+ themeMajorFontLatin?: string | null;
819
+ themeMinorFontLatin?: string | null;
597
820
  /** Explicit chart border color (hex without '#') from
598
821
  * `<c:chartSpace><c:spPr><a:ln><a:solidFill><a:srgbClr>`. Only set when the
599
822
  * XML explicitly declares a paintable line; null otherwise (no default
@@ -668,6 +891,184 @@ declare interface ChartModel {
668
891
  * value axis (the common case). See {@link SecondaryValueAxis}.
669
892
  */
670
893
  secondaryValAxis?: SecondaryValueAxis | null;
894
+ /**
895
+ * `<c:date1904>` (ECMA-376 §21.2.2.38). When true the chart's serial
896
+ * date-times resolve against the 1904 date system (base 1904-01-01) instead
897
+ * of the default 1900 system. Threaded to the date formatters for date-axis
898
+ * category labels and value-axis tick labels. Omitted/false ⇒ 1900 system.
899
+ * Note: per §21.2.2.38 the element's `val` defaults to true when present but
900
+ * the attribute is omitted, so `<c:date1904/>` alone means date1904=true.
901
+ */
902
+ date1904?: boolean;
903
+ /**
904
+ * `<c:doughnutChart><c:holeSize val>` (ECMA-376 §21.2.2.60,
905
+ * `ST_HoleSizePercent` §21.2.3.55) — the doughnut hole diameter as a
906
+ * percentage 1–90 of the outer diameter. Ignored for pie (which has no
907
+ * hole). null/undefined = use the renderer's doughnut default when the
908
+ * element is absent. Note the ECMA `CT_HoleSize` schema default is 10%, but
909
+ * a real doughnut file always writes an explicit `<c:holeSize>` (Excel /
910
+ * PowerPoint emit 50–75%); the renderer falls back to 50% only for the
911
+ * pathological absent case.
912
+ */
913
+ holeSize?: number | null;
914
+ /**
915
+ * `<c:pieChart | doughnutChart><c:firstSliceAng val>` (ECMA-376 §21.2.2.52,
916
+ * `ST_FirstSliceAng` §21.2.3.15) — the angle in degrees (0–360, clockwise
917
+ * from the 12 o'clock position) at which the first slice begins.
918
+ * null/undefined = 0 (start at 12 o'clock), which matches the renderer's
919
+ * historical fixed −90° (canvas up) start.
920
+ */
921
+ firstSliceAngle?: number | null;
922
+ /**
923
+ * `<c:chartSpace><c:chart><c:dispBlanksAs val>` (ECMA-376 §21.2.2.42,
924
+ * `ST_DispBlanksAs` §21.2.3.10) — how blank (null) cells are plotted on
925
+ * line/area charts:
926
+ * - "gap" → leave a gap (break the line). The renderer's historical
927
+ * behavior and the model default when the element is absent.
928
+ * - "zero" → plot the blank as the value 0 (the point drops to the axis).
929
+ * - "span" → skip the blank but connect its neighbours with a straight
930
+ * line (bridge the gap).
931
+ * Note the XSD `@val` default is "zero" (applies when `<c:dispBlanksAs/>` is
932
+ * present but the attribute is omitted); when the ELEMENT is absent entirely
933
+ * Office falls back to "gap", which is what we model as the default. Only
934
+ * consulted for the line and area families. null/undefined = "gap".
935
+ */
936
+ dispBlanksAs?: string | null;
937
+ /**
938
+ * `<c:valAx><c:majorGridlines>` presence (ECMA-376 §21.2.2.100). `false` when
939
+ * the value axis exists but omits the element (Office suppresses value
940
+ * gridlines). null/undefined ⇒ the renderer's historical always-on value
941
+ * gridlines (byte-stable). `true` is redundant with the default but honored.
942
+ */
943
+ valAxisMajorGridlines?: boolean | null;
944
+ /**
945
+ * `<c:catAx><c:majorGridlines>` presence (§21.2.2.100). `true` turns on
946
+ * category-axis gridlines (Office omits them by default). null/undefined/false
947
+ * ⇒ no category gridlines (the historical default, byte-stable).
948
+ */
949
+ catAxisMajorGridlines?: boolean | null;
950
+ /**
951
+ * `<c:valAx><c:majorGridlines><c:spPr><a:ln><a:solidFill>` resolved gridline
952
+ * color (hex without `#`) — ECMA-376 §21.2.2.100. When set, the value-axis
953
+ * major gridlines are stroked in this color instead of the renderer's faint
954
+ * `#e0e0e0` default (e.g. sample-1 slide 5's `accent3` gridlines). null/absent
955
+ * ⇒ the historical default (byte-stable).
956
+ */
957
+ valAxisGridlineColor?: string | null;
958
+ /**
959
+ * `<c:valAx><c:majorGridlines><c:spPr><a:ln w>` gridline width in EMU. When
960
+ * set, the value-axis gridline stroke width is derived from this (floored so a
961
+ * hairline stays visible). null/absent ⇒ the renderer's 0.5 px default.
962
+ */
963
+ valAxisGridlineWidthEmu?: number | null;
964
+ /**
965
+ * `<c:catAx><c:majorGridlines><c:spPr><a:ln><a:solidFill>` resolved gridline
966
+ * color (hex without `#`). Only meaningful when {@link catAxisMajorGridlines}
967
+ * is on. null/absent ⇒ the faint default.
968
+ */
969
+ catAxisGridlineColor?: string | null;
970
+ /** `<c:catAx><c:majorGridlines><c:spPr><a:ln w>` gridline width in EMU. */
971
+ catAxisGridlineWidthEmu?: number | null;
972
+ /** `<c:valAx><c:minorGridlines>` presence (§21.2.2.109). Only drawn when a
973
+ * minor step is resolvable (see {@link valAxisMinorUnit}). */
974
+ valAxisMinorGridlines?: boolean | null;
975
+ /**
976
+ * `<c:valAx><c:majorUnit val>` (§21.2.2.103) — explicit distance between major
977
+ * gridlines/ticks, overriding the Excel-style auto "nice" step. null/undefined
978
+ * ⇒ auto step (byte-stable).
979
+ */
980
+ valAxisMajorUnit?: number | null;
981
+ /** `<c:valAx><c:minorUnit val>` (§21.2.2.112) — explicit minor step. Drives
982
+ * minor gridlines/ticks when present. null ⇒ no minor divisions. */
983
+ valAxisMinorUnit?: number | null;
984
+ /**
985
+ * `<c:valAx><c:scaling><c:logBase val>` (§21.2.2.98, `ST_LogBase` §21.2.3.25)
986
+ * — logarithmic value-axis base (>= 2). When set, values map to pixels in log
987
+ * space and gridlines fall on powers of the base. null/undefined ⇒ linear
988
+ * (byte-stable).
989
+ */
990
+ valAxisLogBase?: number | null;
991
+ /**
992
+ * `<c:valAx><c:scaling><c:orientation val>` (§21.2.2.130, `ST_Orientation`
993
+ * §21.2.3.30) — "minMax" (normal) | "maxMin" (reversed, so the value axis runs
994
+ * top→bottom max→min). null/undefined/"minMax" ⇒ normal (byte-stable).
995
+ */
996
+ valAxisOrientation?: 'minMax' | 'maxMin' | string | null;
997
+ /** `<c:catAx><c:scaling><c:orientation val>` — "maxMin" reverses the category
998
+ * axis left↔right. null/"minMax" ⇒ normal. */
999
+ catAxisOrientation?: 'minMax' | 'maxMin' | string | null;
1000
+ /**
1001
+ * `<c:catAx><c:tickLblPos val>` (§21.2.2.207, `ST_TickLblPos` §21.2.3.47) —
1002
+ * "nextTo" (default) | "low" | "high" | "none". "none" hides the category tick
1003
+ * labels. null/undefined ⇒ nextTo (byte-stable).
1004
+ */
1005
+ catAxisTickLabelPos?: string | null;
1006
+ /** `<c:valAx><c:tickLblPos val>` (§21.2.2.207). "none" hides value tick labels. */
1007
+ valAxisTickLabelPos?: string | null;
1008
+ /**
1009
+ * `<c:catAx><c:txPr><a:bodyPr rot>` (DrawingML `ST_Angle`, 60000ths of a
1010
+ * degree) — category tick-label rotation. e.g. -2700000 = -45°. null/undefined
1011
+ * /0 ⇒ horizontal labels (byte-stable).
1012
+ */
1013
+ catAxisLabelRotation?: number | null;
1014
+ /**
1015
+ * `<c:stockChart><c:hiLowLines>` presence (ECMA-376 §21.2.2.60). When true
1016
+ * the stock renderer draws a vertical line spanning each category's low↔high
1017
+ * value. Only set for `chartType === "stock"`; null/undefined on every other
1018
+ * chart type (byte-stable).
1019
+ */
1020
+ stockHiLowLines?: boolean | null;
1021
+ /**
1022
+ * `<c:hiLowLines><c:spPr><a:ln><a:solidFill>` resolved color (hex, no `#`).
1023
+ * null = the renderer's default gray hi-lo line.
1024
+ */
1025
+ stockHiLowLineColor?: string | null;
1026
+ /**
1027
+ * `<c:stockChart><c:upDownBars>` presence (ECMA-376 §21.2.2.227). Parsed so a
1028
+ * stock file carrying open-close up/down bars is recognized; the renderer does
1029
+ * NOT yet draw them (tracked follow-up). null/undefined when absent.
1030
+ */
1031
+ stockUpDownBars?: boolean | null;
1032
+ /**
1033
+ * Structured box-and-whisker data (`chartType === 'boxWhisker'`). Present
1034
+ * ONLY for boxWhisker charts; null/absent otherwise so the flat
1035
+ * `categories`/`series` model the other chartEx renderers consume is
1036
+ * untouched. The renderer computes quartiles / mean / whiskers / outliers.
1037
+ */
1038
+ chartexBox?: ChartexBoxWhisker | null;
1039
+ /**
1040
+ * Structured sunburst hierarchy (`chartType === 'sunburst'`). Present ONLY
1041
+ * for sunburst charts; null/absent otherwise.
1042
+ */
1043
+ chartexSunburst?: ChartexSunburst | null;
1044
+ /**
1045
+ * Theme accent palette (`accent1..6`, hex without '#') for chartEx charts
1046
+ * that color by branch/series index (boxWhisker series, sunburst branches).
1047
+ * null/absent when the resolver supplies no default palette (pptx); the
1048
+ * renderer then falls back to its own `CHART_PALETTE`.
1049
+ */
1050
+ chartexAccents?: string[] | null;
1051
+ }
1052
+
1053
+ /** ECMA-376 §21.2 — a DrawingML chart embedded in the run flow via
1054
+ * `<w:drawing><wp:inline|wp:anchor>…<a:graphicData uri=".../chart"><c:chart r:id>`.
1055
+ * Mirrors the Rust `ChartRun`. `chart` is the shared {@link ChartModel} the
1056
+ * core `renderChart` consumes (identical to what pptx/xlsx pass), so a docx
1057
+ * chart draws at the same quality through the same code path. `widthPt`/
1058
+ * `heightPt` are the `<wp:extent>` natural size. An inline chart flows as an
1059
+ * inline box of that size; an anchored chart (§20.4.2.3) is painted at its
1060
+ * absolute page box by `renderAnchorImages` — both via `renderChart`. */
1061
+ declare interface ChartRun {
1062
+ chart: ChartModel;
1063
+ widthPt: number;
1064
+ heightPt: number;
1065
+ /** true = `<wp:anchor>` (absolute page position, drawn by the anchor path);
1066
+ * false = `<wp:inline>` (flows with text). */
1067
+ anchor: boolean;
1068
+ anchorXPt?: number;
1069
+ anchorYPt?: number;
1070
+ anchorXFromMargin?: boolean;
1071
+ anchorYFromPara?: boolean;
671
1072
  }
672
1073
 
673
1074
  declare interface ChartSeries {
@@ -777,6 +1178,31 @@ declare interface ChartSeries {
777
1178
  * series.
778
1179
  */
779
1180
  bubbleSizes?: (number | null)[] | null;
1181
+ /**
1182
+ * `<c:ser><c:smooth val>` (ECMA-376 §21.2.2.194) — line/area series flag
1183
+ * requesting a smoothed (spline) curve through the points instead of straight
1184
+ * segments. Only consulted for the line and area families (scatter carries its
1185
+ * smoothing in `ChartModel.scatterStyle`). null/undefined/false = straight
1186
+ * polyline (the default; byte-stable for series that never set it).
1187
+ */
1188
+ smooth?: boolean | null;
1189
+ /**
1190
+ * `<c:ser><c:trendline>` per-series trendlines (ECMA-376 §21.2.2.211,
1191
+ * `CT_Trendline`). A series can carry several (e.g. a linear fit + a moving
1192
+ * average). null/undefined/empty = no trendline (the default; byte-stable for
1193
+ * series that never declare one).
1194
+ */
1195
+ trendLines?: ChartTrendline[] | null;
1196
+ /**
1197
+ * `<c:ser><c:spPr><a:ln><a:noFill/>` (ECMA-376 §21.2.2.198 CT_ShapeProperties
1198
+ * → DrawingML §20.1.2.2.24 CT_LineProperties). true when the series connecting
1199
+ * line is explicitly turned OFF. For a scatter/line series this OVERRIDES the
1200
+ * chart-group `<c:scatterStyle>` (§21.2.2.42) / line default — Excel and
1201
+ * PowerPoint draw markers only (no connecting line) even when the group style
1202
+ * is `lineMarker`. null/undefined = no explicit line-off, so the group default
1203
+ * governs (byte-stable for series that carry a paintable line).
1204
+ */
1205
+ lineHidden?: boolean | null;
780
1206
  }
781
1207
 
782
1208
  declare interface ChartSeriesDataLabels {
@@ -791,6 +1217,51 @@ declare interface ChartSeriesDataLabels {
791
1217
  fontBold?: boolean;
792
1218
  /** Series-level font size for data labels (OOXML hundredths of a point). */
793
1219
  fontSizeHpt?: number;
1220
+ /** Series-default callout box (`<c:dLbls><c:spPr>`, ECMA-376 §21.2.2.49/
1221
+ * §21.2.2.197). When present the pie/doughnut renderer draws Word's boxed
1222
+ * callout layout (box + optional leader line) instead of plain text. */
1223
+ labelBox?: ChartLabelBox;
1224
+ /** `<c:dLbls><c:showLeaderLines val>` (§21.2.2.183) — draw leader lines from
1225
+ * a pulled-away label back to its slice. Default false. */
1226
+ showLeaderLines?: boolean;
1227
+ /** `<c:leaderLines><c:spPr><a:ln><a:solidFill>` (§21.2.2.92) resolved hex
1228
+ * (no `#`). undefined → renderer uses a neutral grey. */
1229
+ leaderLineColor?: string;
1230
+ /** `<c:leaderLines><c:spPr><a:ln w>` leader-line width in EMU. */
1231
+ leaderLineWidthEmu?: number;
1232
+ }
1233
+
1234
+ /**
1235
+ * `<c:ser><c:trendline>` (ECMA-376 §21.2.2.211). A regression/smoothing curve
1236
+ * fitted to the series' data points.
1237
+ */
1238
+ declare interface ChartTrendline {
1239
+ /**
1240
+ * `<c:trendlineType val>` (§21.2.2.213, `ST_TrendlineType` §21.2.3.50):
1241
+ * "linear" | "exp" | "log" | "power" | "poly" | "movingAvg". The renderer
1242
+ * currently draws "linear" (least squares) and "movingAvg"; other types parse
1243
+ * but are not yet plotted (tracked as a follow-up).
1244
+ */
1245
+ trendlineType: string;
1246
+ /** `<c:order val>` — polynomial order (`poly`, default 2). */
1247
+ order?: number | null;
1248
+ /** `<c:period val>` — moving-average window (`movingAvg`, default 2). */
1249
+ period?: number | null;
1250
+ /** `<c:forward val>` — units to extend the line past the last point. */
1251
+ forward?: number | null;
1252
+ /** `<c:backward val>` — units to extend the line before the first point. */
1253
+ backward?: number | null;
1254
+ /** `<c:intercept val>` — forced y-intercept (linear/exp). null = free fit. */
1255
+ intercept?: number | null;
1256
+ /** `<c:dispRSqr val="1">` — show the R² value (label; not yet rendered). */
1257
+ dispRSqr?: boolean | null;
1258
+ /** `<c:dispEq val="1">` — show the fit equation (label; not yet rendered). */
1259
+ dispEq?: boolean | null;
1260
+ /** `<c:spPr><a:ln><a:solidFill>` trendline color (hex without '#'). null =
1261
+ * inherit the series color. */
1262
+ lineColor?: string | null;
1263
+ /** `<c:spPr><a:ln w>` trendline width in EMU. */
1264
+ lineWidthEmu?: number | null;
794
1265
  }
795
1266
 
796
1267
  /**
@@ -798,7 +1269,7 @@ declare interface ChartSeriesDataLabels {
798
1269
  * grouping (`Pct` = percent-stacked) so renderers do not need to inspect
799
1270
  * separate `barDir`/`grouping` fields.
800
1271
  */
801
- declare type ChartType = 'line' | 'stackedLine' | 'stackedLinePct' | 'clusteredBar' | 'clusteredBarH' | 'stackedBar' | 'stackedBarH' | 'stackedBarPct' | 'stackedBarHPct' | 'area' | 'stackedArea' | 'stackedAreaPct' | 'pie' | 'doughnut' | 'scatter' | 'bubble' | 'radar' | 'waterfall' | string;
1272
+ declare type ChartType = 'line' | 'stackedLine' | 'stackedLinePct' | 'clusteredBar' | 'clusteredBarH' | 'stackedBar' | 'stackedBarH' | 'stackedBarPct' | 'stackedBarHPct' | 'area' | 'stackedArea' | 'stackedAreaPct' | 'pie' | 'doughnut' | 'scatter' | 'bubble' | 'radar' | 'waterfall' | 'stock' | 'boxWhisker' | 'sunburst' | string;
802
1273
 
803
1274
  /** ECMA-376 §17.6.3 `<w:col>` — one column's width and trailing space (pt). */
804
1275
  declare interface ColSpec {
@@ -899,7 +1370,7 @@ declare interface DocParagraph {
899
1370
  * both, distribute. Other values (kashida variants, numTab, thaiDistribute)
900
1371
  * are treated as start-aligned.
901
1372
  */
902
- alignment: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | 'both' | 'distribute' | string;
1373
+ alignment: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | 'both' | 'distribute' | 'lowKashida' | 'mediumKashida' | 'highKashida' | 'thaiDistribute' | string;
903
1374
  indentLeft: number;
904
1375
  indentRight: number;
905
1376
  indentFirst: number;
@@ -909,6 +1380,15 @@ declare interface DocParagraph {
909
1380
  numbering: NumberingInfo | null;
910
1381
  tabStops: TabStop_2[];
911
1382
  runs: DocRun[];
1383
+ /**
1384
+ * ECMA-376 §17.13.6.2 `<w:bookmarkStart w:name>` — names of the bookmarks that
1385
+ * start within (or at the head of) this paragraph, in document order. A
1386
+ * `<w:hyperlink w:anchor="X">` internal link (§17.16.23) targets the paragraph
1387
+ * whose `bookmarks` contains `"X"`; {@link buildBookmarkPageMap} turns these
1388
+ * into a `bookmarkName → pageIndex` map after pagination. Absent (`undefined`)
1389
+ * for the common paragraph that anchors nothing.
1390
+ */
1391
+ bookmarks?: string[];
912
1392
  /** Paragraph background hex color (w:shd fill) */
913
1393
  shading?: string | null;
914
1394
  /** Force a page break before this paragraph (w:pageBreakBefore) */
@@ -970,6 +1450,8 @@ declare type DocRun = {
970
1450
  } & DocxTextRun | {
971
1451
  type: 'image';
972
1452
  } & ImageRun | {
1453
+ type: 'chart';
1454
+ } & ChartRun | {
973
1455
  type: 'break';
974
1456
  breakType: 'line' | 'page' | 'column';
975
1457
  } | {
@@ -982,7 +1464,9 @@ declare type DocRun = {
982
1464
  display: boolean;
983
1465
  fontSize: number;
984
1466
  jc?: string;
985
- };
1467
+ } | {
1468
+ type: 'ptab';
1469
+ } & PTabRun;
986
1470
 
987
1471
  declare interface DocSettings {
988
1472
  /** §17.15.1.58 `w:kinsoku` — East-Asian line-breaking toggle. `undefined`
@@ -1015,6 +1499,14 @@ declare interface DocTable {
1015
1499
  cellMarginRight: number;
1016
1500
  /** table horizontal alignment on the page: 'left' | 'center' | 'right'. */
1017
1501
  jc: string;
1502
+ /** ECMA-376 §17.4.50 `<w:tblInd>` — indentation added before the table's
1503
+ * LEADING edge (left in an LTR table, right in an RTL/`bidiVisual` table), in
1504
+ * pt. SIGNED: a negative value pulls the table outward past the leading margin
1505
+ * toward the page edge (Word writes this for a header banner that must reach
1506
+ * the physical page edge). `type="dxa"` only; `pct`/`auto` are dropped by the
1507
+ * parser per §17.4.50. Absent ⇒ no direct indent. The renderer applies it only
1508
+ * when the resolved `jc` is left/leading (§17.4.50). */
1509
+ tblInd?: number;
1018
1510
  /** ECMA-376 §17.4.52 `<w:tblLayout w:type>` — 'fixed' | 'autofit'. Absent
1019
1511
  * (undefined) ⇒ spec default 'autofit'. Both paths size columns from the
1020
1512
  * tblGrid (§17.4.48) scaled to fit: 'fixed' uses the grid verbatim; 'autofit'
@@ -1082,19 +1574,35 @@ export declare namespace docx {
1082
1574
  export {
1083
1575
  DocxDocument,
1084
1576
  LoadOptions_4 as LoadOptions,
1577
+ RenderPageToBitmapOptions,
1085
1578
  WireRenderPageOptions,
1086
1579
  DocxViewer,
1087
1580
  DocxViewerOptions,
1088
1581
  DocxScrollViewer,
1089
1582
  DocxScrollViewerOptions,
1090
1583
  buildDocxTextLayer,
1584
+ buildDocxHighlightLayer,
1585
+ DocxHighlightMatch,
1586
+ DocxHighlightColors,
1587
+ DocxMatchLocation,
1588
+ FindMatch,
1589
+ FindMatchesOptions,
1091
1590
  autoResize,
1092
1591
  AutoResizeOptions,
1592
+ HyperlinkTarget,
1593
+ openExternalHyperlink,
1594
+ OoxmlError,
1595
+ OoxmlErrorCode,
1093
1596
  noteText,
1094
1597
  DocxDocumentModel,
1095
1598
  DocSettings,
1599
+ EmbeddedFontRef,
1096
1600
  SectionProps,
1097
1601
  SectionGeom,
1602
+ PageNumType,
1603
+ PageBorders,
1604
+ PageBorderEdge,
1605
+ LineNumbering,
1098
1606
  ColumnsSpec,
1099
1607
  ColSpec,
1100
1608
  HeadersFooters,
@@ -1103,10 +1611,13 @@ export declare namespace docx {
1103
1611
  BodyElement,
1104
1612
  DocParagraph,
1105
1613
  DocRun,
1614
+ PTabRun,
1106
1615
  DocxTextRun,
1107
1616
  FieldRun,
1108
1617
  ImageRun,
1618
+ ChartRun,
1109
1619
  ShapeRun,
1620
+ TextPath,
1110
1621
  ShapeText_2 as ShapeText,
1111
1622
  ShapeTextRun_2 as ShapeTextRun,
1112
1623
  RubyAnnotation,
@@ -1141,10 +1652,27 @@ declare class DocxDocument {
1141
1652
  private _document;
1142
1653
  private _meta;
1143
1654
  private _pages;
1655
+ /** Lazily-built `bookmarkName → 0-based page index` map for internal hyperlink
1656
+ * anchors (IX-nav). Built on first {@link getBookmarkPage} from the paginated
1657
+ * pages (main) or the worker meta's `bookmarkPages` (worker). Nulled by
1658
+ * {@link destroy} so a reused reference never serves a stale document. */
1659
+ private _bookmarkPages;
1144
1660
  private _mode;
1145
1661
  private _worker;
1146
1662
  private _bridge;
1147
1663
  private _imageCache;
1664
+ /** Embedded `FontFace` objects this document registered into `document.fonts`
1665
+ * (main mode only — in worker mode the worker owns them and terminates with
1666
+ * its own FontFaceSet). Released in {@link destroy} so they do not leak into
1667
+ * the shared FontFaceSet for the lifetime of the SPA (deduped + refcounted in
1668
+ * core, so a font shared with another open document survives until both go). */
1669
+ private _embeddedFontFaces;
1670
+ /** Google-Fonts `FontFace` objects this document preloaded into `document.fonts`
1671
+ * (main mode only — in worker mode the worker owns them and terminates with its
1672
+ * own FontFaceSet). Released in {@link destroy} so they do not leak into the
1673
+ * shared FontFaceSet for the lifetime of the SPA (deduped + refcounted in core,
1674
+ * so a web font shared with another open document survives until both go). */
1675
+ private _googleFontFaces;
1148
1676
  /** One stable closure per instance: core's path-keyed SVG cache namespaces on
1149
1677
  * this identity, so two open documents never swap a shared zip path (e.g.
1150
1678
  * word/media/image1.svg). Reusing one reference also lets the SVG cache hit
@@ -1163,6 +1691,32 @@ declare class DocxDocument {
1163
1691
  * are decoded lazily rather than inlined as base64 at parse time.
1164
1692
  */
1165
1693
  getImage(imagePath: string, mimeType: string): Promise<Blob>;
1694
+ /**
1695
+ * Extract raw bytes for an embedded font part by zip path (e.g.
1696
+ * `word/fonts/font1.odttf`). Routes through the SAME persistent-worker
1697
+ * `extractImage` message as {@link getImage} — `DocxArchive.extract_image`
1698
+ * reads ANY zip entry, not just media — returning the raw (still obfuscated)
1699
+ * `.odttf` bytes rather than a Blob. Consumed by {@link loadEmbeddedFonts},
1700
+ * which de-obfuscates (ECMA-376 §17.8.1) and registers each as a FontFace.
1701
+ */
1702
+ getFontBytes(partPath: string): Promise<Uint8Array>;
1703
+ /**
1704
+ * Project the document to GitHub-flavoured markdown: headings (from
1705
+ * `<w:outlineLvl>`), bullet / numbered lists, tables (with vMerge
1706
+ * continuation), and rich-text formatting (bold / italic / strikethrough /
1707
+ * hyperlink), with footnotes / endnotes / comments collated at the end.
1708
+ * Positioning, section properties, fonts, and drawing shapes are discarded —
1709
+ * the projection is meant for AI ingestion and full-text search, not layout.
1710
+ *
1711
+ * Runs entirely in the worker off the archive opened at {@link load} (no
1712
+ * re-copy of the file, no re-parse of the model on the main thread), so it
1713
+ * works in BOTH `mode: 'main'` and `mode: 'worker'`.
1714
+ *
1715
+ * @example
1716
+ * const doc = await DocxDocument.load(buffer);
1717
+ * const md = await doc.toMarkdown();
1718
+ */
1719
+ toMarkdown(): Promise<string>;
1166
1720
  get pageCount(): number;
1167
1721
  /** The render mode this engine was loaded with ('main' | 'worker'). A fact for
1168
1722
  * integrators and the scroll viewer: an injected engine's mode decides whether
@@ -1198,6 +1752,23 @@ declare class DocxDocument {
1198
1752
  */
1199
1753
  get endnotes(): DocNote[];
1200
1754
  private _getPages;
1755
+ /** Lazily build (and cache) the `bookmarkName → page index` map from either
1756
+ * the worker meta (worker mode) or the paginated pages (main mode). */
1757
+ private _getBookmarkPages;
1758
+ /**
1759
+ * ECMA-376 §17.13.6.2 / §17.16.23 — resolve a bookmark name (a
1760
+ * `<w:hyperlink w:anchor>` internal-link target) to the 0-based index of the
1761
+ * page its `<w:bookmarkStart w:name>` destination falls on, or `undefined`
1762
+ * when the document has no bookmark of that name. When a bookmark's paragraph
1763
+ * spans a page break, the page where it *begins* is returned.
1764
+ *
1765
+ * This is the map an internal-hyperlink click resolves against: a viewer's
1766
+ * `onHyperlinkClick` default (or an integrator) turns the anchor into a page
1767
+ * and calls {@link DocxViewer.goToPage} (or scrolls the scroll viewer to it).
1768
+ * Works in BOTH `main` and `worker` mode (the map rides along in the worker
1769
+ * meta, built from the same paginated pages as `pageSizes`).
1770
+ */
1771
+ getBookmarkPage(bookmarkName: string): number | undefined;
1201
1772
  /**
1202
1773
  * ECMA-376 §17.6.13 / §17.6.11 — the page size (pt) of page `pageIndex`, per
1203
1774
  * section (a mixed portrait/landscape document returns different sizes per page).
@@ -1221,8 +1792,23 @@ declare class DocxDocument {
1221
1792
  * The returned ImageBitmap is owned by the caller: pass it to
1222
1793
  * `transferFromImageBitmap` (which consumes it) or call `bitmap.close()`
1223
1794
  * when done, or its backing memory is held until GC.
1795
+ *
1796
+ * IX6 — an optional `onTextRun` in `opts` receives the page's text-run
1797
+ * geometry (the same stream `renderPage` emits in main mode), so a caller can
1798
+ * build the selection / find overlay from a worker-rendered page on the SAME
1799
+ * code path as main mode. In worker mode the runs ride back beside the bitmap
1800
+ * (one round-trip, no second render).
1224
1801
  */
1225
- renderPageToBitmap(pageIndex: number, opts?: WireRenderPageOptions): Promise<ImageBitmap>;
1802
+ renderPageToBitmap(pageIndex: number, opts?: RenderPageToBitmapOptions): Promise<ImageBitmap>;
1803
+ /**
1804
+ * IX6 — collect a page's text-run geometry (`DocxTextRunInfo[]`) without
1805
+ * painting a visible canvas. Works in BOTH modes: worker mode renders the page
1806
+ * off-thread and ships only the runs (no bitmap transfer); main mode renders
1807
+ * to a throwaway offscreen canvas. Used by the find controller to scan every
1808
+ * page for matches. The geometry is identical to a `renderPage` of the same
1809
+ * page at the same width/dpr.
1810
+ */
1811
+ collectPageRuns(pageIndex: number, opts?: WireRenderPageOptions): Promise<DocxTextRunInfo[]>;
1226
1812
  }
1227
1813
 
1228
1814
  declare interface DocxDocumentModel {
@@ -1243,6 +1829,11 @@ declare interface DocxDocumentModel {
1243
1829
  * entry is absent or classified as "auto".
1244
1830
  */
1245
1831
  fontFamilyClasses?: Record<string, string>;
1832
+ /** ECMA-376 §17.8.3.3-.6 — embedded fonts from `word/fontTable.xml`, resolved
1833
+ * to their `.odttf` part paths + fontKey. The viewer de-obfuscates (§17.8.1)
1834
+ * and registers each as a FontFace before pagination so text measures/draws
1835
+ * with the authored typeface. */
1836
+ embeddedFonts?: EmbeddedFontRef[];
1246
1837
  /** ECMA-376 §17.13.5 — flat list of `<w:ins>` / `<w:del>` events in the
1247
1838
  * body. Each entry carries author / date / text. The renderer marks
1248
1839
  * runs inline via {@link DocxTextRun.revision}; this array is primarily for
@@ -1262,6 +1853,31 @@ declare interface DocxDocumentModel {
1262
1853
  * (kinsoku) configuration. Absent when settings.xml has no relevant
1263
1854
  * elements (the renderer then uses spec defaults: kinsoku ON). */
1264
1855
  settings?: DocSettings;
1856
+ /** RB7 partial degradation: set when `word/document.xml` (the body part) could
1857
+ * not be read or parsed. The document still "opens" — `body` is empty and this
1858
+ * part-tagged error (e.g. `"word/document.xml: <detail>"`) is carried — so the
1859
+ * viewer shows a visible placeholder page instead of throwing. Absent
1860
+ * (`undefined`) for every healthy document. */
1861
+ parseError?: string;
1862
+ }
1863
+
1864
+ declare interface DocxHighlightColors {
1865
+ /** Fill for non-active matches. */
1866
+ match?: string;
1867
+ /** Fill for the active match. */
1868
+ active?: string;
1869
+ }
1870
+
1871
+ /** One page's highlight input: the run-slices a match covers, and whether that
1872
+ * match is the active one (emphasis colour). */
1873
+ declare interface DocxHighlightMatch {
1874
+ slices: MatchRunSlice[];
1875
+ active: boolean;
1876
+ }
1877
+
1878
+ /** Where a docx match lives: its 0-based page index. */
1879
+ declare interface DocxMatchLocation {
1880
+ page: number;
1265
1881
  }
1266
1882
 
1267
1883
  /** ECMA-376 §17.3.2.4 `<w:bdr>` — a run-level border drawn as a box around the
@@ -1277,7 +1893,7 @@ declare interface DocxRunBorder {
1277
1893
  space: number;
1278
1894
  }
1279
1895
 
1280
- declare class DocxScrollViewer {
1896
+ declare class DocxScrollViewer implements ZoomableViewer {
1281
1897
  private _doc;
1282
1898
  private readonly _injected;
1283
1899
  private readonly _opts;
@@ -1298,6 +1914,18 @@ declare class DocxScrollViewer {
1298
1914
  * than a `_scale === 1` sentinel because a fit scale of exactly 1 is a valid
1299
1915
  * established state (a 1× fit would otherwise be re-fit forever). */
1300
1916
  private _scaleEstablished;
1917
+ /**
1918
+ * IX9 F1 — a `setScale` factor requested BEFORE the base fit is established
1919
+ * (pre-load, or a zero-width container), already clamped to
1920
+ * `[zoomMin, zoomMax]`, or `null` when none is pending. The single-canvas
1921
+ * viewers latch a pre-load `setScale` and honour it on the first render; the
1922
+ * scroll viewers used to silently DROP it — the family-unified semantics are
1923
+ * "latch and apply once the layout establishes". `relayout()` applies (and
1924
+ * clears) this right after establishing the base, firing `onScaleChange` at
1925
+ * application time; `getScale()` reports it while pending so the caller sees
1926
+ * the same value a single-canvas viewer would show.
1927
+ */
1928
+ private _pendingScale;
1301
1929
  /** Live slots keyed by page index. */
1302
1930
  private readonly _slots;
1303
1931
  /** Recyclable detached slots (canvas + textLayer reused across pages). */
@@ -1311,6 +1939,27 @@ declare class DocxScrollViewer {
1311
1939
  * reporting an error so a rejection that lands after teardown is swallowed
1312
1940
  * rather than surfaced to a `onError` on a dead viewer. */
1313
1941
  private _destroyed;
1942
+ /** Throwaway 2D context reused to measure text for the §17.3.2.10 縦中横 overlay
1943
+ * clamp (#836). Lazily created; `null` when canvas metrics are unavailable
1944
+ * (headless), in which case the overlay degrades to the un-clamped span. */
1945
+ private _measureCtx;
1946
+ /**
1947
+ * Concurrent-load latch (generation token). Every self-loading `load()`
1948
+ * increments this and captures the value; after its engine finishes loading it
1949
+ * re-checks the live value and BAILS (destroying its own just-loaded engine) if
1950
+ * a newer `load()` has since started. Without it, two overlapping
1951
+ * `load(A)`/`load(B)` calls race the WASM parse / worker init, and whichever
1952
+ * RESOLVES last wins the swap — even the stale `load(A)` resolving after
1953
+ * `load(B)`; the loser's freshly created engine (never installed, or installed
1954
+ * then overwritten) then leaks its worker + pinned WASM allocation. The latch
1955
+ * composes with SC20: the check runs AFTER the new engine loads but BEFORE the
1956
+ * field assignment, `previous?.destroy()`, and the recycle/relayout post-load
1957
+ * work, so a superseded load never touches `this._doc` nor frees the current
1958
+ * (newer) engine. Only the self-loading path uses it — the injected path throws
1959
+ * up-front and never reaches here. `destroy()` also bumps it so a load in flight
1960
+ * at teardown is treated as superseded and its engine cleaned up.
1961
+ */
1962
+ private _loadGen;
1314
1963
  /** Worker mode: page indices whose bitmap render is currently dispatched to the
1315
1964
  * engine. Coalesces a scroll storm — we never dispatch a second render for a
1316
1965
  * page whose first is still in flight — and lets us drop pages that scrolled
@@ -1341,11 +1990,14 @@ declare class DocxScrollViewer {
1341
1990
  * is host-agnostic. */
1342
1991
  private _settleTimer;
1343
1992
  private _wheelListener;
1344
- /** One-shot latch for the worker-mode text-selection warning. The overlay is a
1345
- * main-mode-only feature: in worker mode the per-run `onTextRun` geometry
1346
- * cannot cross the worker boundary, so an `enableTextSelection` overlay stays
1347
- * empty. We warn once (parity with `DocxViewer`) rather than per slot. */
1348
- private _warnedNoTextSelection;
1993
+ /** Gesture-only pointer anchor for the NEXT `setScale`, in scrollHost-viewport
1994
+ * px (`{ x, y }` from the wheel event, relative to the scroll host's top-left).
1995
+ * Set by the Ctrl/⌘+wheel handler right before it calls `setScale` so the zoom
1996
+ * pivots on the cursor ("zoom toward the pointer") in BOTH axes; consumed and
1997
+ * cleared by `setScale`. `null` for every non-gesture source (the public
1998
+ * `setScale`, the +/- steppers, `fitWidth`/`fitPage`, the resize re-fit), which
1999
+ * keep the historical viewport-TOP re-anchor so their behaviour is unchanged. */
2000
+ private _pendingZoomAnchor;
1349
2001
  /** Observes the container so a width change re-fits the base scale. Disconnected
1350
2002
  * in `destroy()`. */
1351
2003
  private _resizeObserver;
@@ -1414,6 +2066,12 @@ declare class DocxScrollViewer {
1414
2066
  * `_positionSlot` (the flush-left floor), and by `_syncSpacer` (the spacer
1415
2067
  * width). Resolved here (not stored) to mirror `_gap()`/`_pad()`. */
1416
2068
  private _padH;
2069
+ /** Index of the page whose slot spans content-offset `y` (largest `i` with
2070
+ * `offsets[i] <= y`), for the pointer-anchored zoom re-anchor. Mirrors the
2071
+ * `topIndex` search `computeVisibleRange` runs for the scrollTop, but for an
2072
+ * ARBITRARY content-y (the pointer, not the viewport top). Clamped into
2073
+ * `[0, n-1]`; a `y` below the first page (inside the leading pad) yields 0. */
2074
+ private _pageIndexAtOffset;
1417
2075
  private _range;
1418
2076
  private _syncSpacer;
1419
2077
  /** Horizontal scroll extent: the widest page (docx pages can differ in width)
@@ -1463,10 +2121,24 @@ declare class DocxScrollViewer {
1463
2121
  * can pass for an old-epoch resolution). We gate them on the captured epoch.
1464
2122
  */
1465
2123
  private _renderSlot;
1466
- /** Warn once when an `enableTextSelection` overlay was requested but the render
1467
- * mode is `worker` (so the overlay stays empty). Same wording as
1468
- * `DocxViewer._render` one warning per viewer, not per slot. */
1469
- private _maybeWarnNoTextSelection;
2124
+ /**
2125
+ * IX1/IX-nav the click handler passed to the text-layer overlay. When the
2126
+ * caller supplied `onHyperlinkClick`, it fully owns the behaviour (the default
2127
+ * is suppressed). Otherwise the built-in default is: an external link opens in
2128
+ * a new tab through core `openExternalHyperlink` (URL sanitised against the
2129
+ * safe scheme allowlist, `noopener,noreferrer`); an internal `<w:anchor>` link
2130
+ * resolves its bookmark name to its destination page via
2131
+ * {@link DocxDocument.getBookmarkPage} (ECMA-376 §17.16.23) and scrolls there
2132
+ * with {@link scrollToPage}. An anchor naming no known bookmark is a safe no-op
2133
+ * rather than a scroll to a guessed page.
2134
+ */
2135
+ private _hyperlinkHandler;
2136
+ /** A width-measurer primed with a run's `font` — used ONLY to clamp a §17.3.2.10
2137
+ * 縦中横 selection span to its drawn one-em cell (#836). Mirrors DocxViewer's
2138
+ * `_measureForFont`. Returns a length-based fallback when canvas metrics are
2139
+ * unavailable so the caller still gets a callable (the overlay then sees scale
2140
+ * 1 and leaves the span un-clamped). */
2141
+ private _measureForFont;
1470
2142
  /** Route an async render failure to `onError`, or `console.error` when none is
1471
2143
  * set (so failures are never fully silent), and never after teardown. */
1472
2144
  private _reportRenderError;
@@ -1496,7 +2168,10 @@ declare class DocxScrollViewer {
1496
2168
  * `[zoomMin ?? 0.1, zoomMax ?? 4]` (absolute bounds, XlsxViewer convention — NOT
1497
2169
  * multiples of the base fit; design §3 keeps the clamp in the viewer, not core),
1498
2170
  * then re-anchor VERTICALLY so the page currently under the viewport top stays
1499
- * fixed. A no-op when nothing is loaded or when the clamped scale is unchanged.
2171
+ * fixed. A no-op when the clamped scale is unchanged. Called BEFORE the doc is
2172
+ * loaded / the base fit is established, the clamped factor is LATCHED (IX9 F1,
2173
+ * family-unified with the single-canvas viewers) and applied by `relayout()`
2174
+ * once the layout establishes — `onScaleChange` fires then.
1500
2175
  *
1501
2176
  * FLICKER-FREE (design §7): this does NOT re-render the visible pages inline.
1502
2177
  * It shows an immediate CSS preview (stretch the existing bitmaps, scale the
@@ -1517,6 +2192,38 @@ declare class DocxScrollViewer {
1517
2192
  * can no longer return below the floor to the original base fit through this API.
1518
2193
  */
1519
2194
  setScale(scale: number): void;
2195
+ /** IX9 {@link ZoomableViewer} — the current zoom factor, where `1` = 100% (a
2196
+ * page at its natural pt→px width). This is the viewer's absolute `_scale`
2197
+ * (`widthPt × PT_TO_PX × _scale` is the drawn width), so it reads `1` at true
2198
+ * 100% and, after the initial fit-to-width, the base fit factor. Before the
2199
+ * fit is established it reports a latched pre-load `setScale` (IX9 F1) if one
2200
+ * is pending — matching what a single-canvas viewer would show — else `1`. */
2201
+ getScale(): number;
2202
+ /** IX9 {@link ZoomableViewer} — step up to the next rung of the shared zoom
2203
+ * ladder above the current factor (clamped to `zoomMax` by {@link setScale}). */
2204
+ zoomIn(): void;
2205
+ /** IX9 {@link ZoomableViewer} — step down to the next lower ladder rung. */
2206
+ zoomOut(): void;
2207
+ /**
2208
+ * IX9 {@link ZoomableViewer} — fit a page's WIDTH to the container (the classic
2209
+ * continuous-scroll "fit width"). Sets the scale to the width-fit base for the
2210
+ * current container, then re-anchors + re-renders via {@link setScale}. Defers
2211
+ * (no-op) while the container is unlaid-out. Note the `zoomMin`/`zoomMax` clamp
2212
+ * still applies, so a fit below `zoomMin` pins to `zoomMin`.
2213
+ */
2214
+ fitWidth(): void;
2215
+ /**
2216
+ * IX9 {@link ZoomableViewer} — fit a WHOLE page (width and height) inside the
2217
+ * container so one page is visible without scrolling; takes the tighter of the
2218
+ * width/height fit. Uses the FIRST page's size (the continuous viewer's fit
2219
+ * reference, matching the base-fit convention). Defers while unlaid-out.
2220
+ */
2221
+ fitPage(): void;
2222
+ /** Shared fit for {@link fitWidth}/{@link fitPage}: the width-fit factor is the
2223
+ * established base (`_baseScale`); the page-fit additionally bounds by the
2224
+ * container height against the first page's height. Applies via {@link setScale}
2225
+ * so the flicker-free re-anchor / settle path and `onScaleChange` all run. */
2226
+ private _fit;
1520
2227
  /**
1521
2228
  * CSS preview of the visible window at the current `_scale` (design §7
1522
2229
  * mechanism 1), WITHOUT re-rendering. Slots leaving the window recycle normally;
@@ -1543,12 +2250,11 @@ declare class DocxScrollViewer {
1543
2250
  private _scheduleSettle;
1544
2251
  /** Full-resolution settle re-render of the visible window (design §7 mechanisms
1545
2252
  * 2+3). Re-renders each mounted slot at the current scale via the double-buffer
1546
- * swap (main) / same-canvas transfer (worker). Main mode also rebuilds the text
1547
- * overlay and clears its preview transform; in worker mode the overlay is
1548
- * permanently empty (text selection is main-mode-only), so the transform is
1549
- * inert there and is reset on recycle. Dispatched at the CURRENT epoch; the
1550
- * existing epoch gate discards it if a later `setScale` supersedes it
1551
- * mid-render. */
2253
+ * swap (main) / same-canvas transfer (worker). Both modes rebuild the text
2254
+ * overlay from the fresh render's run geometry (IX6 — worker mode collects the
2255
+ * runs off-thread via `_renderSlotBitmap`) and clear the preview transform.
2256
+ * Dispatched at the CURRENT epoch; the existing epoch gate discards it if a
2257
+ * later `setScale` supersedes it mid-render. */
1552
2258
  private _settleRender;
1553
2259
  /**
1554
2260
  * Settle-render one slot at the current scale (design §7 mechanism 3).
@@ -1624,6 +2330,8 @@ declare class DocxScrollViewer {
1624
2330
  /* Excluded from this release type: baseScaleForTest */
1625
2331
  /* Excluded from this release type: renderEpochForTest */
1626
2332
  /* Excluded from this release type: resizeForTest */
2333
+ /* Excluded from this release type: contentAtViewportYForTest */
2334
+ /* Excluded from this release type: viewportYOfForTest */
1627
2335
  /**
1628
2336
  * Tear down the viewer: remove the DOM subtree and (only for a self-loaded
1629
2337
  * engine) destroy the engine. An injected engine is left intact — the caller
@@ -1670,9 +2378,10 @@ declare interface DocxScrollViewerOptions extends Omit<RenderPageOptions, 'onTex
1670
2378
  paddingRight?: number;
1671
2379
  /** Pages kept mounted beyond the viewport on each side. Default 1. */
1672
2380
  overscan?: number;
1673
- /** Per-page transparent text-selection overlay. MAIN render mode only:
1674
- * in worker mode `onTextRun` cannot cross the worker boundary, so the overlay
1675
- * stays empty and the viewer logs one warning (design §11). */
2381
+ /** Per-page transparent text-selection overlay. IX6 works in BOTH render
2382
+ * modes: in worker mode the per-run geometry is collected off-thread and
2383
+ * shipped back beside the page bitmap, so the overlay is populated identically
2384
+ * to main mode (no more empty overlay / one-time warning). */
1676
2385
  enableTextSelection?: boolean;
1677
2386
  /** Minimum zoom scale (px-per-pt multiplier floor). Default 0.1. */
1678
2387
  zoomMin?: number;
@@ -1715,6 +2424,17 @@ declare interface DocxScrollViewerOptions extends Omit<RenderPageOptions, 'onTex
1715
2424
  * `computeVisibleRange` (the first page intersecting the viewport top,
1716
2425
  * EXCLUDING overscan). */
1717
2426
  onVisiblePageChange?: (topIndex: number, total: number) => void;
2427
+ /** IX9 — fires whenever the zoom factor actually changes (`1` = 100% = a page
2428
+ * at its natural pt→px size): from {@link DocxScrollViewer.setScale},
2429
+ * `zoomIn`/`zoomOut`, `fitWidth`/`fitPage`, a Ctrl/⌘+wheel gesture, or a
2430
+ * container-resize re-fit. Named `onScaleChange` to match the single-canvas
2431
+ * viewers so all five share one notification shape. */
2432
+ onScaleChange?: (scale: number) => void;
2433
+ /** IX1 (design decision — NOT user-confirmed, integrator may veto). Called when
2434
+ * a hyperlink run is clicked. When omitted, the default is: external → open in a
2435
+ * new tab via core `openExternalHyperlink` (sanitised, noopener,noreferrer);
2436
+ * internal → jump to the page whose text contains the bookmark (best-effort). */
2437
+ onHyperlinkClick?: (target: HyperlinkTarget) => void;
1718
2438
  /** Error callback. When set, `load()` invokes it and resolves (otherwise the
1719
2439
  * error is rethrown — shared viewer error contract). It ALSO fires for async
1720
2440
  * per-slot render failures (both main `renderPage` and worker
@@ -1729,6 +2449,15 @@ declare interface DocxTextRun {
1729
2449
  bold: boolean;
1730
2450
  italic: boolean;
1731
2451
  underline: boolean;
2452
+ /** ECMA-376 §17.3.2.40 `<w:u w:val>` — the raw ST_Underline (§17.18.99) style
2453
+ * value (`double` / `thick` / `dotted` / `wave` / `dashLong` / …). Absent for
2454
+ * the plain single rule (or no underline). The renderer normalizes this
2455
+ * WordprocessingML vocabulary to the shared DrawingML ST_TextUnderlineType
2456
+ * (§20.1.10.82) that `core.drawUnderline` dispatches on. */
2457
+ underlineStyle?: string;
2458
+ /** ECMA-376 §17.3.2.40 `<w:u w:color>` — underline-only colour (hex 6, or the
2459
+ * literal `auto`). Absent ⇒ the underline follows the glyph colour. */
2460
+ underlineColor?: string;
1732
2461
  strikethrough: boolean;
1733
2462
  fontSize: number;
1734
2463
  color: string | null;
@@ -1756,10 +2485,23 @@ declare interface DocxTextRun {
1756
2485
  vertAlign: 'super' | 'sub' | null;
1757
2486
  /** Target URL for hyperlinks (resolved from relationships.xml) */
1758
2487
  hyperlink: string | null;
2488
+ /** ECMA-376 §17.16.23 `<w:hyperlink w:anchor>` — internal bookmark name this
2489
+ * link jumps to (a `<w:bookmarkStart w:name>` in the same document). Set for an
2490
+ * internal cross-reference / TOC entry. When a link carries both `r:id` and
2491
+ * `w:anchor`, {@link DocxTextRun.hyperlink} (external) wins and this still
2492
+ * records the anchor. Absent when the link has no anchor. */
2493
+ hyperlinkAnchor?: string | null;
1759
2494
  allCaps?: boolean;
1760
2495
  smallCaps?: boolean;
1761
2496
  doubleStrikethrough?: boolean;
1762
2497
  highlight?: string | null;
2498
+ /** ECMA-376 §17.3.2.12 `<w:em w:val>` — emphasis (boten / 圏点) mark drawn on
2499
+ * every non-space character of the run (§17.18.24 ST_Em). `'dot'` = filled
2500
+ * dot above, `'comma'` = sesame/comma above, `'circle'` = hollow circle
2501
+ * above, `'underDot'` = filled dot below (horizontal writing). Absent (or the
2502
+ * authored `val="none"`) ⇒ no mark. The renderer stamps the mark per glyph
2503
+ * after the text and does NOT change the glyph advance. */
2504
+ emphasisMark?: EmphasisMark;
1763
2505
  /** ECMA-376 §17.3.3.25 ruby annotation (furigana). Renders above the
1764
2506
  * base text in a smaller font; line height is expanded to fit it. */
1765
2507
  ruby?: RubyAnnotation;
@@ -1791,6 +2533,42 @@ declare interface DocxTextRun {
1791
2533
  /** ECMA-376 §17.3.2.20 `<w:lang w:bidi>` — complex-script (RTL) language tag,
1792
2534
  * lower-cased (e.g. "ar-sa", "ae-ar"). Drives Word's AN digit ordering. */
1793
2535
  langBidi?: string;
2536
+ /** ECMA-376 §17.3.2.35 `<w:spacing w:val>` — character-spacing adjustment in
2537
+ * POINTS (signed): the extra pitch added after each character before the next
2538
+ * is rendered. The renderer feeds it to `ctx.letterSpacing` on BOTH the
2539
+ * measure and paint passes so line breaking / pagination stay consistent.
2540
+ * Absent ⇒ no extra pitch. */
2541
+ charSpacing?: number;
2542
+ /** ECMA-376 §17.3.2.43 `<w:w w:val>` — horizontal text scale as a FRACTION of
2543
+ * normal character width (0.67 = 67%, 2.0 = 200%). Stretches each glyph's
2544
+ * width, not the gap between glyphs. Absent ⇒ 100%. */
2545
+ charScale?: number;
2546
+ /** ECMA-376 §17.3.2.24 `<w:position w:val>` — baseline raise (positive) /
2547
+ * lower (negative) in POINTS, without changing the font size or line box.
2548
+ * Absent ⇒ no shift. */
2549
+ position?: number;
2550
+ /** ECMA-376 §17.3.2.19 `<w:kern w:val>` — font-kerning threshold in POINTS
2551
+ * (the smallest font size that is kerned). Presence enables kerning subject
2552
+ * to the threshold; absent ⇒ kerning off (the hierarchy default). `0` = kern
2553
+ * at all sizes. */
2554
+ kerning?: number;
2555
+ /** ECMA-376 §17.3.2.10 `<w:eastAsianLayout w:vert>` — horizontal-in-vertical
2556
+ * (縦中横 / tate-chū-yoko). `true` means that in a VERTICAL (tbRl) page this
2557
+ * run's characters are laid out horizontally side by side within ONE cell of
2558
+ * the vertical line (rotated 90° relative to the vertical flow). Absent ⇒
2559
+ * normal vertical stacking. Inert in a horizontal page. */
2560
+ eastAsianVert?: boolean;
2561
+ /** ECMA-376 §17.3.2.10 `<w:eastAsianLayout w:vertCompress>` — compress the
2562
+ * 縦中横 run to fit the existing line height without growing the line. Ignored
2563
+ * unless {@link eastAsianVert} is set. Absent ⇒ not compressed. */
2564
+ eastAsianVertCompress?: boolean;
2565
+ /** ECMA-376 §17.3.2.10 `<w:eastAsianLayout w:combine>` — two-lines-in-one.
2566
+ * PARSED for completeness; not yet rendered (no fixture). */
2567
+ eastAsianCombine?: boolean;
2568
+ /** ECMA-376 §17.3.2.10 `<w:eastAsianLayout w:combineBrackets>` (§17.18.8) —
2569
+ * bracket style around two-lines-in-one text. PARSED for completeness; the
2570
+ * two-lines-in-one draw is a follow-up. */
2571
+ eastAsianCombineBrackets?: string;
1794
2572
  /** ECMA-376 §17.11.6/.7/.16/.17 — set when this run is a footnote/endnote
1795
2573
  * reference marker (`<w:footnoteReference>` in the body, `<w:footnoteRef>` at
1796
2574
  * the start of the note's content, and the endnote equivalents). `text` holds
@@ -1814,11 +2592,40 @@ declare interface DocxTextRunInfo {
1814
2592
  fontSize: number;
1815
2593
  /** CSS `font` shorthand used for canvas drawing (e.g. `"bold 16px Arial"`). */
1816
2594
  font: string;
1817
- }
1818
-
1819
- declare class DocxViewer {
2595
+ /** ECMA-376 §17.6.20 (tbRl) — when the page is vertical the canvas is the
2596
+ * physical landscape page rotated +90° at paint, so this run's `x`/`y` are the
2597
+ * PHYSICAL top-left the overlay span must sit at, and `transform` is the CSS
2598
+ * rotation (`"rotate(90deg)"`, applied about the span's top-left) that lays the
2599
+ * horizontal DOM span along the drawn (rotated) glyph run. Absent for
2600
+ * horizontal pages (the span is placed at `x`/`y` untransformed). */
2601
+ transform?: string;
2602
+ /** IX1 — the resolved hyperlink target of this run (ECMA-376 §17.16.22
2603
+ * external URL / §17.16.23 internal `w:anchor` bookmark), or absent for a
2604
+ * non-link run. The text-layer overlay turns a run carrying this into a
2605
+ * clickable region; the drawn glyphs are unaffected. */
2606
+ hyperlink?: HyperlinkTarget;
2607
+ /** ECMA-376 §17.3.2.10 eastAsianLayout `w:vert` (縦中横 / horizontal-in-vertical):
2608
+ * `true` when this run was drawn as tate-chu-yoko — its glyphs laid out
2609
+ * horizontally, side by side, COMPRESSED into ONE em cell of the vertical
2610
+ * column (see {@link drawTateChuYokoRun}). `w` is the drawn cell extent (one
2611
+ * em), NOT the natural text width, so the find / selection overlays must clamp
2612
+ * their horizontal extent to `w` rather than re-measuring the run's natural
2613
+ * glyphs (issue #836). Absent for every ordinary run. */
2614
+ eastAsianVert?: boolean;
2615
+ }
2616
+
2617
+ declare class DocxViewer implements ZoomableViewer {
1820
2618
  private _doc;
1821
2619
  private _currentPage;
2620
+ /**
2621
+ * IX9 explicit zoom factor (`1` = 100% = the page at its natural pt→px width),
2622
+ * or `null` when the caller has never invoked a zoom method. `null` preserves
2623
+ * the pre-IX9 render path EXACTLY: the page renders at `opts.width` (or its
2624
+ * natural width when that is unset), so default rendering is byte-identical. The
2625
+ * first `setScale`/`zoomIn`/`zoomOut`/`fitWidth`/`fitPage` call latches a number
2626
+ * here, after which `_renderPage` derives the canvas width from it instead.
2627
+ */
2628
+ private _scale;
1822
2629
  private _canvas;
1823
2630
  private _wrapper;
1824
2631
  /** The canvas's DOM position BEFORE the constructor reparented it into
@@ -1831,20 +2638,56 @@ declare class DocxViewer {
1831
2638
  * (empty string if it was unset), restored on {@link destroy}. */
1832
2639
  private _originalDisplay;
1833
2640
  private _textLayer;
2641
+ /** IX2 — the find-highlight overlay layer. Always created (independent of
2642
+ * `enableTextSelection`): highlights ride the same positioned-DOM overlay
2643
+ * mechanism as the selection layer but are visible boxes, not transparent
2644
+ * spans. Sits above the text layer so a highlight shows over a link's hit
2645
+ * region without stealing its clicks (`pointer-events:none`). */
2646
+ private _highlightLayer;
2647
+ /** IX2 — find state (per-page runs, matches, active cursor). */
2648
+ private _find;
2649
+ /** A 2d context used only to measure text for highlight geometry (its own
2650
+ * 1×1 offscreen canvas, so measuring never touches the visible canvas). */
2651
+ private _measureCtx;
1834
2652
  private _opts;
1835
2653
  private readonly _mode;
1836
2654
  /** The canvas's bitmaprenderer context, used only in worker mode (a canvas
1837
2655
  * holds one context type for its lifetime; the main-mode 2d render path is
1838
2656
  * never used on the same canvas). */
1839
2657
  private _bitmapCtx;
1840
- private _warnedNoTextSelection;
2658
+ /** Set by {@link destroy} (first line). Guards {@link _reportRenderError} so a
2659
+ * render rejection that lands AFTER teardown is swallowed rather than surfaced
2660
+ * to an `onError` / `console.error` on a dead viewer — parity with the scroll
2661
+ * viewers' `_destroyed` flag. */
2662
+ private _destroyed;
2663
+ /**
2664
+ * Concurrent-load latch (generation token). Every {@link load} increments this
2665
+ * and captures the value; after its engine finishes loading it re-checks the
2666
+ * live value and BAILS (destroying its own just-loaded engine) if a newer
2667
+ * `load()` has since started. Without it, two overlapping `load(A)`/`load(B)`
2668
+ * calls race the WASM parse / worker init, and whichever RESOLVES last wins the
2669
+ * swap — even the stale `load(A)` resolving after `load(B)`; the loser's freshly
2670
+ * created engine (never installed, or installed then overwritten) then leaks its
2671
+ * worker + pinned WASM allocation. The latch composes with SC20: the check runs
2672
+ * AFTER the new engine loads but BEFORE the field assignment and
2673
+ * `previous?.destroy()`, so a superseded load never touches `this._doc` nor
2674
+ * frees the current (newer) engine. {@link destroy} also bumps it so a load in
2675
+ * flight at teardown is treated as superseded and its engine cleaned up.
2676
+ */
2677
+ private _loadGen;
1841
2678
  constructor(canvas: HTMLCanvasElement, opts?: DocxViewerOptions);
1842
2679
  /**
1843
2680
  * Load a DOCX from URL or ArrayBuffer and render the first page.
1844
2681
  *
1845
- * Error contract (shared by all three viewers): on failure, if an `onError`
1846
- * callback was provided it is invoked and `load` resolves normally; if not,
1847
- * the error is rethrown so it is never silently swallowed.
2682
+ * Error contract (shared by all three viewers):
2683
+ * - Parse/load failure (the underlying `DocxDocument.load()` call itself
2684
+ * rejects): if an `onError` callback was provided it is invoked and `load`
2685
+ * resolves normally; if not, the error is rethrown so it is never silently
2686
+ * swallowed.
2687
+ * - Render failure (the first page fails to draw AFTER a successful
2688
+ * parse/load): routed to the shared `_reportRenderError` contract (`onError`
2689
+ * if provided, else `console.error` — never silent) and `load` still
2690
+ * RESOLVES, matching every subsequent navigation call.
1848
2691
  */
1849
2692
  load(source: string | ArrayBuffer): Promise<void>;
1850
2693
  get pageCount(): number;
@@ -1854,6 +2697,91 @@ declare class DocxViewer {
1854
2697
  goToPage(index: number): Promise<void>;
1855
2698
  nextPage(): Promise<void>;
1856
2699
  prevPage(): Promise<void>;
2700
+ /** Natural (100%) CSS-px width of the current page — `widthPt × PT_TO_PX`.
2701
+ * This is the scale-1 reference every zoom factor multiplies. 0 when nothing
2702
+ * is loaded. */
2703
+ private _naturalWidthPx;
2704
+ /**
2705
+ * The width (CSS px) `_renderPage` renders the current page at, honouring the
2706
+ * zoom state. `_scale === null` (no zoom method ever called) ⇒ the pre-IX9
2707
+ * value `opts.width` verbatim (byte-identical default: `undefined` lets the
2708
+ * renderer use the page's natural width). Once a factor latched ⇒
2709
+ * `naturalWidth × scale` (rounded), so the on-screen page is exactly `scale ×`
2710
+ * its natural size regardless of the original `opts.width`.
2711
+ */
2712
+ private _renderWidth;
2713
+ /** IX9 {@link ZoomableViewer} — the current zoom factor (`1` = 100%). Before
2714
+ * any zoom method is called this is the EFFECTIVE scale implied by the current
2715
+ * render width: `opts.width / naturalWidth`, or `1` when `opts.width` is unset
2716
+ * (the page renders at its natural size) or nothing is loaded. */
2717
+ getScale(): number;
2718
+ private _zoomMin;
2719
+ private _zoomMax;
2720
+ /**
2721
+ * IX9 {@link ZoomableViewer} — set the absolute zoom factor (`1` = 100% = the
2722
+ * page at its natural pt→px width), clamped to `[zoomMin, zoomMax]`, and
2723
+ * re-render the current page at the new size. Fires `onScaleChange` when the
2724
+ * clamped factor actually changes. Resolves once the re-render settles. A no-op
2725
+ * (but still latches the scale) when nothing is loaded.
2726
+ */
2727
+ setScale(scale: number): Promise<void>;
2728
+ /** IX9 {@link ZoomableViewer} — step up to the next rung of the shared zoom
2729
+ * ladder (clamped to `zoomMax`). */
2730
+ zoomIn(): Promise<void>;
2731
+ /** IX9 {@link ZoomableViewer} — step down to the next lower ladder rung. */
2732
+ zoomOut(): Promise<void>;
2733
+ /**
2734
+ * IX9 {@link ZoomableViewer} — fit the current page's WIDTH to the host
2735
+ * container (the element the canvas lives in, or `opts.container` if supplied),
2736
+ * then re-render. Defers (no-op) when nothing is loaded or the container is
2737
+ * unlaid-out. Routes through {@link setScale}, so the factor is clamped and
2738
+ * `onScaleChange` fires.
2739
+ */
2740
+ fitWidth(): Promise<void>;
2741
+ /**
2742
+ * IX9 {@link ZoomableViewer} — fit the WHOLE current page (width and height)
2743
+ * inside the container so it is visible without scrolling; takes the tighter of
2744
+ * the width/height fit. Defers when unloaded / unlaid-out.
2745
+ */
2746
+ fitPage(): Promise<void>;
2747
+ /** Shared fit for {@link fitWidth}/{@link fitPage}: measure the natural page
2748
+ * size + the container box, ask core's pure `fitScale`, apply via setScale. */
2749
+ private _fit;
2750
+ /** The element a fit measures against: the explicit `opts.container`, else the
2751
+ * host the wrapper was inserted into (`_wrapper.parentElement`). `null` when
2752
+ * the canvas was mounted detached (no host to fit to). */
2753
+ private _fitContainer;
2754
+ /**
2755
+ * IX2 — find every occurrence of `query` in the document and highlight them
2756
+ * all (a soft box per match, drawn on the highlight overlay over the drawn
2757
+ * glyphs). Returns every match in document order, each tagged with its
2758
+ * `{ page }` (0-based). Case-insensitive by default (browser find-in-page);
2759
+ * pass `{ caseSensitive: true }` to match case exactly.
2760
+ *
2761
+ * Scans all pages, so a large document renders each page once (offscreen) to
2762
+ * read its text (the visible page reuses its on-screen render). IX6 — works in
2763
+ * BOTH `mode: 'main'` and `mode: 'worker'`: in worker mode each page's run
2764
+ * geometry is collected off-thread and shipped back, so find returns the same
2765
+ * matches on the same code path. An empty query clears the find and returns `[]`.
2766
+ */
2767
+ findText(query: string, opts?: FindMatchesOptions): Promise<FindMatch<DocxMatchLocation>[]>;
2768
+ /**
2769
+ * IX2 — move to the next match (wrap-around from last to first), navigating to
2770
+ * its page if needed, and draw it in the distinct active-match colour. Returns
2771
+ * the now-active match, or `null` when there are no matches. Call
2772
+ * {@link findText} first.
2773
+ */
2774
+ findNext(): Promise<FindMatch<DocxMatchLocation> | null>;
2775
+ /** IX2 — move to the previous match (wrap-around from first to last). */
2776
+ findPrev(): Promise<FindMatch<DocxMatchLocation> | null>;
2777
+ /** IX2 — clear all highlights and reset the find state. */
2778
+ clearFind(): void;
2779
+ /** Navigate to the active match's page (if not already there) and redraw the
2780
+ * highlights so the active box shows in the emphasis colour. */
2781
+ private _activateMatch;
2782
+ /** Rebuild the highlight overlay for the current page from cached runs
2783
+ * (no page re-render). */
2784
+ private _redrawHighlights;
1857
2785
  /**
1858
2786
  * Terminate the parser worker and release resources.
1859
2787
  *
@@ -1865,7 +2793,34 @@ declare class DocxViewer {
1865
2793
  */
1866
2794
  destroy(): void;
1867
2795
  private _render;
2796
+ /** Route a render failure to `onError`, or `console.error` when none is given
2797
+ * (never fully silent), and never after teardown. Mirrors the scroll viewers'
2798
+ * `_reportRenderError`. */
2799
+ private _reportRenderError;
2800
+ private _renderPage;
2801
+ /** Draw the find-highlight boxes for the current page from its runs. Clears
2802
+ * the overlay when there is no active find. */
2803
+ private _buildHighlightLayer;
2804
+ /** A width-measurer primed with `font`, backed by a private 1×1 canvas so it
2805
+ * never disturbs the visible canvas's context state. */
2806
+ private _measureForFont;
2807
+ /** Render a page to a throwaway offscreen canvas purely to collect its runs
2808
+ * (text + geometry) for search, without touching the visible canvas. Used by
2809
+ * the find controller for pages other than the one on screen. */
2810
+ private _collectPageRuns;
1868
2811
  private _buildTextLayer;
2812
+ /**
2813
+ * IX1/IX-nav — the click handler passed to the text-layer overlay. When the
2814
+ * caller supplied `onHyperlinkClick`, it fully owns the behaviour (the default
2815
+ * is suppressed). Otherwise the built-in default is: an external link opens in
2816
+ * a new tab through core `openExternalHyperlink` (URL sanitised against the
2817
+ * safe scheme allowlist, `noopener,noreferrer`); an internal `<w:anchor>` link
2818
+ * resolves its bookmark name to a page via
2819
+ * {@link DocxDocument.getBookmarkPage} (ECMA-376 §17.16.23) and jumps there
2820
+ * with {@link goToPage}. An anchor naming no known bookmark is a safe no-op
2821
+ * rather than a jump to a guessed page.
2822
+ */
2823
+ private _hyperlinkHandler;
1869
2824
  }
1870
2825
 
1871
2826
  declare interface DocxViewerOptions extends RenderPageOptions, LoadOptions_4 {
@@ -1877,10 +2832,49 @@ declare interface DocxViewerOptions extends RenderPageOptions, LoadOptions_4 {
1877
2832
  enableTextSelection?: boolean;
1878
2833
  /** Called when a page finishes rendering. */
1879
2834
  onPageChange?: (index: number, total: number) => void;
2835
+ /** IX9 zoom contract ({@link ZoomableViewer}) — the clamp range for
2836
+ * {@link DocxViewer.setScale} / `zoomIn` / `zoomOut` / `fitWidth` / `fitPage`,
2837
+ * as user-facing zoom factors (`1` = 100% = the page at its natural pt→px
2838
+ * size). Defaults 0.1–4 (10%–400%), matching the other viewers. */
2839
+ zoomMin?: number;
2840
+ zoomMax?: number;
2841
+ /** IX9 — fires whenever the zoom factor actually changes (`1` = 100%): from
2842
+ * {@link DocxViewer.setScale}, `zoomIn`/`zoomOut`, or `fitWidth`/`fitPage`.
2843
+ * Named `onScaleChange` to match the pptx/xlsx viewers so all five share one
2844
+ * notification shape. */
2845
+ onScaleChange?: (scale: number) => void;
2846
+ /** IX1 (design decision — NOT user-confirmed, integrator may veto). Called when
2847
+ * a hyperlink run is clicked. When omitted, the default is: external → open in a
2848
+ * new tab via core `openExternalHyperlink` (sanitised, noopener,noreferrer);
2849
+ * internal → jump to the page whose text contains the bookmark (best-effort). */
2850
+ onHyperlinkClick?: (target: HyperlinkTarget) => void;
1880
2851
  /** Called on parse or render errors. */
1881
2852
  onError?: (err: Error) => void;
1882
2853
  }
1883
2854
 
2855
+ /** A duotone effect resolved to its two endpoint colours. Both are 6-char
2856
+ * uppercase hex WITHOUT a leading `#` (the form the Rust parsers emit). `clr1`
2857
+ * is the dark endpoint (luminance 0), `clr2` the light endpoint (luminance 1),
2858
+ * matching the child order of `<a:duotone>` in §20.1.8.23. Any per-colour
2859
+ * transforms (lumMod/lumOff/tint/satMod/…) are already baked into these hexes
2860
+ * by the parser's colour-resolution machinery. */
2861
+ declare interface Duotone {
2862
+ /** First `EG_ColorChoice` child — the dark endpoint. 6-char hex, no `#`. */
2863
+ clr1: string;
2864
+ /** Second `EG_ColorChoice` child — the light endpoint. 6-char hex, no `#`. */
2865
+ clr2: string;
2866
+ }
2867
+
2868
+ /** ECMA-376 §20.1.8.23 `<a:duotone>` image effect, resolved to its two endpoint
2869
+ * colours (mirrors the shared Rust `ooxml_common::blip::Duotone`). `clr1` is the
2870
+ * dark endpoint (luminance 0), `clr2` the light endpoint (luminance 1); both are
2871
+ * 6-char uppercase hex WITHOUT a leading `#`, with per-colour transforms already
2872
+ * applied by the parser. */
2873
+ declare interface Duotone_2 {
2874
+ clr1: string;
2875
+ clr2: string;
2876
+ }
2877
+
1884
2878
  declare interface Dxf {
1885
2879
  font: CellFont | null;
1886
2880
  fill: CellFill | null;
@@ -1892,6 +2886,20 @@ declare interface Dxf {
1892
2886
  numFmt?: NumFmt | null;
1893
2887
  }
1894
2888
 
2889
+ /** ECMA-376 §17.8.3.3-.6 — one embedded font-style slot from
2890
+ * `word/fontTable.xml`, resolved to its obfuscated part path + fontKey. */
2891
+ declare interface EmbeddedFontRef {
2892
+ fontName: string;
2893
+ style: 'regular' | 'bold' | 'italic' | 'boldItalic';
2894
+ partPath: string;
2895
+ fontKey: string;
2896
+ }
2897
+
2898
+ /** ECMA-376 §17.18.24 ST_Em — the emphasis-mark styles a run may carry via
2899
+ * `<w:em w:val>` (§17.3.2.12). `'none'` is filtered out by the parser, so the
2900
+ * model only ever carries one of these four positive marks (or `undefined`). */
2901
+ declare type EmphasisMark = 'dot' | 'comma' | 'circle' | 'underDot';
2902
+
1895
2903
  /**
1896
2904
  * An OMML equation embedded in a paragraph (ECMA-376 §22.1). Parsed into the
1897
2905
  * shared math AST and rendered by `@silurus/ooxml-core`'s math engine.
@@ -1930,6 +2938,10 @@ declare interface FieldRun {
1930
2938
  smallCaps?: boolean;
1931
2939
  doubleStrikethrough?: boolean;
1932
2940
  highlight?: string | null;
2941
+ /** ECMA-376 §17.3.2.12 `<w:em w:val>` — emphasis (boten / 圏点) mark, mirrors
2942
+ * {@link DocxTextRun.emphasisMark} (§17.18.24 ST_Em). Absent (or the
2943
+ * authored `val="none"`) ⇒ no mark. */
2944
+ emphasisMark?: EmphasisMark;
1933
2945
  }
1934
2946
 
1935
2947
  declare type Fill = SolidFill | NoFill | GradientFill | PatternFill | ImageFill;
@@ -1947,6 +2959,47 @@ declare interface FillRect {
1947
2959
  b?: number;
1948
2960
  }
1949
2961
 
2962
+ /**
2963
+ * IX2 public find-result shape, shared by all three viewers.
2964
+ *
2965
+ * `findText` returns an ordered list of {@link FindMatch}. Every match carries
2966
+ * its ordinal position (`matchIndex`, 0-based, document order — the same index
2967
+ * `findNext` / `findPrev` cycle through), the matched `text`, and a
2968
+ * format-specific `location`. The location is where the three formats
2969
+ * legitimately differ — a docx match lives on a page, a pptx match on a slide,
2970
+ * an xlsx match in a sheet cell — so `FindMatch` is generic over it rather than
2971
+ * forcing an artificial common shape. Each viewer instantiates it with its own
2972
+ * location type:
2973
+ *
2974
+ * - `DocxViewer.findText` → `FindMatch<DocxMatchLocation>` ({ page })
2975
+ * - `PptxViewer.findText` → `FindMatch<PptxMatchLocation>` ({ slide })
2976
+ * - `XlsxViewer.findText` → `FindMatch<XlsxMatchLocation>` ({ sheet, ref, … })
2977
+ *
2978
+ * The generic default is `unknown` so `FindMatch` can be referenced without a
2979
+ * type argument (e.g. in generic UI code) while each viewer's return type stays
2980
+ * precise.
2981
+ */
2982
+ declare interface FindMatch<Loc = unknown> {
2983
+ /** 0-based ordinal among all matches, in document order. This is the index
2984
+ * `findNext`/`findPrev` make active, so a caller can correlate the array it
2985
+ * got from `findText` with the active-match reported by navigation. */
2986
+ matchIndex: number;
2987
+ /** The text that matched (the query as it appears in the document — its
2988
+ * original case, not the folded form used for case-insensitive matching). */
2989
+ text: string;
2990
+ /** Where the match is, in the format's own coordinates. */
2991
+ location: Loc;
2992
+ }
2993
+
2994
+ /** Options for {@link findMatches}. */
2995
+ declare interface FindMatchesOptions {
2996
+ /**
2997
+ * Match case exactly. Default `false` (case-insensitive, like a browser's
2998
+ * find-in-page). IX2 default — an integrator can pass `true`.
2999
+ */
3000
+ caseSensitive?: boolean;
3001
+ }
3002
+
1950
3003
  /**
1951
3004
  * ECMA-376 §17.3.1.11 `<w:framePr>` — text-frame / drop-cap properties.
1952
3005
  *
@@ -2050,9 +3103,60 @@ declare type HiddenSlideMode = 'show' | 'skip' | 'dim';
2050
3103
  declare interface Hyperlink {
2051
3104
  col: number;
2052
3105
  row: number;
3106
+ /** External target (ECMA-376 §18.3.1.47 `r:id`, resolved via worksheet rels).
3107
+ * `null` for a purely internal hyperlink. */
2053
3108
  url: string | null;
3109
+ /** Internal target (§18.3.1.47 `location`): a defined name or a cell reference
3110
+ * such as `Sheet1!A1`. Present when the hyperlink navigates within the
3111
+ * workbook rather than to an external URL. */
3112
+ location?: string | null;
3113
+ /** Optional display text (§18.3.1.47 `display`). Not used for rendering. */
3114
+ display?: string | null;
2054
3115
  }
2055
3116
 
3117
+ /**
3118
+ * Shared hyperlink model + URL sanitisation for docx / pptx / xlsx (IX1).
3119
+ *
3120
+ * All three formats carry the same two ECMA-376 concepts:
3121
+ * - an **external** hyperlink — an absolute URL resolved from a relationship
3122
+ * part target (`document.xml.rels` for docx §17.16.22, the slide rels for
3123
+ * pptx §21.1.2.3.5, the worksheet rels for xlsx §18.3.1.47), with
3124
+ * `TargetMode="External"`.
3125
+ * - an **internal** hyperlink — a jump within the document itself:
3126
+ * docx `w:anchor` -> a `<w:bookmarkStart w:name>` (§17.16.23), pptx
3127
+ * `action="ppaction://hlinksldjump"` -> a slide, xlsx `location` -> a defined
3128
+ * name or a `Sheet!A1` cell reference.
3129
+ *
3130
+ * The parsers (Rust, one per format) do the format-specific rels lookup and hand
3131
+ * each run / shape / cell a {@link HyperlinkTarget}. Everything downstream — the
3132
+ * text-layer overlay, the viewer default click behaviour, and any integrator
3133
+ * callback — is format-agnostic and consumes this one shape. Keeping the type +
3134
+ * the pure `sanitizeHyperlinkUrl` predicate here (not duplicated per package)
3135
+ * follows the cross-package unification principle: a scheme-allowlist bug fixed
3136
+ * once is fixed everywhere.
3137
+ */
3138
+ /**
3139
+ * A resolved hyperlink attached to a run, shape, or cell.
3140
+ *
3141
+ * - `external` — `url` is the raw target as authored in the file. It is NOT
3142
+ * guaranteed safe; run it through {@link sanitizeHyperlinkUrl} before
3143
+ * navigating. It is kept verbatim here so an integrator can apply its own
3144
+ * policy (e.g. allow `file:` on a trusted intranet viewer).
3145
+ * - `internal` — `ref` is the in-document destination, verbatim from the file:
3146
+ * docx: the bookmark name (`w:anchor`).
3147
+ * pptx: the internal action (e.g. `ppaction://hlinksldjump`), with the
3148
+ * resolved 0-based `slideIndex` when the rels target names a slide.
3149
+ * xlsx: the `location` string (a defined name or `Sheet1!A1`).
3150
+ */
3151
+ declare type HyperlinkTarget = {
3152
+ kind: 'external';
3153
+ url: string;
3154
+ } | {
3155
+ kind: 'internal';
3156
+ ref: string;
3157
+ slideIndex?: number;
3158
+ };
3159
+
2056
3160
  /**
2057
3161
  * Image anchored to a rectangle of cells (EMU offsets within the anchor cells).
2058
3162
  * 914400 EMU = 1 inch, 9525 EMU = 1 px @ 96 DPI.
@@ -2101,6 +3205,15 @@ declare interface ImageAnchor {
2101
3205
  r: number;
2102
3206
  b: number;
2103
3207
  };
3208
+ /** ECMA-376 §20.1.8.6 `<a:alphaModFix@amt>` — the blip's overall opacity as a
3209
+ * fraction (0..1). Absent ⇒ opaque. The renderer sets `ctx.globalAlpha` so the
3210
+ * picture composites over the cells beneath it (e.g. a pink translucent photo
3211
+ * over a matching cell fill). */
3212
+ alpha?: number;
3213
+ /** ECMA-376 §20.1.8.23 `<a:duotone>` recolour effect. Absent (the common case)
3214
+ * ⇒ no effect. When present, the renderer remaps the image along the
3215
+ * `clr1`→`clr2` luminance ramp before drawing. */
3216
+ duotone?: Duotone_2;
2104
3217
  }
2105
3218
 
2106
3219
  /**
@@ -2191,6 +3304,19 @@ declare interface ImageRun {
2191
3304
  * transparency. Implements a:clrChange (make-background-transparent).
2192
3305
  */
2193
3306
  colorReplaceFrom?: string;
3307
+ /**
3308
+ * ECMA-376 §20.1.8.23 `<a:duotone>` recolour, resolved to its two endpoint
3309
+ * colours (through the document theme). Absent ⇒ no duotone. When present the
3310
+ * renderer decodes the raster once, remaps it along the `clr1`→`clr2`
3311
+ * luminance ramp, and caches the recoloured bitmap under a colour-suffixed key.
3312
+ */
3313
+ duotone?: Duotone;
3314
+ /**
3315
+ * ECMA-376 §20.1.8.6 `<a:alphaModFix@amt>` opacity as 0..1. Absent ⇒ fully
3316
+ * opaque. When present the renderer multiplies the picture's `globalAlpha` by
3317
+ * this fraction.
3318
+ */
3319
+ alpha?: number;
2194
3320
  /**
2195
3321
  * Wrap mode for anchor images:
2196
3322
  * "square" | "topAndBottom" | "none" | "tight" | "through"
@@ -2280,6 +3406,24 @@ declare interface LineEnd {
2280
3406
  len: string;
2281
3407
  }
2282
3408
 
3409
+ /** ECMA-376 §17.6.8 `<w:lnNumType>` — line numbering for a section. Mirrors the
3410
+ * Rust `LineNumbering`. A number is drawn in the left margin of each body line
3411
+ * whose count is a multiple of `countBy`. Absent on {@link SectionProps}
3412
+ * (`lineNumbering` undefined) ⇒ line numbering off. */
3413
+ declare interface LineNumbering {
3414
+ /** `@w:countBy` — only lines whose number is a multiple of this display a
3415
+ * number. Required (absent ⇒ the whole struct is absent per §17.6.8). */
3416
+ countBy: number;
3417
+ /** `@w:start` — the starting number after each restart. Default 1. */
3418
+ start: number;
3419
+ /** `@w:distance` in pt (twips ÷ 20) — gap between the text margin and the
3420
+ * number glyphs. Absent ⇒ implementation-defined (renderer uses a default). */
3421
+ distance?: number;
3422
+ /** `@w:restart` (§17.18.47): "newPage" (default) | "newSection" |
3423
+ * "continuous" — when the counter resets to `start`. */
3424
+ restart: string;
3425
+ }
3426
+
2283
3427
  declare interface LineSpacing {
2284
3428
  value: number;
2285
3429
  rule: 'auto' | 'exact' | 'atLeast';
@@ -2322,6 +3466,35 @@ declare interface LoadOptions_2 {
2322
3466
  * via `@font-face` in your application CSS.
2323
3467
  */
2324
3468
  useGoogleFonts?: boolean;
3469
+ /**
3470
+ * Password for an encrypted OOXML file ([MS-OFFCRYPTO] Agile Encryption).
3471
+ *
3472
+ * Password-protected Office documents are CFB (OLE2) containers, not ZIPs.
3473
+ * When this is set and the input is Agile-encrypted, `load()` decrypts it on
3474
+ * the main thread (via WebCrypto) and parses the recovered plaintext ZIP.
3475
+ *
3476
+ * Errors (thrown as {@link import('../errors/ooxml-error').OoxmlError}):
3477
+ * - no `password` on an encrypted file → code `'encrypted'`
3478
+ * - wrong `password` → code `'invalid-password'`
3479
+ * - a non-Agile scheme (Standard / Extensible / legacy) → code
3480
+ * `'unsupported-encryption'`
3481
+ *
3482
+ * Note: Agile Encryption uses a high password-hash spin count (commonly
3483
+ * 100,000), so decryption of a protected file adds roughly a second of
3484
+ * WebCrypto work before parsing begins.
3485
+ *
3486
+ * Security notes:
3487
+ * - This value is held as an ordinary JS `string` in memory for the
3488
+ * duration of key derivation. The library does not zero it, and does
3489
+ * not wrap it in a `SecureString`-equivalent — it becomes eligible for
3490
+ * garbage collection like any other string once nothing references it,
3491
+ * but no explicit wipe is performed. It is never logged or included in
3492
+ * thrown errors.
3493
+ * - Decryption recovers the plaintext but does not verify the file's HMAC
3494
+ * data-integrity tag ([MS-OFFCRYPTO] §2.3.4.14), so ciphertext tampering
3495
+ * is not detected — see "Security & Privacy" in the README.
3496
+ */
3497
+ password?: string;
2325
3498
  /**
2326
3499
  * Override the URL the parser worker fetches the WebAssembly module from.
2327
3500
  *
@@ -2403,6 +3576,23 @@ declare interface LoadOptions_4 extends LoadOptions_2 {
2403
3576
  /** @deprecated Use `ChartManualLayout` from @silurus/ooxml-core. */
2404
3577
  declare type ManualLayout = ChartManualLayout;
2405
3578
 
3579
+ /**
3580
+ * The slice of one run a match covers: the run's index in the original `runs[]`
3581
+ * and the `[start, end)` character range within that run's own `text`. A match
3582
+ * that straddles N runs yields N of these (the first sliced from its start
3583
+ * offset to the run end, the last from 0 to its end offset, any middle run
3584
+ * whole). The viewer measures each slice against that run's font to get a pixel
3585
+ * rectangle.
3586
+ */
3587
+ declare interface MatchRunSlice {
3588
+ /** Index into the original `runs[]` handed to {@link buildTextIndex}. */
3589
+ runIndex: number;
3590
+ /** Start offset within `runs[runIndex].text` (inclusive). */
3591
+ start: number;
3592
+ /** End offset within `runs[runIndex].text` (exclusive). */
3593
+ end: number;
3594
+ }
3595
+
2406
3596
  /** Accent (`m:acc`), e.g. hat, bar, vector arrow over the base. */
2407
3597
  declare interface MathAccent {
2408
3598
  kind: 'accent';
@@ -2425,6 +3615,31 @@ declare interface MathBar {
2425
3615
  base: MathNode[];
2426
3616
  }
2427
3617
 
3618
+ /** Border-box object (`m:borderBox`, §22.1.2.11): a border/strikes around the
3619
+ * base. Absent flags ⇒ a full rectangular box. */
3620
+ declare interface MathBorderBox {
3621
+ kind: 'borderBox';
3622
+ /** §22.1.2 hide* — when true the corresponding edge is NOT drawn. */
3623
+ hideTop?: boolean;
3624
+ hideBot?: boolean;
3625
+ hideLeft?: boolean;
3626
+ hideRight?: boolean;
3627
+ /** §22.1.2 strike* — strikeBLTR = bottom-left→top-right, strikeTLBR =
3628
+ * top-left→bottom-right diagonal. */
3629
+ strikeH?: boolean;
3630
+ strikeV?: boolean;
3631
+ strikeBltr?: boolean;
3632
+ strikeTlbr?: boolean;
3633
+ base: MathNode[];
3634
+ }
3635
+
3636
+ /** Box object (`m:box`, §22.1.2.13): a logical grouping (operator emulator /
3637
+ * line-break control). Draws NO border — a transparent group around `base`. */
3638
+ declare interface MathBox {
3639
+ kind: 'box';
3640
+ base: MathNode[];
3641
+ }
3642
+
2428
3643
  declare interface MathDelimiter {
2429
3644
  kind: 'delimiter';
2430
3645
  /** opening char (default '('). */
@@ -2482,7 +3697,23 @@ declare interface MathNary {
2482
3697
  body: MathNode[];
2483
3698
  }
2484
3699
 
2485
- declare type MathNode = MathRun | MathFraction | MathScript | MathNary | MathDelimiter | MathRadical | MathLimit | MathArray | MathGroupChr | MathBar | MathAccent | MathFunc | MathGroup;
3700
+ declare type MathNode = MathRun | MathFraction | MathScript | MathNary | MathDelimiter | MathRadical | MathLimit | MathArray | MathGroupChr | MathBar | MathAccent | MathFunc | MathGroup | MathPhant | MathSPre | MathBox | MathBorderBox;
3701
+
3702
+ /** Phantom object (`m:phant`, §22.1.2.81): contributes the spacing of `base`
3703
+ * while optionally hiding it and/or zeroing individual dimensions. */
3704
+ declare interface MathPhant {
3705
+ kind: 'phant';
3706
+ /** §22.1.2.96 `m:show` — `false` hides the base (invisible but occupies space,
3707
+ * i.e. `<mphantom>`); `true` (default) shows it and the phant only tweaks
3708
+ * spacing. */
3709
+ show: boolean;
3710
+ /** §22.1.2 zeroWid / zeroAsc / zeroDesc — suppress width / ascent / descent so
3711
+ * the base takes no space along that axis. Omitted ⇒ false. */
3712
+ zeroWid?: boolean;
3713
+ zeroAsc?: boolean;
3714
+ zeroDesc?: boolean;
3715
+ base: MathNode[];
3716
+ }
2486
3717
 
2487
3718
  declare interface MathRadical {
2488
3719
  kind: 'radical';
@@ -2525,6 +3756,15 @@ declare interface MathScript {
2525
3756
  sub?: MathNode[];
2526
3757
  }
2527
3758
 
3759
+ /** Pre-sub-superscript object (`m:sPre`, §22.1.2.99): sub + sup to the LEFT of
3760
+ * the base (e.g. ²₁A). */
3761
+ declare interface MathSPre {
3762
+ kind: 'sPre';
3763
+ sub: MathNode[];
3764
+ sup: MathNode[];
3765
+ base: MathNode[];
3766
+ }
3767
+
2528
3768
  declare type MathStyle = 'roman' | 'italic' | 'bold' | 'boldItalic';
2529
3769
 
2530
3770
  declare interface MathSvg {
@@ -2623,6 +3863,124 @@ declare interface NumFmt {
2623
3863
  formatCode: string;
2624
3864
  }
2625
3865
 
3866
+ /**
3867
+ * Typed error thrown by the docx / pptx / xlsx `load()` factories for failures
3868
+ * that carry a stable, programmatic {@link OoxmlErrorCode} (e.g. a
3869
+ * password-protected or legacy-binary file detected from its container magic).
3870
+ *
3871
+ * Note on workers: `instanceof OoxmlError` does not survive a structured-clone
3872
+ * across the worker boundary. Detection that needs a typed error is therefore
3873
+ * done on the main thread (before the worker is involved) so a genuine
3874
+ * `OoxmlError` instance is thrown to the caller. Errors that must cross the
3875
+ * worker boundary should carry the `code` string and be reconstructed on the
3876
+ * main side.
3877
+ */
3878
+ declare class OoxmlError extends Error {
3879
+ readonly code: OoxmlErrorCode;
3880
+ constructor(code: OoxmlErrorCode, message: string);
3881
+ }
3882
+
3883
+ /**
3884
+ * Machine-readable code for a typed load-time failure.
3885
+ *
3886
+ * The container-level failures the `load()` factories detect on the main thread
3887
+ * before handing bytes to the parser worker (see `sniffCfb` / `decryptOoxml`).
3888
+ * This is the seed of the broader typed-error surface tracked as PD4 (OoxmlError
3889
+ * typed errors). Add codes here rather than throwing bare `Error(string)`, so
3890
+ * callers can `switch` on `err.code` instead of matching message text.
3891
+ *
3892
+ * - `'encrypted'` — password-protected, but no `password` was
3893
+ * supplied (pass `LoadOptions.password` to decrypt).
3894
+ * - `'invalid-password'` — a `password` was supplied but did not match.
3895
+ * - `'unsupported-encryption'`— encrypted with a scheme other than Agile
3896
+ * (Standard / Extensible / a legacy binary encryptor), which this library
3897
+ * cannot decrypt (PD8 implements Agile only).
3898
+ * - `'legacy-binary-format'` — a raw .doc / .xls / .ppt (not OOXML).
3899
+ * - `'not-ooxml'` — a CFB of an unrecognised kind, or otherwise
3900
+ * not an OOXML ZIP.
3901
+ */
3902
+ declare type OoxmlErrorCode = 'encrypted' | 'invalid-password' | 'unsupported-encryption' | 'legacy-binary-format' | 'not-ooxml';
3903
+
3904
+ /**
3905
+ * The default action a viewer takes for an **external** hyperlink click when
3906
+ * the integrator supplies no `onHyperlinkClick` handler: sanitise the URL and,
3907
+ * if allowed, open it in a new tab with `noopener,noreferrer` so the opened page
3908
+ * gets no `window.opener` handle back into this document. A blocked scheme is a
3909
+ * silent no-op (returns `false`) — the click does nothing rather than navigate
3910
+ * somewhere dangerous.
3911
+ *
3912
+ * Internal targets are intentionally NOT handled here: the in-document jump
3913
+ * (page / slide / cell) is format-specific and lives in each viewer.
3914
+ *
3915
+ * Split out (not inlined in three viewers) so the "open in new tab, drop opener,
3916
+ * refuse unsafe schemes" policy is defined once. `win` is injected for tests;
3917
+ * defaults to the ambient `window`.
3918
+ *
3919
+ * @returns `true` if navigation was initiated, `false` if the URL was blocked.
3920
+ */
3921
+ declare function openExternalHyperlink(url: string, allowed?: readonly string[], win?: Pick<Window, 'open'> | undefined): boolean;
3922
+
3923
+ /** `<sheetPr><outlinePr>` flags (ECMA-376 §18.3.1.61). Both default to `true`. */
3924
+ declare interface OutlinePr {
3925
+ /** `true` (default) ⇒ a group's summary row sits *below* its detail rows;
3926
+ * `false` ⇒ above. */
3927
+ summaryBelow: boolean;
3928
+ /** `true` (default) ⇒ a group's summary column sits to the *right* of its
3929
+ * detail columns; `false` ⇒ to the left. */
3930
+ summaryRight: boolean;
3931
+ }
3932
+
3933
+ /** ECMA-376 §17.18.4 CT_Border for one edge of `<w:pgBorders>`. Mirrors the Rust
3934
+ * `PageBorderEdge`. Same shape as a paragraph border edge. */
3935
+ declare interface PageBorderEdge {
3936
+ /** `@w:val` — ST_Border line style ("single" | "double" | "dashed" | …). */
3937
+ style: string;
3938
+ /** `@w:color` hex 6, or absent for "auto" (renderer defaults to black). */
3939
+ color?: string;
3940
+ /** `@w:sz` in pt (eighths of a point ÷ 8). */
3941
+ width: number;
3942
+ /** `@w:space` in pt — a POINT measure (§17.18.68, 0–31) for page borders, NOT
3943
+ * twips — the inset from the `offsetFrom` reference. */
3944
+ space: number;
3945
+ }
3946
+
3947
+ /** ECMA-376 §17.6.10 `<w:pgBorders>` — page borders drawn around each page of a
3948
+ * section. Mirrors the Rust `PageBorders`. Each edge is a CT_Border (§17.18.4);
3949
+ * the container carries the placement globals. Absent on {@link SectionProps}
3950
+ * (`pageBorders` undefined) ⇒ no page border (the common case). Art borders
3951
+ * (§17.18.2 decorative-image styles) are unsupported — the renderer draws only
3952
+ * the standard line styles (single/double/dashed/dotted/thick/…). */
3953
+ declare interface PageBorders {
3954
+ /** `@w:offsetFrom` (§17.18.63): "page" ⇒ each edge's `space` is from the PAGE
3955
+ * edge; "text" (the default) ⇒ from the text margin. */
3956
+ offsetFrom: string;
3957
+ /** `@w:display` (§17.18.62): "allPages" (default) | "firstPage" |
3958
+ * "notFirstPage" — which physical pages of the section show the border. */
3959
+ display: string;
3960
+ /** `@w:zOrder` (§17.18.64): "front" (default; over text) | "back" (under). */
3961
+ zOrder: string;
3962
+ top?: PageBorderEdge;
3963
+ bottom?: PageBorderEdge;
3964
+ left?: PageBorderEdge;
3965
+ right?: PageBorderEdge;
3966
+ }
3967
+
3968
+ /** ECMA-376 §17.6.12 `<w:pgNumType>` — a section's page-numbering settings.
3969
+ * Mirrors the Rust `PageNumType`. Only the two attributes that change the
3970
+ * DISPLAYED page number are carried:
3971
+ * - `start` — the number shown on the FIRST page of the section (§17.6.12);
3972
+ * absent ⇒ numbering continues from the previous section's highest number.
3973
+ * Kept as a possibly-zero / possibly-negative integer (Word writes `start="0"`).
3974
+ * - `fmt` — the ST_NumberFormat (§17.18.59) for the section's page numbers
3975
+ * (decimal / upperRoman / lowerLetter / …); absent ⇒ decimal.
3976
+ * `chapStyle`/`chapSep` (chapter-prefixed numbering) are out of scope for this
3977
+ * pass and never surfaced. Field names match the Rust `PageNumType` serialization
3978
+ * (`start`, `fmt`). */
3979
+ declare interface PageNumType {
3980
+ start?: number;
3981
+ fmt?: string;
3982
+ }
3983
+
2626
3984
  declare interface ParaBorderEdge {
2627
3985
  style: string;
2628
3986
  color: string | null;
@@ -2808,6 +4166,38 @@ declare interface PatternFill {
2808
4166
  preset: string;
2809
4167
  }
2810
4168
 
4169
+ /** ECMA-376 §18.18.56 ST_PhoneticAlignment — how the furigana is aligned over
4170
+ * the base text. Absent on {@link PhoneticProperties} defaults to `'left'`. */
4171
+ declare type PhoneticAlignment = 'left' | 'center' | 'distributed' | 'noControl';
4172
+
4173
+ /** ECMA-376 §18.4.3 `<phoneticPr>` — phonetic display properties. */
4174
+ declare interface PhoneticProperties {
4175
+ /** Zero-based index into `Styles.fonts` (§18.18.32 ST_FontId). Out of bounds
4176
+ * falls back to font 0 (§18.4.3). Drives the furigana font size / family. */
4177
+ fontId: number;
4178
+ /** §18.18.57 — absent means `'fullwidthKatakana'` (schema default). */
4179
+ type?: PhoneticType;
4180
+ /** §18.18.56 — absent means `'left'` (schema default). */
4181
+ alignment?: PhoneticAlignment;
4182
+ }
4183
+
4184
+ /** ECMA-376 §18.4.6 `<rPh sb=".." eb="..">` — one furigana run. `sb`/`eb` are
4185
+ * zero-based character offsets into the base text; the hint `text` is shown
4186
+ * over base characters `[sb, eb)`. */
4187
+ declare interface PhoneticRun {
4188
+ /** Zero-based start character offset into the base text (inclusive). */
4189
+ sb: number;
4190
+ /** Zero-based end character offset into the base text (exclusive). */
4191
+ eb: number;
4192
+ /** The phonetic hint text (e.g. the katakana reading). */
4193
+ text: string;
4194
+ }
4195
+
4196
+ /** ECMA-376 §18.18.57 ST_PhoneticType — the East-Asian character set the
4197
+ * furigana is displayed in. Absent on {@link PhoneticProperties} defaults to
4198
+ * `'fullwidthKatakana'` per the CT_PhoneticPr schema. */
4199
+ declare type PhoneticType = 'fullwidthKatakana' | 'halfwidthKatakana' | 'Hiragana' | 'noConversion';
4200
+
2811
4201
  declare interface PictureElement {
2812
4202
  type: 'picture';
2813
4203
  x: number;
@@ -2883,6 +4273,13 @@ declare interface PictureElement {
2883
4273
  };
2884
4274
  /** a:blip > a:alphaModFix@amt as 0..1. Undefined = fully opaque. */
2885
4275
  alpha?: number;
4276
+ /**
4277
+ * ECMA-376 §20.1.8.23 `<a:duotone>` recolour, resolved to its two endpoint
4278
+ * colours (through the slide theme). Undefined ⇒ no duotone. When present the
4279
+ * renderer decodes the raster once, remaps it along the `clr1`→`clr2`
4280
+ * luminance ramp, and caches the recoloured bitmap under a colour-suffixed key.
4281
+ */
4282
+ duotone?: Duotone;
2886
4283
  /**
2887
4284
  * `<p:spPr><a:custGeom>` clipping path. Same `PathCmd` model as
2888
4285
  * `ShapeElement.custGeom` (one entry per `<a:path>`; coords normalized
@@ -2930,9 +4327,19 @@ export declare namespace pptx {
2930
4327
  PptxTextRunInfo,
2931
4328
  TextRunCallback,
2932
4329
  buildPptxTextLayer,
4330
+ buildPptxHighlightLayer,
4331
+ PptxHighlightMatch,
4332
+ PptxHighlightColors,
4333
+ PptxMatchLocation,
4334
+ FindMatch,
4335
+ FindMatchesOptions,
2933
4336
  PresentationHandle,
2934
4337
  autoResize,
2935
4338
  AutoResizeOptions,
4339
+ HyperlinkTarget,
4340
+ openExternalHyperlink,
4341
+ OoxmlError,
4342
+ OoxmlErrorCode,
2936
4343
  Presentation,
2937
4344
  Slide,
2938
4345
  PptxComment,
@@ -2991,6 +4398,21 @@ declare interface PptxComment {
2991
4398
  text: string;
2992
4399
  }
2993
4400
 
4401
+ declare interface PptxHighlightColors {
4402
+ match?: string;
4403
+ active?: string;
4404
+ }
4405
+
4406
+ declare interface PptxHighlightMatch {
4407
+ slices: MatchRunSlice[];
4408
+ active: boolean;
4409
+ }
4410
+
4411
+ /** Where a pptx match lives: its 0-based slide index. */
4412
+ declare interface PptxMatchLocation {
4413
+ slide: number;
4414
+ }
4415
+
2994
4416
  /**
2995
4417
  * Headless PPTX rendering engine.
2996
4418
  *
@@ -3012,15 +4434,24 @@ declare class PptxPresentation {
3012
4434
  private _mode;
3013
4435
  private _presentation;
3014
4436
  private _meta;
4437
+ /** Lazily-built `partName → slide index` map for internal hyperlink slide
4438
+ * jumps (IX-nav). Cleared on {@link destroy}; built on first
4439
+ * {@link getSlideIndexByPartName}/{@link resolveInternalTarget} from either
4440
+ * the parsed slides (main) or the worker meta's `partNames` (worker). */
4441
+ private _slidePartIndex;
3015
4442
  private _mediaCache;
3016
4443
  private _imageCache;
4444
+ /** Google-Fonts `FontFace` objects this deck preloaded into `document.fonts`
4445
+ * (main mode only — in worker mode the worker owns them and terminates with
4446
+ * its own FontFaceSet). Released in {@link destroy} so they do not leak into
4447
+ * the shared FontFaceSet for the lifetime of the SPA (deduped + refcounted in
4448
+ * core, so a web font shared with another open deck survives until both go). */
4449
+ private _googleFontFaces;
3017
4450
  /** One stable closure per instance: the decoded-bitmap and SVG caches key on
3018
4451
  * this identity to scope decodes per deck (so two open decks never swap
3019
4452
  * images for a shared zip path like ppt/media/image1.png). Reusing the same
3020
4453
  * reference across every render also lets those caches hit across slides. */
3021
4454
  private readonly _fetchImage;
3022
- private _workerReady;
3023
- private _workerReadyCallbacks;
3024
4455
  /** Opt-in OMML equation engine, injected once at {@link load}. Every
3025
4456
  * `renderSlide` / `presentSlide` reuses it — equations render when present,
3026
4457
  * and are skipped (engine tree-shaken) when omitted. */
@@ -3028,7 +4459,6 @@ declare class PptxPresentation {
3028
4459
  private constructor();
3029
4460
  /** Parse a PPTX from URL or ArrayBuffer. */
3030
4461
  static load(source: string | ArrayBuffer, opts?: LoadOptions): Promise<PptxPresentation>;
3031
- private _waitForWorker;
3032
4462
  private _parse;
3033
4463
  /** Total number of slides in the loaded presentation. */
3034
4464
  get slideCount(): number;
@@ -3069,6 +4499,44 @@ declare class PptxPresentation {
3069
4499
  * caller's policy (see {@link PptxViewer}'s `hiddenSlideMode` modes).
3070
4500
  */
3071
4501
  isHidden(slideIndex: number): boolean;
4502
+ /** The per-slide `partName` array (`sldIdLst` order) from either the parsed
4503
+ * model (main) or the worker meta (worker). Backs the lazy part-index map. */
4504
+ private _partNames;
4505
+ /** Lazily build (and cache) the `partName → index` map. Nulled by
4506
+ * {@link destroy} so a reused reference never serves a stale deck's indices. */
4507
+ private _partIndex;
4508
+ /**
4509
+ * Resolve a slide's OPC part name (e.g. `ppt/slides/slide3.xml`) to its
4510
+ * 0-based index in `sldIdLst` order, or `undefined` when no slide has that
4511
+ * part name. This is the map an internal hyperlink slide jump
4512
+ * (`<a:hlinkClick action="ppaction://hlinksldjump" r:id>`, ECMA-376
4513
+ * §21.1.2.3.5) resolves against: the click's rel Target names a slide part, and
4514
+ * this turns it into the index a viewer can navigate to. Works in both `main`
4515
+ * and `worker` mode (the part names ride along in the worker meta).
4516
+ */
4517
+ getSlideIndexByPartName(partName: string): number | undefined;
4518
+ /**
4519
+ * Resolve an internal hyperlink target string to a 0-based slide index, or
4520
+ * `undefined` when it names no reachable slide. Handles both
4521
+ * `<a:hlinkClick @action>` classes (§21.1.2.3.5):
4522
+ *
4523
+ * - a **relative** show jump — `ppaction://hlinkshowjump?jump=firstslide |
4524
+ * lastslide | nextslide | previousslide` — resolved arithmetically from
4525
+ * `currentIndex` (clamped at the deck ends);
4526
+ * - a **specific** slide-part jump — `ppaction://hlinksldjump`, whose
4527
+ * resolved target is a slide-rel part name like `../slides/slide3.xml` —
4528
+ * resolved through {@link getSlideIndexByPartName}.
4529
+ *
4530
+ * `ref` is the internal reference a `HyperlinkTarget` of kind `'internal'`
4531
+ * carries: the raw `ppaction://…` action string for a relative jump, or the
4532
+ * resolved slide-part target string for a specific jump. A viewer's
4533
+ * `onHyperlinkClick` default calls this with `ref` and the current slide, then
4534
+ * navigates to the returned index.
4535
+ *
4536
+ * @param ref the internal action/target string.
4537
+ * @param currentIndex the 0-based slide the jump is relative to (default 0).
4538
+ */
4539
+ resolveInternalTarget(ref: string, currentIndex?: number): number | undefined;
3072
4540
  /** Render a slide onto the given canvas. */
3073
4541
  renderSlide(canvas: HTMLCanvasElement | OffscreenCanvas, slideIndex: number, opts?: RenderSlideOptions): Promise<void>;
3074
4542
  /**
@@ -3081,6 +4549,16 @@ declare class PptxPresentation {
3081
4549
  * when done, or its backing memory is held until GC.
3082
4550
  */
3083
4551
  renderSlideToBitmap(slideIndex: number, opts?: RenderSlideToBitmapOptions): Promise<ImageBitmap>;
4552
+ /**
4553
+ * IX6 — collect a slide's text-run geometry (`PptxTextRunInfo[]`) without
4554
+ * painting a visible canvas. Works in BOTH modes: worker mode renders the
4555
+ * slide off-thread and ships only the runs (no bitmap transfer); main mode
4556
+ * renders to a throwaway offscreen canvas. Used by the find controller to scan
4557
+ * every slide for matches. Run geometry is in CSS px (independent of dpr) and
4558
+ * dimming does not move glyphs, so only `width` is threaded — matching the
4559
+ * historical main-mode `_collectSlideRuns`.
4560
+ */
4561
+ collectSlideRuns(slideIndex: number, width?: number): Promise<PptxTextRunInfo[]>;
3084
4562
  /**
3085
4563
  * Extract raw media bytes for a zip path referenced by {@link MediaElement}.
3086
4564
  * Results are cached by path for the lifetime of this instance.
@@ -3095,6 +4573,23 @@ declare class PptxPresentation {
3095
4573
  * decoded lazily rather than inlined as base64 at parse time.
3096
4574
  */
3097
4575
  getImage(imagePath: string, mimeType: string): Promise<Blob>;
4576
+ /**
4577
+ * Project the presentation to GitHub-flavoured markdown: title slides become
4578
+ * `#` headings, body shapes become nested bullets at each paragraph's `lvl`,
4579
+ * tables become pipe tables, charts become summarised bullets, and speaker
4580
+ * notes and comments are collated. Positioning, animations, images, and
4581
+ * drawing detail are discarded — the projection is meant for AI ingestion and
4582
+ * full-text search, not layout.
4583
+ *
4584
+ * Runs entirely in the worker off the archive opened at {@link load} (no
4585
+ * re-copy of the file, no re-parse of the model on the main thread), so it
4586
+ * works in BOTH `mode: 'main'` and `mode: 'worker'`.
4587
+ *
4588
+ * @example
4589
+ * const pres = await PptxPresentation.load(buffer);
4590
+ * const md = await pres.toMarkdown();
4591
+ */
4592
+ toMarkdown(): Promise<string>;
3098
4593
  /**
3099
4594
  * Render a slide and attach canvas-native playback controls for any
3100
4595
  * embedded audio/video. Returns a {@link PresentationHandle} that owns the
@@ -3106,7 +4601,7 @@ declare class PptxPresentation {
3106
4601
  destroy(): void;
3107
4602
  }
3108
4603
 
3109
- declare class PptxScrollViewer {
4604
+ declare class PptxScrollViewer implements ZoomableViewer {
3110
4605
  private _pres;
3111
4606
  private readonly _injected;
3112
4607
  private readonly _opts;
@@ -3130,6 +4625,18 @@ declare class PptxScrollViewer {
3130
4625
  * than a `_scale === 1` sentinel because a fit scale of exactly 1 is a valid
3131
4626
  * established state (a 1× fit would otherwise be re-fit forever). */
3132
4627
  private _scaleEstablished;
4628
+ /**
4629
+ * IX9 F1 — a `setScale` factor requested BEFORE the base fit is established
4630
+ * (pre-load, or a zero-width container), already clamped to
4631
+ * `[zoomMin, zoomMax]`, or `null` when none is pending. The single-canvas
4632
+ * viewers latch a pre-load `setScale` and honour it on the first render; the
4633
+ * scroll viewers used to silently DROP it — the family-unified semantics are
4634
+ * "latch and apply once the layout establishes". `relayout()` applies (and
4635
+ * clears) this right after establishing the base, firing `onScaleChange` at
4636
+ * application time; `getScale()` reports it while pending so the caller sees
4637
+ * the same value a single-canvas viewer would show.
4638
+ */
4639
+ private _pendingScale;
3133
4640
  /** Live slots keyed by slide index. */
3134
4641
  private readonly _slots;
3135
4642
  /** Recyclable detached slots (canvas + textLayer reused across slides). */
@@ -3144,6 +4651,23 @@ declare class PptxScrollViewer {
3144
4651
  * reporting an error so a rejection that lands after teardown is swallowed
3145
4652
  * rather than surfaced to a `onError` on a dead viewer. */
3146
4653
  private _destroyed;
4654
+ /**
4655
+ * Concurrent-load latch (generation token). Every self-loading `load()`
4656
+ * increments this and captures the value; after its engine finishes loading it
4657
+ * re-checks the live value and BAILS (destroying its own just-loaded engine) if
4658
+ * a newer `load()` has since started. Without it, two overlapping
4659
+ * `load(A)`/`load(B)` calls race the WASM parse / worker init, and whichever
4660
+ * RESOLVES last wins the swap — even the stale `load(A)` resolving after
4661
+ * `load(B)`; the loser's freshly created engine (never installed, or installed
4662
+ * then overwritten) then leaks its worker + pinned WASM allocation. The latch
4663
+ * composes with SC20: the check runs AFTER the new engine loads but BEFORE the
4664
+ * field assignment, `previous?.destroy()`, and the recycle/relayout post-load
4665
+ * work, so a superseded load never touches `this._pres` nor frees the current
4666
+ * (newer) engine. Only the self-loading path uses it — the injected path throws
4667
+ * up-front and never reaches here. `destroy()` also bumps it so a load in flight
4668
+ * at teardown is treated as superseded and its engine cleaned up.
4669
+ */
4670
+ private _loadGen;
3147
4671
  /** Worker mode: slide indices whose bitmap render is currently dispatched to the
3148
4672
  * engine. Coalesces a scroll storm — we never dispatch a second render for a
3149
4673
  * slide whose first is still in flight — and lets us drop slides that scrolled
@@ -3174,11 +4698,14 @@ declare class PptxScrollViewer {
3174
4698
  * is host-agnostic. */
3175
4699
  private _settleTimer;
3176
4700
  private _wheelListener;
3177
- /** One-shot latch for the worker-mode text-selection warning. The overlay is a
3178
- * main-mode-only feature: in worker mode the per-run `onTextRun` geometry
3179
- * cannot cross the worker boundary, so an `enableTextSelection` overlay stays
3180
- * empty. We warn once (parity with `PptxViewer`) rather than per slot. */
3181
- private _warnedNoTextSelection;
4701
+ /** Gesture-only pointer anchor for the NEXT `setScale`, in scrollHost-viewport
4702
+ * px (`{ x, y }` from the wheel event, relative to the scroll host's top-left).
4703
+ * Set by the Ctrl/⌘+wheel handler right before it calls `setScale` so the zoom
4704
+ * pivots on the cursor ("zoom toward the pointer") in BOTH axes; consumed and
4705
+ * cleared by `setScale`. `null` for every non-gesture source (the public
4706
+ * `setScale`, the +/- steppers, `fitWidth`/`fitPage`, the resize re-fit), which
4707
+ * keep the historical viewport-TOP re-anchor so their behaviour is unchanged. */
4708
+ private _pendingZoomAnchor;
3182
4709
  /** Observes the container so a width change re-fits the base scale. Disconnected
3183
4710
  * in `destroy()`. */
3184
4711
  private _resizeObserver;
@@ -3254,6 +4781,12 @@ declare class PptxScrollViewer {
3254
4781
  * `_positionSlot` (the flush-left floor), and by `_syncSpacerWidth` (the spacer
3255
4782
  * width). Resolved here (not stored) to mirror `_gap()`/`_pad()`. */
3256
4783
  private _padH;
4784
+ /** Index of the slide whose slot spans content-offset `y` (largest `i` with
4785
+ * `offsets[i] <= y`), for the pointer-anchored zoom re-anchor. Mirrors the
4786
+ * `topIndex` search `computeVisibleRange` runs for the scrollTop, but for an
4787
+ * ARBITRARY content-y (the pointer, not the viewport top). Clamped into
4788
+ * `[0, n-1]`; a `y` below the first slide (inside the leading pad) yields 0. */
4789
+ private _slideIndexAtOffset;
3257
4790
  private _range;
3258
4791
  private _syncSpacer;
3259
4792
  /** Horizontal scroll extent: the (uniform deck-wide) slide width plus both
@@ -3302,10 +4835,6 @@ declare class PptxScrollViewer {
3302
4835
  * can pass for an old-epoch resolution). We gate them on the captured epoch.
3303
4836
  */
3304
4837
  private _renderSlot;
3305
- /** Warn once when an `enableTextSelection` overlay was requested but the render
3306
- * mode is `worker` (so the overlay stays empty). Same wording as
3307
- * `PptxViewer` — one warning per viewer, not per slot. */
3308
- private _maybeWarnNoTextSelection;
3309
4838
  /** Route an async render failure to `onError`, or `console.error` when none is
3310
4839
  * set (so failures are never fully silent), and never after teardown. */
3311
4840
  private _reportRenderError;
@@ -3341,7 +4870,10 @@ declare class PptxScrollViewer {
3341
4870
  * `[zoomMin ?? 0.1, zoomMax ?? 4]` (absolute bounds, XlsxViewer convention — NOT
3342
4871
  * multiples of the base fit; design §3 keeps the clamp in the viewer, not core),
3343
4872
  * then re-anchor VERTICALLY so the slide currently under the viewport top stays
3344
- * fixed. A no-op when nothing is loaded or when the clamped scale is unchanged.
4873
+ * fixed. A no-op when the clamped scale is unchanged. Called BEFORE the deck is
4874
+ * loaded / the base fit is established, the clamped factor is LATCHED (IX9 F1,
4875
+ * family-unified with the single-canvas viewers) and applied by `relayout()`
4876
+ * once the layout establishes — `onScaleChange` fires then.
3345
4877
  *
3346
4878
  * FLICKER-FREE (design §7): this does NOT re-render the visible slides inline.
3347
4879
  * It shows an immediate CSS preview (stretch the existing bitmaps, scale the
@@ -3362,6 +4894,39 @@ declare class PptxScrollViewer {
3362
4894
  * can no longer return below the floor to the original base fit through this API.
3363
4895
  */
3364
4896
  setScale(scale: number): void;
4897
+ /** IX9 {@link ZoomableViewer} — the current zoom factor, where `1` = 100% (a
4898
+ * slide at its natural EMU→px size). This is the viewer's absolute `_scale`
4899
+ * (`slideWidth/EMU_PER_PX × _scale` is the drawn width), so it reads `1` at
4900
+ * true 100% and, after the initial fit-to-width, the base fit factor. Before
4901
+ * the fit is established it reports a latched pre-load `setScale` (IX9 F1) if
4902
+ * one is pending — matching what a single-canvas viewer would show — else `1`. */
4903
+ getScale(): number;
4904
+ /** IX9 {@link ZoomableViewer} — step up to the next rung of the shared zoom
4905
+ * ladder above the current factor (clamped to `zoomMax` by {@link setScale}). */
4906
+ zoomIn(): void;
4907
+ /** IX9 {@link ZoomableViewer} — step down to the next lower ladder rung. */
4908
+ zoomOut(): void;
4909
+ /**
4910
+ * IX9 {@link ZoomableViewer} — fit a slide's WIDTH to the container (the classic
4911
+ * continuous-scroll "fit width"). Sets the scale to the width-fit base for the
4912
+ * current container, then re-anchors + re-renders via {@link setScale}. Defers
4913
+ * (no-op) while the container is unlaid-out. The `zoomMin`/`zoomMax` clamp still
4914
+ * applies, so a fit below `zoomMin` pins to `zoomMin`.
4915
+ */
4916
+ fitWidth(): void;
4917
+ /**
4918
+ * IX9 {@link ZoomableViewer} — fit a WHOLE slide (width and height) inside the
4919
+ * container so one slide is visible without scrolling; takes the tighter of the
4920
+ * width/height fit. Uses the deck-wide (uniform) slide size. Defers while
4921
+ * unlaid-out.
4922
+ */
4923
+ fitPage(): void;
4924
+ /** Shared fit for {@link fitWidth}/{@link fitPage}: the width-fit factor is the
4925
+ * established base (`_baseScale`); the page-fit additionally bounds by the
4926
+ * container height against the (uniform) slide height. Applies via
4927
+ * {@link setScale} so the flicker-free re-anchor / settle path and
4928
+ * `onScaleChange` all run. */
4929
+ private _fit;
3365
4930
  /**
3366
4931
  * CSS preview of the visible window at the current `_scale` (design §7
3367
4932
  * mechanism 1), WITHOUT re-rendering. Slots leaving the window recycle normally;
@@ -3388,12 +4953,11 @@ declare class PptxScrollViewer {
3388
4953
  private _scheduleSettle;
3389
4954
  /** Full-resolution settle re-render of the visible window (design §7 mechanisms
3390
4955
  * 2+3). Re-renders each mounted slot at the current scale via the double-buffer
3391
- * swap (main) / same-canvas transfer (worker). Main mode also rebuilds the text
3392
- * overlay and clears its preview transform; in worker mode the overlay is
3393
- * permanently empty (text selection is main-mode-only), so the transform is
3394
- * inert there and is reset on recycle. Dispatched at the CURRENT epoch; the
3395
- * existing epoch gate discards it if a later `setScale` supersedes it
3396
- * mid-render. */
4956
+ * swap (main) / same-canvas transfer (worker). Both modes rebuild the text
4957
+ * overlay from the fresh render's run geometry (IX6 — worker mode collects the
4958
+ * runs off-thread via `_renderSlotBitmap`) and clear the preview transform.
4959
+ * Dispatched at the CURRENT epoch; the existing epoch gate discards it if a
4960
+ * later `setScale` supersedes it mid-render. */
3397
4961
  private _settleRender;
3398
4962
  /**
3399
4963
  * Settle-render one slot at the current scale (design §7 mechanism 3).
@@ -3435,6 +4999,22 @@ declare class PptxScrollViewer {
3435
4999
  scrollToSlide(index: number, opts?: {
3436
5000
  behavior?: 'auto' | 'smooth';
3437
5001
  }): void;
5002
+ /**
5003
+ * IX1 hyperlink click dispatch (mirrors {@link PptxViewer._onHyperlinkClick}).
5004
+ * When the integrator supplies `opts.onHyperlinkClick` it OWNS the click (no
5005
+ * default). Otherwise: an external link opens in a new tab via the shared,
5006
+ * scheme-sanitised {@link openExternalHyperlink}; an internal slide jump scrolls
5007
+ * to the target slide via {@link scrollToSlide} once the action resolves to a
5008
+ * slide index (a jump resolving to no reachable slide is a safe no-op).
5009
+ */
5010
+ private _onHyperlinkClick;
5011
+ /** Populate an internal {@link HyperlinkTarget}'s `slideIndex` from its `ref`
5012
+ * via the engine's stamped part names. Relative `hlinkshowjump` verbs are
5013
+ * resolved against the slide currently at the viewport top
5014
+ * (`_range().topIndex`); a `../slides/slideN.xml` part target resolves through
5015
+ * the part-name map. An already-set index, an external target, and an
5016
+ * unresolvable ref all pass through unchanged (safe no-op). */
5017
+ private _resolveInternalSlideIndex;
3438
5018
  /**
3439
5019
  * Re-fit the base scale on a container resize while PRESERVING the current zoom
3440
5020
  * multiplier (design §11), then re-anchor + re-render. A `ResizeObserver` fires
@@ -3468,6 +5048,8 @@ declare class PptxScrollViewer {
3468
5048
  /* Excluded from this release type: baseScaleForTest */
3469
5049
  /* Excluded from this release type: renderEpochForTest */
3470
5050
  /* Excluded from this release type: resizeForTest */
5051
+ /* Excluded from this release type: contentAtViewportYForTest */
5052
+ /* Excluded from this release type: viewportYOfForTest */
3471
5053
  /**
3472
5054
  * Tear down the viewer: remove the DOM subtree and (only for a self-loaded
3473
5055
  * engine) destroy the engine. An injected engine is left intact — the caller
@@ -3520,9 +5102,10 @@ declare interface PptxScrollViewerOptions extends Omit<RenderSlideOptions, 'onTe
3520
5102
  paddingRight?: number;
3521
5103
  /** Slides kept mounted beyond the viewport on each side. Default 1. */
3522
5104
  overscan?: number;
3523
- /** Per-slide transparent text-selection overlay. MAIN render mode only:
3524
- * in worker mode `onTextRun` cannot cross the worker boundary, so the overlay
3525
- * stays empty and the viewer logs one warning (design §11). */
5105
+ /** Per-slide transparent text-selection overlay. IX6 works in BOTH render
5106
+ * modes: in worker mode the per-run geometry is collected off-thread and
5107
+ * shipped back beside the slide bitmap, so the overlay is populated identically
5108
+ * to main mode (no more empty overlay / one-time warning). */
3526
5109
  enableTextSelection?: boolean;
3527
5110
  /** Minimum zoom scale — a DIMENSIONLESS multiplier over the 96-dpi natural
3528
5111
  * slide size (10% = 0.1), matching `DocxScrollViewer`. Default 0.1. */
@@ -3566,6 +5149,12 @@ declare interface PptxScrollViewerOptions extends Omit<RenderSlideOptions, 'onTe
3566
5149
  * `computeVisibleRange` (the first slide intersecting the viewport top,
3567
5150
  * EXCLUDING overscan). */
3568
5151
  onVisibleSlideChange?: (topIndex: number, total: number) => void;
5152
+ /** IX9 — fires whenever the zoom factor actually changes (`1` = 100% = a slide
5153
+ * at its natural EMU→px size): from {@link PptxScrollViewer.setScale},
5154
+ * `zoomIn`/`zoomOut`, `fitWidth`/`fitPage`, a Ctrl/⌘+wheel gesture, or a
5155
+ * container-resize re-fit. Named `onScaleChange` to match the single-canvas
5156
+ * viewers so all five share one notification shape. */
5157
+ onScaleChange?: (scale: number) => void;
3569
5158
  /** Error callback. When set, `load()` invokes it and resolves (otherwise the
3570
5159
  * error is rethrown — shared viewer error contract). It ALSO fires for async
3571
5160
  * per-slot render failures (both main `renderSlide` and worker
@@ -3573,6 +5162,17 @@ declare interface PptxScrollViewerOptions extends Omit<RenderSlideOptions, 'onTe
3573
5162
  * crashing the loop. Without an `onError`, render failures are logged via
3574
5163
  * `console.error` so they are never fully silent. */
3575
5164
  onError?: (err: Error) => void;
5165
+ /**
5166
+ * IX1 (design decision — NOT user-confirmed, integrator may veto). Fires on a
5167
+ * hyperlink click in any mounted slide's text overlay (requires
5168
+ * {@link enableTextSelection}). Default when omitted: external →
5169
+ * {@link openExternalHyperlink} (new tab, sanitised, noopener); internal
5170
+ * slide-jump → {@link scrollToSlide} once the action resolves to a slide index
5171
+ * via {@link PptxPresentation.resolveInternalTarget} (a jump that resolves to
5172
+ * no reachable slide is a safe no-op). When provided, the viewer calls this
5173
+ * instead and takes NO default action.
5174
+ */
5175
+ onHyperlinkClick?: (target: HyperlinkTarget) => void;
3576
5176
  }
3577
5177
 
3578
5178
  /** Information about a rendered text segment for building a transparent selection overlay. */
@@ -3605,6 +5205,14 @@ declare interface PptxTextRunInfo {
3605
5205
  * `vert="vert270"` → -90). The CSS overlay must add this to `rotation`.
3606
5206
  */
3607
5207
  textBodyRotation?: number;
5208
+ /**
5209
+ * Resolved hyperlink target for this run (IX1), classified into the shared
5210
+ * {@link HyperlinkTarget} shape. Present only for runs whose `<a:rPr>` carried
5211
+ * an `<a:hlinkClick>`; the overlay makes such spans clickable. The glyph
5212
+ * drawing (colour + underline) is unaffected — this is metadata for the
5213
+ * transparent overlay only.
5214
+ */
5215
+ hyperlink?: HyperlinkTarget;
3608
5216
  }
3609
5217
 
3610
5218
  /**
@@ -3617,9 +5225,18 @@ declare interface PptxTextRunInfo {
3617
5225
  *
3618
5226
  * For custom layouts (multi-canvas, thumbnails, scroll view) use PptxPresentation directly.
3619
5227
  */
3620
- declare class PptxViewer {
5228
+ declare class PptxViewer implements ZoomableViewer {
3621
5229
  private readonly canvas;
3622
5230
  private readonly wrapper;
5231
+ /**
5232
+ * IX9 explicit zoom factor (`1` = 100% = the slide at its natural EMU→px
5233
+ * width), or `null` when the caller has never invoked a zoom method. `null`
5234
+ * preserves the pre-IX9 render path EXACTLY: the slide renders at `opts.width`
5235
+ * (or `canvas.offsetWidth || 960` when unset), so default rendering is
5236
+ * byte-identical. The first zoom call latches a number here, after which
5237
+ * {@link _targetWidth} derives the render width from it.
5238
+ */
5239
+ private _scale;
3623
5240
  /** The canvas's DOM position BEFORE the constructor reparented it into
3624
5241
  * {@link wrapper}, captured so {@link destroy} can return the caller-owned
3625
5242
  * canvas to exactly where it was. `null` parent = canvas was passed
@@ -3630,6 +5247,13 @@ declare class PptxViewer {
3630
5247
  * (empty string if it was unset), restored on {@link destroy}. */
3631
5248
  private readonly _originalDisplay;
3632
5249
  private textLayer;
5250
+ /** IX2 — the find-highlight overlay layer (always created, above the text
5251
+ * layer, `pointer-events:none`). */
5252
+ private highlightLayer;
5253
+ /** IX2 — find state (per-slide runs, matches, active cursor). */
5254
+ private _find;
5255
+ /** Private 2d context for measuring highlight text (own 1×1 canvas). */
5256
+ private _measureCtx;
3633
5257
  private engine;
3634
5258
  private readonly opts;
3635
5259
  private currentSlide;
@@ -3640,14 +5264,39 @@ declare class PptxViewer {
3640
5264
  * render path. The media-playback path keeps a 2d context (via presentSlide),
3641
5265
  * so this is obtained only when worker mode renders without media playback. */
3642
5266
  private _bitmapCtx;
3643
- private _warnedNoTextSelection;
5267
+ /** Set by {@link destroy} (first line). Guards {@link _reportRenderError} so a
5268
+ * render rejection that lands AFTER teardown is swallowed rather than surfaced
5269
+ * to an `onError` / `console.error` on a dead viewer — parity with the scroll
5270
+ * viewers' `_destroyed` flag. */
5271
+ private _destroyed;
5272
+ /**
5273
+ * Concurrent-load latch (generation token). Every {@link load} increments this
5274
+ * and captures the value; after its engine finishes loading it re-checks the
5275
+ * live value and BAILS (destroying its own just-loaded engine) if a newer
5276
+ * `load()` has since started. Without it, two overlapping `load(A)`/`load(B)`
5277
+ * calls race the WASM parse / worker init, and whichever RESOLVES last wins the
5278
+ * swap — even the stale `load(A)` resolving after `load(B)`; the loser's freshly
5279
+ * created engine (never installed, or installed then overwritten) then leaks its
5280
+ * worker + pinned WASM allocation. The latch composes with SC20: the check runs
5281
+ * AFTER the new engine loads but BEFORE the field assignment and
5282
+ * `previous?.destroy()`, so a superseded load never touches `this.engine` nor
5283
+ * frees the current (newer) engine. {@link destroy} also bumps it so a load in
5284
+ * flight at teardown is treated as superseded and its engine cleaned up.
5285
+ */
5286
+ private _loadGen;
3644
5287
  constructor(canvas: HTMLCanvasElement, opts?: PptxViewerOptions);
3645
5288
  /**
3646
5289
  * Load a PPTX from URL or ArrayBuffer and render the first slide.
3647
5290
  *
3648
- * Error contract (shared by all three viewers): on failure, if an `onError`
3649
- * callback was provided it is invoked and `load` resolves normally; if not,
3650
- * the error is rethrown so it is never silently swallowed.
5291
+ * Error contract (shared by all three viewers):
5292
+ * - Parse/load failure (the underlying `PptxPresentation.load()` call itself
5293
+ * rejects): if an `onError` callback was provided it is invoked and `load`
5294
+ * resolves normally; if not, the error is rethrown so it is never silently
5295
+ * swallowed.
5296
+ * - Render failure (the first slide fails to draw AFTER a successful
5297
+ * parse/load): routed to the shared `_reportRenderError` contract (`onError`
5298
+ * if provided, else `console.error` — never silent) and `load` still
5299
+ * RESOLVES, matching every subsequent navigation call.
3651
5300
  */
3652
5301
  load(source: string | ArrayBuffer): Promise<void>;
3653
5302
  /** Navigate to a specific slide (0-indexed). */
@@ -3680,8 +5329,115 @@ declare class PptxViewer {
3680
5329
  getNotes(slideIndex: number): string | null;
3681
5330
  /** The underlying <canvas> element. */
3682
5331
  get canvasElement(): HTMLCanvasElement;
5332
+ /** Natural (100%) CSS-px width of a slide — `slideWidth(EMU) / EMU_PER_PX`.
5333
+ * 0 when nothing is loaded. The scale-1 reference every zoom factor
5334
+ * multiplies. */
5335
+ private _naturalWidthPx;
5336
+ /**
5337
+ * The width (CSS px) the render paths draw the slide at, honouring the zoom
5338
+ * state. `_scale === null` (no zoom method ever called) ⇒ the pre-IX9 value
5339
+ * `opts.width ?? (canvas.offsetWidth || 960)` verbatim (byte-identical
5340
+ * default). Once a factor latched ⇒ `naturalWidth × scale` (rounded), so the
5341
+ * slide is exactly `scale ×` its natural size regardless of `opts.width`.
5342
+ */
5343
+ private _targetWidth;
5344
+ /** IX9 {@link ZoomableViewer} — the current zoom factor (`1` = 100%). Before
5345
+ * any zoom method is called this is the EFFECTIVE scale implied by the render
5346
+ * width: `targetWidth / naturalWidth`, or `1` when nothing is loaded. */
5347
+ getScale(): number;
5348
+ private _zoomMin;
5349
+ private _zoomMax;
5350
+ /**
5351
+ * IX9 {@link ZoomableViewer} — set the absolute zoom factor (`1` = 100% = the
5352
+ * slide at its natural EMU→px width), clamped to `[zoomMin, zoomMax]`, and
5353
+ * re-render the current slide at the new size. Fires `onScaleChange` when the
5354
+ * clamped factor actually changes. Resolves once the re-render settles.
5355
+ */
5356
+ setScale(scale: number): Promise<void>;
5357
+ /** IX9 {@link ZoomableViewer} — step up to the next rung of the shared zoom
5358
+ * ladder (clamped to `zoomMax`). */
5359
+ zoomIn(): Promise<void>;
5360
+ /** IX9 {@link ZoomableViewer} — step down to the next lower ladder rung. */
5361
+ zoomOut(): Promise<void>;
5362
+ /**
5363
+ * IX9 {@link ZoomableViewer} — fit the current slide's WIDTH to the host
5364
+ * container (the element the canvas lives in), then re-render. Defers (no-op)
5365
+ * when nothing is loaded or the container is unlaid-out. Routes through
5366
+ * {@link setScale}.
5367
+ */
5368
+ fitWidth(): Promise<void>;
5369
+ /**
5370
+ * IX9 {@link ZoomableViewer} — fit the WHOLE current slide (width and height)
5371
+ * inside the container so it is fully visible; takes the tighter of the
5372
+ * width/height fit. Defers when unloaded / unlaid-out.
5373
+ */
5374
+ fitPage(): Promise<void>;
5375
+ /** Shared fit for {@link fitWidth}/{@link fitPage}: measure the natural slide
5376
+ * size + the container box, ask core's pure `fitScale`, apply via setScale. */
5377
+ private _fit;
3683
5378
  private renderCurrentSlide;
5379
+ /** Draw the find-highlight boxes for the current slide from its runs. */
5380
+ private _buildHighlightLayer;
5381
+ /** A width-measurer primed with `font`, backed by a private 1×1 canvas. */
5382
+ private _measureForFont;
5383
+ /** IX6 — collect a slide's runs for search without touching the visible
5384
+ * canvas. Delegates to `collectSlideRuns`, which works in BOTH modes (worker:
5385
+ * off-thread, ships only the runs; main: throwaway offscreen canvas). Used for
5386
+ * slides other than the one on screen. */
5387
+ private _collectSlideRuns;
5388
+ /**
5389
+ * IX2 — find every occurrence of `query` across all slides and highlight them
5390
+ * (a soft box per match on the highlight overlay). Returns every match in
5391
+ * document order, each tagged with its `{ slide }` (0-based). Case-insensitive
5392
+ * by default; pass `{ caseSensitive: true }` for an exact match.
5393
+ *
5394
+ * Scans all slides (each rendered once offscreen to read its text; the visible
5395
+ * slide reuses its on-screen render). IX6 — works in BOTH `mode: 'main'` and
5396
+ * `mode: 'worker'`: in worker mode each slide's run geometry is collected
5397
+ * off-thread and shipped back, so find returns the same matches on the same
5398
+ * code path. An empty query clears the find.
5399
+ */
5400
+ findText(query: string, opts?: FindMatchesOptions): Promise<FindMatch<PptxMatchLocation>[]>;
5401
+ /**
5402
+ * IX2 — move to the next match (wrap-around), navigating to its slide if
5403
+ * needed, and draw it in the active-match colour. Returns the now-active
5404
+ * match, or `null` when there are none. Call {@link findText} first.
5405
+ */
5406
+ findNext(): Promise<FindMatch<PptxMatchLocation> | null>;
5407
+ /** IX2 — move to the previous match (wrap-around). */
5408
+ findPrev(): Promise<FindMatch<PptxMatchLocation> | null>;
5409
+ /** IX2 — clear all highlights and reset the find state. */
5410
+ clearFind(): void;
5411
+ private _activateMatch;
5412
+ /** Rebuild the highlight overlay for the current slide from cached runs. */
5413
+ private _redrawHighlights;
3684
5414
  private _buildTextLayer;
5415
+ /**
5416
+ * IX1/IX-nav hyperlink click dispatch. An internal target is first *enriched*
5417
+ * with its resolved 0-based `slideIndex` (via
5418
+ * {@link PptxPresentation.resolveInternalTarget}, relative to the current
5419
+ * slide) so a jump verb / slide-part ref arrives already mapped — this is the
5420
+ * field that was previously always `undefined`. When the integrator supplies
5421
+ * `opts.onHyperlinkClick` it OWNS the (enriched) click and takes NO default
5422
+ * action. Otherwise the viewer's default policy applies: an external link
5423
+ * opens in a new tab via the shared, scheme-sanitised
5424
+ * {@link openExternalHyperlink}; an internal slide jump navigates via
5425
+ * {@link goToSlide} to the resolved index (a target that resolves to no
5426
+ * reachable slide is a safe no-op).
5427
+ */
5428
+ private _onHyperlinkClick;
5429
+ /** Populate an internal {@link HyperlinkTarget}'s `slideIndex` from its `ref`
5430
+ * (a `ppaction://hlinkshowjump?jump=…` verb resolved relative to the current
5431
+ * slide, or a `../slides/slideN.xml` part target resolved through the stamped
5432
+ * part-name map — no filename-suffix heuristic). Any already-set `slideIndex`
5433
+ * is kept; an external target and an unresolvable ref pass through unchanged so
5434
+ * the caller no-ops safely. */
5435
+ private _resolveInternalSlideIndex;
5436
+ /** PD14 render-error contract: route a render failure to `onError`, or
5437
+ * `console.error` when none is given (never fully silent), and never after
5438
+ * teardown. Mirrors the scroll viewers' `_reportRenderError` so all three
5439
+ * single-canvas viewers agree. */
5440
+ private _reportRenderError;
3685
5441
  /**
3686
5442
  * Clean up the viewer and terminate the background worker.
3687
5443
  *
@@ -3699,6 +5455,17 @@ declare interface PptxViewerOptions extends RenderOptions, LoadOptions {
3699
5455
  onSlideChange?: (index: number, total: number) => void;
3700
5456
  /** Called on parse or render errors */
3701
5457
  onError?: (err: Error) => void;
5458
+ /** IX9 zoom contract ({@link ZoomableViewer}) — the clamp range for
5459
+ * {@link PptxViewer.setScale} / `zoomIn` / `zoomOut` / `fitWidth` / `fitPage`,
5460
+ * as user-facing zoom factors (`1` = 100% = the slide at its natural
5461
+ * EMU→px size). Defaults 0.1–4 (10%–400%), matching the other viewers. */
5462
+ zoomMin?: number;
5463
+ zoomMax?: number;
5464
+ /** IX9 — fires whenever the zoom factor actually changes (`1` = 100%): from
5465
+ * {@link PptxViewer.setScale}, `zoomIn`/`zoomOut`, or `fitWidth`/`fitPage`.
5466
+ * Named `onScaleChange` to match the docx/xlsx viewers so all five share one
5467
+ * notification shape. */
5468
+ onScaleChange?: (scale: number) => void;
3702
5469
  /**
3703
5470
  * Enable interactive audio/video playback. When true, slides are rendered
3704
5471
  * via {@link PptxPresentation.presentSlide} so media elements become
@@ -3730,6 +5497,17 @@ declare interface PptxViewerOptions extends RenderOptions, LoadOptions {
3730
5497
  * in sync if {@link DimOptions} gains a field.
3731
5498
  */
3732
5499
  hiddenSlideDim?: Partial<DimOptions>;
5500
+ /**
5501
+ * IX1 (design decision — NOT user-confirmed, integrator may veto). Fires on a
5502
+ * hyperlink click (a text run whose `<a:rPr>` carried an `<a:hlinkClick>`;
5503
+ * requires {@link enableTextSelection} so the overlay spans exist). Default
5504
+ * when omitted: external → {@link openExternalHyperlink} (new tab, sanitised,
5505
+ * noopener); internal slide-jump → {@link goToSlide} once the action resolves
5506
+ * to a slide index via {@link PptxPresentation.resolveInternalTarget} (a jump
5507
+ * that resolves to no reachable slide is a safe no-op). When provided, the
5508
+ * viewer calls this instead and takes NO default action.
5509
+ */
5510
+ onHyperlinkClick?: (target: HyperlinkTarget) => void;
3733
5511
  }
3734
5512
 
3735
5513
  declare interface Presentation {
@@ -3760,6 +5538,22 @@ declare interface PresentationHandle {
3760
5538
  destroy(): void;
3761
5539
  }
3762
5540
 
5541
+ /** ECMA-376 §17.3.3.23 `<w:ptab>` — an absolute-position tab. Advances to a
5542
+ * position derived from {@link PTabRun.alignment} and {@link PTabRun.relativeTo},
5543
+ * independent of the paragraph's custom tab stops / default-tab interval. */
5544
+ declare interface PTabRun {
5545
+ /** ST_PTabAlignment (§17.18.71): where on the line the tab lands, and how the
5546
+ * following text aligns to it. */
5547
+ alignment: 'left' | 'center' | 'right';
5548
+ /** ST_PTabRelativeTo (§17.18.73): the base the position is measured from —
5549
+ * the text margins or the paragraph indents. */
5550
+ relativeTo: 'margin' | 'indent';
5551
+ /** ST_PTabLeader (§17.18.72): the character repeated to fill the tab gap. */
5552
+ leader: 'none' | 'dot' | 'hyphen' | 'underscore' | 'middleDot';
5553
+ /** Resolved run font size (pt) — matches the surrounding text's leader/gap. */
5554
+ fontSize: number;
5555
+ }
5556
+
3763
5557
  /** ECMA-376 §20.1.8.27 (CT_ReflectionEffect) — mirrored copy below the
3764
5558
  * shape with a linear alpha gradient. Carries the spec attributes whose
3765
5559
  * defaults the renderer needs to interpret correctly. */
@@ -3821,7 +5615,10 @@ declare interface RenderPageOptions {
3821
5615
  width?: number;
3822
5616
  dpr?: number;
3823
5617
  defaultTextColor?: string;
3824
- /** Called for each rendered text segment. Used to build a transparent text selection overlay. */
5618
+ /** Called for each rendered text segment. Used to build a transparent text
5619
+ * selection overlay. On a vertical (§17.6.20 tbRl) page `x`/`y` are the
5620
+ * PHYSICAL top-left and `transform` is the CSS rotation the overlay span
5621
+ * applies about its top-left; absent for horizontal pages. */
3825
5622
  onTextRun?: (run: {
3826
5623
  text: string;
3827
5624
  x: number;
@@ -3830,12 +5627,27 @@ declare interface RenderPageOptions {
3830
5627
  h: number;
3831
5628
  fontSize: number;
3832
5629
  font: string;
5630
+ transform?: string;
3833
5631
  }) => void;
3834
5632
  /** Default `true`. When false, ECMA-376 §17.13.5 track-changes runs render
3835
5633
  * in their normal style (no author colour, no underline / strikethrough)
3836
5634
  * — equivalent to Word's "Final / No Markup" view. */
3837
5635
  showTrackChanges?: boolean;
3838
- }
5636
+ /** ECMA-376 §17.16.5.16 DATE / §17.16.5.72 TIME — the "current" instant a
5637
+ * DATE/TIME field formats through its `\@` date picture (§17.16.4.1). A `Date`
5638
+ * or epoch-ms number. Default = the real current time at render. Set a fixed
5639
+ * value for deterministic / reproducible DATE/TIME field output. */
5640
+ currentDate?: Date | number;
5641
+ }
5642
+
5643
+ /** IX6 — options for {@link DocxDocument.renderPageToBitmap}: the serializable
5644
+ * render knobs plus an OPTIONAL `onTextRun`. The callback stays main-thread (it
5645
+ * never crosses the wire); in worker mode the proxy invokes it with the runs
5646
+ * the worker shipped back beside the bitmap, so a caller gets the selection /
5647
+ * find geometry on the same path in both modes. */
5648
+ declare type RenderPageToBitmapOptions = WireRenderPageOptions & {
5649
+ onTextRun?: (run: DocxTextRunInfo) => void;
5650
+ };
3839
5651
 
3840
5652
  /**
3841
5653
  * Render a single slide onto a <canvas> element.
@@ -3870,6 +5682,14 @@ declare interface RenderSlideToBitmapOptions {
3870
5682
  /* Excluded from this release type: skipMediaControls */
3871
5683
  /** Translucent overlay drawn over the finished slide (hidden-slide dimming). */
3872
5684
  dim?: DimOptions;
5685
+ /**
5686
+ * IX6 — receives the slide's text-run geometry (the same stream `renderSlide`
5687
+ * emits in main mode). Stays main-thread (never crosses the wire); in worker
5688
+ * mode the proxy invokes it with the runs the worker shipped back beside the
5689
+ * bitmap, so a caller builds the selection / find overlay on the SAME code
5690
+ * path in both modes.
5691
+ */
5692
+ onTextRun?: TextRunCallback;
3873
5693
  }
3874
5694
 
3875
5695
  declare interface RenderViewportOptions {
@@ -3925,6 +5745,20 @@ declare type ResolvedList = {
3925
5745
  formula: string;
3926
5746
  };
3927
5747
 
5748
+ /**
5749
+ * Resolve every `{ type: 'shared', si }` cell in `ws` to a concrete
5750
+ * `{ type: 'text', text, runs? }` by looking `si` up in the workbook
5751
+ * `sharedStrings` table (ECMA-376 §18.4.8). Mutates cells in place and returns
5752
+ * `ws` for chaining. Out-of-range / missing `si` resolves to empty text —
5753
+ * matching the parser's historical fallback. Idempotent: a `Worksheet` with no
5754
+ * `shared` cells is returned unchanged.
5755
+ *
5756
+ * This keeps the dedup win on the wire (each shared string ships ONCE in the
5757
+ * workbook) while every downstream consumer — renderer, formula engine, number
5758
+ * formatter, markdown — still sees fully-resolved cell text.
5759
+ */
5760
+ declare function resolveSharedStrings(ws: Worksheet, sharedStrings: SharedString[]): Worksheet;
5761
+
3928
5762
  /**
3929
5763
  * 3D rotation in sphere coordinates — ECMA-376 §20.1.5.11 (`CT_SphereCoords`).
3930
5764
  * Angles are in **degrees** (the XML carries 60000ths of a degree; the parser
@@ -3944,6 +5778,16 @@ declare interface Row {
3944
5778
  index: number;
3945
5779
  height: number | null;
3946
5780
  cells: Cell[];
5781
+ /** Outline (grouping) depth 0-7 (ECMA-376 §18.3.1.73 `<row outlineLevel>`).
5782
+ * Omitted on the wire when `0` (ungrouped); read as `outlineLevel ?? 0`. */
5783
+ outlineLevel?: number;
5784
+ /** `<row collapsed>` (§18.3.1.73): `true` on a summary row whose
5785
+ * one-level-deeper detail rows are collapsed. Omitted when false. */
5786
+ collapsed?: boolean;
5787
+ /** `<row hidden>` (§18.3.1.73): `true` when the row is hidden — most often
5788
+ * because a collapsed outline hides its detail rows. Distinct from
5789
+ * `height === 0`. Omitted on the wire when false. */
5790
+ hidden?: boolean;
3947
5791
  }
3948
5792
 
3949
5793
  declare interface RubyAnnotation {
@@ -4025,6 +5869,13 @@ declare interface SecondaryValueAxis {
4025
5869
  lineHidden: boolean;
4026
5870
  /** `<c:majorTickMark>` — "cross" (default) | "out" | "in" | "none". */
4027
5871
  majorTickMark: string;
5872
+ /**
5873
+ * `<c:valAx><c:majorUnit val>` (§21.2.2.103) — explicit distance between
5874
+ * major ticks/gridlines on THIS secondary axis, overriding the Excel-style
5875
+ * auto "nice" step. null/undefined ⇒ auto step (byte-stable). Symmetric with
5876
+ * {@link ChartModel.valAxisMajorUnit} on the primary axis.
5877
+ */
5878
+ majorUnit?: number | null;
4028
5879
  /** `<c:title>` run-prop font size (hpt). */
4029
5880
  titleFontSizeHpt?: number | null;
4030
5881
  /** `<c:title>` run-prop bold flag. */
@@ -4077,6 +5928,17 @@ declare interface SectionProps {
4077
5928
  * spec default). Non-final sections carry their start type on their own
4078
5929
  * SectionBreak marker. */
4079
5930
  sectionStart?: string | null;
5931
+ /** ECMA-376 §17.6.20 `<w:textDirection w:val>` — the section's flow direction,
5932
+ * using the TRANSITIONAL ST_TextDirection enum Word writes (Part 4 §14.11.7:
5933
+ * `lrTb`|`tbRl`|`btLr`|`lrTbV`|`tbLrV`|`tbRlV`), NOT the Part 1 §17.18.93
5934
+ * Strict set. Absent / `null` ⇒ "lrTb" (horizontal, left→right / top→bottom,
5935
+ * the default). `"tbRl"` = vertical Japanese (glyphs stack top→bottom, lines
5936
+ * advance right→left); the renderer (see `isVerticalSection`) lays the page out
5937
+ * horizontally and rotates it +90° at paint for the vertical values
5938
+ * (`tbRl`/`tbRlV`/`tbLrV`), keeping CJK glyphs upright and Latin sideways. Only
5939
+ * a non-default value is emitted by the parser, so horizontal documents keep
5940
+ * byte-identical rendering. */
5941
+ textDirection?: string | null;
4080
5942
  /** ECMA-376 §17.6.5 w:docGrid/@w:type — "default" | "lines" | "linesAndChars" | "snapToChars". */
4081
5943
  docGridType?: string | null;
4082
5944
  /** ECMA-376 §17.6.5 w:docGrid/@w:linePitch in pt. When docGridType is "lines" or
@@ -4095,6 +5957,23 @@ declare interface SectionProps {
4095
5957
  * body text flows top-to-bottom through `count` columns (newspaper fill);
4096
5958
  * see {@link computeColumns}. */
4097
5959
  columns?: ColumnsSpec | null;
5960
+ /** ECMA-376 §17.6.12 `<w:pgNumType>` — the body (final) section's page-numbering
5961
+ * settings (start / fmt). `null`/absent ⇒ numbering continues; decimal. The
5962
+ * renderer resolves the displayed page number per physical page from this plus
5963
+ * the per-section `SectionBreak.pageNumType` markers. */
5964
+ pageNumType?: PageNumType | null;
5965
+ /** ECMA-376 §17.6.10 `<w:pgBorders>` — page borders for this section.
5966
+ * `null`/absent ⇒ no page border (the common case). */
5967
+ pageBorders?: PageBorders | null;
5968
+ /** ECMA-376 §17.6.8 `<w:lnNumType>` — line numbering for this section.
5969
+ * `null`/absent ⇒ line numbering off. */
5970
+ lineNumbering?: LineNumbering | null;
5971
+ /** ECMA-376 §17.6.23 `<w:vAlign w:val>` — body vertical alignment between the
5972
+ * top/bottom margins ("top" | "center" | "both" | "bottom"). `null`/absent ⇒
5973
+ * "top" (body flows from the top margin unchanged). "both" (vertical
5974
+ * justification) is parsed but rendered as "top" until distribution is
5975
+ * implemented (see renderer note). */
5976
+ vAlign?: string | null;
4098
5977
  }
4099
5978
 
4100
5979
  declare type SelectionMode_2 = 'cells' | 'rows' | 'cols' | 'all';
@@ -4188,6 +6067,15 @@ declare interface ShapeElement {
4188
6067
  /** `<a:sp3d>` 3D shape properties (ECMA-376 §20.1.5.12). Parsed but not
4189
6068
  * rendered in Phase A. */
4190
6069
  sp3d?: Sp3d;
6070
+ /** Shape-level hyperlink target resolved from `<p:cNvPr><a:hlinkClick @r:id>`
6071
+ * via slide _rels (ECMA-376 §21.1.2.3.5). For an external link this is the
6072
+ * URL; for an internal slide jump it is the resolved internal part name.
6073
+ * Undefined when the shape carries no hlinkClick. */
6074
+ hyperlink?: string;
6075
+ /** Raw `<a:hlinkClick @action>` (e.g. `"ppaction://hlinksldjump"`) when the
6076
+ * shape link is an internal PowerPoint action rather than an external URL.
6077
+ * Undefined when absent. */
6078
+ hyperlinkAction?: string;
4191
6079
  }
4192
6080
 
4193
6081
  declare type ShapeFill = {
@@ -4238,6 +6126,12 @@ declare type ShapeGeom = {
4238
6126
  r: number;
4239
6127
  b: number;
4240
6128
  };
6129
+ /** ECMA-376 §20.1.8.6 `<a:alphaModFix@amt>` opacity fraction (0..1) on the
6130
+ * leaf pic. Absent ⇒ opaque. Applied via `globalAlpha`. */
6131
+ alpha?: number;
6132
+ /** ECMA-376 §20.1.8.23 `<a:duotone>` recolour on the leaf pic. Absent ⇒
6133
+ * no effect. */
6134
+ duotone?: Duotone_2;
4241
6135
  };
4242
6136
 
4243
6137
  declare interface ShapeInfo {
@@ -4360,6 +6254,15 @@ declare interface ShapeRun {
4360
6254
  wrapSide?: string | null;
4361
6255
  /** Text rendered INSIDE the shape's bounding box (`<wps:txbx><w:txbxContent>`). */
4362
6256
  textBlocks?: ShapeText_2[];
6257
+ /** ECMA-376 §20.1.4.1.17 `<wps:style><a:fontRef>` — the shape's DEFAULT text
6258
+ * color (hex, no `#`). A text-box run ({@link ShapeTextRun}) with no explicit
6259
+ * {@link ShapeTextRun.color} inherits this before falling back to the
6260
+ * document/theme default (black); an explicit run color still wins. This is
6261
+ * the color axis of the fontRef only — the `@idx` (major/minor/none) font-face
6262
+ * selection is out of scope (fonts resolve via rFonts/docDefaults). Mirrors
6263
+ * pptx's per-shape default text color from the placeholder fontRef. Absent ⇒
6264
+ * no shape default (the run color or black applies). */
6265
+ defaultTextColor?: string | null;
4363
6266
  /** "t" | "ctr" | "b" — vertical anchor for the shape's text body (`<wps:bodyPr @anchor>`). */
4364
6267
  textAnchor?: string | null;
4365
6268
  /** ECMA-376 §21.1.2.1.1 auto-fit mode from `<wps:bodyPr>`, normalized to the
@@ -4372,6 +6275,16 @@ declare interface ShapeRun {
4372
6275
  textInsetT?: number;
4373
6276
  textInsetR?: number;
4374
6277
  textInsetB?: number;
6278
+ /** ECMA-376 Part 4 §19.1.2.23 `<v:textpath>` — WordArt text laid on the
6279
+ * shape path (a text watermark). When set the renderer draws this string,
6280
+ * scaled to fill the box (`fitshape`), rotated by {@link ShapeRun.rotation},
6281
+ * filled with {@link ShapeRun.fill} at {@link ShapeRun.fillOpacity} alpha —
6282
+ * INSTEAD of a fill/stroke panel + body text. */
6283
+ textPath?: TextPath | null;
6284
+ /** ECMA-376 Part 4 §19.1.2.5 `<v:fill opacity>` — fill alpha in `[0, 1]`
6285
+ * (default 1 = opaque). Used with {@link ShapeRun.textPath} to draw the
6286
+ * watermark semi-transparently. Absent ⇒ opaque. */
6287
+ fillOpacity?: number | null;
4375
6288
  }
4376
6289
 
4377
6290
  declare interface ShapeText {
@@ -4530,6 +6443,12 @@ declare interface ShapeTextRun_2 {
4530
6443
  declare interface SharedString {
4531
6444
  text: string;
4532
6445
  runs?: Run[];
6446
+ /** ECMA-376 §18.4.6 phonetic runs (furigana). Absent when the `<si>` has no
6447
+ * `<rPh>`. */
6448
+ phoneticRuns?: PhoneticRun[];
6449
+ /** ECMA-376 §18.4.3 phonetic display properties. Absent when the `<si>` has
6450
+ * no `<phoneticPr>`. */
6451
+ phoneticPr?: PhoneticProperties;
4533
6452
  }
4534
6453
 
4535
6454
  declare interface SheetMeta {
@@ -4572,6 +6491,17 @@ declare interface Slide {
4572
6491
  index: number;
4573
6492
  /** 1-based slide number (index + 1); used to render slidenum fields */
4574
6493
  slideNumber: number;
6494
+ /**
6495
+ * The slide's normalized OPC part name (e.g. `ppt/slides/slide3.xml`),
6496
+ * resolved through `presentation.xml.rels` in `sldIdLst` order (ECMA-376
6497
+ * §19.3.1.42). An internal hyperlink slide jump
6498
+ * (`<a:hlinkClick action="ppaction://hlinksldjump" r:id>`, §21.1.2.3.5)
6499
+ * carries a rel Target that resolves to this same part name — so
6500
+ * {@link PptxPresentation.getSlideIndexByPartName} can turn a click into a
6501
+ * slide index. Absent (`undefined`) only for a slide whose part path was not
6502
+ * recorded; healthy and broken slides both carry it.
6503
+ */
6504
+ partName?: string;
4575
6505
  background: Fill | null;
4576
6506
  elements: SlideElement[];
4577
6507
  /**
@@ -4595,6 +6525,14 @@ declare interface Slide {
4595
6525
  * slide modes (read it via `PptxPresentation.isHidden`).
4596
6526
  */
4597
6527
  hidden?: boolean;
6528
+ /**
6529
+ * RB7 partial degradation: set when this slide's part could not be parsed. The
6530
+ * deck still opens with the OTHER slides intact; this one is a placeholder
6531
+ * (`elements` empty) whose `parseError` names the offending part (e.g.
6532
+ * `"ppt/slides/slide3.xml: <detail>"`). Absent (`undefined`) for every healthy
6533
+ * slide. The renderer paints a visible error box instead of slide content.
6534
+ */
6535
+ parseError?: string;
4598
6536
  }
4599
6537
 
4600
6538
  declare type SlideElement = ShapeElement | PictureElement | TableElement | ChartElement | MediaElement;
@@ -4886,6 +6824,19 @@ declare interface TextBody extends TextBody_2 {
4886
6824
  * omitted from JSON when false. Only meaningful when `numCol > 1`.
4887
6825
  */
4888
6826
  rtlCol?: boolean;
6827
+ /**
6828
+ * `<a:bodyPr><a:prstTxWarp>` (ECMA-376 §20.1.9.19) — WordArt text warp. When
6829
+ * present the renderer maps each glyph through the named envelope
6830
+ * (presetTextWarpDefinitions) instead of laying text out flat. Omitted from
6831
+ * JSON when the body has no warp, so unwarped bodies are byte-identical.
6832
+ */
6833
+ textWarp?: {
6834
+ /** The `prst` name, e.g. `"textArchUp"`, `"textWave1"`. */
6835
+ preset: string;
6836
+ /** `<a:avLst>` adjust values (adj1, adj2, …) in thousandths of a percent.
6837
+ * Omitted when the author supplied none (preset defaults apply). */
6838
+ adj?: number[];
6839
+ };
4889
6840
  /**
4890
6841
  * Narrow the inherited `paragraphs` to the PPTX `Paragraph` so consumers see
4891
6842
  * the PPTX-only `eaLnBrk` flag. PPTX `Paragraph extends CoreParagraph`, so
@@ -4943,6 +6894,20 @@ declare interface TextOutline {
4943
6894
  color?: string;
4944
6895
  }
4945
6896
 
6897
+ /** ECMA-376 Part 4 §19.1.2.23 `<v:textpath>` — a WordArt vector text path,
6898
+ * emitted by Word for text watermarks (the `PowerPlusWaterMarkObject` shape).
6899
+ * The text is stretched to fit the shape box (`fitshape`, the WordArt
6900
+ * `#_x0000_t136` shapetype default), so its drawn size derives from the shape
6901
+ * geometry rather than the nominal `font-size` in the textpath style. */
6902
+ declare interface TextPath {
6903
+ /** The `string` attribute — the watermark text (e.g. "DRAFT"). */
6904
+ string: string;
6905
+ /** `font-family` from the textpath style (quotes stripped). */
6906
+ fontFamily?: string | null;
6907
+ bold?: boolean;
6908
+ italic?: boolean;
6909
+ }
6910
+
4946
6911
  /** Absolute text-frame rectangle in EMU (from SmartArt `<dsp:txXfrm>`). */
4947
6912
  declare interface TextRect {
4948
6913
  x: number;
@@ -5018,10 +6983,19 @@ declare interface TextRunData {
5018
6983
  /** Set for OOXML field runs (e.g. "slidenum"). When set, renderer replaces text with field value. */
5019
6984
  fieldType?: string;
5020
6985
  /**
5021
- * Hyperlink target URL resolved from rPr > a:hlinkClick @r:id via the slide's _rels.
5022
- * Undefined for runs without a hyperlink. ECMA-376 §21.1.2.3.5 (CT_Hyperlink).
6986
+ * Hyperlink target resolved from rPr > a:hlinkClick @r:id via the slide's _rels.
6987
+ * For an external link this is the URL; for an internal slide jump it is the
6988
+ * resolved internal part name (e.g. "../slides/slide3.xml"). Undefined for runs
6989
+ * without a hyperlink. ECMA-376 §21.1.2.3.5 (CT_Hyperlink).
5023
6990
  */
5024
6991
  hyperlink?: string;
6992
+ /**
6993
+ * Raw `<a:hlinkClick @action>` string (e.g. "ppaction://hlinksldjump") when
6994
+ * present — its presence marks {@link hyperlink} as an INTERNAL PowerPoint
6995
+ * action (slide jump / first / last …) rather than an external URL. Undefined
6996
+ * when the hlinkClick has no @action. ECMA-376 §21.1.2.3.5. (IX1)
6997
+ */
6998
+ hyperlinkAction?: string;
5025
6999
  /**
5026
7000
  * Run-level drop shadow on glyphs (`<a:rPr><a:effectLst><a:outerShdw>`),
5027
7001
  * ECMA-376 §20.1.8.45. Independent of the shape-level shadow on `spPr`.
@@ -5083,11 +7057,51 @@ declare type WireRenderPageOptions = Omit<RenderPageOptions, 'onTextRun'>;
5083
7057
 
5084
7058
  /** Serializable subset of RenderViewportOptions: drop the callback, the image
5085
7059
  * cache, and the `fetchImage` loader (all non-cloneable; the worker owns its
5086
- * own cache and supplies its own in-worker fetchImage). */
5087
- declare type WireRenderViewportOptions = Omit<RenderViewportOptions, 'onTextRun' | 'loadedImages' | 'fetchImage'>;
7060
+ * own cache and supplies its own in-worker fetchImage). Extended with the
7061
+ * optional {@link WireSizeOverrides} so view-only size mutations reach the
7062
+ * worker's local sheet copy; absent (the common case) when nothing has been
7063
+ * resized or collapsed, keeping the wire payload unchanged. */
7064
+ declare type WireRenderViewportOptions = Omit<RenderViewportOptions, 'onTextRun' | 'loadedImages' | 'fetchImage'> & {
7065
+ sizeOverrides?: WireSizeOverrides;
7066
+ };
7067
+
7068
+ /**
7069
+ * View-only per-band size overrides for one sheet, carried with every worker
7070
+ * `renderViewport` request. The render worker draws from its own worker-local
7071
+ * parsed-sheet cache, so main-thread Worksheet mutations (outline
7072
+ * collapse/expand via the size-0 hidden encoding, drag-to-resize #567) never
7073
+ * reach it on their own — without this channel the gutter/overlays update but
7074
+ * the grid bitmap stays stale.
7075
+ *
7076
+ * Semantics: keys are 1-based band indices; a number is the band's current
7077
+ * `rowHeights` / `colWidths` model value, `null` means "no entry — fall back
7078
+ * to the sheet default". The main thread accumulates every band the user has
7079
+ * touched this session (entries are updated in place, never removed), so
7080
+ * re-applying the full map is idempotent and converges the worker's cached
7081
+ * sheet to the main model even across worker-side re-parses.
7082
+ */
7083
+ declare interface WireSizeOverrides {
7084
+ rows?: Record<number, number | null>;
7085
+ cols?: Record<number, number | null>;
7086
+ }
5088
7087
 
5089
7088
  declare interface Workbook {
5090
7089
  sheets: SheetMeta[];
7090
+ /** Workbook date system (`<workbookPr date1904>`, ECMA-376 §18.2.28).
7091
+ * `true` selects the 1904 date system (Mac-authored workbooks); serial
7092
+ * dates are resolved against the 1904 epoch (§18.17.4.1). Omitted from the
7093
+ * parser JSON when false (default 1900 date system). */
7094
+ date1904?: boolean;
7095
+ /** #773 partial degradation: a WORKBOOK-LEVEL degradation that leaves every
7096
+ * sheet openable. Set when a shared workbook part was PRESENT but corrupt —
7097
+ * most commonly `xl/sharedStrings.xml` (§18.4.9): a broken shared-string table
7098
+ * silently blanks every string cell across ALL sheets, so unlike a per-sheet
7099
+ * break it can't be attributed to one placeholder sheet. Tagged with the
7100
+ * offending part (e.g. `"xl/sharedStrings.xml: <detail>"`) so the loss is
7101
+ * surfaced instead of silent, while every sheet still renders its non-string
7102
+ * content. Absent (`undefined`) when every shared part read cleanly. Also set
7103
+ * (`"(zip container): <detail>"`) for a whole-container degradation (#774). */
7104
+ parseError?: string;
5091
7105
  }
5092
7106
 
5093
7107
  declare interface Worksheet {
@@ -5095,6 +7109,17 @@ declare interface Worksheet {
5095
7109
  rows: Row[];
5096
7110
  colWidths: Record<number, number>;
5097
7111
  rowHeights: Record<number, number>;
7112
+ /** Per-column outline (grouping) depth 0-7 (ECMA-376 §18.3.1.13
7113
+ * `<col outlineLevel>`), keyed by 1-based column index. Present only for
7114
+ * grouped columns; absent (⇒ level 0) on outline-free sheets. */
7115
+ colOutlineLevels?: Record<number, number>;
7116
+ /** Per-column `<col collapsed>` (§18.3.1.13): `true` on a summary column whose
7117
+ * one-level-deeper detail columns are collapsed. Only `true` entries. */
7118
+ colCollapsed?: Record<number, boolean>;
7119
+ /** Per-column `<col hidden>` (§18.3.1.13): `true` when the column is hidden
7120
+ * (e.g. a collapsed outline hides its detail columns). Distinct from
7121
+ * `colWidths[c] === 0`. Only `true` entries. */
7122
+ colHidden?: Record<number, boolean>;
5098
7123
  defaultColWidth: number;
5099
7124
  defaultRowHeight: number;
5100
7125
  mergeCells: MergeCell[];
@@ -5117,6 +7142,11 @@ declare interface Worksheet {
5117
7142
  * grid so column A sits on the right (ECMA-376 §18.3.1.87
5118
7143
  * `<sheetView rightToLeft>`). Defaults to false. */
5119
7144
  rightToLeft?: boolean;
7145
+ /** Outline display flags from `<sheetPr><outlinePr>` (ECMA-376 §18.3.1.61).
7146
+ * Absent when the sheet declares no `<outlinePr>`; consumers apply the
7147
+ * schema defaults (`summaryBelow` / `summaryRight` both `true`). Decides
7148
+ * which side of a group the summary row/column (and its +/- toggle) sits. */
7149
+ outlinePr?: OutlinePr;
5120
7150
  /** Sheet tab color (ECMA-376 §18.3.1.79). */
5121
7151
  tabColor?: string | null;
5122
7152
  /** AutoFilter header range (ECMA-376 §18.3.1.2). */
@@ -5160,6 +7190,17 @@ declare interface Worksheet {
5160
7190
  defaultFontFamily?: string;
5161
7191
  /** Point size of the workbook's Normal-style font (`<fonts>[N].sz.val`). */
5162
7192
  defaultFontSize?: number;
7193
+ /** Workbook date system (`<workbookPr date1904>`, ECMA-376 §18.2.28),
7194
+ * denormalized onto every worksheet by the parser so the cell formatter can
7195
+ * resolve serial dates (§18.17.4.1) without a workbook back-reference.
7196
+ * `true` = 1904 date system. Omitted (⇒ false) for the default 1900 system. */
7197
+ date1904?: boolean;
7198
+ /** RB7 partial degradation: set when THIS sheet's part could not be
7199
+ * read/parsed. The workbook still opens with the OTHER sheets intact; this one
7200
+ * is an empty placeholder (`rows` empty) whose `parseError` names the offending
7201
+ * part (e.g. `"xl/worksheets/sheet3.xml: <detail>"`). Absent (`undefined`) for
7202
+ * every healthy sheet. The renderer paints a visible error overlay. */
7203
+ parseError?: string;
5163
7204
  }
5164
7205
 
5165
7206
  export declare namespace xlsx {
@@ -5167,6 +7208,7 @@ export declare namespace xlsx {
5167
7208
  XlsxWorkbook,
5168
7209
  LoadOptions_3 as LoadOptions,
5169
7210
  WireRenderViewportOptions,
7211
+ WireSizeOverrides,
5170
7212
  XlsxViewer,
5171
7213
  ResolvedList,
5172
7214
  XlsxViewerOptions,
@@ -5174,12 +7216,21 @@ export declare namespace xlsx {
5174
7216
  CellAddress,
5175
7217
  CellRange_2 as CellRange,
5176
7218
  SelectionMode_2 as SelectionMode,
7219
+ XlsxMatchLocation,
7220
+ FindMatch,
7221
+ FindMatchesOptions,
5177
7222
  autoResize,
5178
7223
  AutoResizeOptions,
7224
+ HyperlinkTarget,
7225
+ openExternalHyperlink,
7226
+ resolveSharedStrings,
7227
+ OoxmlError,
7228
+ OoxmlErrorCode,
5179
7229
  Workbook,
5180
7230
  SheetMeta,
5181
7231
  SheetVisibility,
5182
7232
  Worksheet,
7233
+ OutlinePr,
5183
7234
  Row,
5184
7235
  Cell,
5185
7236
  CellValue,
@@ -5198,6 +7249,10 @@ export declare namespace xlsx {
5198
7249
  Run,
5199
7250
  RunFont,
5200
7251
  SharedString,
7252
+ PhoneticRun,
7253
+ PhoneticProperties,
7254
+ PhoneticType,
7255
+ PhoneticAlignment,
5201
7256
  Dxf,
5202
7257
  GradientFillSpec,
5203
7258
  ConditionalFormat,
@@ -5216,6 +7271,7 @@ export declare namespace xlsx {
5216
7271
  SparklineGroup,
5217
7272
  Sparkline,
5218
7273
  ImageAnchor,
7274
+ Duotone_2 as Duotone,
5219
7275
  ChartAnchor,
5220
7276
  ShapeAnchor,
5221
7277
  ShapeInfo,
@@ -5266,6 +7322,20 @@ declare interface XlsxComment {
5266
7322
  text: string;
5267
7323
  }
5268
7324
 
7325
+ /** Where an xlsx match lives: the sheet, its name, and the cell (A1 + row/col). */
7326
+ declare interface XlsxMatchLocation {
7327
+ /** 0-based sheet index. */
7328
+ sheet: number;
7329
+ /** The sheet's display name. */
7330
+ sheetName: string;
7331
+ /** A1 cell reference, e.g. `"B7"`. */
7332
+ ref: string;
7333
+ /** 1-based row. */
7334
+ row: number;
7335
+ /** 1-based column. */
7336
+ col: number;
7337
+ }
7338
+
5269
7339
  /** Emitted once per cell that has text, with the cell's canvas-pixel bounds. */
5270
7340
  declare interface XlsxTextRunInfo {
5271
7341
  text: string;
@@ -5281,13 +7351,53 @@ declare interface XlsxTextRunInfo {
5281
7351
  col: number;
5282
7352
  }
5283
7353
 
5284
- declare class XlsxViewer {
7354
+ declare class XlsxViewer implements ZoomableViewer {
5285
7355
  private wb;
5286
7356
  /** The single subtree root the constructor appended to the caller's
5287
7357
  * container. destroy() removes it to return the container to its original
5288
7358
  * (empty) state. */
5289
7359
  private wrapper;
5290
7360
  private canvas;
7361
+ /** Region holding the outline gutters (top/left) and the inset {@link canvasArea}.
7362
+ * When the active sheet has no outlining the gutters collapse to 0 px and this
7363
+ * is a transparent pass-through, so an outline-free sheet lays out identically. */
7364
+ private gridRegion;
7365
+ /** Left gutter canvas: row group brackets + toggles (XL4). */
7366
+ private rowGutter;
7367
+ /** Top gutter canvas: column group brackets + toggles (XL4). */
7368
+ private colGutter;
7369
+ /** Top-left corner canvas: numbered level buttons (XL4). */
7370
+ private cornerGutter;
7371
+ /** Cached extents (unscaled CSS px) of the current sheet's gutters; both 0 for
7372
+ * an outline-free sheet. `w` insets {@link canvasArea} from the left, `h` from
7373
+ * the top. */
7374
+ private gutter;
7375
+ /** Per-axis outline layout (group brackets + toggles) for the current sheet,
7376
+ * recomputed on sheet switch and after each collapse/expand. `null` axis ⇒ no
7377
+ * outlining on that axis. */
7378
+ private rowOutline;
7379
+ private colOutline;
7380
+ private rowOutlineBands;
7381
+ private colOutlineBands;
7382
+ /** Original row heights / column widths stashed the first time a band is
7383
+ * collapsed, so expanding restores a custom size rather than the default.
7384
+ * Keyed by band index; per current worksheet (cleared on sheet switch). */
7385
+ private stashedRowHeights;
7386
+ private stashedColWidths;
7387
+ /**
7388
+ * Per-sheet cumulative record of every view-only size mutation (outline
7389
+ * collapse/expand, drag-to-resize #567), keyed by sheet index. Value = the
7390
+ * band's current model size, or `null` when the model has no entry (default
7391
+ * size). Serialized as {@link WireSizeOverrides} with every worker
7392
+ * `renderViewport` so the worker's local sheet cache converges to the
7393
+ * main-thread model — without it the worker keeps drawing the file's
7394
+ * original sizes and the grid bitmap goes stale under the (up-to-date)
7395
+ * gutter and overlays. Entries are updated in place and never removed
7396
+ * (idempotent re-application); the whole store resets when a new workbook
7397
+ * loads. Main mode never reads it (the main renderer draws from the mutated
7398
+ * model directly).
7399
+ */
7400
+ private sizeOverrideStore;
5291
7401
  private canvasArea;
5292
7402
  private scrollHost;
5293
7403
  private spacer;
@@ -5311,6 +7421,26 @@ declare class XlsxViewer {
5311
7421
  * holds one context type for its lifetime, so this is obtained once and the
5312
7422
  * main-mode 2d render path is never used on the same canvas. */
5313
7423
  private _bitmapCtx;
7424
+ /** Set by {@link destroy} (first line). Guards {@link _reportRenderError} so a
7425
+ * render rejection that lands AFTER teardown is swallowed rather than surfaced
7426
+ * to an `onError` / `console.error` on a dead viewer — parity with the scroll
7427
+ * viewers' `_destroyed` flag. */
7428
+ private _destroyed;
7429
+ /**
7430
+ * Concurrent-load latch (generation token). Every {@link load} increments this
7431
+ * and captures the value; after its workbook finishes loading it re-checks the
7432
+ * live value and BAILS (destroying its own just-loaded workbook) if a newer
7433
+ * `load()` has since started. Without it, two overlapping `load(A)`/`load(B)`
7434
+ * calls race the WASM parse / worker init, and whichever RESOLVES last wins the
7435
+ * swap — even the stale `load(A)` resolving after `load(B)`; the loser's freshly
7436
+ * created workbook (never installed, or installed then overwritten) then leaks
7437
+ * its worker + pinned WASM allocation. The latch composes with SC20: the check
7438
+ * runs AFTER the new workbook loads but BEFORE the field assignment and
7439
+ * `previous?.destroy()`, so a superseded load never touches `this.wb` nor frees
7440
+ * the current (newer) workbook. {@link destroy} also bumps it so a load in
7441
+ * flight at teardown is treated as superseded and its workbook cleaned up.
7442
+ */
7443
+ private _loadGen;
5314
7444
  private resizeObserver;
5315
7445
  /**
5316
7446
  * Pending `requestAnimationFrame` handle for a coalesced re-render, or `null`
@@ -5349,13 +7479,27 @@ declare class XlsxViewer {
5349
7479
  * strand the view at the sheet's far end once the host gains its real size.
5350
7480
  */
5351
7481
  private effectiveH;
7482
+ /** Gesture-only pointer anchor for the NEXT `setScale`, in canvasArea-viewport
7483
+ * px (`{ x, y }` from the wheel event, relative to the grid's top-left). Set by
7484
+ * the Ctrl/⌘+wheel handler right before it calls `setScale` so the zoom pivots
7485
+ * on the cursor ("zoom toward the pointer") in BOTH axes, past the fixed
7486
+ * header + frozen-pane lead-in; consumed and cleared by `setScale`. `null` for
7487
+ * every non-gesture source (the public `setScale`, the +/- steppers, the zoom
7488
+ * slider, `fitWidth`/`fitPage`), which keep the historical START-anchored
7489
+ * (top-left) preservation so their behaviour is unchanged. */
7490
+ private _pendingZoomAnchor;
5352
7491
  private anchorCell;
5353
7492
  private activeCell;
5354
7493
  private selectionMode;
5355
7494
  private isSelecting;
5356
7495
  private selectionOverlay;
7496
+ /** IX2 — find-highlight overlay (matched-cell boxes). */
7497
+ private findOverlay;
7498
+ /** IX2 — find state (matches + active cursor). */
7499
+ private _find;
5357
7500
  private keydownHandler;
5358
7501
  private pendingTap;
7502
+ private pendingClick;
5359
7503
  private resizeDrag;
5360
7504
  /** DOM overlay element that shows the hovered cell's comment. Lives in
5361
7505
  * canvasArea above the scrollHost; `pointer-events:none` so it never blocks
@@ -5363,6 +7507,11 @@ declare class XlsxViewer {
5363
7507
  private commentPopup;
5364
7508
  /** `"row:col"` → comment for the current sheet, rebuilt on every showSheet. */
5365
7509
  private commentMap;
7510
+ /** IX1 — `"row:col"` → hyperlink for the current sheet, rebuilt on every
7511
+ * showSheet. Keys mirror the renderer's `hyperlinkMap` (1-based row/col, the
7512
+ * first cell of a hyperlink `ref` range per the parser), so a `getCellAt`
7513
+ * {row,col} looks up directly. */
7514
+ private hyperlinkMap;
5366
7515
  /** `"row:col"` of the cell whose popup is currently shown (or pending), so a
5367
7516
  * pointermove within the same cell doesn't restart the show timer. */
5368
7517
  private commentPopupKey;
@@ -5384,17 +7533,80 @@ declare class XlsxViewer {
5384
7533
  * click; installed only while the panel is open. */
5385
7534
  private validationOutsideHandler;
5386
7535
  constructor(container: HTMLElement, opts?: XlsxViewerOptions);
7536
+ /** Every non-empty cell of a sheet with its rendered display text (IX2 find
7537
+ * source). Reads the parsed worksheet model directly — no render — so search
7538
+ * covers the whole sheet, not just the on-screen viewport. */
7539
+ private _collectSheetCells;
5387
7540
  /**
5388
7541
  * Load an XLSX from URL or ArrayBuffer and render the first sheet.
5389
7542
  *
5390
- * Error contract (shared by all three viewers): on failure, if an `onError`
5391
- * callback was provided it is invoked and `load` resolves normally; if not,
5392
- * the error is rethrown so it is never silently swallowed.
7543
+ * Error contract (shared by all three viewers):
7544
+ * - Parse/load failure (the underlying `XlsxWorkbook.load()` call itself
7545
+ * rejects): if an `onError` callback was provided it is invoked and `load`
7546
+ * resolves normally; if not, the error is rethrown so it is never silently
7547
+ * swallowed.
7548
+ * - Render failure (the first sheet fails to draw AFTER a successful
7549
+ * parse/load): routed to the shared `_reportRenderError` contract (`onError`
7550
+ * if provided, else `console.error` — never silent) and `load` still
7551
+ * RESOLVES, matching every subsequent navigation call.
5393
7552
  */
5394
7553
  load(source: string | ArrayBuffer): Promise<void>;
5395
7554
  /** The loaded workbook, or throws if {@link load} has not completed. */
5396
7555
  private get workbook();
5397
7556
  showSheet(index: number): Promise<void>;
7557
+ /** Recompute the per-axis outline layout for `ws` and cache the band lists.
7558
+ * Both axes are `null` (gutters collapse to 0) when the sheet has no
7559
+ * outlining, so an outline-free sheet is untouched. */
7560
+ private buildOutline;
7561
+ /** Size and place the three gutter canvases (corner / col / row) from the
7562
+ * current outline, and inset {@link canvasArea} by the gutter extents. When
7563
+ * neither axis is grouped both extents are 0 and canvasArea covers the whole
7564
+ * region — pixel-identical to a viewer built before XL4. */
7565
+ private layoutGutters;
7566
+ /** Paint all visible gutter strips for the current scroll offset. Called at the
7567
+ * end of every grid render so the brackets track scroll / zoom exactly. */
7568
+ private renderGutters;
7569
+ /** Draw one axis's group brackets and +/- toggles into its gutter canvas,
7570
+ * aligned to the on-screen band positions via {@link getCellRect}. */
7571
+ private paintAxisGutter;
7572
+ /** Draw a small square +/- toggle centered at (cx, cy) in gutter-canvas CSS px. */
7573
+ private drawToggleBox;
7574
+ /** Draw one numbered level button centered at (cx, cy) in gutter-canvas CSS
7575
+ * px. Shared by the row bank (in the row gutter's top strip) and the column
7576
+ * bank (in the column gutter's left strip). */
7577
+ private drawLevelButton;
7578
+ /** Paint the corner (intersection of the two gutters) as plain background.
7579
+ * The numbered level banks live in each axis gutter's own header strip
7580
+ * (see paintAxisGutter), so the corner carries no interactive content. */
7581
+ private paintCornerGutter;
7582
+ /** Handle a click in a row/col gutter: hit-test the +/- toggles and toggle the
7583
+ * matching group's collapse state. */
7584
+ private onGutterPointerDown;
7585
+ /** Flip a single group's collapse state in the in-memory model, then rebuild
7586
+ * the outline + repaint. View-only: the file is never written. */
7587
+ private applyGroupToggle;
7588
+ /** Collapse/expand the whole sheet to `level` on one axis. */
7589
+ private applyLevelButton;
7590
+ /** Set a row/column hidden by mapping to the size-0 encoding the axis/renderer
7591
+ * already understand, stashing the original size so expand can restore it. */
7592
+ private setBandHidden;
7593
+ /** Record band `index`'s CURRENT model size (or `null` = no entry) in the
7594
+ * per-sheet override store. Called after every view-only size mutation —
7595
+ * outline hide/show above and drag-to-resize (#567) — so worker renders
7596
+ * converge to the main model. */
7597
+ private recordSizeOverride;
7598
+ /** The current sheet's override store serialized for the wire, or undefined
7599
+ * when nothing has been mutated (keeps the request payload unchanged). */
7600
+ private wireSizeOverrides;
7601
+ /** Update the `collapsed` flag on a band's model entry so the outline rebuild
7602
+ * reflects the new state. */
7603
+ private setBandCollapsed;
7604
+ /** Shared tail of a gutter interaction: invalidate the axis cache, rebuild the
7605
+ * outline (collapsed flags changed), refresh dependent geometry, re-render. */
7606
+ private afterOutlineMutation;
7607
+ /** Rebuild only the layout + band lists (not the stashes) after a collapse
7608
+ * state change, so the +/- glyphs and bracket set stay in sync. */
7609
+ private buildOutlineLayoutOnly;
5398
7610
  /** True when the current sheet's grid is laid out right-to-left. */
5399
7611
  private get isRtl();
5400
7612
  /** Maximum horizontal scroll offset the native scroll host allows (≥ 0). */
@@ -5519,6 +7731,46 @@ declare class XlsxViewer {
5519
7731
  * whole range) to mirror Excel, which attaches the button to the active
5520
7732
  * cell of the selection. */
5521
7733
  private maybeDrawValidationDropdown;
7734
+ /**
7735
+ * Redraw the find-highlight overlay: one translucent box per matched cell on
7736
+ * the current sheet, the active match in a stronger colour. Uses the SAME
7737
+ * `getCellRect` + `screenX` + header/frozen clamp the selection overlay uses,
7738
+ * so a box lands exactly on the drawn cell at any scroll offset / zoom / RTL.
7739
+ * Rebuilt on every render and scroll (cheap DOM geometry, no canvas paint).
7740
+ */
7741
+ private updateFindOverlay;
7742
+ /**
7743
+ * IX2 — find every occurrence of `query` across every sheet and highlight the
7744
+ * matched cells. Returns every match in document order (sheet ascending, then
7745
+ * row-major within a sheet), each tagged with its
7746
+ * `{ sheet, sheetName, ref, row, col }`. A cell is the search unit: search
7747
+ * runs over each cell's *rendered* display text (number formats, dates, rich
7748
+ * text flattened), so a query matches what the grid shows. Case-insensitive by
7749
+ * default; pass `{ caseSensitive: true }` for an exact match. An empty query
7750
+ * clears the find.
7751
+ */
7752
+ findText(query: string, opts?: FindMatchesOptions): Promise<FindMatch<XlsxMatchLocation>[]>;
7753
+ /**
7754
+ * IX2 — move to the next match (wrap-around), switching sheets and scrolling
7755
+ * the matched cell into view as needed, and highlight it as the active match.
7756
+ * Returns the now-active match, or `null` when there are none. Call
7757
+ * {@link findText} first.
7758
+ */
7759
+ findNext(): Promise<FindMatch<XlsxMatchLocation> | null>;
7760
+ /** IX2 — move to the previous match (wrap-around). */
7761
+ findPrev(): Promise<FindMatch<XlsxMatchLocation> | null>;
7762
+ /** IX2 — clear all highlights and reset the find state. */
7763
+ clearFind(): void;
7764
+ private _activateMatch;
7765
+ /**
7766
+ * Scroll the grid so cell (row, col) is comfortably in view. Computes the
7767
+ * cell's absolute logical offset from the axis metrics (the same the renderer
7768
+ * uses) and nudges `scrollHost.scrollTop` / start-anchored horizontal scroll
7769
+ * only when the cell is outside the scrollable viewport — an in-view cell is
7770
+ * left where it is (Excel's find behaviour). Frozen cells are always visible,
7771
+ * so they need no scroll.
7772
+ */
7773
+ private _scrollCellIntoView;
5522
7774
  /** Toggle the dropdown panel for the active cell's list validation. Called
5523
7775
  * from pointerdown when the arrow rect is hit. Re-clicking the same arrow
5524
7776
  * closes it. */
@@ -5546,6 +7798,31 @@ declare class XlsxViewer {
5546
7798
  * collision (Excel allows at most one note per cell, so this is moot in
5547
7799
  * practice). */
5548
7800
  private buildCommentMap;
7801
+ /** IX1 — index the current sheet's hyperlinks by `"row:col"` (1-based, first
7802
+ * cell of the `ref` range) so a clicked/hovered cell resolves in O(1). Keys
7803
+ * match the renderer's `hyperlinkMap` exactly (`${hl.row}:${hl.col}`). */
7804
+ private buildHyperlinkMap;
7805
+ /** IX1 — the hyperlink at a cell, or null. `getCellAt` returns 1-based
7806
+ * {row,col}, matching the parser/renderer keying. */
7807
+ private hyperlinkAtCell;
7808
+ /**
7809
+ * IX1 — dispatch a click on a hyperlinked cell. Builds a
7810
+ * {@link HyperlinkTarget} from the parsed hyperlink (external `url` wins over
7811
+ * internal `location`, matching Excel: a `<hyperlink>` carrying both navigates
7812
+ * to the external target) and routes it to the caller's `onHyperlinkClick`
7813
+ * (which fully owns behaviour) or the built-in default. Returns true when a
7814
+ * hyperlink was found and dispatched.
7815
+ */
7816
+ private dispatchHyperlink;
7817
+ /**
7818
+ * IX1 default handler for an internal `location` target (§18.3.1.47): a defined
7819
+ * name or a cell ref like `Sheet1!A1`. Best-effort: if the part before `!`
7820
+ * names a sheet in the workbook, switch to it. There is no scroll-to-cell
7821
+ * primitive on this viewer, so the cell part is not yet honoured (switching the
7822
+ * sheet already lands the user on the right surface). A bare defined name that
7823
+ * does not resolve to a sheet is a documented no-op.
7824
+ */
7825
+ private navigateInternalHyperlink;
5549
7826
  /** Show the popup for the comment on `cell` after the hover dwell, anchored to
5550
7827
  * the cell's current on-screen rect. No-op when the cell carries no comment.
5551
7828
  * Re-hovering the same cell does not restart the timer. */
@@ -5580,10 +7857,46 @@ declare class XlsxViewer {
5580
7857
  private zoomPosToScale;
5581
7858
  /** Inverse of {@link zoomPosToScale}: scale factor → slider position [0,100]. */
5582
7859
  private zoomScaleToPos;
5583
- /** Set the cell/header scale and re-lay-out the current sheet. Clamped to the
5584
- * zoom bounds; keeps the slider thumb, percentage label and the row-header-
5585
- * aligned tab-nav width in sync. */
7860
+ /**
7861
+ * IX9 {@link ZoomableViewer} — set the cell/header scale (`1` = 100%; the
7862
+ * viewer's `cellScale`) and re-lay-out the current sheet. Clamped to the zoom
7863
+ * bounds and snapped to whole percent; keeps the slider thumb, percentage label
7864
+ * and the row-header-aligned tab-nav width in sync, and fires `onScaleChange`
7865
+ * when the resolved scale actually changes.
7866
+ */
5586
7867
  setScale(scale: number): void;
7868
+ /** IX9 {@link ZoomableViewer} — the current zoom factor (`1` = 100%). This is
7869
+ * the viewer's `cellScale`; `1` before anything is set. */
7870
+ getScale(): number;
7871
+ /** IX9 {@link ZoomableViewer} — step up to the next rung of the shared zoom
7872
+ * ladder (clamped to `zoomMax` by {@link setScale}). */
7873
+ zoomIn(): void;
7874
+ /** IX9 {@link ZoomableViewer} — step down to the next lower ladder rung. */
7875
+ zoomOut(): void;
7876
+ /**
7877
+ * IX9 {@link ZoomableViewer} — fit the used data range's WIDTH to the canvas
7878
+ * area. The "content" is the natural (100%) width of the row header plus the
7879
+ * used columns; the container is `canvasArea.clientWidth`. A no-op (defers) when
7880
+ * nothing is loaded or the container is unlaid-out. Routes through
7881
+ * {@link setScale}, so the result is clamped/snapped and fires `onScaleChange`.
7882
+ */
7883
+ fitWidth(): void;
7884
+ /**
7885
+ * IX9 {@link ZoomableViewer} — fit the used data range's WIDTH AND HEIGHT inside
7886
+ * the canvas area (header + used columns/rows), so the whole used range is
7887
+ * visible without scrolling. Takes the tighter of the width- and height-fit
7888
+ * factors. Defers when unloaded / unlaid-out; routes through {@link setScale}.
7889
+ */
7890
+ fitPage(): void;
7891
+ /** Shared fit implementation for {@link fitWidth} / {@link fitPage}: derive the
7892
+ * natural (cs=1) content extent of the used data range, ask core's pure
7893
+ * {@link fitScale} for the factor, and apply it via {@link setScale}. */
7894
+ private _fit;
7895
+ /** Natural (unscaled, cs=1) CSS-px extent of a worksheet's used data range:
7896
+ * the row/column header plus every used column width / row height. Mirrors
7897
+ * {@link updateSpacerSize} at cs=1 (same used-range detection) so the fit
7898
+ * targets exactly the region the spacer/scroll extent covers. */
7899
+ private _naturalContentExtent;
5587
7900
  private updateSpacerSize;
5588
7901
  /**
5589
7902
  * Coalesce a re-render into the next animation frame. Called from the
@@ -5599,6 +7912,11 @@ declare class XlsxViewer {
5599
7912
  */
5600
7913
  private scheduleRender;
5601
7914
  private renderCurrentSheet;
7915
+ /** Route a render failure to `onError`, or `console.error` when none is given
7916
+ * (never fully silent), and never after teardown. Mirrors the scroll viewers'
7917
+ * `_reportRenderError`. */
7918
+ private _reportRenderError;
7919
+ private _renderCurrentSheet;
5602
7920
  private computeHeaderHighlight;
5603
7921
  get sheetNames(): string[];
5604
7922
  /** The underlying <canvas> element the grid is drawn on. */
@@ -5635,9 +7953,20 @@ declare interface XlsxViewerOptions extends LoadOptions_2 {
5635
7953
  * own zoom control). */
5636
7954
  showZoomSlider?: boolean;
5637
7955
  /** Lower/upper bounds for the zoom slider as scale factors. Default 0.1–4
5638
- * (10%–400%, matching Excel's zoom range). */
7956
+ * (10%–400%, matching Excel's zoom range). Also the clamp range for the IX9
7957
+ * {@link ZoomableViewer} zoom contract ({@link XlsxViewer.setScale} etc.). */
5639
7958
  zoomMin?: number;
5640
7959
  zoomMax?: number;
7960
+ /**
7961
+ * IX9 — fires whenever the zoom factor actually changes (`1` = 100%), whatever
7962
+ * the source: {@link XlsxViewer.setScale}, {@link XlsxViewer.zoomIn} /
7963
+ * {@link XlsxViewer.zoomOut}, {@link XlsxViewer.fitWidth} /
7964
+ * {@link XlsxViewer.fitPage}, the built-in zoom slider, the +/- buttons, or a
7965
+ * Ctrl/⌘+wheel gesture. Named `onScaleChange` to match the docx/pptx viewers so
7966
+ * all five share one notification shape. Not fired when a call resolves to the
7967
+ * same (clamped/snapped) scale.
7968
+ */
7969
+ onScaleChange?: (scale: number) => void;
5641
7970
  onReady?: (sheetNames: string[]) => void;
5642
7971
  /**
5643
7972
  * Called when the active sheet changes, with the new sheet's zero-based
@@ -5651,6 +7980,16 @@ declare interface XlsxViewerOptions extends LoadOptions_2 {
5651
7980
  onError?: (err: Error) => void;
5652
7981
  /** Called when the selected cell range changes. null means no selection. */
5653
7982
  onSelectionChange?: (selection: CellRange_2 | null) => void;
7983
+ /**
7984
+ * IX1 (design decision — NOT user-confirmed, integrator may veto). Fires when a
7985
+ * cell carrying a hyperlink (ECMA-376 §18.3.1.47) is clicked. Default when
7986
+ * omitted: external → {@link openExternalHyperlink} (new tab, sanitised,
7987
+ * noopener); internal (`location`) → navigate to the referenced sheet/cell
7988
+ * when resolvable. When supplied, this callback fully owns the behaviour and
7989
+ * receives the raw {@link HyperlinkTarget} verbatim (URL sanitisation is the
7990
+ * default handler's job, so a blocked scheme still reaches a custom callback).
7991
+ */
7992
+ onHyperlinkClick?: (target: HyperlinkTarget) => void;
5654
7993
  /**
5655
7994
  * Color of the cell-selection highlight. A single CSS color drives both the
5656
7995
  * selection rectangle's border (drawn in this color) and its fill (the same
@@ -5709,6 +8048,12 @@ declare class XlsxWorkbook {
5709
8048
  * `renderViewport` call reuses it — equations in shapes render when present,
5710
8049
  * and are skipped (engine tree-shaken) when omitted. */
5711
8050
  private math;
8051
+ /** Google-Fonts `FontFace` objects this workbook preloaded into `document.fonts`
8052
+ * (main mode only — in worker mode the worker owns them and terminates with its
8053
+ * own FontFaceSet). Released in {@link destroy} so they do not leak into the
8054
+ * shared FontFaceSet for the lifetime of the SPA (deduped + refcounted in core,
8055
+ * so a web font shared with another open workbook survives until both go). */
8056
+ private googleFontFaces;
5712
8057
  private _mode;
5713
8058
  private constructor();
5714
8059
  /** Parse an XLSX from a URL or ArrayBuffer. */
@@ -5748,6 +8093,22 @@ declare class XlsxWorkbook {
5748
8093
  * route-through-worker decision).
5749
8094
  */
5750
8095
  getImage(imagePath: string, mimeType: string): Promise<Blob>;
8096
+ /**
8097
+ * Project the workbook to GitHub-flavoured markdown: each sheet becomes a
8098
+ * `## SheetName` section followed by a pipe table of its populated bounding
8099
+ * box (fully-empty middle rows trimmed, ULP noise masked). Styling, charts,
8100
+ * and drawings are discarded — the projection is meant for AI ingestion and
8101
+ * full-text search, not layout.
8102
+ *
8103
+ * Runs entirely in the worker off the archive opened at {@link load} (no
8104
+ * re-copy of the file, no re-parse of the model on the main thread), so it
8105
+ * works in BOTH `mode: 'main'` and `mode: 'worker'`.
8106
+ *
8107
+ * @example
8108
+ * const wb = await XlsxWorkbook.load(buffer);
8109
+ * const md = await wb.toMarkdown();
8110
+ */
8111
+ toMarkdown(): Promise<string>;
5751
8112
  /**
5752
8113
  * Resolve a `list`-type data-validation `formula1` (ECMA-376 §18.3.1.32) into
5753
8114
  * the set of allowed values to display, evaluated relative to `sheetIndex`
@@ -5764,6 +8125,16 @@ declare class XlsxWorkbook {
5764
8125
  * Read-only: this only reads cell values for display; it never writes.
5765
8126
  */
5766
8127
  resolveValidationList(sheetIndex: number, formula1: string | undefined): Promise<ResolvedList>;
8128
+ /**
8129
+ * IX2 — the display string a cell shows on the grid, i.e. exactly what
8130
+ * {@link renderViewport} would draw (number formats, dates, booleans, rich
8131
+ * text flattened). Used by {@link XlsxViewer.findText} to search the *rendered*
8132
+ * text rather than the raw stored value, so a search matches what the user
8133
+ * sees. Threads the workbook styles + the sheet's date system through the
8134
+ * shared {@link formatCellValue} (the same call the renderer and
8135
+ * validation-list expansion use). Returns `''` before the workbook is loaded.
8136
+ */
8137
+ cellText(ws: Worksheet, cell: Cell): string;
5767
8138
  renderViewport(target: HTMLCanvasElement | OffscreenCanvas, sheetIndex: number, viewport: ViewportRange, opts?: RenderViewportOptions): Promise<void>;
5768
8139
  /**
5769
8140
  * Render a sheet viewport and return it as an ImageBitmap (both modes; in
@@ -5783,4 +8154,90 @@ declare class XlsxWorkbook {
5783
8154
  destroy(): void;
5784
8155
  }
5785
8156
 
8157
+ /**
8158
+ * IX9 — the shared zoom API contract for every viewer (DocxViewer, PptxViewer,
8159
+ * DocxScrollViewer, PptxScrollViewer, XlsxViewer).
8160
+ *
8161
+ * This module owns ONLY the pure, DOM-free pieces of the contract: the type
8162
+ * ({@link ZoomableViewer}), the discrete zoom-step ladder ({@link nextZoomStep} /
8163
+ * {@link prevZoomStep}), the fit-to-content scale math ({@link fitScale}), and the
8164
+ * range clamp ({@link clampScale}). Each viewer implements the interface with its
8165
+ * own scale field and re-render path; this keeps ONE definition of "what a zoom
8166
+ * factor means" and "what the +/- steps are" across all five, so a host can drive
8167
+ * any viewer through the same six calls without special-casing the format.
8168
+ *
8169
+ * SCALE SEMANTICS (the contract): a scale of `1` means 100% — the content at its
8170
+ * natural size (a docx page at `widthPt × PT_TO_PX`, a pptx slide at
8171
+ * `slideWidth / EMU_PER_PX`, an xlsx grid at `cellScale` 1). `getScale()` and
8172
+ * `setScale(n)` speak this user-facing factor for EVERY viewer.
8173
+ *
8174
+ * KNOWN FAMILY DIFFERENCE — the INITIAL scale right after load (deliberate,
8175
+ * documented rather than papered over): the single-canvas viewers (DocxViewer /
8176
+ * PptxViewer) and XlsxViewer start at `1` (or the effective factor implied by an
8177
+ * explicit `width` option); the continuous-scroll viewers (DocxScrollViewer /
8178
+ * PptxScrollViewer) AUTO-FIT to the container on first layout, so their
8179
+ * `getScale()` right after load reports the fit-to-width BASE factor (≠ 1 unless
8180
+ * the container happens to match the natural width). The unit is identical — only
8181
+ * the starting point differs, because fit-to-width is the natural resting state
8182
+ * of a continuous document viewer.
8183
+ *
8184
+ * PRE-LOAD `setScale` (family-unified, IX9 F1): a `setScale` called before the
8185
+ * content is loaded / before the layout is established is LATCHED — never
8186
+ * silently dropped — and applied once the viewer establishes its scale (the
8187
+ * single-canvas viewers honour it on the first render; the scroll viewers apply
8188
+ * it right after the base fit establishes, firing `onScaleChange` at application
8189
+ * time). `getScale()` reports the latched factor while it is pending.
8190
+ *
8191
+ * API SHAPE (idiomatic default — the integrator MAY veto; see the IX9 PR): a
8192
+ * six-method surface plus one change notification (`onScaleChange`). Deliberately
8193
+ * NO new UI here — the contract is API only (design decision IX9 §4). Touch-pinch
8194
+ * (IX8) is out of scope.
8195
+ */
8196
+ /**
8197
+ * The zoom contract every viewer satisfies. All scales are the user-facing factor
8198
+ * where `1` = 100% (see the module note). `fitWidth`/`fitPage` are async because a
8199
+ * fit re-renders at the new scale; the getters/steppers resolve synchronously.
8200
+ */
8201
+ declare interface ZoomableViewer {
8202
+ /** The current zoom factor (`1` = 100%). Never throws — returns the default
8203
+ * (`1`) before anything is loaded, or the latched pending factor when a
8204
+ * pre-load `setScale` is waiting to be applied (see the module note). */
8205
+ getScale(): number;
8206
+ /** Set the absolute zoom factor (`1` = 100%), clamped to the viewer's
8207
+ * `[zoomMin, zoomMax]`. Re-renders at the new scale and fires `onScaleChange`
8208
+ * when the clamped value actually changes. Called BEFORE the content is
8209
+ * loaded / the layout is established, the (clamped) factor is LATCHED and
8210
+ * applied once the viewer establishes its scale — family-unified semantics
8211
+ * (IX9 F1): never silently dropped by any viewer. */
8212
+ setScale(scale: number): void | Promise<void>;
8213
+ /** Step up to the next larger rung of the shared zoom ladder (25 %→400 %),
8214
+ * clamped to `zoomMax`. Equivalent to `setScale(nextZoomStep(getScale()))`. */
8215
+ zoomIn(): void | Promise<void>;
8216
+ /** Step down to the next smaller ladder rung, clamped to `zoomMin`. */
8217
+ zoomOut(): void | Promise<void>;
8218
+ /** Fit the content's WIDTH to the container (the common "fit width" / "fit
8219
+ * page width" verb). Sets the scale so one page/slide/sheet-column-run spans
8220
+ * the available width, then re-renders. Resolves once the fit render settles.
8221
+ *
8222
+ * PERSISTENCE is viewer-implementation-dependent (deliberate, by family): the
8223
+ * single-canvas viewers (DocxViewer / PptxViewer) and XlsxViewer apply the fit
8224
+ * ONE-SHOT — they observe no container resizes, so a later resize does NOT
8225
+ * re-fit (call `fitWidth()` again after a layout change). The continuous-
8226
+ * scroll viewers (DocxScrollViewer / PptxScrollViewer) re-fit their width-fit
8227
+ * base on every container resize, so a `fitWidth()` there effectively
8228
+ * PERSISTS across resizes (the resize re-fit preserves the width-fit state). */
8229
+ fitWidth(): void | Promise<void>;
8230
+ /** Fit the WHOLE content (width AND height) inside the container, so an entire
8231
+ * page/slide is visible without scrolling. Sets the scale to the smaller of the
8232
+ * width- and height-fit factors, then re-renders.
8233
+ *
8234
+ * PERSISTENCE is viewer-implementation-dependent, and — unlike `fitWidth` —
8235
+ * a page fit does NOT persist across container resizes on ANY viewer: the
8236
+ * single-canvas viewers and XlsxViewer observe no resizes at all (one-shot),
8237
+ * and the continuous-scroll viewers' resize handler re-applies the WIDTH fit
8238
+ * (preserving the zoom multiplier), not the page fit. Re-invoke `fitPage()`
8239
+ * after a layout change to re-fit. */
8240
+ fitPage(): void | Promise<void>;
8241
+ }
8242
+
5786
8243
  export { }