Skip to content
HN On Hacker News ↗

GitHub - yukiyokotani/office-open-xml-viewer

▲ 142 points 54 comments by maxloh 1d ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is a mix of AI-generated, and human-written content

79 %

AI likelihood · overall

Mixed
21% human-written 79% AI-generated
SEGMENTS · HUMAN 1 of 6
SEGMENTS · AI 5 of 6
WORD COUNT 1,243
PEAK AI % 99% · §4
Analyzed
Jun 7
backend: pangram/v3.3
Segments scanned
6 windows
avg 207 words each
Distribution
21 / 79%
human / AI fraction
Verdict
Mixed
Pangram v3.3

Article text · 1,243 words · 6 segments analyzed

Human AI-generated
§1 AI · 99%

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.

Demo (Storybook) 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 import { DocxViewer } from '@silurus/ooxml/docx'; import { XlsxViewer } from '@silurus/ooxml/xlsx'; import { PptxViewer } from '@silurus/ooxml/pptx';

// DOCX — caller provides the <canvas> 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 <canvas> + tab bar const container =

§2 AI · 98%

document.getElementById('xlsx-container') as HTMLElement; const xlsx = new XlsxViewer(container); await xlsx.load('/workbook.xlsx');

// PPTX — caller provides the <canvas> 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

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 --

§3 AI · 97%

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["&lt;canvas&gt;"] 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.

§4 AI · 99%

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 // 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<HTMLCanvasElement>(null); const viewerRef = useRef<PptxViewer | null>(null); const [slide, setSlide] = useState({ current: 0, total: 0 });

useEffect(() => { const canvas = canvasRef.current; if (!

§5 AI · 93%

canvas) return;

const viewer = new PptxViewer(canvas, { onSlideChange: (i, total) => setSlide({ current: i, total }), }); viewerRef.current = viewer; viewer.load(src); }, [src]);

return ( <div> <canvas ref={canvasRef} style={{ width: 800 }} /> <button onClick={() => viewerRef.current?.prevSlide()}>‹ Prev</button> <span> {slide.current + 1} / {slide.total} </span> <button onClick={() => viewerRef.current?.nextSlide()}>Next ›</button> </div> ); }

Vue 3.5 <!-- Vue 3.5 — useTemplateRef is a 3.5+ feature --> <script setup lang="ts"> import { useTemplateRef, onMounted, ref } from 'vue'; import { PptxViewer } from '@silurus/ooxml/pptx';

const props = defineProps<{ src: string }>();

const canvas = useTemplateRef<HTMLCanvasElement>('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); }); </script>

<template> <div> <canvas ref="canvas" style="width: 800px" /> <button @click="viewer?.prevSlide()">‹ Prev</button> <span> {{ current + 1 }} / {{ total }} </span> <button @click="viewer?.nextSlide()">Next ›</button> </div> </template>

Angular 19 // Angular 19 — standalone component with signal-based state import { Component, ElementRef, viewChild, signal, AfterViewInit, } from '@angular/core'; import

§6 Human · 20%

{ PptxViewer } from '@silurus/ooxml/pptx';

@Component({ selector: 'app-pptx-viewer', standalone: true, template: ` <div> <canvas #canvas style="width: 800px"></canvas> <button (click)="prev()">‹ Prev</button> <span> {{ current() + 1 }} / {{ total() }} </span> <button (click)="next()">Next ›</button> </div> `, }) export class PptxViewerComponent implements AfterViewInit { canvasEl = viewChild.required<ElementRef<HTMLCanvasElement>>('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 <!-- Svelte 5 — runes syntax ($props, $state) --> <script lang="ts"> 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);