@silurus/ooxml 0.69.0 → 0.70.1

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,23 @@ 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 and
33
+ > Parcel detect that reference and copy the asset automatically — no extra
34
+ > WebAssembly plugin is required. esbuild and the Angular CLI (whose application
35
+ > builder is esbuild-based) do **not** process that reference
36
+ > ([esbuild#795](https://github.com/evanw/esbuild/issues/795)): copy the `.wasm`
37
+ > into your served output yourself and point the viewer at it with the `wasmUrl`
38
+ > load option — see the [Angular example](#framework-examples) for the two-step
39
+ > setup. `wasmUrl` also serves the parser WASM from a CDN or any path you
40
+ > control:
41
+ >
42
+ > ```typescript
43
+ > new DocxViewer(canvas, { wasmUrl: 'https://cdn.example.com/docx_parser_bg.wasm' });
44
+ > ```
45
+
46
+ > **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
47
 
34
48
  ---
35
49
 
@@ -62,11 +76,14 @@ pptx.nextSlide();
62
76
 
63
77
  OMML equations (`m:oMath` / `m:oMathPara`) in `.docx`, `.pptx` and `.xlsx` are rendered with
64
78
  [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
79
+ That engine is ~3 MB, so it is **opt-in**: import the `math` engine from the separate
66
80
  `@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.
81
+ omit it and the engine is referenced nowhere, so a bundler leaves it out of your build
82
+ entirely (equations are simply skipped). When you *do* pass it, the ~3 MB engine ships
83
+ as a **standalone asset file** next to the bundle rather than an inline data URL, and is
84
+ fetched **on demand — only the first time a document actually contains an equation**, so
85
+ equation-free documents never pay for it. It is fully self-contained: served from your own
86
+ origin, no cross-origin requests.
70
87
 
71
88
  ```typescript
72
89
  import { DocxViewer } from '@silurus/ooxml/docx';
@@ -125,6 +142,97 @@ Notes:
125
142
  transferred back as an `ImageBitmap`, so a single render can be marginally
126
143
  slower than `mode: 'main'`. Choose it for non-blocking UI, not raw speed.
127
144
 
145
+ ### Continuous scroll viewers
146
+
147
+ `DocxScrollViewer` and `PptxScrollViewer` render the whole document as one
148
+ vertically-scrolling, PDF-reader-style surface instead of a single page/slide at
149
+ a time. Unlike `DocxViewer` / `PptxViewer` (which take a `<canvas>`), the scroll
150
+ viewers take a **container** `<div>` — they own the scroll host, virtualize the
151
+ page/slide list (only the visible window plus a small overscan is mounted), and
152
+ recycle canvases as you scroll.
153
+
154
+ ```typescript
155
+ import { DocxScrollViewer } from '@silurus/ooxml/docx';
156
+
157
+ const container = document.getElementById('docx-scroll') as HTMLElement;
158
+ const viewer = new DocxScrollViewer(container);
159
+ await viewer.load('/document.docx');
160
+ // viewer.scrollToPage(3);
161
+ // viewer.pageCount, viewer.topVisiblePage
162
+ ```
163
+
164
+ ```typescript
165
+ import { PptxScrollViewer } from '@silurus/ooxml/pptx';
166
+
167
+ const container = document.getElementById('pptx-scroll') as HTMLElement;
168
+ const viewer = new PptxScrollViewer(container);
169
+ await viewer.load('/deck.pptx');
170
+ // viewer.scrollToSlide(2);
171
+ // viewer.slideCount, viewer.topVisibleSlide
172
+ ```
173
+
174
+ The container must have a bounded height (e.g. `height: 100vh` or a flex child)
175
+ so the viewer can size its scroll host to it. Base zoom fits the first page/slide
176
+ width to the container width and re-fits on resize; a `0`-width container defers
177
+ layout until it has width. Call `destroy()` to tear down (a self-loaded engine is
178
+ destroyed with it; an injected one is not — see below).
179
+
180
+ **Desk appearance.** The viewer paints each page/slide on its own white canvas
181
+ with a soft drop shadow, over a transparent "desk". Style the desk and the sheet
182
+ gaps without any wrapper CSS:
183
+
184
+ ```typescript
185
+ const viewer = new DocxScrollViewer(container, {
186
+ background: '#f3f4f6', // the desk behind / between pages
187
+ gap: 24, // vertical gap between pages
188
+ paddingTop: 32, // desk padding above the first page
189
+ pageShadow: '0 0 0 1px #c8ccd0', // crisp 1px "border" look (box-shadow never shifts layout)
190
+ // pageShadow: false, // flat pages, no shadow
191
+ });
192
+ ```
193
+
194
+ `paddingBottom`, `paddingLeft` and `paddingRight` each default to `gap`, so the
195
+ sheet sits inside a uniform desk margin; pass `0` for a flush edge.
196
+
197
+ **Zoom.** `Ctrl`/`⌘` + mouse-wheel (and trackpad pinch) zooms the surface;
198
+ bare-wheel still scrolls natively. Zoom is flicker-free — a rapid gesture shows a
199
+ CSS preview and settles into a crisp re-render when it pauses. Bounds are the
200
+ absolute scale factors `zoomMin` / `zoomMax` (default `0.1` / `4`), and
201
+ `setScale(scale)` sets it programmatically. Pass `enableZoom: false` to disable.
202
+
203
+ **Text selection** (main mode only). Pass `enableTextSelection: true` to overlay
204
+ a transparent, selectable text layer per page/slide for native copy. In
205
+ `mode: 'worker'` the overlay stays empty (the per-run geometry cannot cross the
206
+ worker boundary) and the viewer logs one warning — use the default `mode: 'main'`
207
+ for selectable text.
208
+
209
+ **Master–detail / shared engine.** Inject an already-loaded headless engine so a
210
+ paged viewer and a scroll viewer (or several panes) share **one** parse. When you
211
+ inject, `load()` is unsupported (the engine is already loaded), the engine's own
212
+ `mode` wins, and `destroy()` leaves the injected engine intact — the caller owns
213
+ its lifecycle:
214
+
215
+ ```typescript
216
+ import { DocxDocument, DocxScrollViewer } from '@silurus/ooxml/docx';
217
+
218
+ const doc = await DocxDocument.load('/document.docx'); // parse once
219
+ const scroll = new DocxScrollViewer(container, { document: doc });
220
+ // ...also drive a thumbnail grid, a paged view, etc. from the same `doc`.
221
+ scroll.destroy(); // the injected `doc` is NOT destroyed — you own it
222
+ doc.destroy(); // release it yourself when every pane is gone
223
+ ```
224
+
225
+ `PptxScrollViewer` takes the same shape with `{ presentation: pres }`
226
+ (`await PptxPresentation.load(...)`).
227
+
228
+ Both viewers also expose `relayout()` (force a re-fit when the container resizes
229
+ in a way a `ResizeObserver` cannot see — e.g. a late web-font load),
230
+ `onVisiblePageChange` / `onVisibleSlideChange` (fires when the top-most visible
231
+ page/slide changes), and `onError` (async per-page render failures are routed
232
+ here instead of crashing the scroll loop). The parse/render knobs from the
233
+ headless engines (`mode`, `useGoogleFonts`, `maxZipEntryBytes`, `math`, `dpr`)
234
+ are accepted too.
235
+
128
236
  ---
129
237
 
130
238
  <details>
@@ -200,7 +308,7 @@ All three formats follow the same shape: the worker parses the `.docx` / `.xlsx`
200
308
  <summary><strong>React 19</strong></summary>
201
309
 
202
310
  ```tsx
203
- // React 19.1 — vite-plugin-wasm required in vite.config.ts
311
+ // React 19.1 — Vite copies the parser .wasm asset automatically; no extra plugin needed.
204
312
  import { useEffect, useRef, useState } from 'react';
205
313
  import { PptxViewer } from '@silurus/ooxml/pptx';
206
314
 
@@ -272,6 +380,28 @@ onMounted(async () => {
272
380
  <details>
273
381
  <summary><strong>Angular 19</strong></summary>
274
382
 
383
+ The Angular CLI's esbuild-based builder does not process the `new URL('…', import.meta.url)`
384
+ asset reference the parsers use ([angular-cli#22388](https://github.com/angular/angular-cli/issues/22388)),
385
+ so the `.wasm` never reaches the build output — and under `ng serve` the dependency
386
+ optimizer additionally rewrites the reference into its own cache path. **Both steps
387
+ below are required** (the asset copy alone fixes only production builds; `ng serve`
388
+ still 404s without `wasmUrl`):
389
+
390
+ ```jsonc
391
+ // angular.json — copy the parser WASM into the served root
392
+ // (restart `ng serve` after editing this file)
393
+ "architect": {
394
+ "build": {
395
+ "options": {
396
+ "assets": [
397
+ { "glob": "*_parser_bg.wasm", "input": "node_modules/@silurus/ooxml/dist", "output": "/" },
398
+ { "glob": "**/*", "input": "public" }
399
+ ]
400
+ }
401
+ }
402
+ }
403
+ ```
404
+
275
405
  ```typescript
276
406
  // Angular 19 — standalone component with signal-based state
277
407
  import {
@@ -300,6 +430,7 @@ export class PptxViewerComponent implements AfterViewInit {
300
430
 
301
431
  ngAfterViewInit(): void {
302
432
  this.viewer = new PptxViewer(this.canvasEl().nativeElement, {
433
+ wasmUrl: '/pptx_parser_bg.wasm',
303
434
  onSlideChange: (i, t) => { this.current.set(i); this.total.set(t); },
304
435
  });
305
436
  this.viewer.load('/deck.pptx');
@@ -310,7 +441,10 @@ export class PptxViewerComponent implements AfterViewInit {
310
441
  }
311
442
  ```
312
443
 
313
- > Add `"allowSyntheticDefaultImports": true` and configure `@angular-builders/custom-webpack` (or use `esbuild` builder) with WASM support in your Angular workspace.
444
+ > The `*_parser_bg.wasm` glob copies all three parsers; narrow it to
445
+ > `pptx_parser_bg.wasm` if you only use one format. If you deploy under a
446
+ > non-root `base href`, adjust `wasmUrl` so it resolves under your base (a
447
+ > relative `wasmUrl` is resolved against the document URL).
314
448
 
315
449
  </details>
316
450
 
@@ -459,7 +593,7 @@ export const PptxViewerComponent = component$<{ src: string }>(({ src }) => {
459
593
  | | Math equations (OMML `m:oMath` / `m:oMathPara`, rendered via MathJax — opt-in `@silurus/ooxml/math`) | ✅ |
460
594
  | | Images (inline and anchored, with text wrap) | ✅ |
461
595
  | | 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)) | ✅ |
596
+ | | 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
597
  | | 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
598
  | **Advanced** | Footnotes — reference markers + bottom-of-page bodies with separator rule, numbered (`w:footnoteReference` / `w:footnoteRef`, §17.11) | ✅ |
465
599
  | | Endnotes — reference markers + bodies at document end (`w:endnoteReference`, §17.11) | ✅ |
@@ -468,6 +602,7 @@ export const PptxViewerComponent = component$<{ src: string }>(({ src }) => {
468
602
  | | Comments — author / date / text via the document model (`doc.comments`, §17.13.4; not drawn on the page) | ✅ |
469
603
  | | Mail merge fields | ❌ Not planned |
470
604
  | **Interaction** | Text selection (transparent overlay, native copy) | ✅ |
605
+ | | Continuous scroll viewer (`DocxScrollViewer` — virtualized page list, desk background / shadow, Ctrl/⌘+wheel zoom, engine injection) | ✅ |
471
606
 
472
607
  ---
473
608
 
@@ -612,6 +747,7 @@ export const PptxViewerComponent = component$<{ src: string }>(({ src }) => {
612
747
  | | Font scheme (`+mj-lt`, `+mn-lt`) | ✅ |
613
748
  | | lumMod / lumOff / alpha transforms | ✅ |
614
749
  | **Interaction** | Text selection (transparent overlay, native copy) | ✅ |
750
+ | | Continuous scroll viewer (`PptxScrollViewer` — virtualized slide list, desk background / shadow, Ctrl/⌘+wheel zoom, engine injection) | ✅ |
615
751
 
616
752
  ---
617
753