@silurus/ooxml 0.76.1 → 0.77.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
@@ -46,6 +46,11 @@ npm install @silurus/ooxml
46
46
  pnpm add @silurus/ooxml
47
47
  ```
48
48
 
49
+ > Upgrading from v0.76? Review the
50
+ > [v0.77 migration guide](https://ooxml.silurus.dev/announcements/v077-migration-guide/)
51
+ > for the XLSX selection, selection-context, MCP-tool, option/type, and Viewer
52
+ > error-handling changes.
53
+
49
54
  > **Bundler note**: the Rust parsers ship as real `.wasm` asset files next to the
50
55
  > JavaScript, referenced with the standard `new URL('…', import.meta.url)` form
51
56
  > and fetched (streaming-compiled) at load time. Verified to work with zero
@@ -71,7 +76,31 @@ pnpm add @silurus/ooxml
71
76
  > new DocxViewer(canvas, { wasmUrl: 'https://cdn.example.com/docx_parser_bg.wasm' });
72
77
  > ```
73
78
 
74
- > **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.
79
+ > **Bundle size note**: the package is ESM-only (`.mjs`). npm's *Unpacked
80
+ > Size* sums every entry bundle **and** the standalone MathJax + STIX Two Math
81
+ > asset, so the reported figure is much larger than any single app build. For
82
+ > v0.77.0, the complete npm package is approximately 11.3 MB unpacked (3.8 MB
83
+ > as the downloaded tarball), while a format-specific application graph is
84
+ > approximately:
85
+ >
86
+ > | Imported entry | Reachable assets | gzip | Includes |
87
+ > |---|---:|---:|---|
88
+ > | `@silurus/ooxml/docx` | 4.0 MB | 1.2 MB | DOCX renderer, parser WASM, and lazy worker |
89
+ > | `@silurus/ooxml/xlsx` | 2.6 MB | 0.82 MB | XLSX renderer, parser WASM, and lazy worker |
90
+ > | `@silurus/ooxml/pptx` | 2.5 MB | 0.78 MB | PPTX renderer, parser WASM, and lazy worker |
91
+ > | `@silurus/ooxml/math` | 3.1 MB | 1.1 MB | Optional MathJax + STIX Two Math engine |
92
+ >
93
+ > These are production-artifact estimates, not initial-load figures: each row
94
+ > sums all assets reachable from that entry, including parser WASM and worker
95
+ > chunks loaded on demand. Exact output varies by bundler and compression. Import
96
+ > only the format you need (for example, `@silurus/ooxml/pptx`) so the other
97
+ > formats can be tree-shaken. The math engine is a **separate entry** whose
98
+ > main-thread chunk is a ~1 KB loader referencing the ~3 MB engine as a
99
+ > **sibling file**, not an inline data URL. The engine is fetched lazily, only
100
+ > when a document contains equations, and only if you imported
101
+ > `@silurus/ooxml/math` and passed it to a viewer (see
102
+ > [Rendering equations](#rendering-equations)). Never import the `math` entry
103
+ > and the loader chunk never enters your graph at all.
75
104
 
76
105
  ---
77
106
 
@@ -92,6 +121,23 @@ docx.nextPage();
92
121
  const container = document.getElementById('xlsx-container') as HTMLElement;
93
122
  const xlsx = new XlsxViewer(container);
94
123
  await xlsx.load('/workbook.xlsx');
124
+ xlsx.setSelection('B2:D5'); // A1 strings describe geometry; the normalized upper-left is ActiveCell
125
+
126
+ // Excel keeps Selection and ActiveCell separate. Use structured state when the
127
+ // active cell or Shift-extension anchor is not the area's upper-left cell.
128
+ xlsx.setSelection({
129
+ areas: [{ kind: 'cells', top: 2, left: 2, bottom: 5, right: 4 }],
130
+ activeAreaIndex: 0,
131
+ activeCell: { row: 3, col: 3 },
132
+ extensionAnchor: { row: 2, col: 2 },
133
+ });
134
+
135
+ // Read-only, serializable context for an AI/MCP request. Populated cells are
136
+ // bounded and detached; formulas and Viewer-formatted display text are retained.
137
+ const context = xlsx.getSelectionContext({
138
+ maxCells: 1_000,
139
+ maxTextCharacters: 1_048_576,
140
+ });
95
141
 
96
142
  // XLSX active-sheet surface only — caller provides the <canvas>
97
143
  const sheetCanvas = document.getElementById('xlsx-canvas') as HTMLCanvasElement;
@@ -266,6 +312,74 @@ slides outside the mounted window. Set `findHighlightColors: { match, active }`
266
312
  on any viewer to override the two overlay backgrounds with CSS colors; use an
267
313
  alpha color when the canvas text should remain visible through the highlight.
268
314
 
315
+ **Selection context for AI/MCP.** Every Viewer exposes one read-only query,
316
+ `getSelectionContext()`, for handing the user's current focus to an external
317
+ assistant. The result is a detached, JSON-serializable snapshot discriminated by
318
+ `format` and `kind`; it never exposes a mutable document model or sends data over
319
+ the network. Text, run locators, and populated XLSX cells have hard resource
320
+ limits. Check `truncated` / `truncationReasons` before building a prompt.
321
+
322
+ ```typescript
323
+ const docx = new DocxViewer(docxCanvas, {
324
+ enableTextSelection: true,
325
+ enableElementSelection: true,
326
+ onSelectionContextChange(context) {
327
+ // kind === 'text' or 'element': selected text or a clicked drawing
328
+ updateAskAiButton(context);
329
+ },
330
+ });
331
+
332
+ const pptx = new PptxViewer(pptxCanvas, {
333
+ enableTextSelection: true,
334
+ enableElementSelection: true,
335
+ onSelectionContextChange(context) {
336
+ // Text selection wins while it exists; otherwise a slide-element click
337
+ // yields kind === 'element' with compact bounds, provenance and content.
338
+ updateAskAiButton(context);
339
+ },
340
+ });
341
+
342
+ const spreadsheet = new XlsxViewer(container, {
343
+ enableElementSelection: true,
344
+ onSelectionContextChange(context) {
345
+ updateAskAiButton(context);
346
+ },
347
+ onContextMenu: async ({ originalEvent, getContext }) => {
348
+ // Browser-menu control must happen synchronously, before the first await.
349
+ originalEvent.preventDefault();
350
+ const { clientX, clientY } = originalEvent;
351
+ const context = await getContext();
352
+ openContextMenu({ clientX, clientY, context });
353
+ },
354
+ });
355
+ const spreadsheetContext = spreadsheet.getSelectionContext({ maxCells: 1_000 });
356
+ // kind === 'range': selection state, values and formulas.
357
+ // kind === 'element': a clicked chart, picture or shape.
358
+ ```
359
+
360
+ All three formats expose the same `onSelectionContextChange(context)` handoff;
361
+ callers may instead query on demand. `enableElementSelection` is an independent,
362
+ explicit opt-in because it enables object selection and draws a non-editable outline
363
+ around the focused object; adding the callback alone never enables object
364
+ hit-testing. XLSX separately retains
365
+ `onSelectionStateChange` for canonical UI state such as ActiveCell and multiple
366
+ areas. Its context callback is frame-coalesced so drag selection does not build
367
+ one snapshot per pointer event.
368
+ `onContextMenu` is also common to every Viewer. It receives the real browser
369
+ event synchronously so the host can call `preventDefault()`, plus a clearly
370
+ asynchronous `getContext()` lookup for the right-click target. The lookup starts
371
+ on the first call and is memoized. Omitting the callback installs no listener and leaves
372
+ the native browser menu unchanged.
373
+ `DocxDocument.getElementContextAt()` and
374
+ `PptxPresentation.getElementContextAt()` provide the identical compact element
375
+ query for custom/headless page or slide surfaces in both modes. PPTX
376
+ element provenance is limited to `master | layout | slide`; editor tree indexes,
377
+ archive paths, save/round-trip handles, and mutation APIs are deliberately absent.
378
+ When switching on `kind`, retain a default branch so a future read-only focus kind
379
+ can be added without changing the transport envelope.
380
+ See the [selection-context guide](docs/selection-context.md) for the complete
381
+ contract, resource bounds, PPTX hit-testing semantics, and extension policy.
382
+
269
383
  **Hyperlinks.** For DOCX/PPTX the link hit regions live on the text-selection
270
384
  overlay, so hyperlink interaction requires `enableTextSelection: true`; when that
271
385
  overlay is enabled, links are interactive by default. XLSX hit-tests cells
@@ -344,20 +458,9 @@ const md = await doc.toMarkdown();
344
458
  bullets, notes / comments collated) and `XlsxWorkbook.toMarkdown()` (each sheet →
345
459
  a `## SheetName` pipe table) are the twins.
346
460
 
347
- For a one-off conversion outside a viewer, the standalone
348
- `@silurus/ooxml-markdown` package exposes the low-level functions and a CLI:
349
-
350
- ```typescript
351
- import { docxToMarkdown, initDocxFromBytes } from '@silurus/ooxml-markdown';
352
-
353
- initDocxFromBytes(wasmBytes); // the docx parser's `_bg.wasm`
354
- const md = docxToMarkdown(fileBytes); // ArrayBuffer | Uint8Array | Buffer
355
- ```
356
-
357
- ```bash
358
- npx ooxml-md document.docx # → stdout
359
- npx ooxml-md deck.pptx -o deck.md # → file
360
- ```
461
+ The repository also contains a low-level adapter and CLI for workspace tooling.
462
+ They are internal implementation utilities, not separately published packages;
463
+ installed applications should use the format model's `toMarkdown()` method.
361
464
 
362
465
  ---
363
466
 
@@ -492,9 +595,10 @@ file without uploading it.
492
595
  | | `w:snapToGrid` opt-out of the document grid (§17.3.1.32) | ✅ |
493
596
  | | Track changes (`w:ins` / `w:del` — author-coloured underline / strikethrough) | ✅ |
494
597
  | | Comments — author / date / text via the document model (`doc.comments`, §17.13.4; not drawn on the page) | ✅ |
495
- | | Markdown export (`DocxDocument.toMarkdown()` — headings, lists, tables, footnotes / comments; also `@silurus/ooxml-markdown` + the `ooxml-md` CLI) | ✅ |
598
+ | | Markdown export (`DocxDocument.toMarkdown()` — headings, lists, tables, footnotes / comments) | ✅ |
496
599
  | | Mail merge fields | ❌ Not planned |
497
600
  | **Interaction** | Text selection (transparent overlay, native copy) | ✅ |
601
+ | | Bounded read-only text/element context (`getSelectionContext()`, page/source locators, element selection, AI/MCP callback) | ✅ |
498
602
  | | In-document find (`findText` / `findNext` / `findPrev` / `clearFind` — full-text search, all hits highlighted, each match tagged with its page) | ✅ |
499
603
  | | Runtime zoom (`getScale` / `setScale` / `fitWidth` / `fitPage`) | ✅ |
500
604
  | | Clickable hyperlinks (overlay hit-test, `onHyperlinkClick`; internal bookmark / anchor navigation) | ✅ |
@@ -547,12 +651,12 @@ file without uploading it.
547
651
  | | Pivot tables (saved worksheet output renders unchanged; read-only metadata is exposed. Refresh, recalculation, filtering, restructuring, and interactivity are unsupported) | ⚠️ Partial |
548
652
  | | Cell comments / notes (classic `xl/commentsN.xml` + Office-365 threaded comments — red triangle indicator + author / text via the worksheet model, shown in an Excel-style hover popup) | ✅ |
549
653
  | | Data validation (rules via the worksheet model; `list`-type dropdown arrow on the selected cell whose click opens a panel showing the allowed values — read-only) | ✅ |
550
- | | Markdown export (`XlsxWorkbook.toMarkdown()` — each sheet as a `## SheetName` pipe table; also `@silurus/ooxml-markdown` + the `ooxml-md` CLI) | ✅ |
551
- | **Interaction** | Cell selection (single / range / row / column / all) | ✅ |
654
+ | | Markdown export (`XlsxWorkbook.toMarkdown()` — each sheet as a `## SheetName` pipe table) | ✅ |
655
+ | **Interaction** | Cell selection (single / range / row / column / all / multiple areas; `setSelection('B2:D5')` or canonical structured state) | ✅ |
552
656
  | | Excel-style row / column header highlight on selection | ✅ |
553
657
  | | Shift+click to extend, Ctrl+C to copy as TSV | ✅ |
554
658
  | | Text selection inside cells (transparent overlay) | ✅ |
555
- | | `onSelectionChange` callback, `getCellAt(x, y)` API | ✅ |
659
+ | | `onSelectionStateChange`, bounded range/element `getSelectionContext()` / `copySelection()`, chart/picture/shape selection, `getCellAt(x, y)` | ✅ |
556
660
  | | Zoom slider (Excel-style, right of the tab bar, 10–400% with 100% centered; `showZoomSlider` option) | ✅ |
557
661
  | | Ctrl/⌘ + mouse-wheel and trackpad-pinch zoom (in addition to the slider) | ✅ |
558
662
  | | Runtime fit / zoom API (`fitWidth` / `fitPage` / `getScale` / `setScale`, in addition to the slider) | ✅ |
@@ -574,7 +678,7 @@ file without uploading it.
574
678
  | | Slide background (solid, gradient, image) | ✅ |
575
679
  | | Slide numbers | ✅ |
576
680
  | | Speaker notes (plain text via `getNotes()`) | ✅ |
577
- | | Markdown export (`PptxPresentation.toMarkdown()` — title slides → headings, body → nested bullets, notes / comments collated; also `@silurus/ooxml-markdown` + the `ooxml-md` CLI) | ✅ |
681
+ | | Markdown export (`PptxPresentation.toMarkdown()` — title slides → headings, body → nested bullets, notes / comments collated) | ✅ |
578
682
  | | Animations / transitions | ❌ Not planned |
579
683
  | **Element types** | Shapes (`sp`) | ✅ |
580
684
  | | Pictures (`pic`) | ✅ |
@@ -656,6 +760,7 @@ file without uploading it.
656
760
  | | Font scheme (`+mj-lt`, `+mn-lt`) | ✅ |
657
761
  | | lumMod / lumOff / alpha transforms | ✅ |
658
762
  | **Interaction** | Text selection (transparent overlay, native copy) | ✅ |
763
+ | | Bounded text/element selection context (`getSelectionContext()`, element selection, master/layout/slide provenance, main + worker) | ✅ |
659
764
  | | In-document find (`findText` / `findNext` / `findPrev` / `clearFind` — matches tagged with slide) | ✅ |
660
765
  | | Runtime zoom (`getScale` / `setScale` / `fitWidth` / `fitPage`) | ✅ |
661
766
  | | Clickable hyperlinks (`onHyperlinkClick`; internal slide-jump navigation) | ✅ |
@@ -670,10 +775,10 @@ file without uploading it.
670
775
 
671
776
  ## Companion packages
672
777
 
673
- - **[`packages/markdown/`](packages/markdown/)** — `@silurus/ooxml-markdown` and the `ooxml-md` CLI convert `.pptx` / `.docx` / `.xlsx` to GitHub-flavoured markdown via the workspace WASM parsers. Same projection used by the MCP server (~21× smaller than the raw XML on the demo deck, ~8% bigger than a flat-text extractor). Includes a node20-based GitHub Action for bulk repo-wide conversion.
778
+ - **[`packages/markdown/`](packages/markdown/)** — internal workspace adapter and `ooxml-md` development CLI for the same GitHub-flavoured Markdown projection exposed by each format model's `toMarkdown()` method.
674
779
  - **[`packages/node/`](packages/node/)** — the implementation behind the public Node-only `@silurus/ooxml/node` subpath. Its canonical APIs are the explicitly owned, bounded `openPptxPresentation`, `openDocxDocument`, and `openXlsxWorkbook` sessions. Async `materializePptxPresentation`, `materializeDocxDocument`, `materializeXlsxWorkbookIndex`, `materializeXlsxWorksheet`, and `materializeXlsxWorkbook` are provided when a complete caller-owned graph is actually needed. Each `open*` call returns an explicit, idempotent `close()`-able session; PPTX streams `slides()`, DOCX completes format-required sequential pagination before streaming `pages()`, and XLSX parses its workbook index once before sequential `worksheetRows(sheetIndex)` streams reuse the retained archive. Useful for CI checks and headless rendering pipelines; canvas rendering accepts a user-supplied backend such as `skia-canvas` without making it a runtime dependency.
675
780
  See the [0.75 to 0.76 migration guide](docs/migration-0.76.md) for every removed synchronous helper and its replacement.
676
- - **[`packages/vscode-extension/`](packages/vscode-extension/)** — VS Code extension (`ooxml-viewer`) that registers `CustomEditorProvider`s for `.docx`, `.xlsx`, and `.pptx`, and (opt-in) auto-installs and registers the `ooxml-mcp-server` so AI coding agents in the same window (Copilot Agent mode, Claude, …) can read those files via dedicated tools. The preview is offline by default; an opt-in `ooxmlViewer.useGoogleFonts` setting (off, and force-disabled in untrusted workspaces) surfaces the library's metric-compatible font substitution, widening the webview CSP to the Google Fonts CDN only while enabled.
781
+ - **[`packages/vscode-extension/`](packages/vscode-extension/)** — VS Code extension (`ooxml-viewer`) that registers `CustomEditorProvider`s for `.docx`, `.xlsx`, and `.pptx`, and (opt-in) auto-installs and registers the `ooxml-mcp-server` for GitHub Copilot Chat in Agent mode, including active Viewer selection. Claude Code and Codex can configure the same binary separately for path-based file tools, but do not receive the active selection bridge. The preview is offline by default; an opt-in `ooxmlViewer.useGoogleFonts` setting (off, and force-disabled in untrusted workspaces) surfaces the library's metric-compatible font substitution, widening the webview CSP to the Google Fonts CDN only while enabled.
677
782
  - **[`packages/mcp-server/`](packages/mcp-server/)** — Rust MCP server (`ooxml-mcp-server`) exposing the parsers as tools for AI agents (Claude, Copilot, Codex, etc.). Provides structured queries (`docx_get_structure`, `xlsx_get_cell_range`, `pptx_get_slide_structure`, …) so agents can inspect OOXML files without shelling out to `unzip`. Prebuilt binaries are attached to each [GitHub Release](https://github.com/yukiyokotani/office-open-xml-viewer/releases) for macOS / Linux / Windows; the VS Code extension downloads them on demand.
678
783
 
679
784
  ---
@@ -712,16 +817,18 @@ cd packages/pptx/parser && wasm-pack build --target web && cp pkg/pptx_parser_bg
712
817
 
713
818
  ## Error handling
714
819
 
715
- Headless APIs (`DocxDocument`, `XlsxWorkbook`, and `PptxPresentation`) report
716
- load and render failures by rejecting the returned Promise. Viewer APIs also
717
- support an `onError(error)` callback, with an important delivery rule:
820
+ Headless APIs (`DocxDocument`, `XlsxWorkbook`, and `PptxPresentation`) and
821
+ Viewer APIs report failures from awaitable operations by rejecting the returned
822
+ Promise. This includes `viewer.load()` parsing and its initial render, whether
823
+ or not the Viewer has an `onError(error)` callback. A failure is never delivered
824
+ through both channels.
718
825
 
719
- - Without `onError`, a load/parse failure rejects `viewer.load()`.
720
- - With `onError`, that failure is delivered to the callback and
721
- `viewer.load()` resolves. Do not treat resolution alone as proof that the
722
- document rendered.
723
- - Later Viewer-managed render or media failures are delivered to `onError`, or
724
- logged with `console.error` when the callback is omitted.
826
+ Use `onError` for later Viewer-managed work that has no directly awaitable
827
+ result, such as virtualized scroll-view rendering or embedded-media playback.
828
+ Those failures are logged with `console.error` when the callback is omitted.
829
+ `PptxPresentation.presentSlide()` follows the same boundary: initialization
830
+ rejects its Promise, while `PresentSlideOptions.onError` observes only media
831
+ decode or playback failures after the presentation handle has been returned.
725
832
 
726
833
  Stable failures can be narrowed without parsing message strings:
727
834
 
@@ -754,6 +861,7 @@ import {
754
861
  } from '@silurus/ooxml/docx';
755
862
 
756
863
  const viewer = new DocxViewer(canvas, {
864
+ // Background failures after an awaited operation has completed.
757
865
  onError(error) {
758
866
  if (error instanceof OoxmlResourceLimitError) {
759
867
  const { limit, observed } = error.details.violation;
@@ -768,7 +876,11 @@ const viewer = new DocxViewer(canvas, {
768
876
  },
769
877
  });
770
878
 
771
- await viewer.load(file);
879
+ try {
880
+ await viewer.load(file);
881
+ } catch (error) {
882
+ reportUnexpectedError(error);
883
+ }
772
884
  ```
773
885
 
774
886
  ## Security & Privacy