@silurus/ooxml 0.69.0 → 0.70.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.
@@ -60,6 +60,10 @@ export declare type BodyElement = {
60
60
  headers?: HeadersFooters;
61
61
  footers?: HeadersFooters;
62
62
  titlePage?: boolean;
63
+ /** ECMA-376 §17.6.13 / §17.6.11 — this ENDING section's page geometry
64
+ * (size + margins). Absent when the sectPr inherits both pgSz and pgMar
65
+ * (the renderer then falls back to the body-level section geometry). */
66
+ geom?: SectionGeom;
63
67
  };
64
68
 
65
69
  export declare interface BorderSpec {
@@ -68,6 +72,24 @@ export declare interface BorderSpec {
68
72
  style: string;
69
73
  }
70
74
 
75
+ /**
76
+ * Build the transparent text-selection overlay for a rendered docx page: one
77
+ * absolutely-positioned, color-transparent `<span>` per {@link DocxTextRunInfo}
78
+ * (emitted by `renderPage`'s `onTextRun`), so the browser's native selection
79
+ * lands on the drawn glyphs. Extracted verbatim from `DocxViewer._buildTextLayer`
80
+ * so both the pager (DocxViewer) and the continuous-scroll viewer (DocxScrollViewer)
81
+ * share one implementation; also public API for integrators building their own
82
+ * overlay (design §10). MAIN render mode only — `onTextRun` cannot cross the
83
+ * worker boundary.
84
+ *
85
+ * @param layer the overlay div (position:relative parent expected).
86
+ * @param runs per-run geometry from `renderPage({ onTextRun })`.
87
+ * @param canvasCssWidth the rendered canvas's CSS width (e.g. `"700px"`), used
88
+ * to size the overlay to match the canvas.
89
+ * @param canvasCssHeight the rendered canvas's CSS height.
90
+ */
91
+ export declare function buildDocxTextLayer(layer: HTMLDivElement, runs: DocxTextRunInfo[], canvasCssWidth: string, canvasCssHeight: string): void;
92
+
71
93
  export declare interface CellBorders {
72
94
  top: BorderSpec | null;
73
95
  bottom: BorderSpec | null;
@@ -343,6 +365,11 @@ export declare class DocxDocument {
343
365
  */
344
366
  getImage(imagePath: string, mimeType: string): Promise<Blob>;
345
367
  get pageCount(): number;
368
+ /** The render mode this engine was loaded with ('main' | 'worker'). A fact for
369
+ * integrators and the scroll viewer: an injected engine's mode decides whether
370
+ * pages render via renderPage (main) or renderPageToBitmap (worker) — no
371
+ * probing (design §11: no silent mis-pathing). */
372
+ get mode(): 'main' | 'worker';
346
373
  /**
347
374
  * The raw parsed document model. Available only in `mode: 'main'`; in
348
375
  * `mode: 'worker'` the model stays in the worker and this throws.
@@ -372,6 +399,20 @@ export declare class DocxDocument {
372
399
  */
373
400
  get endnotes(): DocNote[];
374
401
  private _getPages;
402
+ /**
403
+ * ECMA-376 §17.6.13 / §17.6.11 — the page size (pt) of page `pageIndex`, per
404
+ * section (a mixed portrait/landscape document returns different sizes per page).
405
+ * Available in BOTH modes: worker mode reads the worker-built `pageSizes` meta;
406
+ * main mode reads the paginated pages' stamped geometry. Returns the body-level
407
+ * section size for an out-of-range index (clamped) or a page with no stamped
408
+ * geometry. `{ 0, 0 }` means "not loaded" (before `load()` resolves or after
409
+ * `destroy()`). Returns a fresh object per call — safe to mutate.
410
+ * The recommended way to ask "how big is page i?" for layout.
411
+ */
412
+ pageSize(pageIndex: number): {
413
+ widthPt: number;
414
+ heightPt: number;
415
+ };
375
416
  renderPage(target: HTMLCanvasElement | OffscreenCanvas, pageIndex: number, opts?: RenderPageOptions): Promise<void>;
376
417
  /**
377
418
  * Render a page and return it as an ImageBitmap. Works in both modes; in
@@ -437,6 +478,453 @@ export declare interface DocxRunBorder {
437
478
  space: number;
438
479
  }
439
480
 
481
+ export declare class DocxScrollViewer {
482
+ private _doc;
483
+ private readonly _injected;
484
+ private readonly _opts;
485
+ private readonly _container;
486
+ private readonly _wrapper;
487
+ private readonly _scrollHost;
488
+ private readonly _spacer;
489
+ /** Resolved render mode. When an engine is injected the engine's own `mode`
490
+ * is authoritative (design §11 — no silent mis-pathing / no probing); an
491
+ * explicitly conflicting `opts.mode` is rejected at construction. When self-
492
+ * loading, `opts.mode` decides and `load()` passes it to `DocxDocument.load`. */
493
+ private _mode;
494
+ /** px-per-pt zoom multiplier. Base fit maps the first page's width to the
495
+ * container width (or opts.width). Zoom multiplies this (design §7). */
496
+ private _scale;
497
+ /** Whether the base fit scale has been established. Set true the first time
498
+ * `relayout()` resolves a positive base scale. We use an explicit flag rather
499
+ * than a `_scale === 1` sentinel because a fit scale of exactly 1 is a valid
500
+ * established state (a 1× fit would otherwise be re-fit forever). */
501
+ private _scaleEstablished;
502
+ /** Live slots keyed by page index. */
503
+ private readonly _slots;
504
+ /** Recyclable detached slots (canvas + textLayer reused across pages). */
505
+ private readonly _free;
506
+ /** Cached per-page heights in px at the current scale (index-aligned). */
507
+ private _heights;
508
+ private _lastRange;
509
+ private _lastTopIndex;
510
+ private _scrollListener;
511
+ /** Set by `destroy()`. Async render callbacks (main + worker) check it before
512
+ * reporting an error so a rejection that lands after teardown is swallowed
513
+ * rather than surfaced to a `onError` on a dead viewer. */
514
+ private _destroyed;
515
+ /** Worker mode: page indices whose bitmap render is currently dispatched to the
516
+ * engine. Coalesces a scroll storm — we never dispatch a second render for a
517
+ * page whose first is still in flight — and lets us drop pages that scrolled
518
+ * out of the window before dispatch (design §11 worker coalescing).
519
+ *
520
+ * T4 ZOOM HAZARD (RESOLVED by the render epoch below): coalescing keys on page
521
+ * INDEX only, with no notion of the scale a dispatch was made at. Once
522
+ * `setScale` can change the zoom mid-flight, an in-flight bitmap dispatched at
523
+ * the OLD scale can still pass the on-resolution identity check if the SAME
524
+ * slot object is re-mounted for page `i` (the pool reuses slot objects, so
525
+ * `_slots.get(i) === slot && slot.renderedPage === i` can hold for an old
526
+ * dispatch), and get painted at the WRONG resolution. We fix this with a render
527
+ * epoch (`_renderEpoch`): each dispatch captures the epoch, and on resolution a
528
+ * moved epoch ⇒ STALE (close + re-dispatch the live slot). See
529
+ * `_renderSlotBitmap`. */
530
+ private readonly _bitmapInFlight;
531
+ /** Render generation, bumped on every effective `setScale` (and the resize
532
+ * re-fit in `_onResize`, which routes through `setScale`). Stamped into each async render
533
+ * dispatch; a resolution whose captured epoch ≠ this value is STALE — its
534
+ * pixels/geometry are at a superseded scale. Worker path: close the orphan
535
+ * bitmap + re-dispatch the live slot. Main path: skip the (stale) text-layer
536
+ * build; the engine's per-canvas token already discards the stale pixels. */
537
+ private _renderEpoch;
538
+ /** Pending settle-render timer handle (design §7 mechanism 2). Set by
539
+ * `_scheduleSettle` after each `setScale`, reset on the next one so a burst
540
+ * dispatches ONE settle at the end, and cleared in `destroy()`. `ReturnType`
541
+ * of `setTimeout` (a number in the DOM, a Timeout object in node) so the type
542
+ * is host-agnostic. */
543
+ private _settleTimer;
544
+ private _wheelListener;
545
+ /** One-shot latch for the worker-mode text-selection warning. The overlay is a
546
+ * main-mode-only feature: in worker mode the per-run `onTextRun` geometry
547
+ * cannot cross the worker boundary, so an `enableTextSelection` overlay stays
548
+ * empty. We warn once (parity with `DocxViewer`) rather than per slot. */
549
+ private _warnedNoTextSelection;
550
+ /** Observes the container so a width change re-fits the base scale. Disconnected
551
+ * in `destroy()`. */
552
+ private _resizeObserver;
553
+ /** The base fit scale at the last established/re-fit layout. `_onResize` divides
554
+ * `_scale` by this to recover the current zoom multiplier so a width change
555
+ * re-fits the base while preserving the user's zoom (design §11). */
556
+ private _prevBase;
557
+ /** The fit width (px) the base scale was last established at. Lets `_onResize`
558
+ * skip the re-fit when only the height changed (a ResizeObserver fires on ANY
559
+ * box change, but only a WIDTH change alters the fit-to-width base scale). */
560
+ private _lastFitWidth;
561
+ /** Resolved page-canvas `box-shadow` (design: the recipe drop shadow by
562
+ * default). Resolved ONCE with `??` — NOT `||` — so `pageShadow: false`
563
+ * survives as the "no shadow" sentinel (a `||` would treat `false` as absent
564
+ * and wrongly re-apply the default). Applied by `_applyPageShadow` at EVERY
565
+ * canvas-creation site (`_acquireSlot` and the double-buffer spare in
566
+ * `_settleSlot`) so a recycled/re-mounted slot and a settle-swapped spare all
567
+ * carry it. */
568
+ private readonly _pageShadow;
569
+ constructor(container: HTMLElement, opts?: DocxScrollViewerOptions);
570
+ /**
571
+ * Load a DOCX from URL or ArrayBuffer and render the first window.
572
+ * UNSUPPORTED when an engine was injected via `opts.document` (throws) — the
573
+ * caller already owns the parsed engine.
574
+ */
575
+ load(source: string | ArrayBuffer): Promise<void>;
576
+ get pageCount(): number;
577
+ /** CSS px width of page `i` at the current scale. */
578
+ private _pageWidthPx;
579
+ /** CSS px height of page `i` at the current scale. */
580
+ private _pageHeightPx;
581
+ /** The fit width (px), deferring when the container is unlaid-out. An EXPLICIT
582
+ * `opts.width` is the page's CSS-width contract and is returned UNCHANGED (the
583
+ * gutters still apply around placement, not to the width). The container-derived
584
+ * default instead targets `containerWidth − padL − padR` so a page sits INSIDE
585
+ * the horizontal gutters at 100%. A non-positive result (gutters wider than the
586
+ * container) is treated as unlaid-out — the same deferral as a zero-width box. */
587
+ private _fitWidthPx;
588
+ /** Base scale: first page's width fit to the fit-width. Returns 0 when the
589
+ * container has no width yet (deferral). */
590
+ private _baseScale;
591
+ /**
592
+ * Recompute per-page heights + the spacer and re-mount the visible window.
593
+ *
594
+ * The viewer already calls this automatically after `load()`, an injected
595
+ * engine, a container resize, and a zoom, so most integrations never need it.
596
+ * It is public as a deliberate escape hatch: if the host mutates the layout in
597
+ * a way the `ResizeObserver` cannot observe (e.g. a CSS change on an ancestor
598
+ * that resizes the container without a box-size event, or a font that finishes
599
+ * loading after first paint), call `relayout()` to force a re-fit. Idempotent —
600
+ * safe to call repeatedly, and a no-op while the container has zero width (the
601
+ * fit is deferred until width appears, design §11).
602
+ */
603
+ relayout(): void;
604
+ private _recomputeHeights;
605
+ private _gap;
606
+ private _overscan;
607
+ /** Desk padding fed to `computeVisibleRange`: `paddingTop`/`paddingBottom`,
608
+ * each defaulting to `gap` (uniform rhythm). Resolved here (not stored) to
609
+ * mirror `_gap()`/`_overscan()`, and consumed at EVERY `computeVisibleRange`
610
+ * call site so the padded offsets are the single source of geometry. */
611
+ private _pad;
612
+ /** Horizontal desk gutters: `paddingLeft`/`paddingRight`, each defaulting to
613
+ * `gap` (uniform rhythm — the horizontal gutters match the vertical padding).
614
+ * Consumed by `_fitWidthPx` (to shrink the container-derived fit), by
615
+ * `_positionSlot` (the flush-left floor), and by `_syncSpacer` (the spacer
616
+ * width). Resolved here (not stored) to mirror `_gap()`/`_pad()`. */
617
+ private _padH;
618
+ private _range;
619
+ private _syncSpacer;
620
+ /** Horizontal scroll extent: the widest page (docx pages can differ in width)
621
+ * plus both gutters. A spacer NARROWER than the container never creates a
622
+ * scrollbar (scrollWidth = max(clientWidth, content)), so it is always safe to
623
+ * set — it only matters when a zoomed-in page grows past the viewport, where it
624
+ * gives the gutters something to scroll to on either side. Max over per-page
625
+ * widths so the extent covers the widest page in the document. Called from
626
+ * `_syncSpacer` and after every scale change (zoom / resize re-fit) so the
627
+ * extent tracks the current page px width. */
628
+ private _syncSpacerWidth;
629
+ private _onScroll;
630
+ /** Mount/recycle slots for the current visible window. */
631
+ private _mountVisible;
632
+ /** Apply the resolved page-canvas shadow (design: recipe drop shadow by
633
+ * default, `false` ⇒ none). Single source so `_acquireSlot` and the
634
+ * double-buffer spare in `_settleSlot` stay in lock-step — a spare that missed
635
+ * this would lose the shadow on the settle swap. `box-shadow` never affects
636
+ * layout, so this is safe to (re)set on a live/pooled canvas without shifting
637
+ * any offset. */
638
+ private _applyPageShadow;
639
+ private _acquireSlot;
640
+ private _recycleSlot;
641
+ private _positionSlot;
642
+ /** Device-pixel ratio for a render (opts override → window → 1). */
643
+ private _dpr;
644
+ /**
645
+ * Render page `i` into `slot`. Routes strictly on the constructor-resolved
646
+ * `_mode` (design §11 — no probing, no silent mis-pathing): `main` ⇒ paint the
647
+ * slot's canvas directly via `renderPage`; `worker` ⇒ transfer an ImageBitmap
648
+ * from `renderPageToBitmap`.
649
+ *
650
+ * Slot-identity guard: a slot recycled to a DIFFERENT page while a previous
651
+ * render is in flight must not repaint the stale page. `slot.renderedPage`
652
+ * tracks the page this slot is committed to; we stamp it up-front and bail on
653
+ * resolution if it changed (the engine's own token guard is per-canvas; this is
654
+ * the viewer's per-slot page-identity check).
655
+ *
656
+ * Render epoch (main path): pixel staleness after a mid-flight `setScale` is
657
+ * already handled by the engine's per-canvas token (the newer renderPage on the
658
+ * same canvas wins) — `setScale` recycles + re-mounts, and the re-mount always
659
+ * re-dispatches `renderPage` (renderedPage reset to -1), so a fresh render is
660
+ * always issued. But the viewer-side side effects of a STALE resolution — the
661
+ * text-layer build (its run geometry is at the OLD scale) and the renderedPage
662
+ * bookkeeping — must NOT run, or a superseded render would rebuild the overlay
663
+ * with stale x/y/w/h (the pool reuses slot objects, so the identity check alone
664
+ * can pass for an old-epoch resolution). We gate them on the captured epoch.
665
+ */
666
+ private _renderSlot;
667
+ /** Warn once when an `enableTextSelection` overlay was requested but the render
668
+ * mode is `worker` (so the overlay stays empty). Same wording as
669
+ * `DocxViewer._render` — one warning per viewer, not per slot. */
670
+ private _maybeWarnNoTextSelection;
671
+ /** Route an async render failure to `onError`, or `console.error` when none is
672
+ * set (so failures are never fully silent), and never after teardown. */
673
+ private _reportRenderError;
674
+ /**
675
+ * Worker-mode slot render: dispatch `renderPageToBitmap`, transfer the result
676
+ * via a per-slot `bitmaprenderer` context, and manage the ImageBitmap lifecycle.
677
+ *
678
+ * Coalescing / drop-stale (design §11):
679
+ * - Skip if page `i` is already in flight (a scroll storm won't double-dispatch).
680
+ * - Skip if page `i` already left the mounted window before dispatch.
681
+ * - On resolution, if `slot` is no longer THIS page's live slot (it recycled to
682
+ * another page, or page `i` re-mounted onto a DIFFERENT slot while this render
683
+ * was in flight), close the orphan bitmap and skip the paint. In that
684
+ * re-mount case a live slot for `i` still awaits a render, so once we clear
685
+ * the in-flight guard we re-dispatch it — a page that recycled and re-mounted
686
+ * mid-flight must never stay blank.
687
+ * - RENDER EPOCH: the dispatch captures `this._renderEpoch`. `setScale` bumps
688
+ * the epoch, so a resolution whose captured epoch ≠ the live epoch is STALE
689
+ * even when the SAME slot object is still mounted for page `i` (the pool
690
+ * reuses slot objects, so the identity check alone can't catch a zoom that
691
+ * happened mid-flight). A moved epoch ⇒ close the orphan + re-dispatch the
692
+ * live slot at the new scale, never paint the old-scale bitmap.
693
+ */
694
+ private _renderSlotBitmap;
695
+ /**
696
+ * Set the absolute px-per-pt zoom scale, clamped inline to
697
+ * `[zoomMin ?? 0.1, zoomMax ?? 4]` (absolute bounds, XlsxViewer convention — NOT
698
+ * multiples of the base fit; design §3 keeps the clamp in the viewer, not core),
699
+ * then re-anchor VERTICALLY so the page currently under the viewport top stays
700
+ * fixed. A no-op when nothing is loaded or when the clamped scale is unchanged.
701
+ *
702
+ * FLICKER-FREE (design §7): this does NOT re-render the visible pages inline.
703
+ * It shows an immediate CSS preview (stretch the existing bitmaps, scale the
704
+ * overlays) and DEBOUNCES a full-resolution settle re-render for ZOOM_SETTLE_MS,
705
+ * so a wheel/pinch burst never blanks a page and coalesces into one crisp render.
706
+ *
707
+ * Re-anchor (written from scratch — XlsxViewer only re-anchors horizontally):
708
+ * capture `top = topIndex` and the intra-page fraction `intraFrac` from the
709
+ * CURRENT range BEFORE rescale; after recomputing heights at the new scale,
710
+ * `newScrollTop = offsets'[top] + intraFrac × heights'[top]`, clamped to
711
+ * `[0, totalHeight' − viewportHeight]`. Because a page's height scales linearly
712
+ * with `_scale`, the same fractional position maps exactly to the new geometry.
713
+ *
714
+ * CAVEAT — base fit below the floor: `relayout()` sets `_scale = base` WITHOUT
715
+ * clamping to `[zoomMin, zoomMax]`. If the base fit is below `zoomMin` (a wide
716
+ * page in a narrow container), the initial scale sits under the floor, but once
717
+ * the user zooms via `setScale` the clamp pins the minimum to `zoomMin`, so they
718
+ * can no longer return below the floor to the original base fit through this API.
719
+ */
720
+ setScale(scale: number): void;
721
+ /**
722
+ * CSS preview of the visible window at the current `_scale` (design §7
723
+ * mechanism 1), WITHOUT re-rendering. Slots leaving the window recycle normally;
724
+ * slots ENTERING the window mount fresh (rendered at the current scale directly,
725
+ * so they never need a preview); slots that STAY are repositioned and their
726
+ * canvas + text overlay are CSS-transformed to the new size (the device buffer
727
+ * is untouched — that is the whole point: no synchronous clear, no blank frame).
728
+ */
729
+ private _previewVisible;
730
+ /**
731
+ * CSS-preview a single already-mounted slot at the new geometry (design §7): the
732
+ * wrapper is repositioned + sized (via `_positionSlot`), the canvas bitmap is
733
+ * STRETCHED to the new CSS size (no `canvas.width` — the device buffer, and thus
734
+ * the drawn pixels, are left intact, just scaled by the browser), and the text
735
+ * overlay is scaled by `newScale / renderedScale` so it tracks the stretched
736
+ * page. `renderedScale <= 0` means the slot's first render hasn't resolved yet
737
+ * (nothing to stretch); the pending render captured the current scale, so it
738
+ * lands correct and no preview is needed.
739
+ */
740
+ private _previewSlot;
741
+ /** (Re)schedule the debounced settle re-render (design §7 mechanism 2). Resets
742
+ * the timer on every call so a burst of `setScale` dispatches ONE settle
743
+ * ZOOM_SETTLE_MS after the LAST call. Cleared in `destroy()`. */
744
+ private _scheduleSettle;
745
+ /** Full-resolution settle re-render of the visible window (design §7 mechanisms
746
+ * 2+3). Re-renders each mounted slot at the current scale via the double-buffer
747
+ * swap (main) / same-canvas transfer (worker). Main mode also rebuilds the text
748
+ * overlay and clears its preview transform; in worker mode the overlay is
749
+ * permanently empty (text selection is main-mode-only), so the transform is
750
+ * inert there and is reset on recycle. Dispatched at the CURRENT epoch; the
751
+ * existing epoch gate discards it if a later `setScale` supersedes it
752
+ * mid-render. */
753
+ private _settleRender;
754
+ /**
755
+ * Settle-render one slot at the current scale (design §7 mechanism 3).
756
+ *
757
+ * WORKER: re-dispatch the bitmap render into the SAME canvas. The worker path
758
+ * sizes the device buffer and `transferFromImageBitmap`s it in ONE synchronous
759
+ * step (no await between `canvas.width = …` and the transfer), so the browser
760
+ * never composites an intermediate blank frame — no spare canvas is needed. The
761
+ * `renderedScale === _scale` gate in `_settleRender` plus the epoch gate inside
762
+ * `_renderSlotBitmap` keep this correct and idempotent.
763
+ *
764
+ * MAIN: `renderPage` (via renderDocumentToCanvas) synchronously sets
765
+ * `canvas.width = …` (which CLEARS the backing store to blank) BEFORE its first
766
+ * await and paints AFTER — so rendering into the on-screen canvas would flash it
767
+ * white. Render into a SPARE off-DOM canvas instead; only once it resolves at the
768
+ * current epoch do we swap it into the wrapper (replacing the old canvas, which is
769
+ * DISCARDED — the pooled unit is the slot, not the canvas). The old canvas keeps
770
+ * showing the stretched preview until the instant of the swap — blank-free.
771
+ */
772
+ private _settleSlot;
773
+ /**
774
+ * Scroll so page `index`'s top edge sits at the viewport top. Clamps `index` to
775
+ * `[0, pageCount-1]` (the pager convention) and the resulting scrollTop to
776
+ * `[0, totalHeight − viewportHeight]` so the last pages don't scroll past the
777
+ * end. A no-op when nothing is loaded or the document is empty.
778
+ *
779
+ * `opts.behavior` ('auto' | 'smooth', default 'auto') is honoured via
780
+ * `scrollHost.scrollTo({ top, behavior })` when the host supports it (a real
781
+ * browser); the stub-DOM has no `scrollTo`, so the fallback sets `scrollTop`
782
+ * directly (which is what the tests assert). We then call `_mountVisible` once.
783
+ *
784
+ * MOUNTING CAVEAT: synchronous mounting of the target page is guaranteed only on
785
+ * the DEFAULT/'auto' path — there `scrollTop` has already jumped to `top`, so the
786
+ * `_mountVisible` call reads the final scroll position and the target page's slots
787
+ * exist immediately. With `behavior: 'smooth'` the scroll animates ASYNCHRONOUSLY:
788
+ * `scrollTop` is still near the old position when `_mountVisible` runs, so the
789
+ * target page mounts lazily via the animation's subsequent `scroll` events, not
790
+ * from this call.
791
+ */
792
+ scrollToPage(index: number, opts?: {
793
+ behavior?: 'auto' | 'smooth';
794
+ }): void;
795
+ /**
796
+ * Re-fit the base scale on a container resize while PRESERVING the current zoom
797
+ * multiplier (design §11), then re-anchor + re-render. A `ResizeObserver` fires
798
+ * on any box change, but only a WIDTH change alters the fit-to-width base scale;
799
+ * a height-only change skips the re-fit yet STILL re-mounts the visible window
800
+ * (via `_mountVisible`), because a taller viewport reveals rows that were below
801
+ * the fold and would otherwise stay blank until the next scroll. Empty/unloaded
802
+ * ⇒ no-op; a still-zero width ⇒ defer.
803
+ *
804
+ * Zero-width recovery: a container that was 0-wide at construction never
805
+ * established a scale (`_scaleEstablished` is false), so the first non-zero
806
+ * resize establishes it here via `relayout()` — completing the T2 deferral.
807
+ *
808
+ * Re-fit math (zoom multiplier preserved):
809
+ * mult = _scale / _prevBase (the user's zoom over the old base)
810
+ * newScale = newBase × mult
811
+ * Routing through `setScale(newScale)` bumps `_renderEpoch` (resize IS an epoch
812
+ * event — T4 banner) and re-anchors + CSS-previews + debounces a settle re-render
813
+ * of every slot at the new geometry, exactly like a zoom (design §7 flicker-free
814
+ * path — a rapid ResizeObserver burst therefore also coalesces into one settle).
815
+ * `setScale`'s clamp/no-op guards apply: an unchanged newScale (identical width)
816
+ * is a no-op there — so we short-circuit BEFORE it when the fit-width is
817
+ * unchanged (mounting the revealed window without a needless re-render), and
818
+ * after it we call `_mountVisible` again to cover the case where the clamp made
819
+ * `setScale` no-op yet the viewport still grew.
820
+ */
821
+ private _onResize;
822
+ get topVisiblePage(): number;
823
+ /* Excluded from this release type: mountedPageIndicesForTest */
824
+ /* Excluded from this release type: scaleForTest */
825
+ /* Excluded from this release type: baseScaleForTest */
826
+ /* Excluded from this release type: renderEpochForTest */
827
+ /* Excluded from this release type: resizeForTest */
828
+ /**
829
+ * Tear down the viewer: remove the DOM subtree and (only for a self-loaded
830
+ * engine) destroy the engine. An injected engine is left intact — the caller
831
+ * owns its lifecycle. Per-slot worker ImageBitmaps are closed on recycle.
832
+ */
833
+ destroy(): void;
834
+ }
835
+
836
+ /**
837
+ * Options for {@link DocxScrollViewer}. Extends `RenderPageOptions` (per-page
838
+ * render knobs, minus `onTextRun`) and `LoadOptions` (parse/worker knobs). See
839
+ * design §8.1.
840
+ *
841
+ * `onTextRun` is omitted deliberately: the viewer drives it internally per
842
+ * mounted slot to build the optional per-page selection overlay (gated by
843
+ * `enableTextSelection`), so exposing it here would let a caller's callback be
844
+ * silently overridden.
845
+ */
846
+ export declare interface DocxScrollViewerOptions extends Omit<RenderPageOptions, 'onTextRun'>, LoadOptions {
847
+ /** Base fit width in CSS px → base zoom scale. Default: the container's width
848
+ * at first non-zero layout (design §7/§11 zero-width deferral). */
849
+ width?: number;
850
+ /** Vertical gap (px) between consecutive pages. Default 16. */
851
+ gap?: number;
852
+ /** Desk padding (px) ABOVE the FIRST page — the margin a PDF reader leaves
853
+ * between the top of the scroll surface and the first sheet. Default: `gap`
854
+ * (uniform desk rhythm — the first page sits the same distance from the top as
855
+ * pages sit from each other). Pass `0` for a flush-top layout. */
856
+ paddingTop?: number;
857
+ /** Desk padding (px) BELOW the LAST page — the margin below the final sheet.
858
+ * Default: `gap`. Pass `0` for a flush-bottom layout. */
859
+ paddingBottom?: number;
860
+ /** Desk gutter (px) to the LEFT of the pages — the horizontal margin between the
861
+ * left edge of the scroll surface and a page sitting flush-left (i.e. once
862
+ * zoomed wide enough that centering no longer applies). Default: `gap` (uniform
863
+ * desk rhythm — the horizontal gutters match the vertical ones). It also shrinks
864
+ * the container-derived FIT width so a page sits inside the gutters at 100%
865
+ * (an EXPLICIT `opts.width` is the page's CSS-width contract and is NOT reduced;
866
+ * the gutters still apply around placement). Pass `0` for a flush-left layout. */
867
+ paddingLeft?: number;
868
+ /** Desk gutter (px) to the RIGHT of the pages. Default: `gap`. Shrinks the
869
+ * container-derived fit width symmetrically with `paddingLeft`. Pass `0` for a
870
+ * flush-right layout. */
871
+ paddingRight?: number;
872
+ /** Pages kept mounted beyond the viewport on each side. Default 1. */
873
+ overscan?: number;
874
+ /** Per-page transparent text-selection overlay. MAIN render mode only:
875
+ * in worker mode `onTextRun` cannot cross the worker boundary, so the overlay
876
+ * stays empty and the viewer logs one warning (design §11). */
877
+ enableTextSelection?: boolean;
878
+ /** Minimum zoom scale (px-per-pt multiplier floor). Default 0.1. */
879
+ zoomMin?: number;
880
+ /** Maximum zoom scale. Default 4. */
881
+ zoomMax?: number;
882
+ /** Enable `Ctrl`/`Cmd`+wheel zoom. Default true. */
883
+ enableZoom?: boolean;
884
+ /**
885
+ * CSS `background` shorthand for the scroll surface (the "desk") visible
886
+ * behind and between pages — the gray a PDF reader paints around the sheet.
887
+ * Applied to the viewer-owned scroll host. The pages themselves are always
888
+ * drawn on the document's own white canvas and are unaffected. Default
889
+ * `undefined`: the scroll surface stays transparent so the host container's
890
+ * background shows through (non-breaking).
891
+ */
892
+ background?: string;
893
+ /**
894
+ * CSS `box-shadow` painted on every page CANVAS (not the wrapper — the
895
+ * text-selection overlay must not cast its own shadow). The soft drop shadow a
896
+ * PDF reader leaves under each sheet.
897
+ *
898
+ * - Default (`undefined`): `'0 1px 3px rgba(0,0,0,0.2)'` — the recipe look, so
899
+ * the scroll viewer reproduces the Examples appearance with zero config.
900
+ * - `false`: NO shadow (flat pages).
901
+ * - A custom string is applied verbatim. A spread-only ring such as
902
+ * `'0 0 0 1px #c8ccd0'` gives a crisp 1px BORDER look — and because
903
+ * `box-shadow` never affects layout (unlike `border`, which would grow the
904
+ * box and shift every offset), a border and a drop shadow are the SAME knob
905
+ * here rather than two competing options.
906
+ */
907
+ pageShadow?: string | false;
908
+ /**
909
+ * Inject an already-loaded engine to share one parse across panes (design §14).
910
+ * When set: `load()` is unsupported (throws), the engine's own `mode` wins (an
911
+ * explicitly conflicting `opts.mode` throws at construction, design §11), and
912
+ * `destroy()` does NOT destroy this engine (the caller owns its lifecycle).
913
+ */
914
+ document?: DocxDocument;
915
+ /** Fires when the top-most visible page changes. `topIndex` from
916
+ * `computeVisibleRange` (the first page intersecting the viewport top,
917
+ * EXCLUDING overscan). */
918
+ onVisiblePageChange?: (topIndex: number, total: number) => void;
919
+ /** Error callback. When set, `load()` invokes it and resolves (otherwise the
920
+ * error is rethrown — shared viewer error contract). It ALSO fires for async
921
+ * per-slot render failures (both main `renderPage` and worker
922
+ * `renderPageToBitmap` rejections); a failed page is left blank rather than
923
+ * crashing the loop. Without an `onError`, render failures are logged via
924
+ * `console.error` so they are never fully silent. */
925
+ onError?: (err: Error) => void;
926
+ }
927
+
440
928
  export declare interface DocxTextRun {
441
929
  text: string;
442
930
  bold: boolean;
@@ -534,6 +1022,15 @@ export declare class DocxViewer {
534
1022
  private _currentPage;
535
1023
  private _canvas;
536
1024
  private _wrapper;
1025
+ /** The canvas's DOM position BEFORE the constructor reparented it into
1026
+ * {@link _wrapper}, captured so {@link destroy} can return the caller-owned
1027
+ * canvas to exactly where it was. `null` parent = canvas was passed
1028
+ * detached. */
1029
+ private _originalParent;
1030
+ private _originalNextSibling;
1031
+ /** The canvas's inline `display` before the constructor forced `block`
1032
+ * (empty string if it was unset), restored on {@link destroy}. */
1033
+ private _originalDisplay;
537
1034
  private _textLayer;
538
1035
  private _opts;
539
1036
  private readonly _mode;
@@ -558,7 +1055,15 @@ export declare class DocxViewer {
558
1055
  goToPage(index: number): Promise<void>;
559
1056
  nextPage(): Promise<void>;
560
1057
  prevPage(): Promise<void>;
561
- /** Terminate the parser worker and release resources. */
1058
+ /**
1059
+ * Terminate the parser worker and release resources.
1060
+ *
1061
+ * The caller-owned `<canvas>` is returned to the DOM position it held before
1062
+ * the constructor was called (same parent, same next-sibling) and its inline
1063
+ * `display` is restored, so the canvas can be reused — e.g. to construct a new
1064
+ * viewer on the same element. If the canvas was passed detached (no parent) it
1065
+ * is simply removed from the internal wrapper. Safe to call more than once.
1066
+ */
562
1067
  destroy(): void;
563
1068
  private _render;
564
1069
  private _buildTextLayer;
@@ -821,6 +1326,21 @@ declare interface LoadOptions_2 {
821
1326
  * via `@font-face` in your application CSS.
822
1327
  */
823
1328
  useGoogleFonts?: boolean;
1329
+ /**
1330
+ * Override the URL the parser worker fetches the WebAssembly module from.
1331
+ *
1332
+ * By default each format resolves the `.wasm` asset that ships next to its
1333
+ * bundle (relative to the module URL), so no configuration is needed. Set
1334
+ * this to serve the parser WASM from a CDN or a self-hosted path instead — a
1335
+ * relative value is resolved against the current document URL. The same
1336
+ * dependency-injection contract across docx / pptx / xlsx.
1337
+ *
1338
+ * The referenced file must be the matching format's `*_parser_bg.wasm`
1339
+ * artifact (the one wasm-bindgen emitted for that parser); pointing it at a
1340
+ * mismatched or missing file makes `load()` reject when the worker
1341
+ * instantiates it.
1342
+ */
1343
+ wasmUrl?: string | URL;
824
1344
  /**
825
1345
  * Override the per-entry ZIP decompression cap (bytes) used by the zip-bomb
826
1346
  * guard in the Rust parser. Defaults to 512 MiB. Raise it to load documents
@@ -828,6 +1348,16 @@ declare interface LoadOptions_2 {
828
1348
  * untrusted input. Zero / negative values fall back to the default.
829
1349
  */
830
1350
  maxZipEntryBytes?: number;
1351
+ /**
1352
+ * Reject the parse request if the parser worker does not answer within this
1353
+ * many milliseconds. Opt-in safety net for a wedged or crashed worker that
1354
+ * would otherwise leave `load()` pending forever. **Default: unlimited** —
1355
+ * parsing a large document with heavy embedded media can legitimately take
1356
+ * tens of seconds, so no timeout is imposed unless you set one. A worker that
1357
+ * throws or fails to load already rejects immediately regardless of this
1358
+ * value; this bound only covers the "silent, never-responds" case.
1359
+ */
1360
+ workerTimeoutMs?: number;
831
1361
  /**
832
1362
  * Opt-in OMML equation engine (MathJax + STIX Two Math, ~3 MB). Inject it
833
1363
  * **once** here and every render of this document / presentation / workbook
@@ -1070,7 +1600,11 @@ export declare type PathCmd = {
1070
1600
  };
1071
1601
 
1072
1602
  export declare interface RenderPageOptions {
1073
- /** Canvas CSS width in px; height is auto-computed from page aspect ratio */
1603
+ /** Canvas CSS width in px; height is auto-computed from page aspect ratio.
1604
+ * Applies per CALL — pages of different physical widths (per-section pgSz,
1605
+ * §17.6.13) rendered at the same `width` get different px-per-pt scales.
1606
+ * For a uniform document scale, derive a per-page width from
1607
+ * `DocxDocument.pageSize(i)` instead of passing a constant. */
1074
1608
  width?: number;
1075
1609
  dpr?: number;
1076
1610
  defaultTextColor?: string;
@@ -1105,6 +1639,32 @@ export declare interface RunRevision {
1105
1639
  date?: string;
1106
1640
  }
1107
1641
 
1642
+ /** ECMA-376 §17.6.13 `<w:pgSz>` + §17.6.11 `<w:pgMar>` — a section's page
1643
+ * geometry: page size + margins + header/footer distances (pt). Mirrors the Rust
1644
+ * `SectionGeom`. Carried on a {@link BodyElement} `sectionBreak` arm (`geom`) so a
1645
+ * mid-body section keeps its own page size; the FINAL section's geometry lives on
1646
+ * {@link DocxDocumentModel.section}. Also stamped per {@link PaginatedBodyElement}
1647
+ * (`sectionGeom`) by the paginator so the renderer sizes each page from its own
1648
+ * section. `orient` is omitted — Word swaps w/h for landscape, so verbatim w/h
1649
+ * already give the correct dims.
1650
+ *
1651
+ * ⚠ Spread over the body-level {@link SectionProps} in `renderDocumentToCanvas`
1652
+ * (`{ ...doc.section, ...pageGeom }`): only add per-section PAGE-BOX fields that
1653
+ * exist on `SectionProps` with the same name and semantics — an optional field
1654
+ * colliding with a non-geometry `SectionProps` name would silently override the
1655
+ * body-level value the renderer promises to preserve. */
1656
+ export declare interface SectionGeom {
1657
+ pageWidth: number;
1658
+ pageHeight: number;
1659
+ /** §17.6.11 — top/bottom MAY be negative (ST_SignedTwipsMeasure); keep the sign. */
1660
+ marginTop: number;
1661
+ marginRight: number;
1662
+ marginBottom: number;
1663
+ marginLeft: number;
1664
+ headerDistance: number;
1665
+ footerDistance: number;
1666
+ }
1667
+
1108
1668
  export declare interface SectionProps {
1109
1669
  pageWidth: number;
1110
1670
  pageHeight: number;
@@ -1235,6 +1795,12 @@ export declare interface ShapeRun {
1235
1795
  textBlocks?: ShapeText[];
1236
1796
  /** "t" | "ctr" | "b" — vertical anchor for the shape's text body (`<wps:bodyPr @anchor>`). */
1237
1797
  textAnchor?: string | null;
1798
+ /** ECMA-376 §21.1.2.1.1 auto-fit mode from `<wps:bodyPr>`, normalized to the
1799
+ * shared core `autoFit` vocabulary (core `src/types/common.ts`): "none"
1800
+ * (`<a:noAutofit/>`, fixed box — overflowing text is CLIPPED to the box),
1801
+ * "sp" (`<a:spAutoFit/>`, box grows to text), or "norm" (`<a:normAutofit/>`,
1802
+ * text shrinks). Absent ⇒ overflow visible. */
1803
+ textAutofit?: string | null;
1238
1804
  textInsetL?: number;
1239
1805
  textInsetT?: number;
1240
1806
  textInsetR?: number;
@@ -1261,6 +1827,12 @@ export declare interface ShapeText {
1261
1827
  /** ECMA-376 §17.3.1.33 `<w:spacing w:after>` of this text-box paragraph, in
1262
1828
  * pt — reserved BELOW the paragraph. Absent/0 ⇒ no offset. */
1263
1829
  spaceAfter?: number;
1830
+ /** ECMA-376 §17.3.1.33 line spacing value (style-chain resolved). Encoded per
1831
+ * {@link lineSpacingRule}: "auto" ⇒ a MULTIPLIER on the natural line box
1832
+ * (1.15 = 276/240), "exact"/"atLeast" ⇒ pt. Absent ⇒ single (natural). */
1833
+ lineSpacingVal?: number;
1834
+ /** "auto" | "exact" | "atLeast" — see {@link lineSpacingVal}. */
1835
+ lineSpacingRule?: string;
1264
1836
  /** ECMA-376 §17.3.1.12 `<w:ind w:left/@start>` — paragraph left indent (pt).
1265
1837
  * Absent/0 ⇒ flush to the box's inner left edge. */
1266
1838
  indentLeft?: number;
@@ -1273,6 +1845,18 @@ export declare interface ShapeText {
1273
1845
  * applies a signed hanging first-line list-independently). Absent/0 ⇒ the
1274
1846
  * first line aligns with the continuation lines. */
1275
1847
  indentFirst?: number;
1848
+ /** ECMA-376 §17.3.1.37 `<w:tabs>` — explicit tab stops of this text-box
1849
+ * paragraph, resolved through the style chain like {@link DocParagraph.tabStops}.
1850
+ * Absent/empty ⇒ only the automatic default-tab grid applies. The renderer
1851
+ * feeds these to the SAME line engine the body uses so a `\t` inside a text box
1852
+ * advances to its stop (the old shape wrapper dropped tabs entirely). */
1853
+ tabStops?: TabStop[];
1854
+ /** ECMA-376 §17.3.1.6 `<w:bidi>` — right-to-left text-box paragraph, resolved
1855
+ * through the style chain like {@link DocParagraph.bidi}. `true` = RTL,
1856
+ * `false` = explicitly LTR, absent = unspecified. Consumed as the paragraph
1857
+ * base direction for the UAX#9 reordering pass (the body renderer reads the
1858
+ * identical field). */
1859
+ bidi?: boolean;
1276
1860
  /** Zip path of an inline image inside this text-box paragraph
1277
1861
  * (`<w:drawing><wp:inline><a:blip r:embed>`), e.g. `word/media/image1.emf`.
1278
1862
  * Absent for a text-only paragraph. */