@silurus/ooxml 0.75.3 → 0.75.5

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
@@ -30,7 +30,7 @@ So I'm building this library with AI coding agents, spec-first, and keeping it f
30
30
 
31
31
  A browser-based viewer for Office Open XML documents that renders to an HTML Canvas element.
32
32
  The parsers are written in Rust and compiled to WebAssembly; the renderers use the Canvas 2D API.
33
- Each format also exposes a headless engine (`DocxDocument` / `XlsxWorkbook` / `PptxPresentation`) that renders into any caller-supplied canvas, so you can compose your own UI — scroll views, thumbnail grids, master-detail panes — instead of being locked into the built-in viewer. See the `Examples` section in [the Storybook demo](https://ooxml.silurus.dev/storybook/).
33
+ Each format also exposes a headless engine (`DocxDocument` / `XlsxWorkbook` / `PptxPresentation`) that renders into any caller-supplied canvas, so you can compose your own UI — scroll views, thumbnail grids, master-detail panes — instead of being locked into the built-in viewer. See the [live framework examples](https://ooxml.silurus.dev/frameworks/) for runnable React, Vue, Svelte, and Solid projects.
34
34
 
35
35
  ## Project scope: read-only viewing
36
36
 
@@ -61,8 +61,9 @@ pnpm add @silurus/ooxml
61
61
  > `new URL` asset references are not processed
62
62
  > ([esbuild#795](https://github.com/evanw/esbuild/issues/795)). Copy the
63
63
  > `.wasm` into your served output and point the viewer at it with the
64
- > `wasmUrl` load option see the [Angular example](#framework-examples) for
65
- > the two-step setup.
64
+ > `wasmUrl` load option. For Angular CLI, copy the required
65
+ > `*_parser_bg.wasm` asset from `node_modules/@silurus/ooxml/dist` into the
66
+ > served output and pass its public URL to the viewer.
66
67
  >
67
68
  > `wasmUrl` also serves the parser WASM from a CDN or any path you control:
68
69
  >
@@ -394,256 +395,13 @@ All three formats follow the same shape: the worker parses the `.docx` / `.xlsx`
394
395
 
395
396
  ## Framework Examples
396
397
 
397
- <details>
398
- <summary><strong>React 19</strong></summary>
399
-
400
- ```tsx
401
- // React 19.1 Vite copies the parser .wasm asset automatically; no extra plugin needed.
402
- import { useEffect, useRef, useState } from 'react';
403
- import { PptxViewer } from '@silurus/ooxml/pptx';
404
-
405
- export function PptxViewerComponent({ src }: { src: string }) {
406
- const canvasRef = useRef<HTMLCanvasElement>(null);
407
- const viewerRef = useRef<PptxViewer | null>(null);
408
- const [slide, setSlide] = useState({ current: 0, total: 0 });
409
-
410
- useEffect(() => {
411
- const canvas = canvasRef.current;
412
- if (!canvas) return;
413
-
414
- const viewer = new PptxViewer(canvas, {
415
- onSlideChange: (i, total) => setSlide({ current: i, total }),
416
- });
417
- viewerRef.current = viewer;
418
- viewer.load(src);
419
- }, [src]);
420
-
421
- return (
422
- <div>
423
- <canvas ref={canvasRef} style={{ width: 800 }} />
424
- <button onClick={() => viewerRef.current?.prevSlide()}>‹ Prev</button>
425
- <span> {slide.current + 1} / {slide.total} </span>
426
- <button onClick={() => viewerRef.current?.nextSlide()}>Next ›</button>
427
- </div>
428
- );
429
- }
430
- ```
431
-
432
- </details>
433
-
434
- <details>
435
- <summary><strong>Vue 3.5</strong></summary>
436
-
437
- ```vue
438
- <!-- Vue 3.5 — useTemplateRef is a 3.5+ feature -->
439
- <script setup lang="ts">
440
- import { useTemplateRef, onMounted, ref } from 'vue';
441
- import { PptxViewer } from '@silurus/ooxml/pptx';
442
-
443
- const props = defineProps<{ src: string }>();
444
-
445
- const canvas = useTemplateRef<HTMLCanvasElement>('canvas');
446
- let viewer: PptxViewer | null = null;
447
- const current = ref(0);
448
- const total = ref(0);
449
-
450
- onMounted(async () => {
451
- viewer = new PptxViewer(canvas.value as HTMLCanvasElement, {
452
- onSlideChange: (i, t) => { current.value = i; total.value = t; },
453
- });
454
- await viewer.load(props.src);
455
- });
456
- </script>
457
-
458
- <template>
459
- <div>
460
- <canvas ref="canvas" style="width: 800px" />
461
- <button @click="viewer?.prevSlide()">‹ Prev</button>
462
- <span> {{ current + 1 }} / {{ total }} </span>
463
- <button @click="viewer?.nextSlide()">Next ›</button>
464
- </div>
465
- </template>
466
- ```
467
-
468
- </details>
469
-
470
- <details>
471
- <summary><strong>Angular 19</strong></summary>
472
-
473
- The Angular CLI's esbuild-based builder does not process the `new URL('…', import.meta.url)`
474
- asset reference the parsers use ([angular-cli#22388](https://github.com/angular/angular-cli/issues/22388)),
475
- so the `.wasm` never reaches the build output — and under `ng serve` the dependency
476
- optimizer additionally rewrites the reference into its own cache path. **Both steps
477
- below are required** (the asset copy alone fixes only production builds; `ng serve`
478
- still 404s without `wasmUrl`):
479
-
480
- ```jsonc
481
- // angular.json — copy the parser WASM into the served root
482
- // (restart `ng serve` after editing this file)
483
- "architect": {
484
- "build": {
485
- "options": {
486
- "assets": [
487
- { "glob": "*_parser_bg.wasm", "input": "node_modules/@silurus/ooxml/dist", "output": "/" },
488
- { "glob": "**/*", "input": "public" }
489
- ]
490
- }
491
- }
492
- }
493
- ```
494
-
495
- ```typescript
496
- // Angular 19 — standalone component with signal-based state
497
- import {
498
- Component, ElementRef, viewChild,
499
- signal, AfterViewInit,
500
- } from '@angular/core';
501
- import { PptxViewer } from '@silurus/ooxml/pptx';
502
-
503
- @Component({
504
- selector: 'app-pptx-viewer',
505
- standalone: true,
506
- template: `
507
- <div>
508
- <canvas #canvas style="width: 800px"></canvas>
509
- <button (click)="prev()">‹ Prev</button>
510
- <span> {{ current() + 1 }} / {{ total() }} </span>
511
- <button (click)="next()">Next ›</button>
512
- </div>
513
- `,
514
- })
515
- export class PptxViewerComponent implements AfterViewInit {
516
- canvasEl = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');
517
- current = signal(0);
518
- total = signal(0);
519
- private viewer?: PptxViewer;
520
-
521
- ngAfterViewInit(): void {
522
- this.viewer = new PptxViewer(this.canvasEl().nativeElement, {
523
- wasmUrl: '/pptx_parser_bg.wasm',
524
- onSlideChange: (i, t) => { this.current.set(i); this.total.set(t); },
525
- });
526
- this.viewer.load('/deck.pptx');
527
- }
528
-
529
- prev(): void { this.viewer?.prevSlide(); }
530
- next(): void { this.viewer?.nextSlide(); }
531
- }
532
- ```
533
-
534
- > The `*_parser_bg.wasm` glob copies all three parsers; narrow it to
535
- > `pptx_parser_bg.wasm` if you only use one format. If you deploy under a
536
- > non-root `base href`, adjust `wasmUrl` so it resolves under your base (a
537
- > relative `wasmUrl` is resolved against the document URL).
538
-
539
- </details>
540
-
541
- <details>
542
- <summary><strong>Svelte 5</strong></summary>
543
-
544
- ```svelte
545
- <!-- Svelte 5 — runes syntax ($props, $state) -->
546
- <script lang="ts">
547
- import { onMount } from 'svelte';
548
- import { PptxViewer } from '@silurus/ooxml/pptx';
549
-
550
- let { src }: { src: string } = $props();
551
-
552
- let canvas: HTMLCanvasElement;
553
- let viewer: PptxViewer;
554
- let current = $state(0);
555
- let total = $state(0);
556
-
557
- onMount(async () => {
558
- viewer = new PptxViewer(canvas, {
559
- onSlideChange: (i, t) => { current = i; total = t; },
560
- });
561
- await viewer.load(src);
562
- });
563
- </script>
564
-
565
- <div>
566
- <canvas bind:this={canvas} style="width: 800px"></canvas>
567
- <button onclick={() => viewer?.prevSlide()}>‹ Prev</button>
568
- <span> {current + 1} / {total} </span>
569
- <button onclick={() => viewer?.nextSlide()}>Next ›</button>
570
- </div>
571
- ```
572
-
573
- </details>
574
-
575
- <details>
576
- <summary><strong>SolidJS 1.9</strong></summary>
577
-
578
- ```tsx
579
- // SolidJS 1.9
580
- import { createSignal, onMount, onCleanup } from 'solid-js';
581
- import { PptxViewer } from '@silurus/ooxml/pptx';
582
-
583
- export function PptxViewerComponent(props: { src: string }) {
584
- let canvasEl!: HTMLCanvasElement;
585
- let viewer: PptxViewer | undefined;
586
- const [current, setCurrent] = createSignal(0);
587
- const [total, setTotal ] = createSignal(0);
588
-
589
- onMount(async () => {
590
- viewer = new PptxViewer(canvasEl, {
591
- onSlideChange: (i, t) => { setCurrent(i); setTotal(t); },
592
- });
593
- await viewer.load(props.src);
594
- });
595
-
596
- onCleanup(() => { /* viewer?.destroy?.() */ });
597
-
598
- return (
599
- <div>
600
- <canvas ref={canvasEl} style={{ width: '800px' }} />
601
- <button onClick={() => viewer?.prevSlide()}>‹ Prev</button>
602
- <span> {current() + 1} / {total()} </span>
603
- <button onClick={() => viewer?.nextSlide()}>Next ›</button>
604
- </div>
605
- );
606
- }
607
- ```
608
-
609
- </details>
610
-
611
- <details>
612
- <summary><strong>Qwik 2</strong></summary>
613
-
614
- ```tsx
615
- // Qwik 2.0 — dynamic import to keep WASM out of SSR bundle
616
- import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
617
- import type { PptxViewer as PptxViewerType } from '@silurus/ooxml/pptx';
618
-
619
- export const PptxViewerComponent = component$<{ src: string }>(({ src }) => {
620
- const canvasRef = useSignal<HTMLCanvasElement>();
621
- const current = useSignal(0);
622
- const total = useSignal(0);
623
- let viewer: PptxViewerType | undefined;
624
-
625
- // useVisibleTask$ runs only in the browser, never during SSR
626
- useVisibleTask$(async () => {
627
- if (!canvasRef.value) return;
628
- const { PptxViewer } = await import('@silurus/ooxml/pptx');
629
- viewer = new PptxViewer(canvasRef.value, {
630
- onSlideChange: (i, t) => { current.value = i; total.value = t; },
631
- });
632
- await viewer.load(src);
633
- });
634
-
635
- return (
636
- <div>
637
- <canvas ref={canvasRef} style={{ width: '800px' }} />
638
- <button onClick$={() => viewer?.prevSlide()}>‹ Prev</button>
639
- <span> {current.value + 1} / {total.value} </span>
640
- <button onClick$={() => viewer?.nextSlide()}>Next ›</button>
641
- </div>
642
- );
643
- });
644
- ```
645
-
646
- </details>
398
+ Runnable TypeScript projects are available for
399
+ [React](https://ooxml.silurus.dev/frameworks/react/),
400
+ [Vue](https://ooxml.silurus.dev/frameworks/vue/),
401
+ [Svelte](https://ooxml.silurus.dev/frameworks/svelte/), and
402
+ [Solid](https://ooxml.silurus.dev/frameworks/solid/). Each guide embeds the
403
+ complete StackBlitz project and supports selecting a local DOCX, XLSX, or PPTX
404
+ file without uploading it.
647
405
 
648
406
  ---
649
407
 
@@ -689,7 +447,7 @@ export const PptxViewerComponent = component$<{ src: string }>(({ src }) => {
689
447
  | | Math equations (OMML `m:oMath` / `m:oMathPara`, rendered via MathJax — opt-in `@silurus/ooxml/math`) | ✅ |
690
448
  | | Images (inline and anchored, with text wrap) | ✅ |
691
449
  | | SVG images (`asvg:svgBlip` MS-2016 extension — vector drawn from the embedded `.svg`, raster fallback) | ✅ |
692
- | | 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 | ✅ |
450
+ | | Text boxes / drawing shapes (inline and anchored `wps:wsp` / `wps:txbx`, including solid, gradient, and image fills; `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 | ✅ |
693
451
  | | WMF **and EMF** metafile images (legacy vector, incl. inside text boxes) — rasterized via a built-in player: window→viewport mapping (MS-EMF map modes, world transform), pens/brushes, poly/rect/ellipse, text-out, path clipping, and embedded DIB blits | ✅ |
694
452
  | | OLE embedded objects (`w:object` — the baked VML `v:imagedata` preview is drawn; the embedded app is not run) | ✅ |
695
453
  | **Advanced** | Footnotes — reference markers + bottom-of-page bodies with separator rule, numbered (`w:footnoteReference` / `w:footnoteRef`, §17.11) | ✅ |