Silurus/ooxml: Pixel-faithful Office documents, rendered in the browser
Source: Hacker News
This entire codebase — Rust parsers, TypeScript renderers, tests, and tooling — was implemented by Claude (Anthropic’s AI assistant) through iterative prompting. No human-written application code exists in this repository.
A browser-based viewer for Office Open XML documents that renders to an HTML Canvas element.
The parsers are written in Rust and compiled to WebAssembly; the renderers use the Canvas 2D API.
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.
DOCX XLSX PPTX
npm install @silurus/ooxml
or
pnpm add @silurus/ooxml
Bundler note: this package embeds .wasm files. With Vite add vite-plugin-wasm; with webpack use experiments.asyncWebAssembly.
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, ~3 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). Viewers that never receive a math engine — and all xlsx usage — tree-shake the ~3 MB away entirely.
Quick Start
const canvas = document.getElementById(‘docx-canvas’) as HTMLCanvasElement; const docx = new DocxViewer(canvas); await docx.load(‘/document.docx’); docx.nextPage();
// XLSX — viewer manages its own + tab bar const container = document.getElementById(‘xlsx-container’) as HTMLElement; const xlsx = new XlsxViewer(container); await xlsx.load(‘/workbook.xlsx’);
// PPTX — caller provides the const canvas = document.getElementById(‘pptx-canvas’) as HTMLCanvasElement; const pptx = new PptxViewer(canvas); await pptx.load(‘/deck.pptx’); pptx.nextSlide();“>import { DocxViewer } from ‘@silurus/ooxml/docx’; import { XlsxViewer } from ‘@silurus/ooxml/xlsx’; import { PptxViewer } from ‘@silurus/ooxml/pptx’;
// DOCX — caller provides the const canvas = document.getElementById(‘docx-canvas’) as HTMLCanvasElement; const docx = new DocxViewer(canvas); await docx.load(‘/document.docx’); docx.nextPage();
// XLSX — viewer manages its own + tab bar const container = document.getElementById(‘xlsx-container’) as HTMLElement; const xlsx = new XlsxViewer(container); await xlsx.load(‘/workbook.xlsx’);
// PPTX — caller provides the const canvas = document.getElementById(‘pptx-canvas’) as HTMLCanvasElement; const pptx = new PptxViewer(canvas); await pptx.load(‘/deck.pptx’); pptx.nextSlide();
Rendering equations
OMML equations (m:oMath / m:oMathPara) in .docx / .pptx are rendered with
MathJax + STIX Two Math.
That engine is ~3 MB, so it is opt-in: import the math engine from the separate
@silurus/ooxml/math entry and pass it to the viewer. Pass it and equations render;
omit it and the engine is referenced nowhere, so a bundler tree-shakes the ~3 MB
away entirely (equations are simply skipped). It is fully self-contained: no
network, no cross-origin requests.
import { DocxViewer } from ‘@silurus/ooxml/docx’;
import { math } from ‘@silurus/ooxml/math’;
const canvas = document.getElementById(‘docx-canvas’) as HTMLCanvasElement;
const docx = new DocxViewer(canvas, { math }); // ← equations now render
await docx.load(‘/paper-with-equations.docx’);
The same math engine works for PptxViewer and the headless DocxDocument /
PptxPresentation APIs (which take math in their options). xlsx has no equation
support and never references the engine.
Architecture diagram
docx_wasm[“docx_parser.wasm”] xlsx_rs — wasm-pack —> xlsx_wasm[“xlsx_parser.wasm”] pptx_rs — wasm-pack —> pptx_wasm[“pptx_parser.wasm”] end
subgraph browser["🌐 Runtime (Browser)"]
subgraph core_pkg["@silurus/ooxml-core (shared primitives)"]
CORE["renderChart · resolveFill · applyStroke\nbuildCustomPath · autoResize · shared types"]
end
subgraph docx_pkg["@silurus/ooxml · docx"]
DV["DocxViewer"] --> DD["DocxDocument"]
DD --> DW["worker.ts\n〈Web Worker — parse only〉"]
DD --> DR["renderer.ts\n〈Canvas 2D — main thread〉"]
end
subgraph xlsx_pkg["@silurus/ooxml · xlsx"]
XV["XlsxViewer"] --> XB["XlsxWorkbook"]
XB --> XW["worker.ts\n〈Web Worker — parse only〉"]
XB --> XR["renderer.ts\n〈Canvas 2D — main thread〉"]
end
subgraph pptx_pkg["@silurus/ooxml · pptx"]
PV["PptxViewer"] --> PP["PptxPresentation"]
PP --> PW["worker.ts\n〈Web Worker — parse only〉"]
PP --> PR["renderer.ts\n〈Canvas 2D — main thread〉"]
end
DR -. uses .-> CORE
XR -. uses .-> CORE
PR -. uses .-> CORE
end
docx_wasm --> DW
xlsx_wasm --> XW
pptx_wasm --> PW
DR --> canvas[""]
XR --> canvas
PR --> canvas
”> flowchart TB subgraph build[”🦀 Build-time (Rust → WebAssembly)”] direction LR docx_rs[“packages/docx/parser/src/lib.rs”] xlsx_rs[“packages/xlsx/parser/src/lib.rs”] pptx_rs[“packages/pptx/parser/src/lib.rs”] docx_rs — wasm-pack —> docx_wasm[“docx_parser.wasm”] xlsx_rs — wasm-pack —> xlsx_wasm[“xlsx_parser.wasm”] pptx_rs — wasm-pack —> pptx_wasm[“pptx_parser.wasm”] end
subgraph browser["🌐 Runtime (Browser)"]
subgraph core_pkg["@silurus/ooxml-core (shared primitives)"]
CORE["renderChart · resolveFill · applyStroke\nbuildCustomPath · autoResize · shared types"]
end
subgraph docx_pkg["@silurus/ooxml · docx"]
DV["DocxViewer"] --> DD["DocxDocument"]
DD --> DW["worker.ts\n〈Web Worker — parse only〉"]
DD --> DR["renderer.ts\n〈Canvas 2D — main thread〉"]
end
subgraph xlsx_pkg["@silurus/ooxml · xlsx"]
XV["XlsxViewer"] --> XB["XlsxWorkbook"]
XB --> XW["worker.ts\n〈Web Worker — parse only〉"]
XB --> XR["renderer.ts\n〈Canvas 2D — main thread〉"]
end
subgraph pptx_pkg["@silurus/ooxml · pptx"]
PV["PptxViewer"] --> PP["PptxPresentation"]
PP --> PW["worker.ts\n〈Web Worker — parse only〉"]
PP --> PR["renderer.ts\n〈Canvas 2D — main thread〉"]
end
DR -. uses .-> CORE
XR -. uses .-> CORE
PR -. uses .-> CORE
end
docx_wasm --> DW
xlsx_wasm --> XW
pptx_wasm --> PW
DR --> canvas[""]
XR --> canvas
PR --> canvas
Loading
All three formats follow the same shape: the worker parses the .docx / .xlsx / .pptx archive via WASM and posts a JSON model back to the main thread, where the renderer draws to the canvas. Rendering stays on the main thread so the canvas shares the document’s FontFaceSet — an OffscreenCanvas in a worker has its own font registry and would silently fall back to a system font, producing subtly different text measurements (and wrap positions) from the installed theme webfonts. @silurus/ooxml-core holds the cross-format primitives that the three renderers all depend on: a unified chart renderer (bar / line / area / radar / waterfall), shape helpers (resolveFill, applyStroke, buildCustomPath, hexToRgba), the autoResize viewer utility, and the shared type definitions.
Key files
File Role
packages/docx/parser/src/lib.rs
Rust WASM parser — DOCX ZIP → Document JSON
packages/xlsx/parser/src/lib.rs
Rust WASM parser — XLSX ZIP → Workbook JSON
packages/pptx/parser/src/lib.rs
Rust WASM parser — PPTX ZIP → Presentation JSON
packages/docx/src/renderer.ts
Canvas 2D rendering engine with text layout (main thread)
packages/xlsx/src/renderer.ts
Canvas 2D rendering engine with virtual scroll (main thread)
packages/pptx/src/renderer.ts
Canvas 2D rendering engine (main thread)
packages/*/src/worker.ts
Web Worker: WASM init and parsing only (one per format)
packages/*/src/viewer.ts
Public Viewer API — canvas lifecycle, navigation
packages/core/src/index.ts
Cross-format primitives — chart renderer, shape helpers, autoResize, shared types
Framework Examples
React 19 (null); const viewerRef = useRef(null); const [slide, setSlide] = useState({ current: 0, total: 0 });
useEffect(() => { const canvas = canvasRef.current; if (!canvas) return;
const viewer = new PptxViewer(canvas, {
onSlideChange: (i, total) => setSlide({ current: i, total }),
});
viewerRef.current = viewer;
viewer.load(src);
}, [src]);
return (
viewerRef.current?.prevSlide()}>‹ Prev
{slide.current + 1} / {slide.total}
viewerRef.current?.nextSlide()}>Next ›
); }”>// React 19.1 — vite-plugin-wasm required in vite.config.ts import { useEffect, useRef, useState } from ‘react’; import { PptxViewer } from ‘@silurus/ooxml/pptx’;
export function PptxViewerComponent({ src }: { src: string }) { const canvasRef = useRef(null); const viewerRef = useRef(null); const [slide, setSlide] = useState({ current: 0, total: 0 });
useEffect(() => { const canvas = canvasRef.current; if (!canvas) return;
const viewer = new PptxViewer(canvas, {
onSlideChange: (i, total) => setSlide({ current: i, total }),
});
viewerRef.current = viewer;
viewer.load(src);
}, [src]);
return (
viewerRef.current?.prevSlide()}>‹ Prev
{slide.current + 1} / {slide.total}
viewerRef.current?.nextSlide()}>Next ›
); }
Vue 3.5
import { useTemplateRef, onMounted, ref } from ‘vue’; import { PptxViewer } from ‘@silurus/ooxml/pptx’;
const props = defineProps();
const canvas = useTemplateRef(‘canvas’); let viewer: PptxViewer | null = null; const current = ref(0); const total = ref(0);
onMounted(async () => { viewer = new PptxViewer(canvas.value!, { onSlideChange: (i, t) => { current.value = i; total.value = t; }, }); await viewer.load(props.src); });
‹ Prev
{{ current + 1 }} / {{ total }}
Next ›
”>
import { useTemplateRef, onMounted, ref } from ‘vue’; import { PptxViewer } from ‘@silurus/ooxml/pptx’;
const props = defineProps();
const canvas = useTemplateRef(‘canvas’); let viewer: PptxViewer | null = null; const current = ref(0); const total = ref(0);
onMounted(async () => { viewer = new PptxViewer(canvas.value!, { onSlideChange: (i, t) => { current.value = i; total.value = t; }, }); await viewer.load(props.src); });
‹ Prev
{{ current + 1 }} / {{ total }}
Next ›
Angular 19
‹ Prev
{{ current() + 1 }} / {{ total() }}
Next ›
`, }) export class PptxViewerComponent implements AfterViewInit { canvasEl = viewChild.required>(‘canvas’); current = signal(0); total = signal(0); private viewer?: PptxViewer;
ngAfterViewInit(): void { this.viewer = new PptxViewer(this.canvasEl().nativeElement, { onSlideChange: (i, t) => { this.current.set(i); this.total.set(t); }, }); this.viewer.load(‘/deck.pptx’); }
prev(): void { this.viewer?.prevSlide(); } next(): void { this.viewer?.nextSlide(); } }”>// Angular 19 — standalone component with signal-based state import { Component, ElementRef, viewChild, signal, AfterViewInit, } from ‘@angular/core’; import { PptxViewer } from ‘@silurus/ooxml/pptx’;
@Component({ selector: ‘app-pptx-viewer’, standalone: true, template: `
‹ Prev
{{ current() + 1 }} / {{ total() }}
Next ›
`, }) export class PptxViewerComponent implements AfterViewInit { canvasEl = viewChild.required>(‘canvas’); current = signal(0); total = signal(0); private viewer?: PptxViewer;
ngAfterViewInit(): void { this.viewer = new PptxViewer(this.canvasEl().nativeElement, { onSlideChange: (i, t) => { this.current.set(i); this.total.set(t); }, }); this.viewer.load(‘/deck.pptx’); }
prev(): void { this.viewer?.prevSlide(); } next(): void { this.viewer?.nextSlide(); } }
Add "allowSyntheticDefaultImports": true and configure @angular-builders/custom-webpack (or use esbuild builder) with WASM support in your Angular workspace.
Svelte 5
import { onMount } from ‘svelte’; import { PptxViewer } from ‘@silurus/ooxml/pptx’;
let { src }: { src: string } = $props();
let canvas: HTMLCanvasElement; let viewer: PptxViewer; let current = $state(0); let total = $state(0);
onMount(async () => { viewer = new PptxViewer(canvas, { onSlideChange: (i, t) => { current = i; total = t; }, }); await viewer.load(src); });
viewer?.prevSlide()}>‹ Prev {current + 1} / {total} viewer?.nextSlide()}>Next › ”>
import { onMount } from ‘svelte’; import { PptxViewer } from ‘@silurus/ooxml/pptx’;
let { src }: { src: string } = $props();
let canvas: HTMLCanvasElement; let viewer: PptxViewer; let current = $state(0); let total = $state(0);
onMount(async () => { viewer = new PptxViewer(canvas, { onSlideChange: (i, t) => { current = i; total = t; }, }); await viewer.load(src); });
viewer?.prevSlide()}>‹ Prev {current + 1} / {total} viewer?.nextSlide()}>Next ›
SolidJS 1.9 { viewer = new PptxViewer(canvasEl, { onSlideChange: (i, t) => { setCurrent(i); setTotal(t); }, }); await viewer.load(props.src); });
onCleanup(() => { /* viewer?.destroy?.() */ });
return (
viewer?.prevSlide()}>‹ Prev
{current() + 1} / {total()}
viewer?.nextSlide()}>Next ›
); }”>// SolidJS 1.9 import { createSignal, onMount, onCleanup } from ‘solid-js’; import { PptxViewer } from ‘@silurus/ooxml/pptx’;
export function PptxViewerComponent(props: { src: string }) { let canvasEl!: HTMLCanvasElement; let viewer: PptxViewer | undefined; const [current, setCurrent] = createSignal(0); const [total, setTotal ] = createSignal(0);
onMount(async () => { viewer = new PptxViewer(canvasEl, { onSlideChange: (i, t) => { setCurrent(i); setTotal(t); }, }); await viewer.load(props.src); });
onCleanup(() => { /* viewer?.destroy?.() */ });
return (
viewer?.prevSlide()}>‹ Prev
{current() + 1} / {total()}
viewer?.nextSlide()}>Next ›
); }
Qwik 2 (({ src }) => { const canvasRef = useSignal(); const current = useSignal(0); const total = useSignal(0); let viewer: PptxViewerType | undefined;
// useVisibleTask$ runs only in the browser, never during SSR useVisibleTask$(async () => { if (!canvasRef.value) return; const { PptxViewer } = await import(‘@silurus/ooxml/pptx’); viewer = new PptxViewer(canvasRef.value, { onSlideChange: (i, t) => { current.value = i; total.value = t; }, }); await viewer.load(src); });
return (
viewer?.prevSlide()}>‹ Prev
{current.value + 1} / {total.value}
viewer?.nextSlide()}>Next ›
); });”>// Qwik 2.0 — dynamic import to keep WASM out of SSR bundle import { component$, useSignal, useVisibleTask$ } from ‘@builder.io/qwik’; import type { PptxViewer as PptxViewerType } from ‘@silurus/ooxml/pptx’;
export const PptxViewerComponent = component$(({ src }) => { const canvasRef = useSignal(); const current = useSignal(0); const total = useSignal(0); let viewer: PptxViewerType | undefined;
// useVisibleTask$ runs only in the browser, never during SSR useVisibleTask$(async () => { if (!canvasRef.value) return; const { PptxViewer } = await import(‘@silurus/ooxml/pptx’); viewer = new PptxViewer(canvasRef.value, { onSlideChange: (i, t) => { current.value = i; total.value = t; }, }); await viewer.load(src); });
return (
viewer?.prevSlide()}>‹ Prev
{current.value + 1} / {total.value}
viewer?.nextSlide()}>Next ›
); });
Feature Support
Word (.docx)
Category Feature Status
Document Page rendering ✅
Page size and margins ✅
Headers / footers (default / first / even) ✅
Section breaks (continuous / nextPage / oddPage / evenPage) ✅
Text Paragraphs ✅
Bold, italic, underline, strikethrough ✅
Font family, size, color ✅
Hyperlinks ✅
Superscript / subscript (w:vertAlign)
✅
Ruby annotations / furigana (w:ruby)
✅
Formatting Paragraph alignment (left/center/right/justify) ✅
Line spacing (auto / atLeast / exact) ✅
Line grid (w:docGrid, §17.6.5)
✅
Margin collapsing between paragraphs ✅
Indents and tab stops ✅
Lists (bullet and numbered) ✅
Paragraph styles (Heading 1–9, Normal, custom) ✅
Table style w:pPr cascade (§17.7.6)
✅
Table style borders / shading / banding (tblStylePr, cnfStyle, §17.4.7)
✅
Table of contents (TOC field) — dot leaders, right-aligned page numbers ✅
keepNext / keepLines / widowControl ✅
Elements Tables (with borders, fills, merges, banding, alignment) ✅
Math equations (OMML m:oMath / m:oMathPara, rendered via MathJax — opt-in @silurus/ooxml/math)
✅
Images (inline and anchored, with text wrap) ✅
Text boxes / drawing shapes ✅
WMF / EMF metafile images (legacy vector) ❌ Not planned
Advanced Footnote / endnote reference markers ✅
Track changes (w:ins / w:del — author-coloured underline / strikethrough)
✅
Comments / footnote bodies (parsed, not yet rendered inline) ⚠️
Mail merge fields ❌ Not planned
Interaction Text selection (transparent overlay, native copy) ✅
Excel (.xlsx)
Category Feature Status
Workbook Multiple sheets, sheet names ✅
Sheet tab colors (“ — theme / tint / indexed / rgb) ✅
Cells Text, number, boolean, error values ✅
Formula results (from cached “) ✅
Dates (ECMA-376 date format codes) ✅
Rich text (per-run formatting) ✅
Formatting
Bold, italic, underline (single / double / singleAccounting / doubleAccounting), strikethrough
✅
Superscript / subscript (vertAlign)
✅
Font family, size, color ✅
Cell background color (solid + gradient) ✅
Pattern fills (gray125 / gray0625 / lightGray / mediumGray / darkGray and the 12 light* / dark* directional hatches)
✅
Borders (thin, medium, thick, hair, double, dashed, dotted, dashDotDot, …) ✅
Diagonal borders (diagonalUp / diagonalDown, single + double)
✅
Horizontal / vertical alignment ✅
Text wrapping ✅
Number formats (0.00, %, #,##0, custom date/time)
✅
Structure Merged cells ✅
Frozen panes ✅
Row / column sizing (custom widths and heights) ✅
Hidden rows / columns ✅
Elements Images (“) ✅
Drawing shapes / text boxes (xdr:sp, xdr:txBody)
✅
Charts (bar, line, area, radar, scatter / bubble) ✅
Chart markers (circle / square / diamond / triangle / x / plus / star / dot / dash, per-point “ overrides) ✅
Chart data labels (“ per-point with CELLRANGE / VALUE / SERIESNAME / CATEGORYNAME field references, position l/r/t/b/ctr/outEnd)
✅
Chart error bars (“ X/Y direction, cust / fixedVal / stdErr / stdDev / percentage, dashed/styled lines)
✅
Chart manual layout (and)
✅
Sparklines (x14:sparklineGroup — line / column / win-loss, with markers and high/low/first/last/negative highlights)
✅
Advanced
Conditional formatting (cellIs, colorScale, dataBar, iconSet, top10, aboveAverage)
✅
Slicers (static, Office 2010 extension) ✅
Pivot tables ❌ Not planned
Data validation / comments ❌ Not planned
Interaction Cell selection (single / range / row / column / all) ✅
Excel-style row / column header highlight on selection ✅
Shift+click to extend, Ctrl+C to copy as TSV ✅
Text selection inside cells (transparent overlay) ✅
onSelectionChange callback, getCellAt(x, y) API
✅
Zoom slider (Excel-style, right of the tab bar, 10–400% with 100% centered; showZoomSlider option)
✅
PowerPoint (.pptx)
Category Feature Status
Slides Slide rendering ✅
Slide layout / master inheritance ✅
Slide size (custom dimensions) ✅
Slide background (solid, gradient, image) ✅
Slide numbers ✅
Notes pages ❌
Animations / transitions ❌ Not planned
Element types
Shapes (sp)
✅
Pictures (pic)
✅
Groups (grpSp) with nested transforms
✅
Connectors (cxnSp)
✅
Tables (tbl in graphicFrame)
✅
Charts (bar, line, area, radar, waterfall) ✅
Charts (pie, doughnut) ✅
Charts (scatter — scatterStyle marker / line / smooth variants)
✅
Charts (bubble — bubbleSize per-point area scaling)
✅
SmartArt ❌
OLE objects ❌
Video / audio (poster + interactive playback) ✅
Ink / handwriting (p:contentPart, raster fallback)
✅
Shape geometry
130+ preset shapes (prstGeom)
✅
Custom geometry (custGeom) on shapes and pictures (clipping)
✅
Rotation and flip (flipH / flipV) ✅
3D preset shapes ❌
Fills
Solid fill (solidFill)
✅
Linear / radial gradient (gradFill)
✅
No fill (noFill)
✅
Pattern fill (pattFill) — 30 preset bitmaps incl. pct5–pct90 / horz / vert / cross / diag / grid / brick / check / trellis
✅
Image fill on shapes (blipFill in sp)
✅
Strokes Solid line color and width ✅
Dash / dot styles ✅
Arrow heads (headEnd / tailEnd)
✅
Compound / double lines ( a:ea — separate typeface for CJK glyphs)
✅
Caps transform (all / small)
✅
Letter spacing (spc)
✅
Superscript / subscript ✅
Hyperlinks (hlinkClick — theme hlink colour + auto underline)
✅
Text shadow (rPr > effectLst > outerShdw)
✅
Text outline (rPr > a:ln)
✅
Math equations (OMML m:oMath / m:oMathPara, incl. a14:m / mc:AlternateContent; STIX Two Math via MathJax — opt-in @silurus/ooxml/math)
✅
Text — paragraphs Horizontal alignment (left / center / right / justify) ✅
Vertical anchor (top / center / bottom) ✅
Line spacing (spcPct, spcPts)
✅
Space before / after paragraph ✅
Bullet points (character and auto-numbered) ✅
Tab stops ✅
Indent / margin ✅
Vertical text (bodyPr@vert — vert / vert270 / eaVert)
✅
Right-to-left paragraph (pPr@rtl — Arabic / Hebrew default alignment + browser bidi)
✅
Text — body Text padding (insets) ✅
normAutoFit (shrink to fit) ✅
spAutoFit (expand box; suppresses wrap when text fits in one line) ✅
Word wrap / no wrap ✅
Multi-column text body (numCol / spcCol — balanced flow)
✅
Theme object-default inheritance (“ bodyPr fallback) ✅
Tables Cells, rows, columns ✅
Cell merges (horizontal / vertical) ✅
Cell borders ✅
Cell fills (solid / gradient) ✅
Cell diagonal lines (lnTlToBr / lnBlToTr)
✅
Table theme styles (74 built-in PowerPoint presets) ✅
Theme Scheme colors (dk1/lt1/accent1–6) ✅
Font scheme (+mj-lt, +mn-lt)
✅
lumMod / lumOff / alpha transforms ✅
Interaction Text selection (transparent overlay, native copy) ✅
A note on text selection. Across DOCX / PPTX / XLSX, text selection is currently implemented by rendering glyphs to the canvas while overlaying a transparent DOM layer that mirrors the canvas text positions for native browser selection. This dual-layer approach is a deliberate stop-gap: once the Canvas drawElement API (proposed in WICG/html-in-canvas, currently in Chromium Origin Trial) ships across browsers, the project plans to migrate to a single DOM-as-source-of-truth pipeline where the canvas mirrors the DOM directly — eliminating the duplication while keeping z-order correctness and native selection / a11y.
Companion packages
-
packages/markdown/—@silurus/ooxml-markdownand theooxml-mdCLI convert.pptx/.docx/.xlsxto 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. -
packages/node/— Node-side parsers (@silurus/ooxml-node) exposingparsePptx/parseDocx/parseXlsx/parseXlsxAllSheetsagainst the workspace WASM artifacts, with no DOM or Web Worker dependency. Useful for CI checks, headless rendering pipelines, and CLI tools. Includes anooxml-thumbnailCLI (pptx-only first pass; requiresskia-canvas). -
packages/vscode-extension/— VS Code extension (ooxml-viewer) that registersCustomEditorProviders for.docx,.xlsx, and.pptx, and (opt-in) auto-installs and registers theooxml-mcp-serverso AI coding agents in the same window (Copilot Agent mode, Claude, …) can read those files via dedicated tools. -
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 tounzip. Prebuilt binaries are attached to each GitHub Release for macOS / Linux / Windows; the VS Code extension downloads them on demand.
Development
Install dependencies
pnpm install
Build all WASM parsers (requires Rust + wasm-pack)
pnpm build:wasm
Start Storybook dev server (port 6006)
pnpm storybook
Type-check all packages
pnpm typecheck
Run visual regression tests (local only — not run in CI)
pnpm vrt
Adopt the current rendering as the new reference baseline
UPDATE_REFS=1 pnpm vrt
Build the library
pnpm build
WASM build (individual packages)
cd packages/docx/parser && wasm-pack build —target web && cp pkg/docx_parser_bg.wasm pkg/docx_parser.js ../src/wasm/ cd packages/xlsx/parser && wasm-pack build —target web && cp pkg/xlsx_parser_bg.wasm pkg/xlsx_parser.js ../src/wasm/ cd packages/pptx/parser && wasm-pack build —target web && cp pkg/pptx_parser_bg.wasm pkg/pptx_parser.js ../src/wasm/
Security & Privacy
- Canvas-only rendering. Documents are decoded and drawn to an
HTMLCanvasElement. No script, link, form, or other active content from the source file is executed or injected into the DOM.
ZIP decompression cap. Each entry in the source archive is limited to 512 MiB of uncompressed output by default to block zip-bomb DoS. Override per viewer with maxZipEntryBytes (bytes) — raise it for legitimate decks with large embedded media, lower it to tighten the budget for untrusted input:
new PptxViewer(canvas, { maxZipEntryBytes: 64 * 1024 * 1024 }); // 64 MiB
Supported uniformly by DocxViewer, PptxViewer, and XlsxViewer. Zero / negative values fall back to the default.
-
No network by default. The library does not send telemetry or analytics, and does not contact third-party services unless you ask it to. In particular, theme webfonts (and Office font metric substitutes for XLSX) are not loaded from Google Fonts unless you pass
useGoogleFonts: trueto the relevantViewer/load(...)options — supported uniformly byDocxViewer,PptxViewer, andXlsxViewer. Enabling that option causes the end-user’s browser to send an HTTP request (IP and User-Agent) tofonts.googleapis.com, which may have GDPR implications for your application — consider self-hosting the required fonts via@font-faceinstead. -
XML parsing. Uses
roxmltree, which does not resolve external entities (XXE-safe by default).
License
MIT


