@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.
package/README.md CHANGED
@@ -27,9 +27,19 @@ npm install @silurus/ooxml
27
27
  pnpm add @silurus/ooxml
28
28
  ```
29
29
 
30
- > **Bundler note**: this package embeds `.wasm` files. With Vite add [`vite-plugin-wasm`](https://github.com/Menci/vite-plugin-wasm); with webpack use [`experiments.asyncWebAssembly`](https://webpack.js.org/configuration/experiments/).
31
-
32
- > **Bundle size note**: the package is ESM-only (`.mjs`). npm's *Unpacked Size* sums all four entry bundles, including the **opt-in** math engine (MathJax + STIX Two Math, ~4 MB). What actually lands in your app is much smaller — import only the format you need (e.g. `@silurus/ooxml/pptx`). The math engine is a **separate entry** (`@silurus/ooxml/math`): it is bundled **only if you import it and pass it to a viewer** (see [Rendering equations](#rendering-equations)). Viewers that never receive a `math` engine tree-shake the ~4 MB away entirely.
30
+ > **Bundler note**: the Rust parsers ship as real `.wasm` asset files next to the
31
+ > JavaScript, referenced with the standard `new URL('…', import.meta.url)` form
32
+ > and fetched (streaming-compiled) at load time. Vite, webpack 5, Rollup,
33
+ > esbuild and Parcel detect that reference and copy the asset automatically — no
34
+ > extra WebAssembly plugin is required. If your bundler cannot emit the asset (or
35
+ > you want to serve the parser WASM from a CDN or a path you control), pass its
36
+ > URL via the `wasmUrl` load option:
37
+ >
38
+ > ```typescript
39
+ > new DocxViewer(canvas, { wasmUrl: 'https://cdn.example.com/docx_parser_bg.wasm' });
40
+ > ```
41
+
42
+ > **Bundle size note**: the package is ESM-only (`.mjs`). npm's *Unpacked Size* sums every entry bundle **and** the standalone MathJax + STIX Two Math asset (`mathjax-stix2.js`, ~3 MB) that ships in the tarball, so the reported figure is much larger than any single app build. What actually lands in your app is smaller on two counts: import only the format you need (e.g. `@silurus/ooxml/pptx`), and the math engine is a **separate entry** (`@silurus/ooxml/math`). Its main-thread chunk is a ~1 KB loader that references the ~3 MB engine asset as a **sibling file** (not an inline data URL): the engine is fetched **lazily, only when a document actually contains equations** — and only if you imported `@silurus/ooxml/math` and passed it to a viewer in the first place (see [Rendering equations](#rendering-equations)). Never import the `math` entry and the loader chunk never enters your graph at all.
33
43
 
34
44
  ---
35
45
 
@@ -62,11 +72,14 @@ pptx.nextSlide();
62
72
 
63
73
  OMML equations (`m:oMath` / `m:oMathPara`) in `.docx`, `.pptx` and `.xlsx` are rendered with
64
74
  [MathJax](https://www.mathjax.org/) + [STIX Two Math](https://github.com/stipub/stixfonts).
65
- That engine is ~4 MB, so it is **opt-in**: import the `math` engine from the separate
75
+ That engine is ~3 MB, so it is **opt-in**: import the `math` engine from the separate
66
76
  `@silurus/ooxml/math` entry and pass it to the viewer. Pass it and equations render;
67
- omit it and the engine is referenced nowhere, so a bundler **tree-shakes the ~4 MB
68
- away entirely** (equations are simply skipped). It is fully self-contained: no
69
- network, no cross-origin requests.
77
+ omit it and the engine is referenced nowhere, so a bundler leaves it out of your build
78
+ entirely (equations are simply skipped). When you *do* pass it, the ~3 MB engine ships
79
+ as a **standalone asset file** next to the bundle rather than an inline data URL, and is
80
+ fetched **on demand — only the first time a document actually contains an equation**, so
81
+ equation-free documents never pay for it. It is fully self-contained: served from your own
82
+ origin, no cross-origin requests.
70
83
 
71
84
  ```typescript
72
85
  import { DocxViewer } from '@silurus/ooxml/docx';
@@ -125,6 +138,97 @@ Notes:
125
138
  transferred back as an `ImageBitmap`, so a single render can be marginally
126
139
  slower than `mode: 'main'`. Choose it for non-blocking UI, not raw speed.
127
140
 
141
+ ### Continuous scroll viewers
142
+
143
+ `DocxScrollViewer` and `PptxScrollViewer` render the whole document as one
144
+ vertically-scrolling, PDF-reader-style surface instead of a single page/slide at
145
+ a time. Unlike `DocxViewer` / `PptxViewer` (which take a `<canvas>`), the scroll
146
+ viewers take a **container** `<div>` — they own the scroll host, virtualize the
147
+ page/slide list (only the visible window plus a small overscan is mounted), and
148
+ recycle canvases as you scroll.
149
+
150
+ ```typescript
151
+ import { DocxScrollViewer } from '@silurus/ooxml/docx';
152
+
153
+ const container = document.getElementById('docx-scroll') as HTMLElement;
154
+ const viewer = new DocxScrollViewer(container);
155
+ await viewer.load('/document.docx');
156
+ // viewer.scrollToPage(3);
157
+ // viewer.pageCount, viewer.topVisiblePage
158
+ ```
159
+
160
+ ```typescript
161
+ import { PptxScrollViewer } from '@silurus/ooxml/pptx';
162
+
163
+ const container = document.getElementById('pptx-scroll') as HTMLElement;
164
+ const viewer = new PptxScrollViewer(container);
165
+ await viewer.load('/deck.pptx');
166
+ // viewer.scrollToSlide(2);
167
+ // viewer.slideCount, viewer.topVisibleSlide
168
+ ```
169
+
170
+ The container must have a bounded height (e.g. `height: 100vh` or a flex child)
171
+ so the viewer can size its scroll host to it. Base zoom fits the first page/slide
172
+ width to the container width and re-fits on resize; a `0`-width container defers
173
+ layout until it has width. Call `destroy()` to tear down (a self-loaded engine is
174
+ destroyed with it; an injected one is not — see below).
175
+
176
+ **Desk appearance.** The viewer paints each page/slide on its own white canvas
177
+ with a soft drop shadow, over a transparent "desk". Style the desk and the sheet
178
+ gaps without any wrapper CSS:
179
+
180
+ ```typescript
181
+ const viewer = new DocxScrollViewer(container, {
182
+ background: '#f3f4f6', // the desk behind / between pages
183
+ gap: 24, // vertical gap between pages
184
+ paddingTop: 32, // desk padding above the first page
185
+ pageShadow: '0 0 0 1px #c8ccd0', // crisp 1px "border" look (box-shadow never shifts layout)
186
+ // pageShadow: false, // flat pages, no shadow
187
+ });
188
+ ```
189
+
190
+ `paddingBottom`, `paddingLeft` and `paddingRight` each default to `gap`, so the
191
+ sheet sits inside a uniform desk margin; pass `0` for a flush edge.
192
+
193
+ **Zoom.** `Ctrl`/`⌘` + mouse-wheel (and trackpad pinch) zooms the surface;
194
+ bare-wheel still scrolls natively. Zoom is flicker-free — a rapid gesture shows a
195
+ CSS preview and settles into a crisp re-render when it pauses. Bounds are the
196
+ absolute scale factors `zoomMin` / `zoomMax` (default `0.1` / `4`), and
197
+ `setScale(scale)` sets it programmatically. Pass `enableZoom: false` to disable.
198
+
199
+ **Text selection** (main mode only). Pass `enableTextSelection: true` to overlay
200
+ a transparent, selectable text layer per page/slide for native copy. In
201
+ `mode: 'worker'` the overlay stays empty (the per-run geometry cannot cross the
202
+ worker boundary) and the viewer logs one warning — use the default `mode: 'main'`
203
+ for selectable text.
204
+
205
+ **Master–detail / shared engine.** Inject an already-loaded headless engine so a
206
+ paged viewer and a scroll viewer (or several panes) share **one** parse. When you
207
+ inject, `load()` is unsupported (the engine is already loaded), the engine's own
208
+ `mode` wins, and `destroy()` leaves the injected engine intact — the caller owns
209
+ its lifecycle:
210
+
211
+ ```typescript
212
+ import { DocxDocument, DocxScrollViewer } from '@silurus/ooxml/docx';
213
+
214
+ const doc = await DocxDocument.load('/document.docx'); // parse once
215
+ const scroll = new DocxScrollViewer(container, { document: doc });
216
+ // ...also drive a thumbnail grid, a paged view, etc. from the same `doc`.
217
+ scroll.destroy(); // the injected `doc` is NOT destroyed — you own it
218
+ doc.destroy(); // release it yourself when every pane is gone
219
+ ```
220
+
221
+ `PptxScrollViewer` takes the same shape with `{ presentation: pres }`
222
+ (`await PptxPresentation.load(...)`).
223
+
224
+ Both viewers also expose `relayout()` (force a re-fit when the container resizes
225
+ in a way a `ResizeObserver` cannot see — e.g. a late web-font load),
226
+ `onVisiblePageChange` / `onVisibleSlideChange` (fires when the top-most visible
227
+ page/slide changes), and `onError` (async per-page render failures are routed
228
+ here instead of crashing the scroll loop). The parse/render knobs from the
229
+ headless engines (`mode`, `useGoogleFonts`, `maxZipEntryBytes`, `math`, `dpr`)
230
+ are accepted too.
231
+
128
232
  ---
129
233
 
130
234
  <details>
@@ -200,7 +304,7 @@ All three formats follow the same shape: the worker parses the `.docx` / `.xlsx`
200
304
  <summary><strong>React 19</strong></summary>
201
305
 
202
306
  ```tsx
203
- // React 19.1 — vite-plugin-wasm required in vite.config.ts
307
+ // React 19.1 — Vite copies the parser .wasm asset automatically; no extra plugin needed.
204
308
  import { useEffect, useRef, useState } from 'react';
205
309
  import { PptxViewer } from '@silurus/ooxml/pptx';
206
310
 
@@ -459,7 +563,7 @@ export const PptxViewerComponent = component$<{ src: string }>(({ src }) => {
459
563
  | | Math equations (OMML `m:oMath` / `m:oMathPara`, rendered via MathJax — opt-in `@silurus/ooxml/math`) | ✅ |
460
564
  | | Images (inline and anchored, with text wrap) | ✅ |
461
565
  | | SVG images (`asvg:svgBlip` MS-2016 extension — vector drawn from the embedded `.svg`, raster fallback) | ✅ |
462
- | | Text boxes / drawing shapes (`wps:txbx`, `a:prstGeom` — 186 preset geometries via the shared engine; connector arrow heads `headEnd` / `tailEnd` (§20.1.8.3) and `prstDash` dash patterns (§20.1.8.48)) | ✅ |
566
+ | | Text boxes / drawing shapes (`wps:txbx`, `a:prstGeom` — 186 preset geometries via the shared engine; connector arrow heads `headEnd` / `tailEnd` (§20.1.8.3) and `prstDash` dash patterns (§20.1.8.48)). Text-box paragraphs run through the **same line-layout engine as body text**, so kinsoku 行頭/行末禁則 (§17.15.1.58–60), UAX#9 bidi (`w:bidi`, §17.3.1.6), justification (§17.18.44) and tab stops (§17.3.1.37) all apply inside a box | ✅ |
463
567
  | | WMF metafile images (legacy vector, incl. inside text boxes) — rasterized via a built-in player (window mapping, pens/brushes, poly/rect); true EMF detected but not yet rendered | ✅ |
464
568
  | **Advanced** | Footnotes — reference markers + bottom-of-page bodies with separator rule, numbered (`w:footnoteReference` / `w:footnoteRef`, §17.11) | ✅ |
465
569
  | | Endnotes — reference markers + bodies at document end (`w:endnoteReference`, §17.11) | ✅ |
@@ -468,6 +572,7 @@ export const PptxViewerComponent = component$<{ src: string }>(({ src }) => {
468
572
  | | Comments — author / date / text via the document model (`doc.comments`, §17.13.4; not drawn on the page) | ✅ |
469
573
  | | Mail merge fields | ❌ Not planned |
470
574
  | **Interaction** | Text selection (transparent overlay, native copy) | ✅ |
575
+ | | Continuous scroll viewer (`DocxScrollViewer` — virtualized page list, desk background / shadow, Ctrl/⌘+wheel zoom, engine injection) | ✅ |
471
576
 
472
577
  ---
473
578
 
@@ -612,6 +717,7 @@ export const PptxViewerComponent = component$<{ src: string }>(({ src }) => {
612
717
  | | Font scheme (`+mj-lt`, `+mn-lt`) | ✅ |
613
718
  | | lumMod / lumOff / alpha transforms | ✅ |
614
719
  | **Interaction** | Text selection (transparent overlay, native copy) | ✅ |
720
+ | | Continuous scroll viewer (`PptxScrollViewer` — virtualized slide list, desk background / shadow, Ctrl/⌘+wheel zoom, engine injection) | ✅ |
615
721
 
616
722
  ---
617
723