메뉴
HN
Hacker News 51일 전

AI가 작성한 코드 100%… 브라우저에서 완벽 렌더링되는 오피스 뷰어

IMP
8/10
핵심 요약

Claude(Anthropic의 AI)가 코드를 전적으로 작성한 새로운 오픈소스 프로젝트가 등장했습니다. Rust/WebAssembly와 Canvas 2D API를 활용해 웹 브라우저에서 픽셀 단위까지 완벽하게 구현되는 DOCX, XLSX, PPTX 뷰어를 제공합니다. 필요에 따라 수식(MathJax) 렌더링 기능을 선택적으로 추가할 수 있으며, 번들 크기를 최적화할 수 있는 구조적 장점도 갖췄습니다.

번역된 본문

이 코드베이스 전체 — Rust 파서, TypeScript 렌더러, 테스트, 도구 — 는 Claude(Anthropic의 AI 어시스턴트)가 반복적인 프롬프트를 통해 작성했습니다. 이 리포지토리에는 인간이 작성한 애플리케이션 코드가 전혀 없습니다.

office-open-xml-viewer 데모 (Storybook)

Office Open XML 문서를 HTML Canvas 요소로 렌더링하는 브라우저 기반 뷰어입니다. 파서는 Rust로 작성되어 WebAssembly로 컴파일되며, 렌더러는 Canvas 2D API를 사용합니다. 또한 각 포맷은 호출자가 제공하는 캔버스에 렌더링하는 헤드리스 엔진(DocxDocument / XlsxWorkbook / PptxPresentation)을 노출하므로, 내장된 뷰어에 종속되지 않고 스크롤 뷰, 썸네일 그리드, 마스터-디테일 패인 등 자체 UI를 구성할 수 있습니다. Storybook 데모의 예제(Examples) 섹션을 참조하세요.

지원 포맷: DOCX, XLSX, PPTX

npm install @silurus/ooxml

또는 pnpm add @silurus/ooxml

번들러 참고: 이 패키지는 .wasm 파일을 포함하고 있습니다. Vite를 사용할 때는 vite-plugin-wasm을 추가하고, webpack을 사용할 때는 experiments.asyncWebAssembly를 사용하세요.

번들 크기 참고: 이 패키지는 ESM 전용(.mjs)입니다. npm의 Unpacked Size는 선택적 수식 엔진(MathJax + STIX Two Math, 약 3MB)을 포함하여 4개의 모든 엔트리 번들을 합산한 것입니다. 실제로 앱에 포함되는 크기는 훨씬 작습니다 — 필요한 포맷만 임포트하세요(예: @silurus/ooxml/pptx). 수식 엔진은 별도의 엔트리(@silurus/ooxml/math)로 제공됩니다. 이를 임포트하여 뷰어에 전달할 때만 번들에 포함됩니다(수식 렌더링 참조). 수식 엔진을 받지 않는 뷰어와 모든 xlsx 사용 사례는 약 3MB를 완전히 트리 셰이킹(tree-shake)하여 제거합니다.

빠른 시작: import { DocxViewer } from '@silurus/ooxml/docx'; import { XlsxViewer } from '@silurus/ooxml/xlsx'; import { PptxViewer } from '@silurus/ooxml/pptx';

// DOCX — 호출자가 를 제공함 const canvas = document.getElementById('docx-canvas') as HTMLCanvasElement; const docx = new DocxViewer(canvas); await docx.load('/document.docx'); docx.nextPage();

// XLSX — 뷰어가 자체 + 탭 바를 관리함 const container = document.getElementById('xlsx-container') as HTMLElement; const xlsx = new XlsxViewer(container); await xlsx.load('/workbook.xlsx');

// PPTX — 호출자가 를 제공함 const canvas = document.getElementById('pptx-canvas') as HTMLCanvasElement; const pptx = new PptxViewer(canvas); await pptx.load('/deck.pptx'); pptx.nextSlide();

수식 렌더링: .docx / .pptx의 OMML 수식(m:oMath / m:oMathPara)은 MathJax + STIX Two Math로 렌더링됩니다. 이 엔진은 약 3MB이므로 선택적으로(opt-in) 사용합니다: 별도의 @silurus/ooxml/math 엔트리에서 수식 엔진을 임포트하여 뷰어에 전달하세요. 전달하면 수식이 렌더링되고, 생략하면 엔진이 참조되지 않아 번들러가 약 3MB를 완전히 트리 셰이킹합니다(수식은 단순히 건너뜀). 이 엔진은 완전히 독립적으로 작동하며, 네트워크나 교차 출처(cross-origin) 요청이 필요 없습니다.

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 }); // ← 이제 수식이 렌더링됨 await docx.load('/paper-with-equations.docx');

동일한 수식 엔진이 PptxViewer 및 헤드리스 DocxDocument / PptxPresentation API(옵션에서 math를 받음)에서도 작동합니다. xlsx는 수식을 지원하지 않으며 엔진을 참조하지 않습니다.

아키텍처 다이어그램: flowchart TB subgraph build["🦀 빌드 타임 (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["🌐 런타임 (Browser)"] subgraph core_pkg["@silurus/ooxml-core (공유 프리미티브)"] CORE["renderChart · resolveFill · applyStroke\nbuildCustomPath · autoResize · 공유 타입"] end subgraph docx_pkg["@silurus/ooxml · docx"] DV["DocxViewer"] --> DD["DocxDocument"]

원문 보기
원문 보기 (영어)
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. office-open-xml-viewer 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 = 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 -- 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["<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 // 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 ( ! 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 : 800 px " /> < button @click = " viewer?.prevSlide() "