A framework-agnostic, universal document renderer with optional chunked loading polyrender.wisp.place/
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Base package pre-name change

aria 4b32fcaa a8ff9758

+7839
+20
LICENSE
··· 1 + Copyright (c) 2026 Aria Quinlan 2 + 3 + This software is provided ‘as-is’, without any express or implied 4 + warranty. In no event will the authors be held liable for any damages 5 + arising from the use of this software. 6 + 7 + Permission is granted to anyone to use this software for any purpose, 8 + including commercial applications, and to alter it and redistribute it 9 + freely, subject to the following restrictions: 10 + 11 + 1. The origin of this software must not be misrepresented; you must not 12 + claim that you wrote the original software. If you use this software 13 + in a product, an acknowledgment in the product documentation would be 14 + appreciated but is not required. 15 + 16 + 2. Altered source versions must be plainly marked as such, and must not be 17 + misrepresented as being the original software. 18 + 19 + 3. This notice may not be removed or altered from any source 20 + distribution.
+404
README.md
··· 1 + # DocView 2 + 3 + A framework-agnostic, universal document renderer for the browser. Render PDFs, EPUBs, DOCX files, CSVs, source code, and plain text — with optional support for pre-rendered page images and chunked streaming for large documents. 4 + 5 + **Core** (`@docview/core`) is a vanilla TypeScript library with zero framework dependencies. **React** (`@docview/react`) provides a thin wrapper component and hook. Both are designed for drop-in use in any web project. 6 + 7 + ## Features 8 + 9 + - **Multi-format rendering** — PDF, EPUB, DOCX, CSV/TSV, source code (100+ languages), plain text 10 + - **Chunked loading** — Stream large documents via pre-rendered page images or split PDF chunks 11 + - **Fetch adapters** — Pass data directly or provide a lazy-loading callback for on-demand fetching 12 + - **CSS variable theming** — Dark and light themes built in, fully customizable via `--dv-*` variables 13 + - **Framework-agnostic** — Use vanilla JS, React, or build your own wrapper 14 + - **Lazy peer dependencies** — Only loads renderer libraries (pdfjs, epubjs, etc.) when that format is actually used 15 + - **Custom renderers** — Register your own renderer for any format via the plugin registry 16 + - **TypeScript-first** — Complete type definitions for all APIs 17 + 18 + ## Installation 19 + 20 + ```bash 21 + # Core (vanilla JS) 22 + npm install @docview/core 23 + 24 + # React wrapper 25 + npm install @docview/react 26 + 27 + # Install peer dependencies for the formats you need: 28 + npm install pdfjs-dist # PDF 29 + npm install epubjs # EPUB 30 + npm install docx-preview # DOCX 31 + npm install papaparse # CSV/TSV 32 + npm install highlight.js # Code syntax highlighting 33 + ``` 34 + 35 + You only need to install peer dependencies for the formats you plan to render. Unused formats won't add to your bundle. 36 + 37 + ## Quick Start 38 + 39 + ### Vanilla JS 40 + 41 + ```typescript 42 + import { DocView } from '@docview/core' 43 + import '@docview/core/styles.css' 44 + 45 + const viewer = new DocView(document.getElementById('viewer')!, { 46 + source: { type: 'url', url: '/document.pdf' }, 47 + theme: 'dark', 48 + toolbar: true, 49 + onReady: (info) => { 50 + console.log(`Loaded: ${info.pageCount} pages`) 51 + }, 52 + onPageChange: (page, total) => { 53 + console.log(`Page ${page} of ${total}`) 54 + }, 55 + }) 56 + 57 + // Imperative control 58 + viewer.goToPage(5) 59 + viewer.setZoom('fit-width') 60 + 61 + // Clean up 62 + viewer.destroy() 63 + ``` 64 + 65 + ### React 66 + 67 + ```tsx 68 + import { DocumentViewer } from '@docview/react' 69 + import '@docview/core/styles.css' 70 + 71 + function App() { 72 + return ( 73 + <DocumentViewer 74 + source={{ type: 'url', url: '/report.pdf' }} 75 + theme="dark" 76 + style={{ width: '100%', height: '80vh' }} 77 + onReady={(info) => console.log(`${info.pageCount} pages`)} 78 + onPageChange={(page, total) => console.log(`${page}/${total}`)} 79 + /> 80 + ) 81 + } 82 + ``` 83 + 84 + ### React with Ref 85 + 86 + ```tsx 87 + import { useRef } from 'react' 88 + import { DocumentViewer, type DocumentViewerRef } from '@docview/react' 89 + import '@docview/core/styles.css' 90 + 91 + function App() { 92 + const viewerRef = useRef<DocumentViewerRef>(null) 93 + 94 + return ( 95 + <> 96 + <DocumentViewer 97 + ref={viewerRef} 98 + source={{ type: 'url', url: '/report.pdf' }} 99 + style={{ width: '100%', height: '80vh' }} 100 + /> 101 + <button onClick={() => viewerRef.current?.goToPage(1)}> 102 + Go to first page 103 + </button> 104 + </> 105 + ) 106 + } 107 + ``` 108 + 109 + ### React Hook (headless) 110 + 111 + ```tsx 112 + import { useDocumentRenderer } from '@docview/react' 113 + import '@docview/core/styles.css' 114 + 115 + function CustomViewer({ url }: { url: string }) { 116 + const { containerRef, state, goToPage, setZoom } = useDocumentRenderer({ 117 + source: { type: 'url', url }, 118 + theme: 'dark', 119 + toolbar: false, // Hide built-in toolbar, build your own 120 + }) 121 + 122 + return ( 123 + <div> 124 + <div ref={containerRef} style={{ width: '100%', height: '600px' }} /> 125 + <div> 126 + <button onClick={() => goToPage(state.currentPage - 1)}>Prev</button> 127 + <span>{state.currentPage} / {state.totalPages}</span> 128 + <button onClick={() => goToPage(state.currentPage + 1)}>Next</button> 129 + <button onClick={() => setZoom(state.zoom * 1.2)}>Zoom In</button> 130 + </div> 131 + </div> 132 + ) 133 + } 134 + ``` 135 + 136 + ## Document Sources 137 + 138 + DocView accepts four types of document sources: 139 + 140 + ### File (binary data) 141 + 142 + ```typescript 143 + // From a File input 144 + const file = inputElement.files[0] 145 + source = { type: 'file', data: file, filename: file.name } 146 + 147 + // From an ArrayBuffer 148 + source = { type: 'file', data: arrayBuffer, mimeType: 'application/pdf' } 149 + 150 + // From a Uint8Array 151 + source = { type: 'file', data: uint8Array, filename: 'doc.pdf' } 152 + ``` 153 + 154 + ### URL 155 + 156 + ```typescript 157 + source = { type: 'url', url: 'https://example.com/doc.pdf' } 158 + 159 + // With custom headers (e.g., auth) 160 + source = { 161 + type: 'url', 162 + url: '/api/documents/123.pdf', 163 + fetchOptions: { headers: { Authorization: 'Bearer ...' } }, 164 + } 165 + ``` 166 + 167 + ### Pre-rendered Pages (for browsing without the original document) 168 + 169 + ```typescript 170 + // Direct data 171 + source = { 172 + type: 'pages', 173 + pages: [ 174 + { pageNumber: 1, imageUrl: '/pages/1.webp', width: 1654, height: 2339 }, 175 + { pageNumber: 2, imageUrl: '/pages/2.webp', width: 1654, height: 2339 }, 176 + ], 177 + } 178 + 179 + // Lazy fetch adapter (loads pages on demand as user scrolls) 180 + source = { 181 + type: 'pages', 182 + pages: { 183 + totalPages: 500, 184 + fetchPage: async (pageNumber) => ({ 185 + pageNumber, 186 + imageUrl: `/api/pages/${pageNumber}.webp`, 187 + width: 1654, 188 + height: 2339, 189 + }), 190 + }, 191 + } 192 + ``` 193 + 194 + ### Chunked PDF (streaming large documents) 195 + 196 + ```typescript 197 + source = { 198 + type: 'chunked', 199 + totalPages: 500, 200 + // PDF chunks for full-fidelity rendering 201 + chunks: { 202 + totalChunks: 10, 203 + totalPages: 500, 204 + fetchChunk: async (index) => { 205 + const res = await fetch(`/api/chunks/${index}.pdf`) 206 + return { 207 + data: await res.arrayBuffer(), 208 + pageStart: index * 50 + 1, 209 + pageEnd: Math.min((index + 1) * 50, 500), 210 + } 211 + }, 212 + getChunkIndexForPage: (page) => Math.floor((page - 1) / 50), 213 + }, 214 + // Optional: fast browse images while chunks load 215 + browsePages: { 216 + totalPages: 500, 217 + fetchPage: async (pageNumber) => ({ 218 + pageNumber, 219 + imageUrl: `/api/browse/${pageNumber}.webp`, 220 + width: 1654, 221 + height: 2339, 222 + }), 223 + }, 224 + } 225 + ``` 226 + 227 + ## Theming 228 + 229 + DocView uses CSS custom properties for all visual styling. Override any `--dv-*` variable to customize: 230 + 231 + ```css 232 + /* Custom theme */ 233 + .my-viewer .docview { 234 + --dv-bg: #1e1e2e; 235 + --dv-surface: #2a2a3e; 236 + --dv-text: #cdd6f4; 237 + --dv-accent: #89b4fa; 238 + --dv-border: #45475a; 239 + --dv-page-shadow: 0 2px 12px rgba(0, 0, 0, 0.4); 240 + --dv-font-sans: 'JetBrains Mono', monospace; 241 + } 242 + ``` 243 + 244 + Built-in themes: `dark` (default) and `light`. Set via the `theme` prop/option, or `'system'` to auto-detect from `prefers-color-scheme`. 245 + 246 + ### Key CSS Variables 247 + 248 + | Variable | Description | 249 + |----------|-------------| 250 + | `--dv-bg` | Background color | 251 + | `--dv-surface` | Toolbar and panel backgrounds | 252 + | `--dv-text` | Primary text color | 253 + | `--dv-text-secondary` | Secondary/muted text | 254 + | `--dv-accent` | Accent color (links, focus rings) | 255 + | `--dv-border` | Border color | 256 + | `--dv-page-bg` | Document page background | 257 + | `--dv-page-shadow` | Document page drop shadow | 258 + | `--dv-font-sans` | Sans-serif font stack | 259 + | `--dv-font-mono` | Monospace font stack | 260 + | `--dv-radius` | Border radius | 261 + | `--dv-toolbar-height` | Toolbar height | 262 + 263 + See `styles.css` for the complete list. 264 + 265 + ## Format-Specific Options 266 + 267 + ### PDF 268 + 269 + ```typescript 270 + { 271 + pdf: { 272 + workerSrc: '/pdf.worker.min.js', // pdf.js worker URL 273 + cMapUrl: '/cmaps/', // Character map directory 274 + textLayer: true, // Enable text selection (default true) 275 + annotationLayer: false, // Show PDF annotations 276 + } 277 + } 278 + ``` 279 + 280 + ### Code 281 + 282 + ```typescript 283 + { 284 + code: { 285 + language: 'typescript', // Force language (auto-detected from extension) 286 + lineNumbers: true, // Show line numbers (default true) 287 + wordWrap: false, // Enable word wrapping (default false) 288 + tabSize: 2, // Tab width in spaces (default 2) 289 + } 290 + } 291 + ``` 292 + 293 + ### CSV 294 + 295 + ```typescript 296 + { 297 + csv: { 298 + delimiter: ',', // Field delimiter (auto-detected) 299 + header: true, // First row is header (default true) 300 + maxRows: 10000, // Max rows to render (default 10000) 301 + sortable: true, // Enable column sorting (default true) 302 + } 303 + } 304 + ``` 305 + 306 + ### EPUB 307 + 308 + ```typescript 309 + { 310 + epub: { 311 + flow: 'paginated', // 'paginated' or 'scrolled' (default 'paginated') 312 + fontSize: 16, // Font size in pixels (default 16) 313 + fontFamily: 'Georgia', // Font override 314 + } 315 + } 316 + ``` 317 + 318 + ## Custom Renderers 319 + 320 + Register a renderer for any format: 321 + 322 + ```typescript 323 + import { DocView, BaseRenderer, type DocViewOptions, type DocumentFormat } from '@docview/core' 324 + 325 + class MarkdownRenderer extends BaseRenderer { 326 + readonly format: DocumentFormat = 'custom-markdown' 327 + 328 + protected async onMount(viewport: HTMLElement, options: DocViewOptions) { 329 + // Your rendering logic here 330 + const text = await this.loadText(options.source) 331 + const html = myMarkdownLib.render(text) 332 + viewport.innerHTML = html 333 + this.setReady({ format: 'custom-markdown', pageCount: 1 }) 334 + } 335 + 336 + protected onDestroy() {} 337 + } 338 + 339 + // Register globally 340 + DocView.registerRenderer('custom-markdown', () => new MarkdownRenderer()) 341 + 342 + // Use it 343 + new DocView(container, { 344 + source: { type: 'url', url: '/readme.md' }, 345 + format: 'custom-markdown', 346 + }) 347 + ``` 348 + 349 + ## Supported Formats 350 + 351 + | Format | Peer Dependency | Auto-detected Extensions | 352 + |--------|----------------|-------------------------| 353 + | PDF | `pdfjs-dist` | `.pdf` | 354 + | EPUB | `epubjs` | `.epub` | 355 + | DOCX | `docx-preview` | `.docx`, `.doc` | 356 + | CSV/TSV | `papaparse` | `.csv`, `.tsv` | 357 + | Code | `highlight.js` | `.js`, `.ts`, `.py`, `.rs`, `.go`, `.java`, `.c`, `.cpp`, +80 more | 358 + | Text | _(none)_ | `.txt` | 359 + | Markdown | `highlight.js` | `.md` (rendered as syntax-highlighted code) | 360 + | JSON | `highlight.js` | `.json` | 361 + | XML/HTML | `highlight.js` | `.xml`, `.html`, `.svg` | 362 + | Pages | _(none)_ | N/A (explicit `type: 'pages'`) | 363 + | Chunked PDF | `pdfjs-dist` | N/A (explicit `type: 'chunked'`) | 364 + 365 + ## Browser Support 366 + 367 + - Chrome/Edge 88+ 368 + - Firefox 78+ 369 + - Safari 15.4+ (OffscreenCanvas support for Web Worker rendering) 370 + 371 + ## Project Structure 372 + 373 + ``` 374 + packages/ 375 + ├── core/ @docview/core — Framework-agnostic TypeScript core 376 + │ ├── src/ 377 + │ │ ├── types.ts # All interfaces and types 378 + │ │ ├── docview.ts # Main DocView class 379 + │ │ ├── renderer.ts # Abstract base renderer 380 + │ │ ├── registry.ts # Format → renderer factory mapping 381 + │ │ ├── toolbar.ts # Built-in toolbar DOM builder 382 + │ │ ├── utils.ts # Format detection, data conversion, DOM helpers 383 + │ │ ├── styles.css # CSS variables theme system 384 + │ │ └── renderers/ 385 + │ │ ├── pdf.ts # PDF (pdfjs-dist) 386 + │ │ ├── browse-pages.ts # Pre-rendered page images 387 + │ │ ├── chunked-pdf.ts # Chunked PDF streaming 388 + │ │ ├── epub.ts # EPUB (epubjs) 389 + │ │ ├── docx.ts # DOCX (docx-preview) 390 + │ │ ├── csv.ts # CSV/TSV (papaparse) 391 + │ │ ├── code.ts # Code (highlight.js) 392 + │ │ └── text.ts # Plain text 393 + │ └── package.json 394 + └── react/ @docview/react — React wrapper 395 + ├── src/ 396 + │ ├── DocumentViewer.tsx # Drop-in component 397 + │ ├── useDocumentRenderer.ts # Headless hook 398 + │ └── index.ts 399 + └── package.json 400 + ``` 401 + 402 + ## License 403 + 404 + MIT
+20
examples/basic/LICENSE
··· 1 + Copyright (c) 2026 Aria Quinlan 2 + 3 + This software is provided ‘as-is’, without any express or implied 4 + warranty. In no event will the authors be held liable for any damages 5 + arising from the use of this software. 6 + 7 + Permission is granted to anyone to use this software for any purpose, 8 + including commercial applications, and to alter it and redistribute it 9 + freely, subject to the following restrictions: 10 + 11 + 1. The origin of this software must not be misrepresented; you must not 12 + claim that you wrote the original software. If you use this software 13 + in a product, an acknowledgment in the product documentation would be 14 + appreciated but is not required. 15 + 16 + 2. Altered source versions must be plainly marked as such, and must not be 17 + misrepresented as being the original software. 18 + 19 + 3. This notice may not be removed or altered from any source 20 + distribution.
+79
examples/basic/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>DocView — Basic Example</title> 7 + <style> 8 + * { 9 + margin: 0; 10 + padding: 0; 11 + box-sizing: border-box; 12 + } 13 + 14 + body { 15 + font-family: system-ui, -apple-system, sans-serif; 16 + background: #0f0f0f; 17 + color: #e0e0e0; 18 + display: flex; 19 + flex-direction: column; 20 + align-items: center; 21 + min-height: 100vh; 22 + padding: 2rem; 23 + } 24 + 25 + h1 { 26 + font-size: 1.5rem; 27 + font-weight: 600; 28 + margin-bottom: 0.5rem; 29 + color: #fff; 30 + } 31 + 32 + .subtitle { 33 + font-size: 0.875rem; 34 + color: #888; 35 + margin-bottom: 1.5rem; 36 + } 37 + 38 + #viewer { 39 + width: 100%; 40 + max-width: 900px; 41 + height: 80vh; 42 + border-radius: 8px; 43 + overflow: hidden; 44 + border: 1px solid #2a2a2a; 45 + } 46 + 47 + .controls { 48 + margin-top: 1rem; 49 + display: flex; 50 + gap: 0.5rem; 51 + } 52 + 53 + .controls label { 54 + font-size: 0.875rem; 55 + color: #aaa; 56 + display: flex; 57 + align-items: center; 58 + gap: 0.5rem; 59 + } 60 + 61 + .controls input[type="file"] { 62 + font-size: 0.8rem; 63 + color: #ccc; 64 + } 65 + </style> 66 + </head> 67 + <body> 68 + <h1>DocView — Basic Example</h1> 69 + <p class="subtitle">Drop-in document viewer. Pick a file below to view it.</p> 70 + <div class="controls"> 71 + <label> 72 + Open a document: 73 + <input type="file" id="file-input" accept=".pdf,.epub,.docx,.odt,.ods,.csv,.tsv,.txt,.md,.json,.js,.ts,.py,.rs,.go,.java,.c,.cpp,.html,.xml,.svg" /> 74 + </label> 75 + </div> 76 + <div id="viewer"></div> 77 + <script type="module" src="/src/main.ts"></script> 78 + </body> 79 + </html>
+25
examples/basic/package.json
··· 1 + { 2 + "name": "@docview/example-basic", 3 + "private": true, 4 + "version": "0.0.0", 5 + "type": "module", 6 + "scripts": { 7 + "dev": "vite", 8 + "build": "tsc && vite build", 9 + "preview": "vite preview" 10 + }, 11 + "dependencies": { 12 + "@docview/core": "workspace:*", 13 + "pdfjs-dist": ">=4.0.0", 14 + "epubjs": ">=0.3.0", 15 + "docx-preview": ">=0.3.0", 16 + "papaparse": ">=5.0.0", 17 + "highlight.js": ">=11.0.0", 18 + "jszip": ">=3.0.0", 19 + "xlsx": ">=0.18.0" 20 + }, 21 + "devDependencies": { 22 + "typescript": "^5.5.0", 23 + "vite": "^6.0.0" 24 + } 25 + }
+39
examples/basic/src/main.ts
··· 1 + import { DocView } from '@docview/core' 2 + import '../../../packages/core/src/styles.css' 3 + import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url' 4 + 5 + const viewerEl = document.getElementById('viewer')! 6 + const fileInput = document.getElementById('file-input') as HTMLInputElement 7 + 8 + let viewer: DocView | null = null 9 + 10 + fileInput.addEventListener('change', () => { 11 + const file = fileInput.files?.[0] 12 + if (!file) return 13 + 14 + // Destroy previous viewer if one exists 15 + if (viewer) { 16 + viewer.destroy() 17 + viewer = null 18 + } 19 + 20 + // Create a new DocView instance with the selected file 21 + viewer = new DocView(viewerEl, { 22 + source: { 23 + type: 'file', 24 + data: file, 25 + filename: file.name, 26 + }, 27 + theme: 'dark', 28 + toolbar: true, 29 + pdf: { 30 + workerSrc: pdfjsWorker, 31 + }, 32 + onReady: (info) => { 33 + console.log(`Loaded "${file.name}" — ${info.pageCount} page(s), format: ${info.format}`) 34 + }, 35 + onError: (err) => { 36 + console.error('DocView error:', err) 37 + }, 38 + }) 39 + })
+1
examples/basic/src/vite-env.d.ts
··· 1 + /// <reference types="vite/client" />
+8
examples/basic/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "outDir": "./dist", 5 + "noEmit": true 6 + }, 7 + "include": ["src"] 8 + }
+67
examples/basic/vite.config.ts
··· 1 + import { defineConfig, type Plugin } from 'vite' 2 + 3 + /** 4 + * The core library's `requirePeerDep` does `import(moduleName)` where 5 + * `moduleName` is a variable — Vite cannot statically analyze that, so the 6 + * import fails at runtime in the browser. 7 + * 8 + * This plugin rewrites the variable import into a lookup of static `import()` 9 + * calls that Vite CAN resolve and pre-bundle. 10 + */ 11 + function resolvePeerDeps(): Plugin { 12 + const peerDeps = [ 13 + 'pdfjs-dist', 14 + 'epubjs', 15 + 'docx-preview', 16 + 'papaparse', 17 + 'highlight.js', 18 + 'jszip', 19 + 'xlsx', 20 + ] 21 + 22 + // Build a code snippet that maps module names → static imports. 23 + // highlight.js needs `.default` unwrapping for ESM compatibility. 24 + const cases = peerDeps 25 + .map((d) => ` case '${d}': return import('${d}').then(m => m.default || m);`) 26 + .join('\n') 27 + 28 + const replacement = [ 29 + '(async (name) => { switch(name) {', 30 + cases, 31 + ' default: throw new Error(`Unknown peer dep: ${name}`);', 32 + ' }})(moduleName)', 33 + ].join('\n') 34 + 35 + return { 36 + name: 'resolve-docview-peer-deps', 37 + enforce: 'pre', 38 + transform(code: string, id: string) { 39 + // Only transform the @docview/core bundle 40 + if (!id.includes('docview')) return 41 + if (!code.includes('moduleName')) return 42 + 43 + // Replace `await import(moduleName)` or `await import(\n moduleName\n)` 44 + const result = code.replace( 45 + /await\s+import\(\s*(?:\/\*.*?\*\/\s*)?moduleName\s*\)/g, 46 + `await ${replacement}`, 47 + ) 48 + 49 + if (result !== code) return result 50 + }, 51 + } 52 + } 53 + 54 + export default defineConfig({ 55 + plugins: [resolvePeerDeps()], 56 + optimizeDeps: { 57 + include: [ 58 + 'pdfjs-dist', 59 + 'epubjs', 60 + 'docx-preview', 61 + 'papaparse', 62 + 'highlight.js', 63 + 'jszip', 64 + 'xlsx', 65 + ], 66 + }, 67 + })
+20
examples/vanilla/LICENSE
··· 1 + Copyright (c) 2026 Aria Quinlan 2 + 3 + This software is provided ‘as-is’, without any express or implied 4 + warranty. In no event will the authors be held liable for any damages 5 + arising from the use of this software. 6 + 7 + Permission is granted to anyone to use this software for any purpose, 8 + including commercial applications, and to alter it and redistribute it 9 + freely, subject to the following restrictions: 10 + 11 + 1. The origin of this software must not be misrepresented; you must not 12 + claim that you wrote the original software. If you use this software 13 + in a product, an acknowledgment in the product documentation would be 14 + appreciated but is not required. 15 + 16 + 2. Altered source versions must be plainly marked as such, and must not be 17 + misrepresented as being the original software. 18 + 19 + 3. This notice may not be removed or altered from any source 20 + distribution.
+99
examples/vanilla/build.js
··· 1 + /** 2 + * Minimal esbuild build script — replaces Vite entirely. 3 + * 4 + * The only "magic" is an esbuild plugin that rewrites the variable-based 5 + * dynamic `import(moduleName)` in @docview/core's `requirePeerDep` into 6 + * static import() calls that esbuild can bundle. 7 + * 8 + * This is the same fundamental fix needed in Vite, webpack, or any other 9 + * bundler — browsers cannot resolve bare specifiers from `import(variable)`. 10 + */ 11 + import { build, context } from 'esbuild' 12 + import { cpSync, existsSync, mkdirSync } from 'fs' 13 + import { resolve, dirname } from 'path' 14 + import { fileURLToPath } from 'url' 15 + 16 + const __dirname = dirname(fileURLToPath(import.meta.url)) 17 + const isWatch = process.argv.includes('--watch') 18 + 19 + /** Plugin to resolve @docview/core's dynamic peer-dep imports. */ 20 + const resolvePeerDeps = { 21 + name: 'resolve-peer-deps', 22 + setup(build) { 23 + build.onLoad({ filter: /docview.*\.(js|ts)$/ }, async (args) => { 24 + const fs = await import('fs') 25 + let contents = fs.readFileSync(args.path, 'utf8') 26 + 27 + if (contents.includes('moduleName') && contents.includes('import(')) { 28 + // Map of peer dep names → static imports 29 + const peerDeps = [ 30 + 'pdfjs-dist', 'epubjs', 'docx-preview', 31 + 'papaparse', 'highlight.js', 'jszip', 'xlsx', 32 + ] 33 + const cases = peerDeps 34 + .map(d => ` case '${d}': return import('${d}').then(m => m.default || m);`) 35 + .join('\n') 36 + 37 + const replacement = [ 38 + '(async (name) => { switch(name) {', 39 + cases, 40 + " default: throw new Error(`Unknown peer dep: ${name}`);", 41 + ' }})(moduleName)', 42 + ].join('\n') 43 + 44 + contents = contents.replace( 45 + /await\s+import\(\s*(?:\/\*.*?\*\/\s*)?moduleName\s*\)/g, 46 + `await ${replacement}`, 47 + ) 48 + } 49 + 50 + return { contents, loader: args.path.endsWith('.ts') ? 'ts' : 'js' } 51 + }) 52 + }, 53 + } 54 + 55 + // Ensure dist directory exists 56 + const distDir = resolve(__dirname, 'dist') 57 + if (!existsSync(distDir)) mkdirSync(distDir, { recursive: true }) 58 + 59 + // Copy index.html to dist 60 + cpSync(resolve(__dirname, 'index.html'), resolve(distDir, 'index.html')) 61 + 62 + // Copy styles.css to dist 63 + const stylesPath = resolve(__dirname, '../../packages/core/src/styles.css') 64 + if (existsSync(stylesPath)) { 65 + cpSync(stylesPath, resolve(distDir, 'styles.css')) 66 + } 67 + 68 + // Copy pdfjs worker to dist 69 + const workerGlob = resolve(__dirname, 'node_modules/pdfjs-dist/build') 70 + if (existsSync(workerGlob)) { 71 + const workerFiles = ['pdf.worker.min.mjs', 'pdf.worker.mjs', 'pdf.worker.min.js'] 72 + for (const wf of workerFiles) { 73 + const src = resolve(workerGlob, wf) 74 + if (existsSync(src)) { 75 + cpSync(src, resolve(distDir, wf)) 76 + break 77 + } 78 + } 79 + } 80 + 81 + const buildOptions = { 82 + entryPoints: [resolve(__dirname, 'src/main.ts')], 83 + bundle: true, 84 + format: 'esm', 85 + outdir: distDir, 86 + sourcemap: true, 87 + target: 'es2022', 88 + plugins: [resolvePeerDeps], 89 + logLevel: 'info', 90 + } 91 + 92 + if (isWatch) { 93 + const ctx = await context(buildOptions) 94 + await ctx.watch() 95 + console.log('Watching for changes...') 96 + } else { 97 + await build(buildOptions) 98 + console.log(`\nBuild complete! Serve with:\n npx serve dist\n`) 99 + }
+80
examples/vanilla/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>DocView — Vanilla TS Example</title> 7 + <link rel="stylesheet" href="./styles.css" /> 8 + <style> 9 + * { 10 + margin: 0; 11 + padding: 0; 12 + box-sizing: border-box; 13 + } 14 + 15 + body { 16 + font-family: system-ui, -apple-system, sans-serif; 17 + background: #0f0f0f; 18 + color: #e0e0e0; 19 + display: flex; 20 + flex-direction: column; 21 + align-items: center; 22 + min-height: 100vh; 23 + padding: 2rem; 24 + } 25 + 26 + h1 { 27 + font-size: 1.5rem; 28 + font-weight: 600; 29 + margin-bottom: 0.5rem; 30 + color: #fff; 31 + } 32 + 33 + .subtitle { 34 + font-size: 0.875rem; 35 + color: #888; 36 + margin-bottom: 1.5rem; 37 + } 38 + 39 + #viewer { 40 + width: 100%; 41 + max-width: 900px; 42 + height: 80vh; 43 + border-radius: 8px; 44 + overflow: hidden; 45 + border: 1px solid #2a2a2a; 46 + } 47 + 48 + .controls { 49 + margin-top: 1rem; 50 + display: flex; 51 + gap: 0.5rem; 52 + } 53 + 54 + .controls label { 55 + font-size: 0.875rem; 56 + color: #aaa; 57 + display: flex; 58 + align-items: center; 59 + gap: 0.5rem; 60 + } 61 + 62 + .controls input[type="file"] { 63 + font-size: 0.8rem; 64 + color: #ccc; 65 + } 66 + </style> 67 + </head> 68 + <body> 69 + <h1>DocView — Vanilla TS Example</h1> 70 + <p class="subtitle">No Vite. Just TypeScript + esbuild. Pick a file below to view it.</p> 71 + <div class="controls"> 72 + <label> 73 + Open a document: 74 + <input type="file" id="file-input" accept=".pdf,.epub,.docx,.odt,.ods,.csv,.tsv,.txt,.md,.json,.js,.ts,.py,.rs,.go,.java,.c,.cpp,.html,.xml,.svg" /> 75 + </label> 76 + </div> 77 + <div id="viewer"></div> 78 + <script type="module" src="./main.js"></script> 79 + </body> 80 + </html>
+25
examples/vanilla/package.json
··· 1 + { 2 + "name": "@docview/example-vanilla", 3 + "private": true, 4 + "version": "0.0.0", 5 + "type": "module", 6 + "scripts": { 7 + "build": "node build.js", 8 + "serve": "node build.js && npx -y serve dist", 9 + "dev": "node build.js --watch" 10 + }, 11 + "dependencies": { 12 + "@docview/core": "workspace:*", 13 + "pdfjs-dist": ">=4.0.0", 14 + "epubjs": ">=0.3.0", 15 + "docx-preview": ">=0.3.0", 16 + "papaparse": ">=5.0.0", 17 + "highlight.js": ">=11.0.0", 18 + "jszip": ">=3.0.0", 19 + "xlsx": ">=0.18.0" 20 + }, 21 + "devDependencies": { 22 + "typescript": "^5.5.0", 23 + "esbuild": "^0.25.0" 24 + } 25 + }
+38
examples/vanilla/src/main.ts
··· 1 + import { DocView } from '@docview/core' 2 + 3 + const viewerEl = document.getElementById('viewer')! 4 + const fileInput = document.getElementById('file-input') as HTMLInputElement 5 + 6 + let viewer: DocView | null = null 7 + 8 + fileInput.addEventListener('change', () => { 9 + const file = fileInput.files?.[0] 10 + if (!file) return 11 + 12 + // Destroy previous viewer if one exists 13 + if (viewer) { 14 + viewer.destroy() 15 + viewer = null 16 + } 17 + 18 + // Create a new DocView instance with the selected file 19 + viewer = new DocView(viewerEl, { 20 + source: { 21 + type: 'file', 22 + data: file, 23 + filename: file.name, 24 + }, 25 + theme: 'dark', 26 + toolbar: true, 27 + pdf: { 28 + // In the bundled output, the worker is a sibling file in dist/ 29 + workerSrc: './pdf.worker.min.mjs', 30 + }, 31 + onReady: (info) => { 32 + console.log(`Loaded "${file.name}" — ${info.pageCount} page(s), format: ${info.format}`) 33 + }, 34 + onError: (err) => { 35 + console.error('DocView error:', err) 36 + }, 37 + }) 38 + })
+8
examples/vanilla/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "outDir": "./dist", 5 + "noEmit": true 6 + }, 7 + "include": ["src"] 8 + }
+20
package.json
··· 1 + { 2 + "name": "docview-monorepo", 3 + "private": true, 4 + "version": "0.0.0", 5 + "description": "A framework-agnostic, universal document renderer with optional chunked loading", 6 + "scripts": { 7 + "build": "pnpm -r build", 8 + "dev": "pnpm -r --parallel dev", 9 + "clean": "pnpm -r clean", 10 + "lint": "pnpm -r lint", 11 + "typecheck": "pnpm -r typecheck" 12 + }, 13 + "devDependencies": { 14 + "typescript": "^5.5.0" 15 + }, 16 + "engines": { 17 + "node": ">=18", 18 + "pnpm": ">=9" 19 + } 20 + }
+20
packages/core/LICENSE
··· 1 + Copyright (c) 2026 Aria Quinlan 2 + 3 + This software is provided ‘as-is’, without any express or implied 4 + warranty. In no event will the authors be held liable for any damages 5 + arising from the use of this software. 6 + 7 + Permission is granted to anyone to use this software for any purpose, 8 + including commercial applications, and to alter it and redistribute it 9 + freely, subject to the following restrictions: 10 + 11 + 1. The origin of this software must not be misrepresented; you must not 12 + claim that you wrote the original software. If you use this software 13 + in a product, an acknowledgment in the product documentation would be 14 + appreciated but is not required. 15 + 16 + 2. Altered source versions must be plainly marked as such, and must not be 17 + misrepresented as being the original software. 18 + 19 + 3. This notice may not be removed or altered from any source 20 + distribution.
+80
packages/core/package.json
··· 1 + { 2 + "name": "@docview/core", 3 + "version": "0.1.0", 4 + "description": "Framework-agnostic universal document renderer — PDF, EPUB, DOCX, CSV, code, and plain text with optional chunked/paged loading", 5 + "license": "Zlib", 6 + "type": "module", 7 + "main": "./dist/index.cjs", 8 + "module": "./dist/index.js", 9 + "types": "./dist/index.d.ts", 10 + "exports": { 11 + ".": { 12 + "import": "./dist/index.js", 13 + "require": "./dist/index.cjs", 14 + "types": "./dist/index.d.ts" 15 + }, 16 + "./styles.css": "./dist/styles.css" 17 + }, 18 + "files": [ 19 + "dist", 20 + "README.md", 21 + "LICENSE" 22 + ], 23 + "sideEffects": [ 24 + "*.css" 25 + ], 26 + "scripts": { 27 + "build": "tsup", 28 + "dev": "tsup --watch", 29 + "clean": "rm -rf dist", 30 + "typecheck": "tsc --noEmit" 31 + }, 32 + "devDependencies": { 33 + "tsup": "^8.0.0", 34 + "typescript": "^5.5.0" 35 + }, 36 + "peerDependencies": { 37 + "pdfjs-dist": ">=4.0.0", 38 + "epubjs": ">=0.3.0", 39 + "docx-preview": ">=0.3.0", 40 + "papaparse": ">=5.0.0", 41 + "highlight.js": ">=11.0.0", 42 + "jszip": ">=3.0.0", 43 + "xlsx": ">=0.18.0" 44 + }, 45 + "peerDependenciesMeta": { 46 + "pdfjs-dist": { 47 + "optional": true 48 + }, 49 + "epubjs": { 50 + "optional": true 51 + }, 52 + "docx-preview": { 53 + "optional": true 54 + }, 55 + "papaparse": { 56 + "optional": true 57 + }, 58 + "highlight.js": { 59 + "optional": true 60 + }, 61 + "jszip": { 62 + "optional": true 63 + }, 64 + "xlsx": { 65 + "optional": true 66 + } 67 + }, 68 + "keywords": [ 69 + "pdf", 70 + "epub", 71 + "docx", 72 + "csv", 73 + "document", 74 + "viewer", 75 + "renderer", 76 + "reader", 77 + "chunked", 78 + "framework-agnostic" 79 + ] 80 + }
+308
packages/core/src/docview.ts
··· 1 + import type { 2 + DocViewOptions, 3 + DocViewState, 4 + DocumentFormat, 5 + Renderer, 6 + RendererFactory, 7 + DocViewEventMap, 8 + DocViewEventType, 9 + ToolbarConfig, 10 + } from './types.js' 11 + import { DocViewError } from './types.js' 12 + import { registry } from './registry.js' 13 + import { detectFormat, getRendererFormat, clearElement } from './utils.js' 14 + import { createToolbar, type ToolbarHandle } from './toolbar.js' 15 + import { registerBuiltinRenderers } from './renderers/index.js' 16 + 17 + // Register built-in renderers on first import 18 + let registered = false 19 + function ensureRegistered() { 20 + if (!registered) { 21 + registerBuiltinRenderers() 22 + registered = true 23 + } 24 + } 25 + 26 + /** 27 + * DocView — Universal Document Viewer 28 + * 29 + * Framework-agnostic entry point. Creates a document viewer inside a container 30 + * element, auto-detecting the format and loading the appropriate renderer. 31 + * 32 + * @example 33 + * ```ts 34 + * import { DocView } from '@docview/core' 35 + * import '@docview/core/styles.css' 36 + * 37 + * const viewer = new DocView(document.getElementById('viewer')!, { 38 + * source: { type: 'url', url: '/document.pdf' }, 39 + * theme: 'dark', 40 + * onReady: (info) => console.log('Loaded:', info.pageCount, 'pages'), 41 + * }) 42 + * 43 + * // Navigate 44 + * viewer.goToPage(5) 45 + * 46 + * // Clean up 47 + * viewer.destroy() 48 + * ``` 49 + */ 50 + export class DocView { 51 + private container: HTMLElement 52 + private options: DocViewOptions 53 + private renderer: Renderer | null = null 54 + private toolbar: ToolbarHandle | null = null 55 + private root: HTMLElement 56 + private listeners = new Map<string, Set<(data: unknown) => void>>() 57 + private destroyed = false 58 + 59 + constructor(container: HTMLElement, options: DocViewOptions) { 60 + ensureRegistered() 61 + 62 + this.container = container 63 + this.options = { ...options } 64 + 65 + // Create root element 66 + this.root = document.createElement('div') 67 + this.root.className = `docview${options.className ? ` ${options.className}` : ''}` 68 + this.root.setAttribute('data-theme', this.resolveTheme(options.theme)) 69 + container.appendChild(this.root) 70 + 71 + // Initialize asynchronously 72 + this.init().catch((err) => { 73 + const error = err instanceof DocViewError 74 + ? err 75 + : new DocViewError('UNKNOWN', String(err), err) 76 + options.onError?.(error) 77 + this.emit('error', error) 78 + }) 79 + } 80 + 81 + // --------------------------------------------------------------------------- 82 + // Public API 83 + // --------------------------------------------------------------------------- 84 + 85 + /** Navigate to a specific page (1-indexed). */ 86 + goToPage(page: number): void { 87 + this.renderer?.goToPage(page) 88 + this.updateToolbar() 89 + } 90 + 91 + /** Get the current page number. */ 92 + getCurrentPage(): number { 93 + return this.renderer?.getCurrentPage() ?? 1 94 + } 95 + 96 + /** Get the total page count. */ 97 + getPageCount(): number { 98 + return this.renderer?.getPageCount() ?? 0 99 + } 100 + 101 + /** Set zoom level. */ 102 + setZoom(zoom: number | 'fit-width' | 'fit-page'): void { 103 + this.renderer?.setZoom(zoom) 104 + this.updateToolbar() 105 + } 106 + 107 + /** Get current zoom as a numeric scale. */ 108 + getZoom(): number { 109 + return this.renderer?.getZoom() ?? 1 110 + } 111 + 112 + /** Get current viewer state. */ 113 + getState(): DocViewState { 114 + if (!this.renderer) { 115 + return { 116 + loading: true, 117 + error: null, 118 + currentPage: 1, 119 + totalPages: 0, 120 + zoom: 1, 121 + documentInfo: null, 122 + } 123 + } 124 + return { 125 + loading: false, 126 + error: null, 127 + currentPage: this.renderer.getCurrentPage(), 128 + totalPages: this.renderer.getPageCount(), 129 + zoom: this.renderer.getZoom(), 130 + documentInfo: null, // Would need to store from onReady 131 + } 132 + } 133 + 134 + /** Update options (theme, zoom, etc.) without re-mounting. */ 135 + async update(changed: Partial<DocViewOptions>): Promise<void> { 136 + Object.assign(this.options, changed) 137 + 138 + if (changed.theme) { 139 + this.root.setAttribute('data-theme', this.resolveTheme(changed.theme)) 140 + } 141 + if (changed.className !== undefined) { 142 + this.root.className = `docview${changed.className ? ` ${changed.className}` : ''}` 143 + } 144 + 145 + await this.renderer?.update(changed) 146 + } 147 + 148 + /** Subscribe to events. Returns an unsubscribe function. */ 149 + on<K extends DocViewEventType>( 150 + event: K, 151 + callback: (data: DocViewEventMap[K]) => void, 152 + ): () => void { 153 + if (!this.listeners.has(event)) { 154 + this.listeners.set(event, new Set()) 155 + } 156 + const cb = callback as (data: unknown) => void 157 + this.listeners.get(event)!.add(cb) 158 + return () => this.listeners.get(event)?.delete(cb) 159 + } 160 + 161 + /** Destroy the viewer and clean up all resources. */ 162 + destroy(): void { 163 + if (this.destroyed) return 164 + this.destroyed = true 165 + 166 + this.toolbar?.destroy() 167 + this.renderer?.destroy() 168 + this.root.remove() 169 + this.listeners.clear() 170 + this.emit('destroy', undefined as never) 171 + } 172 + 173 + /** Register a custom renderer for a format. */ 174 + static registerRenderer(format: DocumentFormat, factory: RendererFactory): void { 175 + ensureRegistered() 176 + registry.register(format, factory) 177 + } 178 + 179 + /** Get all registered format names. */ 180 + static getFormats(): DocumentFormat[] { 181 + ensureRegistered() 182 + return registry.formats() 183 + } 184 + 185 + // --------------------------------------------------------------------------- 186 + // Internal 187 + // --------------------------------------------------------------------------- 188 + 189 + private async init(): Promise<void> { 190 + // Detect format 191 + const explicitFormat = this.options.format 192 + const detectedFormat = detectFormat(this.options.source) 193 + const format = explicitFormat ?? detectedFormat 194 + 195 + if (!format) { 196 + throw new DocViewError( 197 + 'FORMAT_DETECTION_FAILED', 198 + 'Could not detect the document format. Provide a `format` option or ensure ' + 199 + 'the source has a recognizable filename, URL extension, or MIME type.', 200 + ) 201 + } 202 + 203 + // Resolve renderer format (e.g., 'markdown' -> 'code', 'tsv' -> 'csv') 204 + const rendererFormat = getRendererFormat(format) 205 + 206 + // Create renderer 207 + const renderer = registry.create(rendererFormat) 208 + if (!renderer) { 209 + throw new DocViewError( 210 + 'FORMAT_UNSUPPORTED', 211 + `No renderer registered for format "${rendererFormat}". ` + 212 + `Available formats: ${registry.formats().join(', ')}`, 213 + ) 214 + } 215 + 216 + this.renderer = renderer 217 + 218 + // Wire up options callbacks to also emit events 219 + const originalOnReady = this.options.onReady 220 + this.options.onReady = (info) => { 221 + originalOnReady?.(info) 222 + this.emit('ready', info) 223 + this.updateToolbar() 224 + } 225 + 226 + const originalOnPageChange = this.options.onPageChange 227 + this.options.onPageChange = (page, total) => { 228 + originalOnPageChange?.(page, total) 229 + this.emit('pagechange', { page, totalPages: total }) 230 + this.updateToolbar() 231 + } 232 + 233 + const originalOnZoomChange = this.options.onZoomChange 234 + this.options.onZoomChange = (zoom) => { 235 + originalOnZoomChange?.(zoom) 236 + this.emit('zoomchange', { zoom }) 237 + this.updateToolbar() 238 + } 239 + 240 + const originalOnError = this.options.onError 241 + this.options.onError = (err) => { 242 + originalOnError?.(err) 243 + this.emit('error', err) 244 + } 245 + 246 + // Create toolbar (before renderer mount, so it appears above the viewport) 247 + const toolbarOpt = this.options.toolbar 248 + if (toolbarOpt !== false) { 249 + const config: ToolbarConfig = toolbarOpt === true || toolbarOpt === undefined 250 + ? {} // Default config 251 + : toolbarOpt 252 + 253 + this.toolbar = createToolbar(config, { 254 + onPrevPage: () => this.goToPage(this.getCurrentPage() - 1), 255 + onNextPage: () => this.goToPage(this.getCurrentPage() + 1), 256 + onPageInput: (p) => this.goToPage(p), 257 + onZoomIn: () => this.setZoom(this.getZoom() * 1.2), 258 + onZoomOut: () => this.setZoom(this.getZoom() / 1.2), 259 + onFitWidth: () => this.setZoom('fit-width'), 260 + onFullscreen: () => this.toggleFullscreen(), 261 + }, this.getState()) 262 + 263 + if (config.position === 'bottom') { 264 + this.root.appendChild(this.toolbar.element) 265 + } else { 266 + this.root.insertBefore(this.toolbar.element, this.root.firstChild) 267 + } 268 + } 269 + 270 + // Create renderer container 271 + const rendererContainer = document.createElement('div') 272 + rendererContainer.style.display = 'contents' 273 + this.root.appendChild(rendererContainer) 274 + 275 + // Mount renderer 276 + await renderer.mount(rendererContainer, this.options) 277 + } 278 + 279 + private resolveTheme(theme?: 'light' | 'dark' | 'system'): string { 280 + if (theme === 'system') { 281 + return window.matchMedia('(prefers-color-scheme: dark)').matches 282 + ? 'dark' 283 + : 'light' 284 + } 285 + return theme ?? 'dark' 286 + } 287 + 288 + private updateToolbar(): void { 289 + this.toolbar?.updateState(this.getState()) 290 + } 291 + 292 + private toggleFullscreen(): void { 293 + if (document.fullscreenElement === this.root) { 294 + document.exitFullscreen() 295 + } else { 296 + this.root.requestFullscreen?.() 297 + } 298 + } 299 + 300 + private emit<K extends DocViewEventType>(event: K, data: DocViewEventMap[K]): void { 301 + const callbacks = this.listeners.get(event) 302 + if (callbacks) { 303 + for (const cb of callbacks) { 304 + try { cb(data) } catch { /* swallow listener errors */ } 305 + } 306 + } 307 + } 308 + }
+83
packages/core/src/index.ts
··· 1 + // Main entry point 2 + export { DocView } from './docview.js' 3 + 4 + // Types 5 + export type { 6 + // Sources 7 + DocumentSource, 8 + FileSource, 9 + UrlSource, 10 + PagesSource, 11 + ChunkedSource, 12 + 13 + // Data 14 + PageData, 15 + ChunkData, 16 + TextLayerData, 17 + TextItem, 18 + 19 + // Fetch Adapters 20 + PageFetchAdapter, 21 + ChunkFetchAdapter, 22 + TextFetchAdapter, 23 + 24 + // Options 25 + DocViewOptions, 26 + PdfOptions, 27 + CodeOptions, 28 + CsvOptions, 29 + EpubOptions, 30 + OdtOptions, 31 + OdsOptions, 32 + ToolbarConfig, 33 + 34 + // State & Info 35 + DocumentInfo, 36 + DocViewState, 37 + DocumentFormat, 38 + 39 + // Renderer interface (for custom renderers) 40 + Renderer, 41 + RendererFactory, 42 + 43 + // Events 44 + DocViewEventMap, 45 + DocViewEventType, 46 + 47 + // Errors 48 + DocViewErrorCode, 49 + } from './types.js' 50 + 51 + export { DocViewError } from './types.js' 52 + 53 + // Registry (for custom renderer registration) 54 + export { registry } from './registry.js' 55 + 56 + // Base renderer (for building custom renderers) 57 + export { BaseRenderer } from './renderer.js' 58 + 59 + // Built-in renderers (for direct use or extension) 60 + export { 61 + PdfRenderer, 62 + BrowsePagesRenderer, 63 + ChunkedPdfRenderer, 64 + EpubRenderer, 65 + DocxRenderer, 66 + OdtRenderer, 67 + OdsRenderer, 68 + CsvRenderer, 69 + CodeRenderer, 70 + TextRenderer, 71 + } from './renderers/index.js' 72 + 73 + // Utilities (for custom renderer authors) 74 + export { 75 + detectFormat, 76 + getRendererFormat, 77 + getLanguageFromExtension, 78 + getExtension, 79 + toArrayBuffer, 80 + toBlob, 81 + toText, 82 + fetchAsBuffer, 83 + } from './utils.js'
+39
packages/core/src/registry.ts
··· 1 + import type { DocumentFormat, Renderer, RendererFactory } from './types.js' 2 + 3 + /** 4 + * Registry mapping document formats to their renderer factories. 5 + * Built-in renderers are registered by default. Consumers can register 6 + * custom renderers for new formats via `DocView.registerRenderer()`. 7 + */ 8 + class FormatRegistry { 9 + private factories = new Map<DocumentFormat, RendererFactory>() 10 + 11 + /** Register a renderer factory for a format. Overwrites any existing registration. */ 12 + register(format: DocumentFormat, factory: RendererFactory): void { 13 + this.factories.set(format, factory) 14 + } 15 + 16 + /** Create a renderer for the given format. Returns null if no renderer is registered. */ 17 + create(format: DocumentFormat): Renderer | null { 18 + const factory = this.factories.get(format) 19 + return factory ? factory() : null 20 + } 21 + 22 + /** Check if a renderer is registered for the given format. */ 23 + has(format: DocumentFormat): boolean { 24 + return this.factories.has(format) 25 + } 26 + 27 + /** Get all registered format names. */ 28 + formats(): DocumentFormat[] { 29 + return [...this.factories.keys()] 30 + } 31 + 32 + /** Remove a renderer registration. */ 33 + unregister(format: DocumentFormat): void { 34 + this.factories.delete(format) 35 + } 36 + } 37 + 38 + /** Singleton format registry. */ 39 + export const registry = new FormatRegistry()
+167
packages/core/src/renderer.ts
··· 1 + import type { 2 + Renderer, 3 + DocViewOptions, 4 + DocumentFormat, 5 + DocumentInfo, 6 + DocViewState, 7 + } from './types.js' 8 + import { DocViewError } from './types.js' 9 + import { el, clearElement } from './utils.js' 10 + 11 + /** 12 + * Abstract base class for format renderers. Provides common state management, 13 + * DOM scaffolding, and helper methods. Concrete renderers extend this and 14 + * implement the abstract methods. 15 + * 16 + * Subclasses must implement: 17 + * - `onMount(viewport, options)` — render content into the viewport element 18 + * - `onDestroy()` — clean up format-specific resources 19 + * - `format` getter 20 + */ 21 + export abstract class BaseRenderer implements Renderer { 22 + abstract readonly format: DocumentFormat 23 + 24 + protected container!: HTMLElement 25 + protected viewport!: HTMLElement 26 + protected options!: DocViewOptions 27 + protected state: DocViewState = { 28 + loading: true, 29 + error: null, 30 + currentPage: 1, 31 + totalPages: 1, 32 + zoom: 1, 33 + documentInfo: null, 34 + } 35 + 36 + async mount(container: HTMLElement, options: DocViewOptions): Promise<void> { 37 + this.container = container 38 + this.options = options 39 + this.state.currentPage = options.initialPage ?? 1 40 + 41 + // Create viewport element 42 + this.viewport = el('div', 'dv-viewport') 43 + this.viewport.setAttribute('role', 'document') 44 + container.appendChild(this.viewport) 45 + 46 + // Delegate to subclass 47 + try { 48 + await this.onMount(this.viewport, options) 49 + } catch (err) { 50 + const error = err instanceof DocViewError 51 + ? err 52 + : new DocViewError('RENDER_FAILED', String(err), err) 53 + this.state.error = error 54 + this.state.loading = false 55 + this.showError(error) 56 + throw error 57 + } 58 + } 59 + 60 + async update(changed: Partial<DocViewOptions>): Promise<void> { 61 + Object.assign(this.options, changed) 62 + await this.onUpdate(changed) 63 + } 64 + 65 + goToPage(page: number): void { 66 + const clamped = Math.max(1, Math.min(page, this.state.totalPages)) 67 + if (clamped === this.state.currentPage) return 68 + this.state.currentPage = clamped 69 + this.onPageChange(clamped) 70 + this.options.onPageChange?.(clamped, this.state.totalPages) 71 + } 72 + 73 + getPageCount(): number { 74 + return this.state.totalPages 75 + } 76 + 77 + getCurrentPage(): number { 78 + return this.state.currentPage 79 + } 80 + 81 + setZoom(zoom: number | 'fit-width' | 'fit-page'): void { 82 + const resolved = typeof zoom === 'number' 83 + ? zoom 84 + : this.resolveZoomMode(zoom) 85 + this.state.zoom = resolved 86 + this.onZoomChange(resolved) 87 + this.options.onZoomChange?.(resolved) 88 + } 89 + 90 + getZoom(): number { 91 + return this.state.zoom 92 + } 93 + 94 + destroy(): void { 95 + this.onDestroy() 96 + clearElement(this.container) 97 + } 98 + 99 + // --- Subclass hooks --- 100 + 101 + /** Render the document into the viewport. */ 102 + protected abstract onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> 103 + 104 + /** Clean up format-specific resources. */ 105 + protected abstract onDestroy(): void 106 + 107 + /** React to option changes. Default: no-op. */ 108 + protected async onUpdate(_changed: Partial<DocViewOptions>): Promise<void> {} 109 + 110 + /** Navigate to a page in the rendered content. Default: no-op. */ 111 + protected onPageChange(_page: number): void {} 112 + 113 + /** Apply a zoom change. Default: no-op. */ 114 + protected onZoomChange(_zoom: number): void {} 115 + 116 + // --- Helpers available to subclasses --- 117 + 118 + /** Resolve 'fit-width' or 'fit-page' to a numeric scale based on viewport size. */ 119 + protected resolveZoomMode(_mode: 'fit-width' | 'fit-page'): number { 120 + // Default implementation — subclasses with page dimensions override this 121 + return 1 122 + } 123 + 124 + /** Show a loading spinner in the viewport. */ 125 + protected showLoading(message = 'Loading document…'): HTMLElement { 126 + const loading = el('div', 'dv-loading') 127 + loading.innerHTML = `<div class="dv-spinner"></div><span>${message}</span>` 128 + this.viewport.appendChild(loading) 129 + this.state.loading = true 130 + this.options.onLoadingChange?.(true) 131 + return loading 132 + } 133 + 134 + /** Remove loading state. */ 135 + protected hideLoading(): void { 136 + const loading = this.viewport.querySelector('.dv-loading') 137 + if (loading) loading.remove() 138 + this.state.loading = false 139 + this.options.onLoadingChange?.(false) 140 + } 141 + 142 + /** Show an error message in the viewport. */ 143 + protected showError(error: DocViewError): void { 144 + clearElement(this.viewport) 145 + const errorEl = el('div', 'dv-error') 146 + errorEl.innerHTML = ` 147 + <div class="dv-error-code">${error.code}</div> 148 + <div class="dv-error-message">${error.message}</div> 149 + ` 150 + this.viewport.appendChild(errorEl) 151 + } 152 + 153 + /** Mark the document as ready and fire the onReady callback. */ 154 + protected setReady(info: DocumentInfo): void { 155 + this.state.documentInfo = info 156 + this.state.totalPages = info.pageCount 157 + this.state.loading = false 158 + this.hideLoading() 159 + this.options.onReady?.(info) 160 + this.options.onLoadingChange?.(false) 161 + } 162 + 163 + /** Fire page change callback (call after updating state.currentPage). */ 164 + protected emitPageChange(): void { 165 + this.options.onPageChange?.(this.state.currentPage, this.state.totalPages) 166 + } 167 + }
+268
packages/core/src/renderers/browse-pages.ts
··· 1 + import type { 2 + DocViewOptions, 3 + DocumentFormat, 4 + PageData, 5 + PageFetchAdapter, 6 + PagesSource, 7 + TextLayerData, 8 + TextFetchAdapter, 9 + } from '../types.js' 10 + import { DocViewError } from '../types.js' 11 + import { BaseRenderer } from '../renderer.js' 12 + import { el, clamp, debounce } from '../utils.js' 13 + 14 + /** 15 + * Renders pre-rendered page images (WebP, JPEG, PNG) with lazy loading, 16 + * scroll-based page tracking, and optional text layer overlay. 17 + * 18 + * This is the primary viewer for chunked documents in "browse" mode — 19 + * fast, lightweight, works on mobile. 20 + */ 21 + export class BrowsePagesRenderer extends BaseRenderer { 22 + readonly format: DocumentFormat = 'pages' 23 + 24 + private pagesContainer!: HTMLElement 25 + private pageElements: HTMLElement[] = [] 26 + private pages: PageData[] = [] 27 + private fetchAdapter: PageFetchAdapter | null = null 28 + private textData: TextLayerData[] | null = null 29 + private textFetcher: TextFetchAdapter | null = null 30 + private loadedPages = new Set<number>() 31 + private observer: IntersectionObserver | null = null 32 + private debouncedScroll: ReturnType<typeof debounce> | null = null 33 + 34 + protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 35 + const loadingEl = this.showLoading('Loading pages…') 36 + const source = options.source as PagesSource 37 + 38 + // Resolve pages — direct data or fetch adapter 39 + if (Array.isArray(source.pages)) { 40 + this.pages = source.pages.sort((a, b) => a.pageNumber - b.pageNumber) 41 + } else { 42 + this.fetchAdapter = source.pages 43 + } 44 + 45 + // Resolve text layer 46 + if (source.textLayer) { 47 + if (Array.isArray(source.textLayer)) { 48 + this.textData = source.textLayer 49 + } else { 50 + this.textFetcher = source.textLayer 51 + } 52 + } 53 + 54 + const totalPages = this.fetchAdapter?.totalPages ?? this.pages.length 55 + if (totalPages === 0) { 56 + throw new DocViewError('RENDER_FAILED', 'No pages provided.') 57 + } 58 + 59 + // Create pages container 60 + this.pagesContainer = el('div', 'dv-pages') 61 + viewport.appendChild(this.pagesContainer) 62 + 63 + // Create placeholder elements 64 + for (let i = 1; i <= totalPages; i++) { 65 + const pageEl = el('div', 'dv-page dv-browse-page') 66 + pageEl.dataset.page = String(i) 67 + 68 + // Set placeholder dimensions from direct data if available 69 + const pageData = this.pages.find((p) => p.pageNumber === i) 70 + if (pageData) { 71 + const width = pageData.width * this.state.zoom 72 + const height = pageData.height * this.state.zoom 73 + this.setPageSize(pageEl, width, height) 74 + this.createPlaceholder(pageEl, width, height) 75 + } else { 76 + // Default A4-ish placeholder 77 + this.setPageSize(pageEl, 595 * this.state.zoom, 842 * this.state.zoom) 78 + this.createPlaceholder(pageEl, 595 * this.state.zoom, 842 * this.state.zoom) 79 + } 80 + 81 + this.pageElements.push(pageEl) 82 + this.pagesContainer.appendChild(pageEl) 83 + } 84 + 85 + loadingEl.remove() 86 + 87 + // Intersection observer for lazy image loading 88 + this.observer = new IntersectionObserver( 89 + (entries) => { 90 + for (const entry of entries) { 91 + if (entry.isIntersecting) { 92 + const pageNum = parseInt((entry.target as HTMLElement).dataset.page!, 10) 93 + this.loadPage(pageNum) 94 + } 95 + } 96 + }, 97 + { root: viewport, rootMargin: '100% 0px' }, 98 + ) 99 + 100 + for (const pageEl of this.pageElements) { 101 + this.observer.observe(pageEl) 102 + } 103 + 104 + // Scroll tracking 105 + this.debouncedScroll = debounce(() => this.updateCurrentPage(), 100) 106 + viewport.addEventListener('scroll', this.debouncedScroll) 107 + 108 + this.setReady({ 109 + format: 'pages', 110 + pageCount: totalPages, 111 + }) 112 + 113 + if (options.initialPage && options.initialPage > 1) { 114 + this.goToPage(options.initialPage) 115 + } 116 + } 117 + 118 + private setPageSize(pageEl: HTMLElement, width: number, height: number): void { 119 + pageEl.style.width = `${width}px` 120 + pageEl.style.height = `${height}px` 121 + } 122 + 123 + private createPlaceholder(pageEl: HTMLElement, width: number, height: number): void { 124 + const placeholder = el('div', 'dv-browse-page-placeholder') 125 + placeholder.style.width = `${width}px` 126 + placeholder.style.height = `${height}px` 127 + placeholder.textContent = pageEl.dataset.page || '' 128 + pageEl.appendChild(placeholder) 129 + } 130 + 131 + private async loadPage(pageNum: number): Promise<void> { 132 + if (this.loadedPages.has(pageNum)) return 133 + this.loadedPages.add(pageNum) 134 + 135 + const pageEl = this.pageElements[pageNum - 1] 136 + if (!pageEl) return 137 + 138 + try { 139 + // Get page data 140 + let pageData: PageData | undefined 141 + if (this.fetchAdapter) { 142 + pageData = await this.fetchAdapter.fetchPage(pageNum) 143 + } else { 144 + pageData = this.pages.find((p) => p.pageNumber === pageNum) 145 + } 146 + 147 + if (!pageData) { 148 + this.loadedPages.delete(pageNum) 149 + return 150 + } 151 + 152 + // Create image 153 + const img = document.createElement('img') 154 + img.alt = `Page ${pageNum}` 155 + img.loading = 'lazy' 156 + img.draggable = false 157 + 158 + if (pageData.imageUrl) { 159 + img.src = pageData.imageUrl 160 + } else if (pageData.imageBlob) { 161 + img.src = URL.createObjectURL(pageData.imageBlob) 162 + img.addEventListener('load', () => URL.revokeObjectURL(img.src), { once: true }) 163 + } 164 + 165 + const width = pageData.width * this.state.zoom 166 + const height = pageData.height * this.state.zoom 167 + img.style.width = `${width}px` 168 + img.style.height = `${height}px` 169 + 170 + this.setPageSize(pageEl, width, height) 171 + pageEl.innerHTML = '' 172 + pageEl.appendChild(img) 173 + 174 + // Text layer overlay 175 + await this.addTextLayer(pageEl, pageNum, pageData) 176 + } catch { 177 + this.loadedPages.delete(pageNum) 178 + } 179 + } 180 + 181 + private async addTextLayer( 182 + pageEl: HTMLElement, 183 + pageNum: number, 184 + pageData: PageData, 185 + ): Promise<void> { 186 + let textLayerData: TextLayerData | undefined 187 + 188 + if (this.textData) { 189 + textLayerData = this.textData.find((t) => t.pageNumber === pageNum) 190 + } else if (this.textFetcher) { 191 + try { 192 + textLayerData = await this.textFetcher.fetchPageText(pageNum) 193 + } catch { 194 + return 195 + } 196 + } 197 + 198 + if (!textLayerData?.items.length) return 199 + 200 + const layer = el('div', 'dv-text-layer') 201 + for (const item of textLayerData.items) { 202 + const span = document.createElement('span') 203 + span.textContent = item.str 204 + span.style.left = `${item.x * 100}%` 205 + span.style.top = `${item.y * 100}%` 206 + span.style.width = `${item.width * 100}%` 207 + span.style.height = `${item.height * 100}%` 208 + if (item.fontSize) { 209 + span.style.fontSize = `${item.fontSize * this.state.zoom}px` 210 + } 211 + layer.appendChild(span) 212 + } 213 + pageEl.appendChild(layer) 214 + } 215 + 216 + private updateCurrentPage(): void { 217 + const viewportRect = this.viewport.getBoundingClientRect() 218 + const viewportMid = viewportRect.top + viewportRect.height / 2 219 + 220 + for (let i = 0; i < this.pageElements.length; i++) { 221 + const rect = this.pageElements[i].getBoundingClientRect() 222 + if (rect.top <= viewportMid && rect.bottom >= viewportMid) { 223 + const newPage = i + 1 224 + if (newPage !== this.state.currentPage) { 225 + this.state.currentPage = newPage 226 + this.emitPageChange() 227 + } 228 + return 229 + } 230 + } 231 + } 232 + 233 + protected onPageChange(page: number): void { 234 + const pageEl = this.pageElements[page - 1] 235 + if (pageEl) { 236 + pageEl.scrollIntoView({ behavior: 'smooth', block: 'start' }) 237 + } 238 + } 239 + 240 + protected onZoomChange(zoom: number): void { 241 + this.state.zoom = clamp(zoom, 0.25, 5) 242 + // Re-render loaded pages at new size 243 + for (let i = 0; i < this.pageElements.length; i++) { 244 + const pageEl = this.pageElements[i] 245 + const pageData = this.pages[i] 246 + if (pageData) { 247 + const w = pageData.width * this.state.zoom 248 + const h = pageData.height * this.state.zoom 249 + this.setPageSize(pageEl, w, h) 250 + const img = pageEl.querySelector('img') 251 + if (img) { 252 + img.style.width = `${w}px` 253 + img.style.height = `${h}px` 254 + } 255 + } 256 + } 257 + } 258 + 259 + protected onDestroy(): void { 260 + this.observer?.disconnect() 261 + this.debouncedScroll?.cancel() 262 + this.fetchAdapter?.dispose?.() 263 + this.textFetcher?.dispose?.() 264 + this.pageElements = [] 265 + this.pages = [] 266 + this.loadedPages.clear() 267 + } 268 + }
+314
packages/core/src/renderers/chunked-pdf.ts
··· 1 + import type { 2 + DocViewOptions, 3 + DocumentFormat, 4 + ChunkedSource, 5 + ChunkData, 6 + ChunkFetchAdapter, 7 + PageFetchAdapter, 8 + } from '../types.js' 9 + import { DocViewError } from '../types.js' 10 + import { BaseRenderer } from '../renderer.js' 11 + import { el, clamp, debounce, requirePeerDep, toArrayBuffer } from '../utils.js' 12 + 13 + /** 14 + * Streams a large PDF by loading independent PDF chunks on demand. 15 + * Optionally displays pre-rendered browse page images as fast placeholders 16 + * while the full-fidelity PDF chunks load in the background. 17 + * 18 + * Flow: 19 + * 1. Show browse page images immediately (if available) 20 + * 2. When user views a page range, fetch the PDF chunk covering those pages 21 + * 3. Replace the browse image with the pdf.js-rendered canvas 22 + * 4. Prefetch adjacent chunks in the background 23 + */ 24 + export class ChunkedPdfRenderer extends BaseRenderer { 25 + readonly format: DocumentFormat = 'chunked-pdf' 26 + 27 + private pagesContainer!: HTMLElement 28 + private pageElements: HTMLElement[] = [] 29 + private totalPages = 0 30 + 31 + // Chunk management 32 + private chunks: ChunkData[] = [] 33 + private chunkAdapter: ChunkFetchAdapter | null = null 34 + private loadedChunks = new Set<number>() 35 + private loadingChunks = new Set<number>() 36 + 37 + // Browse page fallback 38 + private browseAdapter: PageFetchAdapter | null = null 39 + private browsePages: Map<number, string> = new Map() // pageNum -> objectURL 40 + 41 + private observer: IntersectionObserver | null = null 42 + private debouncedScroll: ReturnType<typeof debounce> | null = null 43 + private pdfjsLib: unknown = null 44 + 45 + protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 46 + const loadingEl = this.showLoading('Loading document…') 47 + 48 + const source = options.source as ChunkedSource 49 + this.totalPages = source.totalPages 50 + 51 + // Resolve chunk source 52 + if (Array.isArray(source.chunks)) { 53 + this.chunks = source.chunks.sort((a, b) => a.pageStart - b.pageStart) 54 + } else { 55 + this.chunkAdapter = source.chunks 56 + } 57 + 58 + // Resolve browse pages (optional fast fallback) 59 + if (source.browsePages && !Array.isArray(source.browsePages)) { 60 + this.browseAdapter = source.browsePages 61 + } 62 + 63 + // Try to load pdfjs for chunk rendering 64 + try { 65 + this.pdfjsLib = await requirePeerDep('pdfjs-dist', 'PDF') 66 + } catch { 67 + // If pdfjs not available, we can still show browse pages 68 + if (!this.browseAdapter && !source.browsePages) { 69 + throw new DocViewError( 70 + 'PEER_DEPENDENCY_MISSING', 71 + 'Chunked PDF rendering requires pdfjs-dist. Install it with: npm install pdfjs-dist', 72 + ) 73 + } 74 + } 75 + 76 + // Create pages container 77 + this.pagesContainer = el('div', 'dv-pages') 78 + viewport.appendChild(this.pagesContainer) 79 + 80 + // Create page placeholders 81 + for (let i = 1; i <= this.totalPages; i++) { 82 + const pageEl = el('div', 'dv-page dv-browse-page') 83 + pageEl.dataset.page = String(i) 84 + // Default A4 placeholder 85 + const w = 595 * this.state.zoom 86 + const h = 842 * this.state.zoom 87 + pageEl.style.width = `${w}px` 88 + pageEl.style.height = `${h}px` 89 + 90 + const placeholder = el('div', 'dv-browse-page-placeholder') 91 + placeholder.style.width = `${w}px` 92 + placeholder.style.height = `${h}px` 93 + placeholder.textContent = String(i) 94 + pageEl.appendChild(placeholder) 95 + 96 + this.pageElements.push(pageEl) 97 + this.pagesContainer.appendChild(pageEl) 98 + } 99 + 100 + loadingEl.remove() 101 + 102 + // Lazy loading via intersection observer 103 + this.observer = new IntersectionObserver( 104 + (entries) => { 105 + for (const entry of entries) { 106 + if (entry.isIntersecting) { 107 + const pageNum = parseInt((entry.target as HTMLElement).dataset.page!, 10) 108 + this.loadPageContent(pageNum) 109 + } 110 + } 111 + }, 112 + { root: viewport, rootMargin: '150% 0px' }, 113 + ) 114 + 115 + for (const pageEl of this.pageElements) { 116 + this.observer.observe(pageEl) 117 + } 118 + 119 + // Scroll tracking 120 + this.debouncedScroll = debounce(() => this.updateCurrentPage(), 100) 121 + viewport.addEventListener('scroll', this.debouncedScroll) 122 + 123 + this.setReady({ 124 + format: 'chunked-pdf', 125 + pageCount: this.totalPages, 126 + }) 127 + 128 + if (options.initialPage && options.initialPage > 1) { 129 + this.goToPage(options.initialPage) 130 + } 131 + } 132 + 133 + /** 134 + * Load content for a page. Strategy: 135 + * 1. Show browse page image immediately (fast) 136 + * 2. Load the PDF chunk in background (slow, high quality) 137 + * 3. Replace image with PDF canvas when chunk is ready 138 + */ 139 + private async loadPageContent(pageNum: number): Promise<void> { 140 + // Step 1: Show browse page quickly 141 + if (this.browseAdapter && !this.browsePages.has(pageNum)) { 142 + try { 143 + const pageData = await this.browseAdapter.fetchPage(pageNum) 144 + const pageEl = this.pageElements[pageNum - 1] 145 + if (pageEl && pageData) { 146 + const img = document.createElement('img') 147 + img.alt = `Page ${pageNum}` 148 + img.draggable = false 149 + 150 + if (pageData.imageUrl) { 151 + img.src = pageData.imageUrl 152 + } else if (pageData.imageBlob) { 153 + const url = URL.createObjectURL(pageData.imageBlob) 154 + img.src = url 155 + this.browsePages.set(pageNum, url) 156 + } 157 + 158 + const w = pageData.width * this.state.zoom 159 + const h = pageData.height * this.state.zoom 160 + img.style.width = `${w}px` 161 + img.style.height = `${h}px` 162 + pageEl.style.width = `${w}px` 163 + pageEl.style.height = `${h}px` 164 + pageEl.innerHTML = '' 165 + pageEl.appendChild(img) 166 + } 167 + } catch { 168 + // Browse page load failed — non-fatal, will try PDF chunk 169 + } 170 + } 171 + 172 + // Step 2: Load PDF chunk for this page (if pdfjs available) 173 + if (this.pdfjsLib) { 174 + const chunkIndex = this.getChunkIndexForPage(pageNum) 175 + if (chunkIndex >= 0 && !this.loadedChunks.has(chunkIndex) && !this.loadingChunks.has(chunkIndex)) { 176 + this.loadingChunks.add(chunkIndex) 177 + try { 178 + await this.loadAndRenderChunk(chunkIndex) 179 + this.loadedChunks.add(chunkIndex) 180 + } catch { 181 + // Chunk load failed — browse images remain as fallback 182 + } finally { 183 + this.loadingChunks.delete(chunkIndex) 184 + } 185 + } 186 + } 187 + } 188 + 189 + private getChunkIndexForPage(pageNum: number): number { 190 + if (this.chunkAdapter) { 191 + return this.chunkAdapter.getChunkIndexForPage(pageNum) 192 + } 193 + return this.chunks.findIndex( 194 + (c) => pageNum >= c.pageStart && pageNum <= c.pageEnd, 195 + ) 196 + } 197 + 198 + private async loadAndRenderChunk(chunkIndex: number): Promise<void> { 199 + let chunkData: ChunkData 200 + if (this.chunkAdapter) { 201 + chunkData = await this.chunkAdapter.fetchChunk(chunkIndex) 202 + } else { 203 + chunkData = this.chunks[chunkIndex] 204 + } 205 + if (!chunkData) return 206 + 207 + const pdfjsLib = this.pdfjsLib as { 208 + getDocument(params: { data: ArrayBuffer }): { promise: Promise<{ 209 + numPages: number 210 + getPage(n: number): Promise<{ 211 + getViewport(p: { scale: number }): { width: number; height: number } 212 + render(p: { canvasContext: CanvasRenderingContext2D; viewport: { width: number; height: number } }): { promise: Promise<void> } 213 + cleanup(): void 214 + }> 215 + destroy(): void 216 + }> } 217 + } 218 + 219 + const data = await toArrayBuffer(chunkData.data instanceof Blob ? chunkData.data : chunkData.data) 220 + const pdf = await pdfjsLib.getDocument({ data }).promise 221 + 222 + // Render each page in the chunk 223 + for (let localPage = 1; localPage <= pdf.numPages; localPage++) { 224 + const globalPage = chunkData.pageStart + localPage - 1 225 + if (globalPage > this.totalPages) break 226 + 227 + const page = await pdf.getPage(localPage) 228 + const viewport = page.getViewport({ scale: this.state.zoom }) 229 + const pageEl = this.pageElements[globalPage - 1] 230 + if (!pageEl) continue 231 + 232 + const canvas = document.createElement('canvas') 233 + const ctx = canvas.getContext('2d')! 234 + const dpr = window.devicePixelRatio || 1 235 + canvas.width = viewport.width * dpr 236 + canvas.height = viewport.height * dpr 237 + canvas.style.width = `${viewport.width}px` 238 + canvas.style.height = `${viewport.height}px` 239 + ctx.scale(dpr, dpr) 240 + 241 + await page.render({ canvasContext: ctx, viewport }).promise 242 + page.cleanup() 243 + 244 + // Replace browse image with canvas 245 + pageEl.style.width = `${viewport.width}px` 246 + pageEl.style.height = `${viewport.height}px` 247 + pageEl.innerHTML = '' 248 + pageEl.appendChild(canvas) 249 + 250 + // Revoke browse page object URL if we had one 251 + const browseUrl = this.browsePages.get(globalPage) 252 + if (browseUrl) { 253 + URL.revokeObjectURL(browseUrl) 254 + this.browsePages.delete(globalPage) 255 + } 256 + } 257 + 258 + pdf.destroy() 259 + } 260 + 261 + private updateCurrentPage(): void { 262 + const viewportRect = this.viewport.getBoundingClientRect() 263 + const viewportMid = viewportRect.top + viewportRect.height / 2 264 + 265 + for (let i = 0; i < this.pageElements.length; i++) { 266 + const rect = this.pageElements[i].getBoundingClientRect() 267 + if (rect.top <= viewportMid && rect.bottom >= viewportMid) { 268 + const newPage = i + 1 269 + if (newPage !== this.state.currentPage) { 270 + this.state.currentPage = newPage 271 + this.emitPageChange() 272 + } 273 + return 274 + } 275 + } 276 + } 277 + 278 + protected onPageChange(page: number): void { 279 + const pageEl = this.pageElements[page - 1] 280 + if (pageEl) { 281 + pageEl.scrollIntoView({ behavior: 'smooth', block: 'start' }) 282 + } 283 + } 284 + 285 + protected onZoomChange(zoom: number): void { 286 + this.state.zoom = clamp(zoom, 0.25, 5) 287 + // Would need to re-render — simplified: just rescale existing elements 288 + for (const pageEl of this.pageElements) { 289 + const canvas = pageEl.querySelector('canvas') 290 + const img = pageEl.querySelector('img') 291 + if (canvas || img) { 292 + // For a full implementation, re-render at new scale 293 + // For now, CSS transform as fast approximation 294 + pageEl.style.transform = '' 295 + } 296 + } 297 + } 298 + 299 + protected onDestroy(): void { 300 + this.observer?.disconnect() 301 + this.debouncedScroll?.cancel() 302 + this.chunkAdapter?.dispose?.() 303 + this.browseAdapter?.dispose?.() 304 + // Revoke all object URLs 305 + for (const url of this.browsePages.values()) { 306 + URL.revokeObjectURL(url) 307 + } 308 + this.browsePages.clear() 309 + this.pageElements = [] 310 + this.chunks = [] 311 + this.loadedChunks.clear() 312 + this.loadingChunks.clear() 313 + } 314 + }
+146
packages/core/src/renderers/code.ts
··· 1 + import type { DocViewOptions, DocumentFormat } from '../types.js' 2 + import { BaseRenderer } from '../renderer.js' 3 + import { el, toText, fetchAsBuffer, requirePeerDep, getLanguageFromExtension } from '../utils.js' 4 + 5 + interface HighlightJS { 6 + highlight(code: string, options: { language: string }): { value: string } 7 + highlightAuto(code: string): { value: string; language: string } 8 + getLanguage(name: string): unknown 9 + } 10 + 11 + /** 12 + * Renders source code and structured text (JSON, XML, YAML, Markdown, HTML) 13 + * with syntax highlighting via highlight.js, line numbers, and optional word wrap. 14 + * 15 + * Falls back to plain text rendering if highlight.js is not installed. 16 + */ 17 + export class CodeRenderer extends BaseRenderer { 18 + readonly format: DocumentFormat = 'code' 19 + 20 + private codeContainer!: HTMLElement 21 + private hljs: HighlightJS | null = null 22 + 23 + protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 24 + this.showLoading('Loading file…') 25 + 26 + // Try to load highlight.js (optional peer dep) 27 + try { 28 + this.hljs = await requirePeerDep<HighlightJS>('highlight.js', 'code') 29 + } catch { 30 + this.hljs = null // Fallback to plain text 31 + } 32 + 33 + const text = await this.loadText(options) 34 + this.hideLoading() 35 + 36 + const codeOpts = options.code ?? {} 37 + const showLineNumbers = codeOpts.lineNumbers !== false 38 + const wordWrap = codeOpts.wordWrap === true 39 + 40 + // Detect language 41 + const language = codeOpts.language 42 + ?? this.detectLanguage(options) 43 + ?? undefined 44 + 45 + // Highlight 46 + let highlightedHtml: string 47 + if (this.hljs && language && this.hljs.getLanguage(language)) { 48 + highlightedHtml = this.hljs.highlight(text, { language }).value 49 + } else if (this.hljs) { 50 + const auto = this.hljs.highlightAuto(text) 51 + highlightedHtml = auto.value 52 + } else { 53 + highlightedHtml = this.escapeHtml(text) 54 + } 55 + 56 + // Build DOM 57 + this.codeContainer = el('div', 'dv-code-container') 58 + viewport.appendChild(this.codeContainer) 59 + 60 + const lines = text.split('\n') 61 + 62 + // Line numbers gutter 63 + if (showLineNumbers) { 64 + const gutter = el('div', 'dv-code-gutter') 65 + gutter.setAttribute('aria-hidden', 'true') 66 + for (let i = 1; i <= lines.length; i++) { 67 + const lineNum = el('div', 'dv-code-gutter-line') 68 + lineNum.textContent = String(i) 69 + gutter.appendChild(lineNum) 70 + } 71 + this.codeContainer.appendChild(gutter) 72 + } 73 + 74 + // Code body 75 + const body = el('pre', `dv-code-body${wordWrap ? ' dv-word-wrap' : ''}`) 76 + const codeEl = document.createElement('code') 77 + if (language) codeEl.className = `language-${language}` 78 + codeEl.innerHTML = highlightedHtml 79 + if (codeOpts.tabSize) { 80 + body.style.tabSize = String(codeOpts.tabSize) 81 + } 82 + body.appendChild(codeEl) 83 + this.codeContainer.appendChild(body) 84 + 85 + this.setReady({ 86 + format: 'code', 87 + pageCount: 1, 88 + filename: this.getFilename(options), 89 + }) 90 + } 91 + 92 + private async loadText(options: DocViewOptions): Promise<string> { 93 + const source = options.source 94 + if (source.type === 'file') return toText(source.data) 95 + if (source.type === 'url') { 96 + const buffer = await fetchAsBuffer(source.url, source.fetchOptions) 97 + return new TextDecoder('utf-8').decode(buffer) 98 + } 99 + return '' 100 + } 101 + 102 + private detectLanguage(options: DocViewOptions): string | null { 103 + // From explicit format 104 + const format = options.format 105 + if (format && format !== 'code') { 106 + const map: Record<string, string> = { 107 + json: 'json', 108 + xml: 'xml', 109 + html: 'html', 110 + markdown: 'markdown', 111 + md: 'markdown', 112 + } 113 + if (map[format]) return map[format] 114 + } 115 + 116 + // From filename 117 + const source = options.source 118 + const name = ('filename' in source ? source.filename : undefined) 119 + ?? (source.type === 'url' ? source.url : undefined) 120 + if (name) { 121 + const lang = getLanguageFromExtension(name) 122 + if (lang) return lang 123 + } 124 + 125 + return null 126 + } 127 + 128 + private escapeHtml(text: string): string { 129 + return text 130 + .replace(/&/g, '&amp;') 131 + .replace(/</g, '&lt;') 132 + .replace(/>/g, '&gt;') 133 + .replace(/"/g, '&quot;') 134 + } 135 + 136 + private getFilename(options: DocViewOptions): string | undefined { 137 + const source = options.source 138 + if ('filename' in source && source.filename) return source.filename 139 + if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0] 140 + return undefined 141 + } 142 + 143 + protected onDestroy(): void { 144 + this.hljs = null 145 + } 146 + }
+180
packages/core/src/renderers/csv.ts
··· 1 + import type { DocViewOptions, DocumentFormat } from '../types.js' 2 + import { BaseRenderer } from '../renderer.js' 3 + import { el, toText, fetchAsBuffer, requirePeerDep } from '../utils.js' 4 + 5 + interface PapaParse { 6 + parse(input: string, config?: { 7 + delimiter?: string 8 + header?: boolean 9 + preview?: number 10 + dynamicTyping?: boolean 11 + skipEmptyLines?: boolean 12 + }): { 13 + data: Record<string, string>[] | string[][] 14 + meta: { fields?: string[]; delimiter: string } 15 + errors: { message: string }[] 16 + } 17 + } 18 + 19 + /** 20 + * Renders CSV and TSV files as a scrollable, sortable table. 21 + * Uses PapaParse for robust parsing and renders a lightweight 22 + * HTML table with sticky headers and striped rows. 23 + */ 24 + export class CsvRenderer extends BaseRenderer { 25 + readonly format: DocumentFormat = 'csv' 26 + 27 + private tableContainer!: HTMLElement 28 + private headers: string[] = [] 29 + private rows: string[][] = [] 30 + private sortCol = -1 31 + private sortAsc = true 32 + 33 + protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 34 + this.showLoading('Parsing data…') 35 + 36 + const Papa = await requirePeerDep<PapaParse>('papaparse', 'CSV') 37 + const text = await this.loadText(options) 38 + 39 + const csvOpts = options.csv ?? {} 40 + const useHeader = csvOpts.header !== false 41 + const maxRows = csvOpts.maxRows ?? 10000 42 + const sortable = csvOpts.sortable !== false 43 + 44 + const result = Papa.parse(text, { 45 + delimiter: csvOpts.delimiter || undefined, 46 + header: false, // We'll handle headers ourselves for more control 47 + preview: maxRows + 1, // +1 for header row 48 + skipEmptyLines: true, 49 + }) 50 + 51 + const rawRows = result.data as string[][] 52 + if (rawRows.length === 0) { 53 + this.hideLoading() 54 + this.setReady({ format: 'csv', pageCount: 1 }) 55 + viewport.appendChild(el('div', 'dv-loading')) 56 + viewport.querySelector('.dv-loading')!.textContent = 'No data found.' 57 + return 58 + } 59 + 60 + if (useHeader && rawRows.length > 1) { 61 + this.headers = rawRows[0].map((h, i) => h || `Column ${i + 1}`) 62 + this.rows = rawRows.slice(1, maxRows + 1) 63 + } else { 64 + this.headers = rawRows[0].map((_, i) => `Column ${i + 1}`) 65 + this.rows = rawRows.slice(0, maxRows) 66 + } 67 + 68 + this.hideLoading() 69 + 70 + // Build table 71 + this.tableContainer = el('div', 'dv-table-container') 72 + viewport.appendChild(this.tableContainer) 73 + this.renderTable(sortable) 74 + 75 + this.setReady({ 76 + format: 'csv', 77 + pageCount: 1, 78 + filename: this.getFilename(options), 79 + }) 80 + } 81 + 82 + private renderTable(sortable: boolean): void { 83 + const table = el('table', 'dv-table') 84 + 85 + // Header 86 + const thead = document.createElement('thead') 87 + const headerRow = document.createElement('tr') 88 + 89 + // Row number column 90 + const rowNumTh = document.createElement('th') 91 + rowNumTh.className = 'dv-table-row-number' 92 + rowNumTh.textContent = '#' 93 + headerRow.appendChild(rowNumTh) 94 + 95 + this.headers.forEach((header, colIdx) => { 96 + const th = document.createElement('th') 97 + th.textContent = header 98 + if (sortable) { 99 + th.dataset.sortable = 'true' 100 + th.addEventListener('click', () => this.sortByColumn(colIdx)) 101 + if (this.sortCol === colIdx) { 102 + th.textContent = `${header} ${this.sortAsc ? '↑' : '↓'}` 103 + } 104 + } 105 + headerRow.appendChild(th) 106 + }) 107 + thead.appendChild(headerRow) 108 + table.appendChild(thead) 109 + 110 + // Body 111 + const tbody = document.createElement('tbody') 112 + this.rows.forEach((row, rowIdx) => { 113 + const tr = document.createElement('tr') 114 + 115 + // Row number 116 + const rowNumTd = document.createElement('td') 117 + rowNumTd.className = 'dv-table-row-number' 118 + rowNumTd.textContent = String(rowIdx + 1) 119 + tr.appendChild(rowNumTd) 120 + 121 + for (let colIdx = 0; colIdx < this.headers.length; colIdx++) { 122 + const td = document.createElement('td') 123 + td.textContent = row[colIdx] ?? '' 124 + td.title = row[colIdx] ?? '' 125 + tr.appendChild(td) 126 + } 127 + tbody.appendChild(tr) 128 + }) 129 + table.appendChild(tbody) 130 + 131 + this.tableContainer.innerHTML = '' 132 + this.tableContainer.appendChild(table) 133 + } 134 + 135 + private sortByColumn(colIdx: number): void { 136 + if (this.sortCol === colIdx) { 137 + this.sortAsc = !this.sortAsc 138 + } else { 139 + this.sortCol = colIdx 140 + this.sortAsc = true 141 + } 142 + 143 + this.rows.sort((a, b) => { 144 + const va = a[colIdx] ?? '' 145 + const vb = b[colIdx] ?? '' 146 + // Try numeric comparison 147 + const na = parseFloat(va) 148 + const nb = parseFloat(vb) 149 + if (!isNaN(na) && !isNaN(nb)) { 150 + return this.sortAsc ? na - nb : nb - na 151 + } 152 + // Fall back to string comparison 153 + return this.sortAsc ? va.localeCompare(vb) : vb.localeCompare(va) 154 + }) 155 + 156 + this.renderTable(true) 157 + } 158 + 159 + private async loadText(options: DocViewOptions): Promise<string> { 160 + const source = options.source 161 + if (source.type === 'file') return toText(source.data) 162 + if (source.type === 'url') { 163 + const buffer = await fetchAsBuffer(source.url, source.fetchOptions) 164 + return new TextDecoder('utf-8').decode(buffer) 165 + } 166 + return '' 167 + } 168 + 169 + private getFilename(options: DocViewOptions): string | undefined { 170 + const source = options.source 171 + if ('filename' in source && source.filename) return source.filename 172 + if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0] 173 + return undefined 174 + } 175 + 176 + protected onDestroy(): void { 177 + this.headers = [] 178 + this.rows = [] 179 + } 180 + }
+91
packages/core/src/renderers/docx.ts
··· 1 + import type { DocViewOptions, DocumentFormat } from '../types.js' 2 + import { BaseRenderer } from '../renderer.js' 3 + import { el, toArrayBuffer, fetchAsBuffer, requirePeerDep } from '../utils.js' 4 + 5 + interface DocxPreview { 6 + renderAsync( 7 + data: ArrayBuffer | Blob, 8 + container: HTMLElement, 9 + styleContainer?: HTMLElement | null, 10 + options?: { 11 + className?: string 12 + inWrapper?: boolean 13 + ignoreWidth?: boolean 14 + ignoreHeight?: boolean 15 + ignoreFonts?: boolean 16 + breakPages?: boolean 17 + ignoreLastRenderedPageBreak?: boolean 18 + experimental?: boolean 19 + trimXmlDeclaration?: boolean 20 + useBase64URL?: boolean 21 + renderHeaders?: boolean 22 + renderFooters?: boolean 23 + renderFootnotes?: boolean 24 + renderEndnotes?: boolean 25 + }, 26 + ): Promise<void> 27 + } 28 + 29 + /** 30 + * Renders DOCX files using docx-preview, preserving layout, styles, 31 + * tables, headers/footers, and embedded images with high fidelity. 32 + */ 33 + export class DocxRenderer extends BaseRenderer { 34 + readonly format: DocumentFormat = 'docx' 35 + 36 + private docxContainer!: HTMLElement 37 + 38 + protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 39 + this.showLoading('Rendering document…') 40 + 41 + const docxPreview = await requirePeerDep<DocxPreview>('docx-preview', 'DOCX') 42 + 43 + const data = await this.loadData(options) 44 + 45 + this.hideLoading() 46 + 47 + this.docxContainer = el('div', 'dv-docx-container') 48 + viewport.appendChild(this.docxContainer) 49 + 50 + await docxPreview.renderAsync(data, this.docxContainer, null, { 51 + className: 'docx', 52 + inWrapper: true, 53 + ignoreWidth: false, 54 + ignoreHeight: false, 55 + breakPages: true, 56 + renderHeaders: true, 57 + renderFooters: true, 58 + renderFootnotes: true, 59 + renderEndnotes: true, 60 + useBase64URL: true, 61 + }) 62 + 63 + // Count rendered page sections for page count 64 + const pages = this.docxContainer.querySelectorAll('section.docx') 65 + const pageCount = Math.max(1, pages.length) 66 + 67 + this.setReady({ 68 + format: 'docx', 69 + pageCount, 70 + filename: this.getFilename(options), 71 + }) 72 + } 73 + 74 + private async loadData(options: DocViewOptions): Promise<ArrayBuffer> { 75 + const source = options.source 76 + if (source.type === 'file') return toArrayBuffer(source.data) 77 + if (source.type === 'url') return fetchAsBuffer(source.url, source.fetchOptions) 78 + throw new Error('DOCX renderer requires a file or url source.') 79 + } 80 + 81 + private getFilename(options: DocViewOptions): string | undefined { 82 + const source = options.source 83 + if ('filename' in source && source.filename) return source.filename 84 + if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0] 85 + return undefined 86 + } 87 + 88 + protected onDestroy(): void { 89 + // docx-preview doesn't expose a destroy method — DOM cleanup is sufficient 90 + } 91 + }
+182
packages/core/src/renderers/epub.ts
··· 1 + import type { DocViewOptions, DocumentFormat } from '../types.js' 2 + import { BaseRenderer } from '../renderer.js' 3 + import { el, toArrayBuffer, fetchAsBuffer, requirePeerDep } from '../utils.js' 4 + 5 + interface EpubJSModule { 6 + default?: new (options?: { openAs?: string }) => EpubBook 7 + } 8 + 9 + interface EpubBook { 10 + open(input: ArrayBuffer | string): Promise<void> 11 + renderTo(element: HTMLElement, options?: { 12 + width?: string | number 13 + height?: string | number 14 + flow?: string 15 + spread?: string 16 + }): EpubRendition 17 + loaded: { 18 + metadata: Promise<{ title?: string; creator?: string }> 19 + spine: Promise<{ items: { index: number }[] }> 20 + } 21 + destroy(): void 22 + } 23 + 24 + interface EpubRendition { 25 + display(target?: string | number): Promise<void> 26 + next(): Promise<void> 27 + prev(): Promise<void> 28 + on(event: string, callback: (...args: any[]) => void): void 29 + themes: { 30 + fontSize(size: string): void 31 + font(family: string): void 32 + override(name: string, value: string): void 33 + } 34 + currentLocation(): EpubLocation | null 35 + destroy(): void 36 + } 37 + 38 + interface EpubLocation { 39 + start: { index: number; displayed: { page: number; total: number } } 40 + end: { index: number } 41 + } 42 + 43 + /** 44 + * Renders EPUB files using epub.js with paginated or scrolled reading modes, 45 + * theme integration, and keyboard navigation. 46 + */ 47 + export class EpubRenderer extends BaseRenderer { 48 + readonly format: DocumentFormat = 'epub' 49 + 50 + private book: EpubBook | null = null 51 + private rendition: EpubRendition | null = null 52 + private epubContainer!: HTMLElement 53 + private keyHandler: ((e: KeyboardEvent) => void) | null = null 54 + 55 + protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 56 + this.showLoading('Loading book…') 57 + 58 + const epubjs = await requirePeerDep<EpubJSModule>('epubjs', 'EPUB') 59 + // Handle both ESM default-unwrapped and wrapped module shapes 60 + const EpubBook = (typeof epubjs === 'function' ? epubjs : epubjs.default) as new (options?: { openAs?: string }) => EpubBook 61 + 62 + this.book = new EpubBook() 63 + 64 + // Load content 65 + const source = options.source 66 + if (source.type === 'file') { 67 + const buffer = await toArrayBuffer(source.data) 68 + await this.book.open(buffer) 69 + } else if (source.type === 'url') { 70 + await this.book.open(source.url) 71 + } else { 72 + throw new Error('EPUB renderer requires a file or url source.') 73 + } 74 + 75 + this.hideLoading() 76 + 77 + // Create container 78 + this.epubContainer = el('div', 'dv-epub-container') 79 + viewport.appendChild(this.epubContainer) 80 + 81 + // epub.js requires concrete pixel dimensions to render 82 + const rect = viewport.getBoundingClientRect() 83 + const width = Math.floor(rect.width) || 600 84 + const height = Math.floor(rect.height - 4) || 400 // -4 for breathing room 85 + 86 + // Render 87 + const epubOpts = options.epub ?? {} 88 + const flow = epubOpts.flow ?? 'paginated' 89 + 90 + this.rendition = this.book.renderTo(this.epubContainer, { 91 + width, 92 + height, 93 + flow, 94 + spread: 'none', 95 + }) 96 + 97 + // Apply theme 98 + if (epubOpts.fontSize) { 99 + this.rendition.themes.fontSize(`${epubOpts.fontSize}px`) 100 + } 101 + if (epubOpts.fontFamily) { 102 + this.rendition.themes.font(epubOpts.fontFamily) 103 + } 104 + 105 + // Theme colors from CSS variables 106 + const isDark = options.theme !== 'light' 107 + if (isDark) { 108 + this.rendition.themes.override('color', '#e6edf3') 109 + this.rendition.themes.override('background', '#0d1117') 110 + } 111 + 112 + // Track location changes 113 + this.rendition.on('relocated', (location: EpubLocation) => { 114 + if (location?.start) { 115 + this.state.currentPage = location.start.displayed.page 116 + this.state.totalPages = location.start.displayed.total 117 + this.emitPageChange() 118 + } 119 + }) 120 + 121 + await this.rendition.display() 122 + 123 + // Keyboard navigation 124 + this.keyHandler = (e: KeyboardEvent) => { 125 + if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { 126 + this.rendition?.next() 127 + } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { 128 + this.rendition?.prev() 129 + } 130 + } 131 + document.addEventListener('keydown', this.keyHandler) 132 + 133 + // Get metadata 134 + let title: string | undefined 135 + let author: string | undefined 136 + try { 137 + const meta = await this.book.loaded.metadata 138 + title = meta.title || undefined 139 + author = meta.creator || undefined 140 + } catch { 141 + // Non-fatal 142 + } 143 + 144 + // Estimate page count from spine 145 + let pageCount = 1 146 + try { 147 + const spine = await this.book.loaded.spine 148 + pageCount = spine.items.length 149 + } catch { 150 + // Non-fatal 151 + } 152 + 153 + this.setReady({ 154 + format: 'epub', 155 + pageCount, 156 + title, 157 + author, 158 + filename: this.getFilename(options), 159 + }) 160 + } 161 + 162 + protected onPageChange(page: number): void { 163 + this.rendition?.display(String(page - 1)) 164 + } 165 + 166 + private getFilename(options: DocViewOptions): string | undefined { 167 + const source = options.source 168 + if ('filename' in source && source.filename) return source.filename 169 + if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0] 170 + return undefined 171 + } 172 + 173 + protected onDestroy(): void { 174 + if (this.keyHandler) { 175 + document.removeEventListener('keydown', this.keyHandler) 176 + } 177 + this.rendition?.destroy() 178 + this.book?.destroy() 179 + this.rendition = null 180 + this.book = null 181 + } 182 + }
+39
packages/core/src/renderers/index.ts
··· 1 + export { PdfRenderer } from './pdf.js' 2 + export { BrowsePagesRenderer } from './browse-pages.js' 3 + export { ChunkedPdfRenderer } from './chunked-pdf.js' 4 + export { EpubRenderer } from './epub.js' 5 + export { DocxRenderer } from './docx.js' 6 + export { OdtRenderer } from './odt.js' 7 + export { OdsRenderer } from './ods.js' 8 + export { CsvRenderer } from './csv.js' 9 + export { CodeRenderer } from './code.js' 10 + export { TextRenderer } from './text.js' 11 + 12 + import { registry } from '../registry.js' 13 + import { PdfRenderer } from './pdf.js' 14 + import { BrowsePagesRenderer } from './browse-pages.js' 15 + import { ChunkedPdfRenderer } from './chunked-pdf.js' 16 + import { EpubRenderer } from './epub.js' 17 + import { DocxRenderer } from './docx.js' 18 + import { OdtRenderer } from './odt.js' 19 + import { OdsRenderer } from './ods.js' 20 + import { CsvRenderer } from './csv.js' 21 + import { CodeRenderer } from './code.js' 22 + import { TextRenderer } from './text.js' 23 + 24 + /** 25 + * Register all built-in renderers with the format registry. 26 + * Called automatically when the library is imported. 27 + */ 28 + export function registerBuiltinRenderers(): void { 29 + registry.register('pdf', () => new PdfRenderer()) 30 + registry.register('pages', () => new BrowsePagesRenderer()) 31 + registry.register('chunked-pdf', () => new ChunkedPdfRenderer()) 32 + registry.register('epub', () => new EpubRenderer()) 33 + registry.register('docx', () => new DocxRenderer()) 34 + registry.register('odt', () => new OdtRenderer()) 35 + registry.register('ods', () => new OdsRenderer()) 36 + registry.register('csv', () => new CsvRenderer()) 37 + registry.register('code', () => new CodeRenderer()) 38 + registry.register('text', () => new TextRenderer()) 39 + }
+233
packages/core/src/renderers/ods.ts
··· 1 + import type { DocViewOptions, DocumentFormat } from '../types.js' 2 + import { BaseRenderer } from '../renderer.js' 3 + import { el, toArrayBuffer, fetchAsBuffer, requirePeerDep } from '../utils.js' 4 + 5 + interface XLSXLib { 6 + read(data: ArrayBuffer, opts?: { type?: string }): XLSXWorkbook 7 + utils: { 8 + sheet_to_json<T>(sheet: XLSXWorksheet, opts?: { header?: 1 | string; raw?: boolean }): T[] 9 + } 10 + } 11 + 12 + interface XLSXWorkbook { 13 + SheetNames: string[] 14 + Sheets: Record<string, XLSXWorksheet> 15 + } 16 + 17 + // eslint-disable-next-line @typescript-eslint/no-empty-interface 18 + interface XLSXWorksheet {} 19 + 20 + /** 21 + * Renders ODS (OpenDocument Spreadsheet) files using the SheetJS (xlsx) 22 + * library. Parses sheets into arrays and renders sortable HTML tables, 23 + * with a tab bar for multi-sheet workbooks. 24 + * 25 + * Peer dependency: xlsx 26 + */ 27 + export class OdsRenderer extends BaseRenderer { 28 + readonly format: DocumentFormat = 'ods' 29 + 30 + private odsContainer!: HTMLElement 31 + private tabBar!: HTMLElement 32 + private tableContainer!: HTMLElement 33 + private sheets: { name: string; headers: string[]; rows: string[][] }[] = [] 34 + private activeSheet = 0 35 + private sortCol = -1 36 + private sortAsc = true 37 + 38 + protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 39 + this.showLoading('Parsing spreadsheet…') 40 + 41 + const XLSX = await requirePeerDep<XLSXLib>('xlsx', 'ODS') 42 + const data = await this.loadData(options) 43 + 44 + const workbook = XLSX.read(data, { type: 'array' }) 45 + 46 + const odsOpts = options.ods ?? {} 47 + const maxRows = odsOpts.maxRows ?? 10000 48 + const useHeader = odsOpts.header !== false 49 + 50 + // Parse each sheet 51 + for (const name of workbook.SheetNames) { 52 + const sheet = workbook.Sheets[name] 53 + const rawRows = XLSX.utils.sheet_to_json<string[]>(sheet, { 54 + header: 1, 55 + raw: false, 56 + }) 57 + 58 + let headers: string[] 59 + let rows: string[][] 60 + 61 + if (rawRows.length === 0) { 62 + headers = [] 63 + rows = [] 64 + } else if (useHeader && rawRows.length > 1) { 65 + headers = rawRows[0].map((h, i) => (h != null && String(h)) || `Column ${i + 1}`) 66 + rows = rawRows.slice(1, maxRows + 1) 67 + } else { 68 + const colCount = Math.max(...rawRows.slice(0, 100).map((r) => r.length), 0) 69 + headers = Array.from({ length: colCount }, (_, i) => `Column ${i + 1}`) 70 + rows = rawRows.slice(0, maxRows) 71 + } 72 + 73 + this.sheets.push({ name, headers, rows }) 74 + } 75 + 76 + this.hideLoading() 77 + 78 + // Build container 79 + this.odsContainer = el('div', 'dv-ods-container') 80 + viewport.appendChild(this.odsContainer) 81 + 82 + // Tab bar (only if more than one sheet) 83 + if (this.sheets.length > 1) { 84 + this.tabBar = el('div', 'dv-ods-tabs') 85 + this.odsContainer.appendChild(this.tabBar) 86 + this.renderTabs() 87 + } 88 + 89 + // Table 90 + this.tableContainer = el('div', 'dv-table-container') 91 + this.odsContainer.appendChild(this.tableContainer) 92 + this.renderTable(odsOpts.sortable !== false) 93 + 94 + this.setReady({ 95 + format: 'ods', 96 + pageCount: this.sheets.length, 97 + filename: this.getFilename(options), 98 + }) 99 + } 100 + 101 + private renderTabs(): void { 102 + this.tabBar.innerHTML = '' 103 + this.sheets.forEach((sheet, idx) => { 104 + const tab = el('button', `dv-ods-tab${idx === this.activeSheet ? ' dv-ods-tab-active' : ''}`) 105 + tab.textContent = sheet.name 106 + tab.addEventListener('click', () => { 107 + this.activeSheet = idx 108 + this.sortCol = -1 // Reset sort on sheet change 109 + this.sortAsc = true 110 + this.renderTabs() 111 + this.renderTable(true) 112 + this.state.currentPage = idx + 1 113 + this.emitPageChange() 114 + }) 115 + this.tabBar.appendChild(tab) 116 + }) 117 + } 118 + 119 + private renderTable(sortable: boolean): void { 120 + const sheet = this.sheets[this.activeSheet] 121 + if (!sheet || sheet.headers.length === 0) { 122 + this.tableContainer.innerHTML = 123 + '<div class="dv-loading"><span>No data in this sheet.</span></div>' 124 + return 125 + } 126 + 127 + const table = el('table', 'dv-table') 128 + 129 + // Header 130 + const thead = document.createElement('thead') 131 + const headerRow = document.createElement('tr') 132 + 133 + // Row number column 134 + const rowNumTh = document.createElement('th') 135 + rowNumTh.className = 'dv-table-row-number' 136 + rowNumTh.textContent = '#' 137 + headerRow.appendChild(rowNumTh) 138 + 139 + sheet.headers.forEach((header, colIdx) => { 140 + const th = document.createElement('th') 141 + th.textContent = header 142 + if (sortable) { 143 + th.dataset.sortable = 'true' 144 + th.addEventListener('click', () => this.sortByColumn(colIdx)) 145 + if (this.sortCol === colIdx) { 146 + th.textContent = `${header} ${this.sortAsc ? '↑' : '↓'}` 147 + } 148 + } 149 + headerRow.appendChild(th) 150 + }) 151 + thead.appendChild(headerRow) 152 + table.appendChild(thead) 153 + 154 + // Body 155 + const tbody = document.createElement('tbody') 156 + sheet.rows.forEach((row, rowIdx) => { 157 + const tr = document.createElement('tr') 158 + 159 + const rowNumTd = document.createElement('td') 160 + rowNumTd.className = 'dv-table-row-number' 161 + rowNumTd.textContent = String(rowIdx + 1) 162 + tr.appendChild(rowNumTd) 163 + 164 + for (let colIdx = 0; colIdx < sheet.headers.length; colIdx++) { 165 + const td = document.createElement('td') 166 + const val = row[colIdx] != null ? String(row[colIdx]) : '' 167 + td.textContent = val 168 + td.title = val 169 + tr.appendChild(td) 170 + } 171 + tbody.appendChild(tr) 172 + }) 173 + table.appendChild(tbody) 174 + 175 + this.tableContainer.innerHTML = '' 176 + this.tableContainer.appendChild(table) 177 + } 178 + 179 + private sortByColumn(colIdx: number): void { 180 + const sheet = this.sheets[this.activeSheet] 181 + if (!sheet) return 182 + 183 + if (this.sortCol === colIdx) { 184 + this.sortAsc = !this.sortAsc 185 + } else { 186 + this.sortCol = colIdx 187 + this.sortAsc = true 188 + } 189 + 190 + sheet.rows.sort((a, b) => { 191 + const va = a[colIdx] != null ? String(a[colIdx]) : '' 192 + const vb = b[colIdx] != null ? String(b[colIdx]) : '' 193 + const na = parseFloat(va) 194 + const nb = parseFloat(vb) 195 + if (!isNaN(na) && !isNaN(nb)) { 196 + return this.sortAsc ? na - nb : nb - na 197 + } 198 + return this.sortAsc ? va.localeCompare(vb) : vb.localeCompare(va) 199 + }) 200 + 201 + this.renderTable(true) 202 + } 203 + 204 + protected onPageChange(page: number): void { 205 + // Navigate to sheet by page number 206 + const idx = page - 1 207 + if (idx >= 0 && idx < this.sheets.length && idx !== this.activeSheet) { 208 + this.activeSheet = idx 209 + this.sortCol = -1 210 + this.sortAsc = true 211 + if (this.tabBar) this.renderTabs() 212 + this.renderTable(true) 213 + } 214 + } 215 + 216 + private async loadData(options: DocViewOptions): Promise<ArrayBuffer> { 217 + const source = options.source 218 + if (source.type === 'file') return toArrayBuffer(source.data) 219 + if (source.type === 'url') return fetchAsBuffer(source.url, source.fetchOptions) 220 + throw new Error('ODS renderer requires a file or url source.') 221 + } 222 + 223 + private getFilename(options: DocViewOptions): string | undefined { 224 + const source = options.source 225 + if ('filename' in source && source.filename) return source.filename 226 + if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0] 227 + return undefined 228 + } 229 + 230 + protected onDestroy(): void { 231 + this.sheets = [] 232 + } 233 + }
+285
packages/core/src/renderers/odt.ts
··· 1 + import type { DocViewOptions, DocumentFormat } from '../types.js' 2 + import { BaseRenderer } from '../renderer.js' 3 + import { el, toArrayBuffer, fetchAsBuffer, requirePeerDep } from '../utils.js' 4 + 5 + interface JSZip { 6 + loadAsync(data: ArrayBuffer | Uint8Array | Blob): Promise<JSZipInstance> 7 + } 8 + 9 + interface JSZipInstance { 10 + file(name: string): JSZipFile | null 11 + files: Record<string, JSZipFile> 12 + } 13 + 14 + interface JSZipFile { 15 + async(type: 'string'): Promise<string> 16 + async(type: 'arraybuffer'): Promise<ArrayBuffer> 17 + } 18 + 19 + /** 20 + * Renders ODT (OpenDocument Text) files by extracting content.xml from the 21 + * ZIP archive and converting ODF XML tags to styled HTML. 22 + * 23 + * Peer dependency: jszip 24 + */ 25 + export class OdtRenderer extends BaseRenderer { 26 + readonly format: DocumentFormat = 'odt' 27 + 28 + private odtContainer!: HTMLElement 29 + 30 + protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 31 + this.showLoading('Rendering ODT document…') 32 + 33 + const JSZipLib = await requirePeerDep<JSZip>('jszip', 'ODT') 34 + const data = await this.loadData(options) 35 + 36 + // Parse the ZIP archive 37 + const zip = await JSZipLib.loadAsync(data) 38 + 39 + // Extract content.xml (the main document content) 40 + const contentFile = zip.file('content.xml') 41 + if (!contentFile) { 42 + throw new Error('Invalid ODT file: missing content.xml') 43 + } 44 + const contentXml = await contentFile.async('string') 45 + 46 + // Extract styles.xml for additional styling info 47 + const stylesFile = zip.file('styles.xml') 48 + const stylesXml = stylesFile ? await stylesFile.async('string') : '' 49 + 50 + this.hideLoading() 51 + 52 + // Build the rendered view 53 + this.odtContainer = el('div', 'dv-odt-container') 54 + viewport.appendChild(this.odtContainer) 55 + 56 + // Apply font overrides 57 + const odtOpts = options.odt ?? {} 58 + if (odtOpts.fontSize) { 59 + this.odtContainer.style.fontSize = `${odtOpts.fontSize}px` 60 + } 61 + if (odtOpts.fontFamily) { 62 + this.odtContainer.style.fontFamily = odtOpts.fontFamily 63 + } 64 + 65 + // Parse and render the XML content 66 + const html = this.convertOdfToHtml(contentXml, stylesXml) 67 + this.odtContainer.innerHTML = html 68 + 69 + this.setReady({ 70 + format: 'odt', 71 + pageCount: 1, 72 + filename: this.getFilename(options), 73 + }) 74 + } 75 + 76 + private convertOdfToHtml(contentXml: string, _stylesXml: string): string { 77 + const parser = new DOMParser() 78 + const doc = parser.parseFromString(contentXml, 'application/xml') 79 + 80 + // Find the office:body > office:text element 81 + const body = doc.getElementsByTagName('office:text')[0] 82 + ?? doc.getElementsByTagName('office:body')[0] 83 + 84 + if (!body) { 85 + return '<p style="color: var(--dv-text-secondary)">No content found in document.</p>' 86 + } 87 + 88 + // Parse automatic-styles for style name → CSS mapping 89 + const styleMap = this.parseAutomaticStyles(doc) 90 + 91 + return this.convertNode(body, styleMap) 92 + } 93 + 94 + private parseAutomaticStyles(doc: Document): Map<string, string> { 95 + const map = new Map<string, string>() 96 + const autoStyles = doc.getElementsByTagName('office:automatic-styles')[0] 97 + if (!autoStyles) return map 98 + 99 + const styles = autoStyles.getElementsByTagName('style:style') 100 + for (let i = 0; i < styles.length; i++) { 101 + const style = styles[i] 102 + const name = style.getAttribute('style:name') 103 + if (!name) continue 104 + 105 + const cssProps: string[] = [] 106 + 107 + // Parse text-properties 108 + const textProps = style.getElementsByTagName('style:text-properties') 109 + for (let j = 0; j < textProps.length; j++) { 110 + const tp = textProps[j] 111 + const bold = tp.getAttribute('fo:font-weight') 112 + const italic = tp.getAttribute('fo:font-style') 113 + const fontSize = tp.getAttribute('fo:font-size') 114 + const color = tp.getAttribute('fo:color') 115 + const underline = tp.getAttribute('style:text-underline-style') 116 + const fontFamily = tp.getAttribute('style:font-name') ?? tp.getAttribute('fo:font-family') 117 + const bgColor = tp.getAttribute('fo:background-color') 118 + const strikethrough = tp.getAttribute('style:text-line-through-style') 119 + 120 + if (bold === 'bold') cssProps.push('font-weight:bold') 121 + if (italic === 'italic') cssProps.push('font-style:italic') 122 + if (fontSize) cssProps.push(`font-size:${fontSize}`) 123 + if (color && color !== 'transparent') cssProps.push(`color:${color}`) 124 + if (underline && underline !== 'none') cssProps.push('text-decoration:underline') 125 + if (strikethrough && strikethrough !== 'none') cssProps.push('text-decoration:line-through') 126 + if (fontFamily) cssProps.push(`font-family:${fontFamily}`) 127 + if (bgColor && bgColor !== 'transparent') cssProps.push(`background-color:${bgColor}`) 128 + } 129 + 130 + // Parse paragraph-properties 131 + const paraProps = style.getElementsByTagName('style:paragraph-properties') 132 + for (let j = 0; j < paraProps.length; j++) { 133 + const pp = paraProps[j] 134 + const align = pp.getAttribute('fo:text-align') 135 + const marginTop = pp.getAttribute('fo:margin-top') 136 + const marginBottom = pp.getAttribute('fo:margin-bottom') 137 + const marginLeft = pp.getAttribute('fo:margin-left') 138 + const lineHeight = pp.getAttribute('fo:line-height') 139 + 140 + if (align) { 141 + const cssAlign = align === 'start' ? 'left' : align === 'end' ? 'right' : align 142 + cssProps.push(`text-align:${cssAlign}`) 143 + } 144 + if (marginTop) cssProps.push(`margin-top:${marginTop}`) 145 + if (marginBottom) cssProps.push(`margin-bottom:${marginBottom}`) 146 + if (marginLeft) cssProps.push(`margin-left:${marginLeft}`) 147 + if (lineHeight) cssProps.push(`line-height:${lineHeight}`) 148 + } 149 + 150 + if (cssProps.length > 0) { 151 + map.set(name, cssProps.join(';')) 152 + } 153 + } 154 + 155 + return map 156 + } 157 + 158 + private convertNode(node: Node, styleMap: Map<string, string>): string { 159 + if (node.nodeType === Node.TEXT_NODE) { 160 + return this.escapeHtml(node.textContent ?? '') 161 + } 162 + 163 + if (node.nodeType !== Node.ELEMENT_NODE) return '' 164 + const elem = node as Element 165 + const tag = elem.tagName 166 + 167 + switch (tag) { 168 + case 'text:p': { 169 + const style = this.getStyleAttr(elem, styleMap) 170 + const content = this.convertChildren(elem, styleMap) 171 + return `<p${style}>${content || '&nbsp;'}</p>` 172 + } 173 + 174 + case 'text:h': { 175 + const level = elem.getAttribute('text:outline-level') ?? '1' 176 + const lvl = Math.min(Math.max(parseInt(level, 10), 1), 6) 177 + const style = this.getStyleAttr(elem, styleMap) 178 + const content = this.convertChildren(elem, styleMap) 179 + return `<h${lvl}${style}>${content}</h${lvl}>` 180 + } 181 + 182 + case 'text:span': { 183 + const style = this.getStyleAttr(elem, styleMap) 184 + return `<span${style}>${this.convertChildren(elem, styleMap)}</span>` 185 + } 186 + 187 + case 'text:a': { 188 + const href = elem.getAttribute('xlink:href') ?? '#' 189 + return `<a href="${this.escapeHtml(href)}" target="_blank" rel="noopener">${this.convertChildren(elem, styleMap)}</a>` 190 + } 191 + 192 + case 'text:list': 193 + return `<ul>${this.convertChildren(elem, styleMap)}</ul>` 194 + 195 + case 'text:list-item': 196 + return `<li>${this.convertChildren(elem, styleMap)}</li>` 197 + 198 + case 'text:line-break': 199 + return '<br>' 200 + 201 + case 'text:tab': 202 + return '&emsp;' 203 + 204 + case 'text:s': { 205 + const count = parseInt(elem.getAttribute('text:c') ?? '1', 10) 206 + return '&nbsp;'.repeat(count) 207 + } 208 + 209 + case 'text:soft-page-break': 210 + return '<hr style="border:none;border-top:1px dashed var(--dv-border);margin:1.5em 0">' 211 + 212 + case 'table:table': { 213 + const content = this.convertChildren(elem, styleMap) 214 + return `<table class="dv-table">${content}</table>` 215 + } 216 + 217 + case 'table:table-header-rows': 218 + return `<thead>${this.convertChildren(elem, styleMap)}</thead>` 219 + 220 + case 'table:table-row': 221 + return `<tr>${this.convertChildren(elem, styleMap)}</tr>` 222 + 223 + case 'table:table-cell': { 224 + const colspan = elem.getAttribute('table:number-columns-spanned') 225 + const rowspan = elem.getAttribute('table:number-rows-spanned') 226 + const attrs: string[] = [] 227 + if (colspan && colspan !== '1') attrs.push(`colspan="${colspan}"`) 228 + if (rowspan && rowspan !== '1') attrs.push(`rowspan="${rowspan}"`) 229 + const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '' 230 + return `<td${attrStr}>${this.convertChildren(elem, styleMap)}</td>` 231 + } 232 + 233 + case 'draw:frame': 234 + case 'draw:image': 235 + // Skip embedded images (would require extracting from ZIP) 236 + return '<span style="color:var(--dv-text-secondary)">[image]</span>' 237 + 238 + default: 239 + // Recurse into unknown elements 240 + return this.convertChildren(elem, styleMap) 241 + } 242 + } 243 + 244 + private convertChildren(elem: Element, styleMap: Map<string, string>): string { 245 + const parts: string[] = [] 246 + for (let i = 0; i < elem.childNodes.length; i++) { 247 + parts.push(this.convertNode(elem.childNodes[i], styleMap)) 248 + } 249 + return parts.join('') 250 + } 251 + 252 + private getStyleAttr(elem: Element, styleMap: Map<string, string>): string { 253 + const styleName = elem.getAttribute('text:style-name') 254 + if (styleName && styleMap.has(styleName)) { 255 + return ` style="${styleMap.get(styleName)!}"` 256 + } 257 + return '' 258 + } 259 + 260 + private escapeHtml(text: string): string { 261 + return text 262 + .replace(/&/g, '&amp;') 263 + .replace(/</g, '&lt;') 264 + .replace(/>/g, '&gt;') 265 + .replace(/"/g, '&quot;') 266 + } 267 + 268 + private async loadData(options: DocViewOptions): Promise<ArrayBuffer> { 269 + const source = options.source 270 + if (source.type === 'file') return toArrayBuffer(source.data) 271 + if (source.type === 'url') return fetchAsBuffer(source.url, source.fetchOptions) 272 + throw new Error('ODT renderer requires a file or url source.') 273 + } 274 + 275 + private getFilename(options: DocViewOptions): string | undefined { 276 + const source = options.source 277 + if ('filename' in source && source.filename) return source.filename 278 + if (source.type === 'url') return source.url.split('/').pop()?.split('?')[0] 279 + return undefined 280 + } 281 + 282 + protected onDestroy(): void { 283 + // DOM cleanup is sufficient 284 + } 285 + }
+360
packages/core/src/renderers/pdf.ts
··· 1 + import type { DocViewOptions, DocumentFormat, DocumentInfo } from '../types.js' 2 + import { DocViewError } from '../types.js' 3 + import { BaseRenderer } from '../renderer.js' 4 + import { 5 + el, 6 + toArrayBuffer, 7 + fetchAsBuffer, 8 + requirePeerDep, 9 + clamp, 10 + debounce, 11 + } from '../utils.js' 12 + 13 + interface PdfjsLib { 14 + getDocument(params: { 15 + data?: ArrayBuffer 16 + url?: string 17 + cMapUrl?: string 18 + cMapPacked?: boolean 19 + enableXfa?: boolean 20 + }): { promise: Promise<PdfjsDocument> } 21 + GlobalWorkerOptions: { workerSrc: string } 22 + } 23 + 24 + interface PdfjsDocument { 25 + numPages: number 26 + getPage(num: number): Promise<PdfjsPage> 27 + getMetadata(): Promise<{ info?: { Title?: string; Author?: string } }> 28 + destroy(): void 29 + } 30 + 31 + interface PdfjsPage { 32 + getViewport(params: { scale: number }): { width: number; height: number } 33 + render(params: { 34 + canvasContext: CanvasRenderingContext2D 35 + viewport: { width: number; height: number } 36 + }): { promise: Promise<void> } 37 + getTextContent(): Promise<{ items: PdfjsTextItem[] }> 38 + cleanup(): void 39 + } 40 + 41 + interface PdfjsTextItem { 42 + str: string 43 + transform: number[] 44 + width: number 45 + height: number 46 + } 47 + 48 + export class PdfRenderer extends BaseRenderer { 49 + readonly format: DocumentFormat = 'pdf' 50 + 51 + private pdfDoc: PdfjsDocument | null = null 52 + private pagesContainer!: HTMLElement 53 + private pageElements: HTMLElement[] = [] 54 + private pageCanvases: HTMLCanvasElement[] = [] 55 + private renderedPages = new Set<number>() 56 + private baseScale = 1 57 + private scrollObserver: IntersectionObserver | null = null 58 + private resizeObserver: ResizeObserver | null = null 59 + private debouncedRender: ReturnType<typeof debounce> | null = null 60 + 61 + protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 62 + const loadingEl = this.showLoading('Loading PDF…') 63 + 64 + // Load pdfjs-dist 65 + const pdfjsLib = await requirePeerDep<PdfjsLib>('pdfjs-dist', 'PDF') 66 + 67 + // Configure worker 68 + if (options.pdf?.workerSrc) { 69 + pdfjsLib.GlobalWorkerOptions.workerSrc = options.pdf.workerSrc 70 + } 71 + 72 + // Load the document 73 + const data = await this.loadSource(options) 74 + const loadingParams: Parameters<PdfjsLib['getDocument']>[0] = {} 75 + 76 + if (typeof data === 'string') { 77 + loadingParams.url = data 78 + } else { 79 + loadingParams.data = data 80 + } 81 + 82 + if (options.pdf?.cMapUrl) { 83 + loadingParams.cMapUrl = options.pdf.cMapUrl 84 + loadingParams.cMapPacked = true 85 + } 86 + 87 + this.pdfDoc = await pdfjsLib.getDocument(loadingParams).promise 88 + 89 + // Get metadata 90 + let title: string | undefined 91 + let author: string | undefined 92 + try { 93 + const meta = await this.pdfDoc.getMetadata() 94 + title = meta.info?.Title || undefined 95 + author = meta.info?.Author || undefined 96 + } catch { 97 + // Metadata extraction can fail on some PDFs — non-fatal 98 + } 99 + 100 + // Create pages container 101 + this.pagesContainer = el('div', 'dv-pages') 102 + viewport.appendChild(this.pagesContainer) 103 + 104 + // Create placeholder elements for every page 105 + const numPages = this.pdfDoc.numPages 106 + for (let i = 1; i <= numPages; i++) { 107 + const pageEl = el('div', 'dv-page') 108 + pageEl.dataset.page = String(i) 109 + this.pageElements.push(pageEl) 110 + this.pagesContainer.appendChild(pageEl) 111 + } 112 + 113 + // Determine initial scale 114 + const firstPage = await this.pdfDoc.getPage(1) 115 + const initialViewport = firstPage.getViewport({ scale: 1 }) 116 + const containerWidth = viewport.clientWidth - 48 // padding 117 + this.baseScale = containerWidth / initialViewport.width 118 + 119 + const zoomOption = options.zoom ?? 'fit-width' 120 + if (typeof zoomOption === 'number') { 121 + this.state.zoom = zoomOption 122 + } else { 123 + this.state.zoom = this.baseScale 124 + } 125 + 126 + // Set placeholder sizes 127 + for (let i = 0; i < numPages; i++) { 128 + const pageEl = this.pageElements[i] 129 + pageEl.style.width = `${initialViewport.width * this.state.zoom}px` 130 + pageEl.style.height = `${initialViewport.height * this.state.zoom}px` 131 + pageEl.style.background = 'var(--dv-page-bg)' 132 + } 133 + 134 + loadingEl.remove() 135 + 136 + // Set up intersection observer for lazy page rendering 137 + this.scrollObserver = new IntersectionObserver( 138 + (entries) => { 139 + for (const entry of entries) { 140 + if (entry.isIntersecting) { 141 + const pageNum = parseInt( 142 + (entry.target as HTMLElement).dataset.page!, 143 + 10, 144 + ) 145 + this.renderPage(pageNum) 146 + } 147 + } 148 + }, 149 + { root: viewport, rootMargin: '200% 0px' }, 150 + ) 151 + 152 + for (const pageEl of this.pageElements) { 153 + this.scrollObserver.observe(pageEl) 154 + } 155 + 156 + // Track current page on scroll 157 + this.debouncedRender = debounce(() => this.updateCurrentPage(), 100) 158 + viewport.addEventListener('scroll', this.debouncedRender) 159 + 160 + // Resize observer for fit-width recalculation 161 + this.resizeObserver = new ResizeObserver(() => { 162 + const newWidth = viewport.clientWidth - 48 163 + if (Math.abs(newWidth - containerWidth) > 10) { 164 + this.baseScale = newWidth / initialViewport.width 165 + } 166 + }) 167 + this.resizeObserver.observe(viewport) 168 + 169 + // Ready 170 + this.setReady({ 171 + format: 'pdf', 172 + pageCount: numPages, 173 + title, 174 + author, 175 + filename: this.getFilename(options), 176 + }) 177 + 178 + // Navigate to initial page 179 + if (options.initialPage && options.initialPage > 1) { 180 + this.goToPage(options.initialPage) 181 + } 182 + } 183 + 184 + private async loadSource( 185 + options: DocViewOptions, 186 + ): Promise<ArrayBuffer | string> { 187 + const source = options.source 188 + if (source.type === 'url') { 189 + // pdfjs can handle URL directly for range requests 190 + return source.url 191 + } 192 + if (source.type === 'file') { 193 + return toArrayBuffer(source.data) 194 + } 195 + throw new DocViewError( 196 + 'SOURCE_LOAD_FAILED', 197 + 'PDF renderer requires a file or url source.', 198 + ) 199 + } 200 + 201 + private async renderPage(pageNum: number): Promise<void> { 202 + if (!this.pdfDoc || this.renderedPages.has(pageNum)) return 203 + this.renderedPages.add(pageNum) 204 + 205 + const page = await this.pdfDoc.getPage(pageNum) 206 + const viewport = page.getViewport({ scale: this.state.zoom }) 207 + const pageEl = this.pageElements[pageNum - 1] 208 + 209 + // Create canvas 210 + const canvas = document.createElement('canvas') 211 + const ctx = canvas.getContext('2d')! 212 + const dpr = window.devicePixelRatio || 1 213 + canvas.width = viewport.width * dpr 214 + canvas.height = viewport.height * dpr 215 + canvas.style.width = `${viewport.width}px` 216 + canvas.style.height = `${viewport.height}px` 217 + ctx.scale(dpr, dpr) 218 + 219 + pageEl.style.width = `${viewport.width}px` 220 + pageEl.style.height = `${viewport.height}px` 221 + pageEl.innerHTML = '' 222 + pageEl.appendChild(canvas) 223 + this.pageCanvases[pageNum - 1] = canvas 224 + 225 + // Render 226 + await page.render({ canvasContext: ctx, viewport }).promise 227 + 228 + // Text layer 229 + const textLayerEnabled = this.options.pdf?.textLayer !== false 230 + if (textLayerEnabled) { 231 + try { 232 + const textContent = await page.getTextContent() 233 + this.renderTextLayer(pageEl, textContent.items, viewport) 234 + } catch { 235 + // Text extraction can fail — non-fatal 236 + } 237 + } 238 + 239 + page.cleanup() 240 + } 241 + 242 + private renderTextLayer( 243 + pageEl: HTMLElement, 244 + items: PdfjsTextItem[], 245 + viewport: { width: number; height: number }, 246 + ): void { 247 + const layer = el('div', 'dv-text-layer') 248 + for (const item of items) { 249 + if (!item.str.trim()) continue 250 + const span = document.createElement('span') 251 + span.textContent = item.str 252 + // item.transform: [scaleX, skewY, skewX, scaleY, translateX, translateY] 253 + const tx = item.transform[4] 254 + const ty = item.transform[5] 255 + const fontSize = Math.sqrt( 256 + item.transform[0] * item.transform[0] + 257 + item.transform[1] * item.transform[1], 258 + ) 259 + span.style.left = `${(tx / viewport.width) * 100}%` 260 + span.style.bottom = `${(ty / viewport.height) * 100}%` 261 + span.style.fontSize = `${fontSize * this.state.zoom}px` 262 + span.style.fontFamily = 'sans-serif' 263 + layer.appendChild(span) 264 + } 265 + pageEl.appendChild(layer) 266 + } 267 + 268 + private updateCurrentPage(): void { 269 + const viewportRect = this.viewport.getBoundingClientRect() 270 + const viewportMid = viewportRect.top + viewportRect.height / 2 271 + 272 + for (let i = 0; i < this.pageElements.length; i++) { 273 + const rect = this.pageElements[i].getBoundingClientRect() 274 + if (rect.top <= viewportMid && rect.bottom >= viewportMid) { 275 + const newPage = i + 1 276 + if (newPage !== this.state.currentPage) { 277 + this.state.currentPage = newPage 278 + this.emitPageChange() 279 + } 280 + return 281 + } 282 + } 283 + } 284 + 285 + protected onPageChange(page: number): void { 286 + const pageEl = this.pageElements[page - 1] 287 + if (pageEl) { 288 + pageEl.scrollIntoView({ behavior: 'smooth', block: 'start' }) 289 + } 290 + } 291 + 292 + protected onZoomChange(zoom: number): void { 293 + const clamped = clamp(zoom, 0.25, 5) 294 + this.state.zoom = clamped 295 + 296 + // Re-render all visible pages at new zoom 297 + this.renderedPages.clear() 298 + for (let i = 0; i < this.pageElements.length; i++) { 299 + const pageEl = this.pageElements[i] 300 + pageEl.innerHTML = '' 301 + // Update placeholder size (approximate from first page ratio) 302 + if (this.pdfDoc) { 303 + const scale = clamped 304 + // We'll just trigger re-render via intersection observer 305 + pageEl.style.width = '' 306 + pageEl.style.height = '' 307 + } 308 + } 309 + 310 + // Re-render by re-observing (intersection observer will trigger) 311 + if (this.scrollObserver && this.pdfDoc) { 312 + this.reRenderVisiblePages() 313 + } 314 + } 315 + 316 + private async reRenderVisiblePages(): Promise<void> { 317 + if (!this.pdfDoc) return 318 + const firstPage = await this.pdfDoc.getPage(1) 319 + const vp = firstPage.getViewport({ scale: this.state.zoom }) 320 + 321 + for (const pageEl of this.pageElements) { 322 + pageEl.style.width = `${vp.width}px` 323 + pageEl.style.height = `${vp.height}px` 324 + } 325 + 326 + // The intersection observer will re-trigger rendering for visible pages 327 + for (const pageEl of this.pageElements) { 328 + this.scrollObserver!.unobserve(pageEl) 329 + this.scrollObserver!.observe(pageEl) 330 + } 331 + } 332 + 333 + protected resolveZoomMode(mode: 'fit-width' | 'fit-page'): number { 334 + return this.baseScale * (mode === 'fit-page' ? 0.95 : 1) 335 + } 336 + 337 + private getFilename(options: DocViewOptions): string | undefined { 338 + const src = options.source 339 + if ('filename' in src && src.filename) return src.filename 340 + if (src.type === 'url') { 341 + const segments = src.url.split('/').filter(Boolean) 342 + return segments.pop()?.split('?')[0] 343 + } 344 + return undefined 345 + } 346 + 347 + protected onDestroy(): void { 348 + this.scrollObserver?.disconnect() 349 + this.resizeObserver?.disconnect() 350 + this.debouncedRender?.cancel() 351 + if (this.viewport) { 352 + this.viewport.removeEventListener('scroll', this.debouncedRender as EventListener) 353 + } 354 + this.pdfDoc?.destroy() 355 + this.pdfDoc = null 356 + this.pageElements = [] 357 + this.pageCanvases = [] 358 + this.renderedPages.clear() 359 + } 360 + }
+69
packages/core/src/renderers/text.ts
··· 1 + import type { DocViewOptions, DocumentFormat } from '../types.js' 2 + import { BaseRenderer } from '../renderer.js' 3 + import { el, toText, fetchAsBuffer } from '../utils.js' 4 + 5 + /** 6 + * Renders plain text files with optional line numbers. 7 + * Uses proportional font for prose (.txt) and monospace for other text files. 8 + */ 9 + export class TextRenderer extends BaseRenderer { 10 + readonly format: DocumentFormat = 'text' 11 + 12 + private textContainer!: HTMLElement 13 + 14 + protected async onMount(viewport: HTMLElement, options: DocViewOptions): Promise<void> { 15 + this.showLoading('Loading text…') 16 + 17 + const text = await this.loadText(options) 18 + 19 + this.hideLoading() 20 + 21 + // Determine if monospace 22 + const ext = this.getExtension(options) 23 + const isMonospace = ext !== 'txt' && ext !== 'text' 24 + 25 + this.textContainer = el('div', `dv-text-container${isMonospace ? ' dv-monospace' : ''}`) 26 + this.textContainer.textContent = text 27 + viewport.appendChild(this.textContainer) 28 + 29 + const lineCount = text.split('\n').length 30 + this.setReady({ 31 + format: 'text', 32 + pageCount: 1, 33 + filename: this.getFilename(options), 34 + }) 35 + } 36 + 37 + private async loadText(options: DocViewOptions): Promise<string> { 38 + const source = options.source 39 + if (source.type === 'file') { 40 + return toText(source.data) 41 + } 42 + if (source.type === 'url') { 43 + const buffer = await fetchAsBuffer(source.url, source.fetchOptions) 44 + return new TextDecoder('utf-8').decode(buffer) 45 + } 46 + return '' 47 + } 48 + 49 + private getExtension(options: DocViewOptions): string { 50 + const source = options.source 51 + const name = ('filename' in source ? source.filename : undefined) 52 + ?? (source.type === 'url' ? source.url : '') 53 + const dot = name.lastIndexOf('.') 54 + return dot >= 0 ? name.slice(dot + 1).toLowerCase() : '' 55 + } 56 + 57 + private getFilename(options: DocViewOptions): string | undefined { 58 + const source = options.source 59 + if ('filename' in source && source.filename) return source.filename 60 + if (source.type === 'url') { 61 + return source.url.split('/').pop()?.split('?')[0] 62 + } 63 + return undefined 64 + } 65 + 66 + protected onDestroy(): void { 67 + // No external resources to clean up 68 + } 69 + }
+699
packages/core/src/styles.css
··· 1 + /* ========================================================================== 2 + DocView — CSS Variables Theme System 3 + Override any --dv-* variable to customize the viewer appearance. 4 + ========================================================================== */ 5 + 6 + /* --- Base / Dark Theme (default) --- */ 7 + .docview { 8 + /* Typography */ 9 + --dv-font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 10 + --dv-font-mono: 'SF Mono', SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace; 11 + --dv-font-size-sm: 12px; 12 + --dv-font-size: 14px; 13 + --dv-font-size-lg: 16px; 14 + --dv-line-height: 1.5; 15 + 16 + /* Spacing & Radii */ 17 + --dv-radius-sm: 4px; 18 + --dv-radius: 6px; 19 + --dv-radius-lg: 10px; 20 + --dv-spacing-xs: 4px; 21 + --dv-spacing-sm: 8px; 22 + --dv-spacing: 12px; 23 + --dv-spacing-lg: 16px; 24 + --dv-spacing-xl: 24px; 25 + 26 + /* Transitions */ 27 + --dv-transition-fast: 100ms ease; 28 + --dv-transition: 150ms ease; 29 + --dv-transition-slow: 250ms ease; 30 + 31 + /* Colors — Dark */ 32 + --dv-bg: #0d1117; 33 + --dv-surface: #161b22; 34 + --dv-surface-raised: #1c2129; 35 + --dv-border: #30363d; 36 + --dv-border-subtle: #21262d; 37 + --dv-text: #e6edf3; 38 + --dv-text-secondary: #8b949e; 39 + --dv-text-muted: #484f58; 40 + --dv-accent: #58a6ff; 41 + --dv-accent-hover: #79b8ff; 42 + --dv-accent-subtle: rgba(88, 166, 255, 0.15); 43 + --dv-error: #f85149; 44 + --dv-warning: #d29922; 45 + 46 + /* Document Page */ 47 + --dv-page-bg: #ffffff; 48 + --dv-page-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 4px 12px rgba(0, 0, 0, 0.2); 49 + --dv-page-gap: 12px; 50 + 51 + /* Toolbar */ 52 + --dv-toolbar-bg: #161b22; 53 + --dv-toolbar-border: #30363d; 54 + --dv-toolbar-height: 44px; 55 + 56 + /* Scrollbar */ 57 + --dv-scrollbar-size: 8px; 58 + --dv-scrollbar-thumb: #484f58; 59 + --dv-scrollbar-thumb-hover: #6e7681; 60 + --dv-scrollbar-track: transparent; 61 + 62 + /* Table (CSV) */ 63 + --dv-table-header-bg: #161b22; 64 + --dv-table-row-hover: rgba(136, 146, 158, 0.08); 65 + --dv-table-stripe: rgba(136, 146, 158, 0.04); 66 + 67 + /* Code */ 68 + --dv-code-bg: #0d1117; 69 + --dv-code-gutter: #484f58; 70 + --dv-code-gutter-bg: #0d1117; 71 + --dv-code-highlight-line: rgba(88, 166, 255, 0.1); 72 + } 73 + 74 + /* --- Light Theme --- */ 75 + .docview[data-theme='light'] { 76 + --dv-bg: #ffffff; 77 + --dv-surface: #f6f8fa; 78 + --dv-surface-raised: #ffffff; 79 + --dv-border: #d0d7de; 80 + --dv-border-subtle: #e1e4e8; 81 + --dv-text: #1f2328; 82 + --dv-text-secondary: #656d76; 83 + --dv-text-muted: #8b949e; 84 + --dv-accent: #0969da; 85 + --dv-accent-hover: #0550ae; 86 + --dv-accent-subtle: rgba(9, 105, 218, 0.08); 87 + --dv-error: #cf222e; 88 + --dv-warning: #9a6700; 89 + --dv-page-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.04); 90 + --dv-toolbar-bg: #f6f8fa; 91 + --dv-toolbar-border: #d0d7de; 92 + --dv-scrollbar-thumb: #c1c8cf; 93 + --dv-scrollbar-thumb-hover: #afb8c1; 94 + --dv-table-header-bg: #f6f8fa; 95 + --dv-table-row-hover: rgba(31, 35, 40, 0.04); 96 + --dv-table-stripe: rgba(31, 35, 40, 0.02); 97 + --dv-code-bg: #f6f8fa; 98 + --dv-code-gutter: #8b949e; 99 + --dv-code-gutter-bg: #f6f8fa; 100 + --dv-code-highlight-line: rgba(9, 105, 218, 0.08); 101 + } 102 + 103 + 104 + /* ========================================================================== 105 + Root Container 106 + ========================================================================== */ 107 + 108 + .docview { 109 + position: relative; 110 + display: flex; 111 + flex-direction: column; 112 + width: 100%; 113 + height: 100%; 114 + min-height: 200px; 115 + overflow: hidden; 116 + background: var(--dv-bg); 117 + color: var(--dv-text); 118 + font-family: var(--dv-font-sans); 119 + font-size: var(--dv-font-size); 120 + line-height: var(--dv-line-height); 121 + border-radius: var(--dv-radius); 122 + border: 1px solid var(--dv-border); 123 + } 124 + 125 + 126 + /* ========================================================================== 127 + Toolbar 128 + ========================================================================== */ 129 + 130 + .dv-toolbar { 131 + display: flex; 132 + align-items: center; 133 + gap: var(--dv-spacing-sm); 134 + height: var(--dv-toolbar-height); 135 + min-height: var(--dv-toolbar-height); 136 + padding: 0 var(--dv-spacing); 137 + background: var(--dv-toolbar-bg); 138 + border-bottom: 1px solid var(--dv-toolbar-border); 139 + user-select: none; 140 + flex-shrink: 0; 141 + z-index: 10; 142 + } 143 + 144 + .dv-toolbar[data-position='bottom'] { 145 + border-bottom: none; 146 + border-top: 1px solid var(--dv-toolbar-border); 147 + order: 1; 148 + } 149 + 150 + .dv-toolbar-group { 151 + display: flex; 152 + align-items: center; 153 + gap: var(--dv-spacing-xs); 154 + } 155 + 156 + .dv-toolbar-spacer { 157 + flex: 1; 158 + } 159 + 160 + .dv-toolbar-separator { 161 + width: 1px; 162 + height: 20px; 163 + background: var(--dv-border); 164 + margin: 0 var(--dv-spacing-xs); 165 + } 166 + 167 + .dv-toolbar-btn { 168 + display: inline-flex; 169 + align-items: center; 170 + justify-content: center; 171 + width: 32px; 172 + height: 32px; 173 + padding: 0; 174 + border: none; 175 + border-radius: var(--dv-radius-sm); 176 + background: transparent; 177 + color: var(--dv-text-secondary); 178 + cursor: pointer; 179 + transition: background var(--dv-transition-fast), color var(--dv-transition-fast); 180 + flex-shrink: 0; 181 + } 182 + 183 + .dv-toolbar-btn:hover { 184 + background: var(--dv-accent-subtle); 185 + color: var(--dv-text); 186 + } 187 + 188 + .dv-toolbar-btn:active { 189 + background: var(--dv-border); 190 + } 191 + 192 + .dv-toolbar-btn[disabled] { 193 + opacity: 0.35; 194 + cursor: default; 195 + pointer-events: none; 196 + } 197 + 198 + .dv-toolbar-btn svg { 199 + width: 16px; 200 + height: 16px; 201 + } 202 + 203 + .dv-toolbar-label { 204 + font-size: var(--dv-font-size-sm); 205 + color: var(--dv-text-secondary); 206 + white-space: nowrap; 207 + overflow: hidden; 208 + text-overflow: ellipsis; 209 + max-width: 200px; 210 + } 211 + 212 + .dv-page-input { 213 + width: 48px; 214 + height: 28px; 215 + padding: 0 var(--dv-spacing-xs); 216 + border: 1px solid var(--dv-border); 217 + border-radius: var(--dv-radius-sm); 218 + background: var(--dv-bg); 219 + color: var(--dv-text); 220 + font-family: var(--dv-font-mono); 221 + font-size: var(--dv-font-size-sm); 222 + text-align: center; 223 + outline: none; 224 + } 225 + 226 + .dv-page-input:focus { 227 + border-color: var(--dv-accent); 228 + box-shadow: 0 0 0 2px var(--dv-accent-subtle); 229 + } 230 + 231 + 232 + /* ========================================================================== 233 + Viewport (Scrollable Document Area) 234 + ========================================================================== */ 235 + 236 + .dv-viewport { 237 + flex: 1; 238 + overflow: auto; 239 + position: relative; 240 + background: var(--dv-bg); 241 + } 242 + 243 + /* Custom scrollbar */ 244 + .dv-viewport::-webkit-scrollbar { 245 + width: var(--dv-scrollbar-size); 246 + height: var(--dv-scrollbar-size); 247 + } 248 + 249 + .dv-viewport::-webkit-scrollbar-thumb { 250 + background: var(--dv-scrollbar-thumb); 251 + border-radius: 999px; 252 + } 253 + 254 + .dv-viewport::-webkit-scrollbar-thumb:hover { 255 + background: var(--dv-scrollbar-thumb-hover); 256 + } 257 + 258 + .dv-viewport::-webkit-scrollbar-track { 259 + background: var(--dv-scrollbar-track); 260 + } 261 + 262 + .dv-viewport { 263 + scrollbar-width: thin; 264 + scrollbar-color: var(--dv-scrollbar-thumb) var(--dv-scrollbar-track); 265 + } 266 + 267 + 268 + /* ========================================================================== 269 + Page Container (for paginated renderers: PDF, images, etc.) 270 + ========================================================================== */ 271 + 272 + .dv-pages { 273 + display: flex; 274 + flex-direction: column; 275 + align-items: center; 276 + padding: var(--dv-spacing-lg); 277 + gap: var(--dv-page-gap); 278 + min-height: 100%; 279 + } 280 + 281 + .dv-page { 282 + position: relative; 283 + background: var(--dv-page-bg); 284 + box-shadow: var(--dv-page-shadow); 285 + border-radius: 2px; 286 + overflow: hidden; 287 + flex-shrink: 0; 288 + } 289 + 290 + .dv-page canvas { 291 + display: block; 292 + } 293 + 294 + .dv-page img { 295 + display: block; 296 + width: 100%; 297 + height: auto; 298 + } 299 + 300 + /* Text layer overlay for copy/paste over rendered pages */ 301 + .dv-text-layer { 302 + position: absolute; 303 + top: 0; 304 + left: 0; 305 + right: 0; 306 + bottom: 0; 307 + overflow: hidden; 308 + opacity: 0.25; 309 + line-height: 1; 310 + pointer-events: all; 311 + } 312 + 313 + .dv-text-layer span { 314 + position: absolute; 315 + white-space: pre; 316 + color: transparent; 317 + cursor: text; 318 + } 319 + 320 + .dv-text-layer span::selection { 321 + background: var(--dv-accent); 322 + color: transparent; 323 + opacity: 0.4; 324 + } 325 + 326 + 327 + /* ========================================================================== 328 + Loading & Error States 329 + ========================================================================== */ 330 + 331 + .dv-loading { 332 + display: flex; 333 + flex-direction: column; 334 + align-items: center; 335 + justify-content: center; 336 + gap: var(--dv-spacing); 337 + padding: var(--dv-spacing-xl); 338 + min-height: 200px; 339 + color: var(--dv-text-secondary); 340 + font-size: var(--dv-font-size); 341 + } 342 + 343 + .dv-spinner { 344 + width: 24px; 345 + height: 24px; 346 + border: 2px solid var(--dv-border); 347 + border-top-color: var(--dv-accent); 348 + border-radius: 50%; 349 + animation: dv-spin 0.6s linear infinite; 350 + } 351 + 352 + @keyframes dv-spin { 353 + to { transform: rotate(360deg); } 354 + } 355 + 356 + .dv-error { 357 + display: flex; 358 + flex-direction: column; 359 + align-items: center; 360 + justify-content: center; 361 + gap: var(--dv-spacing-sm); 362 + padding: var(--dv-spacing-xl); 363 + min-height: 200px; 364 + color: var(--dv-error); 365 + text-align: center; 366 + } 367 + 368 + .dv-error-code { 369 + font-family: var(--dv-font-mono); 370 + font-size: var(--dv-font-size-sm); 371 + color: var(--dv-text-muted); 372 + } 373 + 374 + .dv-error-message { 375 + font-size: var(--dv-font-size); 376 + max-width: 400px; 377 + } 378 + 379 + 380 + /* ========================================================================== 381 + Table Renderer (CSV/TSV) 382 + ========================================================================== */ 383 + 384 + .dv-table-container { 385 + overflow: auto; 386 + flex: 1; 387 + font-size: var(--dv-font-size-sm); 388 + } 389 + 390 + .dv-table { 391 + width: 100%; 392 + border-collapse: collapse; 393 + border-spacing: 0; 394 + font-family: var(--dv-font-mono); 395 + font-size: var(--dv-font-size-sm); 396 + } 397 + 398 + .dv-table thead { 399 + position: sticky; 400 + top: 0; 401 + z-index: 5; 402 + } 403 + 404 + .dv-table th { 405 + background: var(--dv-table-header-bg); 406 + border-bottom: 2px solid var(--dv-border); 407 + padding: var(--dv-spacing-sm) var(--dv-spacing); 408 + text-align: left; 409 + font-weight: 600; 410 + color: var(--dv-text); 411 + white-space: nowrap; 412 + cursor: default; 413 + user-select: none; 414 + } 415 + 416 + .dv-table th[data-sortable='true'] { 417 + cursor: pointer; 418 + } 419 + 420 + .dv-table th[data-sortable='true']:hover { 421 + background: var(--dv-surface-raised); 422 + } 423 + 424 + .dv-table td { 425 + padding: var(--dv-spacing-xs) var(--dv-spacing); 426 + border-bottom: 1px solid var(--dv-border-subtle); 427 + color: var(--dv-text); 428 + max-width: 300px; 429 + overflow: hidden; 430 + text-overflow: ellipsis; 431 + white-space: nowrap; 432 + } 433 + 434 + .dv-table tbody tr:nth-child(even) { 435 + background: var(--dv-table-stripe); 436 + } 437 + 438 + .dv-table tbody tr:hover { 439 + background: var(--dv-table-row-hover); 440 + } 441 + 442 + .dv-table-row-number { 443 + color: var(--dv-text-muted); 444 + min-width: 48px; 445 + text-align: right; 446 + padding-right: var(--dv-spacing) !important; 447 + user-select: none; 448 + } 449 + 450 + 451 + /* ========================================================================== 452 + Code Renderer 453 + ========================================================================== */ 454 + 455 + .dv-code-container { 456 + display: flex; 457 + flex: 1; 458 + overflow: auto; 459 + font-family: var(--dv-font-mono); 460 + font-size: var(--dv-font-size); 461 + line-height: 1.6; 462 + background: var(--dv-code-bg); 463 + tab-size: 2; 464 + } 465 + 466 + .dv-code-gutter { 467 + position: sticky; 468 + left: 0; 469 + display: flex; 470 + flex-direction: column; 471 + padding: var(--dv-spacing) 0; 472 + text-align: right; 473 + color: var(--dv-code-gutter); 474 + background: var(--dv-code-gutter-bg); 475 + user-select: none; 476 + flex-shrink: 0; 477 + border-right: 1px solid var(--dv-border-subtle); 478 + z-index: 1; 479 + } 480 + 481 + .dv-code-gutter-line { 482 + padding: 0 var(--dv-spacing) 0 var(--dv-spacing-lg); 483 + min-width: 48px; 484 + } 485 + 486 + .dv-code-body { 487 + flex: 1; 488 + padding: var(--dv-spacing); 489 + overflow-x: auto; 490 + white-space: pre; 491 + } 492 + 493 + .dv-code-body.dv-word-wrap { 494 + white-space: pre-wrap; 495 + word-break: break-all; 496 + } 497 + 498 + /* --- Built-in Syntax Highlighting (Dark) --- */ 499 + .docview .dv-code-body .hljs-keyword, 500 + .docview .dv-code-body .hljs-selector-tag, 501 + .docview .dv-code-body .hljs-built_in, 502 + .docview .dv-code-body .hljs-type { color: #ff7b72; } 503 + 504 + .docview .dv-code-body .hljs-string, 505 + .docview .dv-code-body .hljs-addition { color: #a5d6ff; } 506 + 507 + .docview .dv-code-body .hljs-number, 508 + .docview .dv-code-body .hljs-literal { color: #79c0ff; } 509 + 510 + .docview .dv-code-body .hljs-comment, 511 + .docview .dv-code-body .hljs-quote { color: #8b949e; font-style: italic; } 512 + 513 + .docview .dv-code-body .hljs-function, 514 + .docview .dv-code-body .hljs-title { color: #d2a8ff; } 515 + 516 + .docview .dv-code-body .hljs-title.function_ { color: #d2a8ff; } 517 + .docview .dv-code-body .hljs-title.class_ { color: #f0883e; } 518 + 519 + .docview .dv-code-body .hljs-variable, 520 + .docview .dv-code-body .hljs-template-variable { color: #ffa657; } 521 + 522 + .docview .dv-code-body .hljs-attr, 523 + .docview .dv-code-body .hljs-attribute { color: #79c0ff; } 524 + 525 + .docview .dv-code-body .hljs-tag { color: #7ee787; } 526 + 527 + .docview .dv-code-body .hljs-name { color: #7ee787; } 528 + 529 + .docview .dv-code-body .hljs-regexp { color: #a5d6ff; } 530 + 531 + .docview .dv-code-body .hljs-symbol, 532 + .docview .dv-code-body .hljs-bullet { color: #ffa657; } 533 + 534 + .docview .dv-code-body .hljs-meta, 535 + .docview .dv-code-body .hljs-meta .hljs-keyword { color: #79c0ff; } 536 + 537 + .docview .dv-code-body .hljs-deletion { color: #ffa198; background: rgba(255, 129, 130, 0.1); } 538 + .docview .dv-code-body .hljs-addition { background: rgba(63, 185, 80, 0.1); } 539 + 540 + .docview .dv-code-body .hljs-params { color: #e6edf3; } 541 + 542 + .docview .dv-code-body .hljs-operator { color: #ff7b72; } 543 + 544 + .docview .dv-code-body .hljs-property { color: #79c0ff; } 545 + 546 + .docview .dv-code-body .hljs-punctuation { color: #8b949e; } 547 + 548 + .docview .dv-code-body .hljs-emphasis { font-style: italic; } 549 + .docview .dv-code-body .hljs-strong { font-weight: bold; } 550 + 551 + .docview .dv-code-body .hljs-section { color: #79c0ff; font-weight: bold; } 552 + 553 + .docview .dv-code-body .hljs-link { color: #58a6ff; text-decoration: underline; } 554 + 555 + /* --- Syntax Highlighting (Light Override) --- */ 556 + .docview[data-theme='light'] .dv-code-body .hljs-keyword, 557 + .docview[data-theme='light'] .dv-code-body .hljs-selector-tag, 558 + .docview[data-theme='light'] .dv-code-body .hljs-built_in, 559 + .docview[data-theme='light'] .dv-code-body .hljs-type { color: #cf222e; } 560 + 561 + .docview[data-theme='light'] .dv-code-body .hljs-string, 562 + .docview[data-theme='light'] .dv-code-body .hljs-addition { color: #0a3069; } 563 + 564 + .docview[data-theme='light'] .dv-code-body .hljs-number, 565 + .docview[data-theme='light'] .dv-code-body .hljs-literal { color: #0550ae; } 566 + 567 + .docview[data-theme='light'] .dv-code-body .hljs-comment, 568 + .docview[data-theme='light'] .dv-code-body .hljs-quote { color: #6e7781; font-style: italic; } 569 + 570 + .docview[data-theme='light'] .dv-code-body .hljs-function, 571 + .docview[data-theme='light'] .dv-code-body .hljs-title { color: #8250df; } 572 + 573 + .docview[data-theme='light'] .dv-code-body .hljs-title.function_ { color: #8250df; } 574 + .docview[data-theme='light'] .dv-code-body .hljs-title.class_ { color: #953800; } 575 + 576 + .docview[data-theme='light'] .dv-code-body .hljs-variable, 577 + .docview[data-theme='light'] .dv-code-body .hljs-template-variable { color: #953800; } 578 + 579 + .docview[data-theme='light'] .dv-code-body .hljs-attr, 580 + .docview[data-theme='light'] .dv-code-body .hljs-attribute { color: #0550ae; } 581 + 582 + .docview[data-theme='light'] .dv-code-body .hljs-tag { color: #116329; } 583 + .docview[data-theme='light'] .dv-code-body .hljs-name { color: #116329; } 584 + 585 + .docview[data-theme='light'] .dv-code-body .hljs-regexp { color: #0a3069; } 586 + 587 + .docview[data-theme='light'] .dv-code-body .hljs-symbol, 588 + .docview[data-theme='light'] .dv-code-body .hljs-bullet { color: #953800; } 589 + 590 + .docview[data-theme='light'] .dv-code-body .hljs-meta, 591 + .docview[data-theme='light'] .dv-code-body .hljs-meta .hljs-keyword { color: #0550ae; } 592 + 593 + .docview[data-theme='light'] .dv-code-body .hljs-deletion { color: #82071e; background: rgba(255, 129, 130, 0.1); } 594 + .docview[data-theme='light'] .dv-code-body .hljs-addition { background: rgba(63, 185, 80, 0.1); } 595 + 596 + .docview[data-theme='light'] .dv-code-body .hljs-params { color: #1f2328; } 597 + .docview[data-theme='light'] .dv-code-body .hljs-operator { color: #cf222e; } 598 + .docview[data-theme='light'] .dv-code-body .hljs-property { color: #0550ae; } 599 + .docview[data-theme='light'] .dv-code-body .hljs-punctuation { color: #6e7781; } 600 + .docview[data-theme='light'] .dv-code-body .hljs-section { color: #0550ae; font-weight: bold; } 601 + .docview[data-theme='light'] .dv-code-body .hljs-link { color: #0969da; text-decoration: underline; } 602 + 603 + 604 + /* ========================================================================== 605 + Text Renderer 606 + ========================================================================== */ 607 + 608 + .dv-text-container { 609 + flex: 1; 610 + overflow: auto; 611 + padding: var(--dv-spacing-lg); 612 + font-family: var(--dv-font-sans); 613 + font-size: var(--dv-font-size-lg); 614 + line-height: 1.7; 615 + color: var(--dv-text); 616 + white-space: pre-wrap; 617 + word-wrap: break-word; 618 + max-width: 72ch; 619 + margin: 0 auto; 620 + } 621 + 622 + .dv-text-container.dv-monospace { 623 + font-family: var(--dv-font-mono); 624 + font-size: var(--dv-font-size); 625 + max-width: none; 626 + } 627 + 628 + 629 + /* ========================================================================== 630 + EPUB Renderer 631 + ========================================================================== */ 632 + 633 + .dv-epub-container { 634 + flex: 1; 635 + overflow: hidden; 636 + position: relative; 637 + } 638 + 639 + .dv-epub-container iframe { 640 + border: none; 641 + width: 100%; 642 + height: 100%; 643 + } 644 + 645 + 646 + /* ========================================================================== 647 + DOCX Renderer 648 + ========================================================================== */ 649 + 650 + .dv-docx-container { 651 + flex: 1; 652 + overflow: auto; 653 + padding: var(--dv-spacing-lg); 654 + background: var(--dv-bg); 655 + } 656 + 657 + /* docx-preview renders into a wrapper div — scope some resets */ 658 + .dv-docx-container .docx-wrapper { 659 + background: transparent !important; 660 + padding: 0 !important; 661 + } 662 + 663 + .dv-docx-container .docx-wrapper > section.docx { 664 + background: var(--dv-page-bg) !important; 665 + box-shadow: var(--dv-page-shadow) !important; 666 + margin: 0 auto var(--dv-page-gap) !important; 667 + border-radius: 2px; 668 + } 669 + 670 + 671 + /* ========================================================================== 672 + Page Image Renderer (browse pages / pre-rendered) 673 + ========================================================================== */ 674 + 675 + .dv-browse-page { 676 + position: relative; 677 + display: flex; 678 + align-items: center; 679 + justify-content: center; 680 + } 681 + 682 + .dv-browse-page img { 683 + display: block; 684 + max-width: 100%; 685 + height: auto; 686 + background: var(--dv-page-bg); 687 + box-shadow: var(--dv-page-shadow); 688 + border-radius: 2px; 689 + } 690 + 691 + .dv-browse-page-placeholder { 692 + display: flex; 693 + align-items: center; 694 + justify-content: center; 695 + background: var(--dv-surface); 696 + border-radius: 2px; 697 + color: var(--dv-text-muted); 698 + font-size: var(--dv-font-size-sm); 699 + }
+150
packages/core/src/toolbar.ts
··· 1 + import type { ToolbarConfig, DocViewState } from './types.js' 2 + import { el, svgIcon, icons } from './utils.js' 3 + 4 + export interface ToolbarActions { 5 + onPrevPage(): void 6 + onNextPage(): void 7 + onPageInput(page: number): void 8 + onZoomIn(): void 9 + onZoomOut(): void 10 + onFitWidth(): void 11 + onFullscreen(): void 12 + onDownload?(): void 13 + } 14 + 15 + export interface ToolbarHandle { 16 + /** Root toolbar element. */ 17 + element: HTMLElement 18 + /** Update displayed state (page, total, zoom). */ 19 + updateState(state: DocViewState): void 20 + /** Destroy and clean up listeners. */ 21 + destroy(): void 22 + } 23 + 24 + /** 25 + * Build a toolbar DOM element wired to the provided actions. 26 + * Returns a handle for updating state and destroying. 27 + */ 28 + export function createToolbar( 29 + config: ToolbarConfig, 30 + actions: ToolbarActions, 31 + initialState: DocViewState, 32 + ): ToolbarHandle { 33 + const toolbar = el('div', 'dv-toolbar') 34 + if (config.position === 'bottom') { 35 + toolbar.setAttribute('data-position', 'bottom') 36 + } 37 + 38 + let pageInput: HTMLInputElement | null = null 39 + let pageLabel: HTMLSpanElement | null = null 40 + let zoomLabel: HTMLSpanElement | null = null 41 + let prevBtn: HTMLButtonElement | null = null 42 + let nextBtn: HTMLButtonElement | null = null 43 + 44 + // --- Navigation group --- 45 + if (config.navigation !== false) { 46 + const navGroup = el('div', 'dv-toolbar-group') 47 + 48 + prevBtn = el('button', 'dv-toolbar-btn') 49 + prevBtn.title = 'Previous page' 50 + prevBtn.appendChild(svgIcon(icons.chevronLeft)) 51 + prevBtn.addEventListener('click', actions.onPrevPage) 52 + 53 + pageInput = el('input', 'dv-page-input') as HTMLInputElement 54 + pageInput.type = 'text' 55 + pageInput.inputMode = 'numeric' 56 + pageInput.value = String(initialState.currentPage) 57 + pageInput.title = 'Go to page' 58 + pageInput.addEventListener('keydown', (e) => { 59 + if (e.key === 'Enter') { 60 + const val = parseInt(pageInput!.value, 10) 61 + if (!isNaN(val)) actions.onPageInput(val) 62 + } 63 + }) 64 + pageInput.addEventListener('blur', () => { 65 + pageInput!.value = String(initialState.currentPage) 66 + }) 67 + 68 + pageLabel = el('span', 'dv-toolbar-label') 69 + pageLabel.textContent = `/ ${initialState.totalPages}` 70 + 71 + nextBtn = el('button', 'dv-toolbar-btn') 72 + nextBtn.title = 'Next page' 73 + nextBtn.appendChild(svgIcon(icons.chevronRight)) 74 + nextBtn.addEventListener('click', actions.onNextPage) 75 + 76 + navGroup.append(prevBtn, pageInput, pageLabel, nextBtn) 77 + toolbar.appendChild(navGroup) 78 + } 79 + 80 + // --- Info (filename/format) --- 81 + if (config.info !== false && initialState.documentInfo?.filename) { 82 + const sep = el('div', 'dv-toolbar-separator') 83 + const infoLabel = el('span', 'dv-toolbar-label') 84 + infoLabel.textContent = initialState.documentInfo.filename 85 + infoLabel.title = initialState.documentInfo.filename 86 + toolbar.append(sep, infoLabel) 87 + } 88 + 89 + // Spacer 90 + toolbar.appendChild(el('div', 'dv-toolbar-spacer')) 91 + 92 + // --- Zoom group --- 93 + if (config.zoom !== false) { 94 + const zoomGroup = el('div', 'dv-toolbar-group') 95 + 96 + const zoomOutBtn = el('button', 'dv-toolbar-btn') 97 + zoomOutBtn.title = 'Zoom out' 98 + zoomOutBtn.appendChild(svgIcon(icons.zoomOut)) 99 + zoomOutBtn.addEventListener('click', actions.onZoomOut) 100 + 101 + zoomLabel = el('span', 'dv-toolbar-label') 102 + zoomLabel.textContent = `${Math.round(initialState.zoom * 100)}%` 103 + 104 + const zoomInBtn = el('button', 'dv-toolbar-btn') 105 + zoomInBtn.title = 'Zoom in' 106 + zoomInBtn.appendChild(svgIcon(icons.zoomIn)) 107 + zoomInBtn.addEventListener('click', actions.onZoomIn) 108 + 109 + const fitWidthBtn = el('button', 'dv-toolbar-btn') 110 + fitWidthBtn.title = 'Fit width' 111 + fitWidthBtn.appendChild(svgIcon(icons.fitWidth)) 112 + fitWidthBtn.addEventListener('click', actions.onFitWidth) 113 + 114 + zoomGroup.append(zoomOutBtn, zoomLabel, zoomInBtn, fitWidthBtn) 115 + toolbar.appendChild(zoomGroup) 116 + } 117 + 118 + // --- Fullscreen --- 119 + if (config.fullscreen !== false) { 120 + const sep = el('div', 'dv-toolbar-separator') 121 + const fsBtn = el('button', 'dv-toolbar-btn') 122 + fsBtn.title = 'Toggle fullscreen' 123 + fsBtn.appendChild(svgIcon(icons.fullscreen)) 124 + fsBtn.addEventListener('click', actions.onFullscreen) 125 + toolbar.append(sep, fsBtn) 126 + } 127 + 128 + // --- Download --- 129 + if (config.download && actions.onDownload) { 130 + const dlBtn = el('button', 'dv-toolbar-btn') 131 + dlBtn.title = 'Download' 132 + dlBtn.appendChild(svgIcon(icons.download)) 133 + dlBtn.addEventListener('click', actions.onDownload) 134 + toolbar.appendChild(dlBtn) 135 + } 136 + 137 + return { 138 + element: toolbar, 139 + updateState(state: DocViewState) { 140 + if (pageInput) pageInput.value = String(state.currentPage) 141 + if (pageLabel) pageLabel.textContent = `/ ${state.totalPages}` 142 + if (zoomLabel) zoomLabel.textContent = `${Math.round(state.zoom * 100)}%` 143 + if (prevBtn) prevBtn.disabled = state.currentPage <= 1 144 + if (nextBtn) nextBtn.disabled = state.currentPage >= state.totalPages 145 + }, 146 + destroy() { 147 + toolbar.remove() 148 + }, 149 + } 150 + }
+433
packages/core/src/types.ts
··· 1 + // --------------------------------------------------------------------------- 2 + // Document Sources — what consumers pass in to tell DocView what to render 3 + // --------------------------------------------------------------------------- 4 + 5 + /** A direct file provided as binary data. */ 6 + export interface FileSource { 7 + type: 'file' 8 + /** The file content as a Blob, ArrayBuffer, or Uint8Array. */ 9 + data: Blob | ArrayBuffer | Uint8Array 10 + /** MIME type override. If omitted, detected from filename or data. */ 11 + mimeType?: string 12 + /** Original filename, used for format detection and display. */ 13 + filename?: string 14 + } 15 + 16 + /** A URL pointing to a remotely-hosted document. */ 17 + export interface UrlSource { 18 + type: 'url' 19 + /** URL to fetch the document from. */ 20 + url: string 21 + /** MIME type override. If omitted, detected from URL extension or response headers. */ 22 + mimeType?: string 23 + /** Display filename. If omitted, derived from the URL path. */ 24 + filename?: string 25 + /** Custom fetch options (headers, credentials, etc.). */ 26 + fetchOptions?: RequestInit 27 + } 28 + 29 + /** Pre-rendered page images — for browsing without the original document. */ 30 + export interface PagesSource { 31 + type: 'pages' 32 + /** Direct page data array, OR a fetch adapter for lazy loading. */ 33 + pages: PageData[] | PageFetchAdapter 34 + /** Optional text layer data for search and copy/paste over images. */ 35 + textLayer?: TextLayerData[] | TextFetchAdapter 36 + } 37 + 38 + /** 39 + * Chunked document source — for streaming large files piece by piece. 40 + * Can provide PDF chunks (for full-fidelity rendering) and/or browse page 41 + * images (for fast initial display while chunks load). 42 + */ 43 + export interface ChunkedSource { 44 + type: 'chunked' 45 + /** PDF chunks for high-quality streaming, OR a fetch adapter. */ 46 + chunks: ChunkData[] | ChunkFetchAdapter 47 + /** Total page count across all chunks. */ 48 + totalPages: number 49 + /** Optional pre-rendered page images as fast fallback while chunks load. */ 50 + browsePages?: PageData[] | PageFetchAdapter 51 + /** Optional text layer for browse pages. */ 52 + textLayer?: TextLayerData[] | TextFetchAdapter 53 + } 54 + 55 + /** Union of all document source types. */ 56 + export type DocumentSource = FileSource | UrlSource | PagesSource | ChunkedSource 57 + 58 + 59 + // --------------------------------------------------------------------------- 60 + // Page & Chunk Data — individual units of content 61 + // --------------------------------------------------------------------------- 62 + 63 + /** A single pre-rendered page image. */ 64 + export interface PageData { 65 + /** 1-indexed page number. */ 66 + pageNumber: number 67 + /** URL to the page image (mutually exclusive with imageBlob). */ 68 + imageUrl?: string 69 + /** Blob containing the page image (mutually exclusive with imageUrl). */ 70 + imageBlob?: Blob 71 + /** Image width in pixels. */ 72 + width: number 73 + /** Image height in pixels. */ 74 + height: number 75 + } 76 + 77 + /** A chunk of a PDF or other paginated document. */ 78 + export interface ChunkData { 79 + /** Binary data of the chunk (a valid, renderable PDF for PDF chunks). */ 80 + data: ArrayBuffer | Blob 81 + /** First page in this chunk (1-indexed). */ 82 + pageStart: number 83 + /** Last page in this chunk (1-indexed, inclusive). */ 84 + pageEnd: number 85 + } 86 + 87 + /** Extracted text content for a single page, enabling search and copy/paste. */ 88 + export interface TextLayerData { 89 + /** 1-indexed page number this text belongs to. */ 90 + pageNumber: number 91 + /** Array of text items with position information. */ 92 + items: TextItem[] 93 + } 94 + 95 + /** A single text fragment with position data for overlay rendering. */ 96 + export interface TextItem { 97 + /** The text string. */ 98 + str: string 99 + /** X position as fraction of page width (0–1). */ 100 + x: number 101 + /** Y position as fraction of page height (0–1). */ 102 + y: number 103 + /** Width as fraction of page width. */ 104 + width: number 105 + /** Height as fraction of page height. */ 106 + height: number 107 + /** Font size in points (optional). */ 108 + fontSize?: number 109 + } 110 + 111 + 112 + // --------------------------------------------------------------------------- 113 + // Fetch Adapters — for lazy-loading pages and chunks on demand 114 + // --------------------------------------------------------------------------- 115 + 116 + /** Adapter for lazily fetching individual page images. */ 117 + export interface PageFetchAdapter { 118 + /** Total number of pages in the document. */ 119 + totalPages: number 120 + /** Fetch a single page image by 1-indexed page number. */ 121 + fetchPage(pageNumber: number): Promise<PageData> 122 + /** Optional batch fetch for a range of pages (inclusive). */ 123 + fetchRange?(startPage: number, endPage: number): Promise<PageData[]> 124 + /** Optional: called when adapter is no longer needed, to clean up. */ 125 + dispose?(): void 126 + } 127 + 128 + /** Adapter for lazily fetching document chunks (e.g., split PDFs). */ 129 + export interface ChunkFetchAdapter { 130 + /** Total number of chunks. */ 131 + totalChunks: number 132 + /** Total number of pages across all chunks. */ 133 + totalPages: number 134 + /** Fetch a chunk by 0-indexed chunk index. */ 135 + fetchChunk(index: number): Promise<ChunkData> 136 + /** Given a 1-indexed page number, return the chunk index that contains it. */ 137 + getChunkIndexForPage(pageNumber: number): number 138 + /** Optional: called when adapter is no longer needed. */ 139 + dispose?(): void 140 + } 141 + 142 + /** Adapter for lazily fetching text layer data. */ 143 + export interface TextFetchAdapter { 144 + /** Fetch text layer for a single page. */ 145 + fetchPageText(pageNumber: number): Promise<TextLayerData> 146 + /** Optional batch fetch. */ 147 + fetchRange?(startPage: number, endPage: number): Promise<TextLayerData[]> 148 + /** Optional dispose. */ 149 + dispose?(): void 150 + } 151 + 152 + 153 + // --------------------------------------------------------------------------- 154 + // Options — configuration for the DocView instance 155 + // --------------------------------------------------------------------------- 156 + 157 + export interface DocViewOptions { 158 + /** The document to render. */ 159 + source: DocumentSource 160 + /** Explicit format override. If omitted, auto-detected from source. */ 161 + format?: DocumentFormat 162 + /** Color theme. Defaults to 'dark'. */ 163 + theme?: 'light' | 'dark' | 'system' 164 + /** Additional CSS class(es) to add to the root container. */ 165 + className?: string 166 + /** Page to display initially (1-indexed). Defaults to 1. */ 167 + initialPage?: number 168 + /** Initial zoom level. Number = scale factor, string = fit mode. */ 169 + zoom?: number | 'fit-width' | 'fit-page' | 'auto' 170 + 171 + /** Toolbar configuration. `true` = default toolbar, `false` = hidden. */ 172 + toolbar?: boolean | ToolbarConfig 173 + /** Show page number / total in the toolbar or overlay. */ 174 + showPageNumbers?: boolean 175 + 176 + // --- Callbacks --- 177 + 178 + /** Called when the document is loaded and first page is rendered. */ 179 + onReady?: (info: DocumentInfo) => void 180 + /** Called when the visible page changes. */ 181 + onPageChange?: (page: number, totalPages: number) => void 182 + /** Called on unrecoverable errors. */ 183 + onError?: (error: DocViewError) => void 184 + /** Called when zoom level changes. */ 185 + onZoomChange?: (zoom: number) => void 186 + /** Called when loading state changes. */ 187 + onLoadingChange?: (loading: boolean) => void 188 + 189 + // --- Format-specific options --- 190 + 191 + pdf?: PdfOptions 192 + code?: CodeOptions 193 + csv?: CsvOptions 194 + epub?: EpubOptions 195 + odt?: OdtOptions 196 + ods?: OdsOptions 197 + } 198 + 199 + export interface PdfOptions { 200 + /** URL to the pdf.js worker script. If omitted, uses bundled worker or CDN. */ 201 + workerSrc?: string 202 + /** URL to the character map files directory. */ 203 + cMapUrl?: string 204 + /** Whether to render the transparent text layer for selection. Default true. */ 205 + textLayer?: boolean 206 + /** Whether to render the annotation layer. Default false. */ 207 + annotationLayer?: boolean 208 + } 209 + 210 + export interface CodeOptions { 211 + /** Language identifier for syntax highlighting (e.g., 'typescript', 'python'). */ 212 + language?: string 213 + /** Show line numbers. Default true. */ 214 + lineNumbers?: boolean 215 + /** Enable word wrapping. Default false. */ 216 + wordWrap?: boolean 217 + /** Tab size in spaces. Default 2. */ 218 + tabSize?: number 219 + } 220 + 221 + export interface CsvOptions { 222 + /** Field delimiter character. Default auto-detected, falling back to ','. */ 223 + delimiter?: string 224 + /** Whether the first row is a header. Default true. */ 225 + header?: boolean 226 + /** Maximum number of rows to render. Default 10000. */ 227 + maxRows?: number 228 + /** Enable column sorting. Default true. */ 229 + sortable?: boolean 230 + } 231 + 232 + export interface EpubOptions { 233 + /** Flow mode: paginated (book-like) or scrolled. Default 'paginated'. */ 234 + flow?: 'paginated' | 'scrolled' 235 + /** Font size in pixels. Default 16. */ 236 + fontSize?: number 237 + /** Font family override. */ 238 + fontFamily?: string 239 + } 240 + 241 + export interface OdtOptions { 242 + /** Font size in pixels to use as a base. Default 16. */ 243 + fontSize?: number 244 + /** Font family override. */ 245 + fontFamily?: string 246 + } 247 + 248 + export interface OdsOptions { 249 + /** Maximum number of rows to render per sheet. Default 10000. */ 250 + maxRows?: number 251 + /** Enable column sorting. Default true. */ 252 + sortable?: boolean 253 + /** Whether the first row is a header. Default true. */ 254 + header?: boolean 255 + } 256 + 257 + export interface ToolbarConfig { 258 + /** Show page navigation controls. Default true. */ 259 + navigation?: boolean 260 + /** Show zoom controls. Default true. */ 261 + zoom?: boolean 262 + /** Show format/filename display. Default true. */ 263 + info?: boolean 264 + /** Show download button (if source is downloadable). Default false. */ 265 + download?: boolean 266 + /** Show fullscreen toggle. Default true. */ 267 + fullscreen?: boolean 268 + /** Toolbar position. Default 'top'. */ 269 + position?: 'top' | 'bottom' 270 + } 271 + 272 + 273 + // --------------------------------------------------------------------------- 274 + // Document Info & State 275 + // --------------------------------------------------------------------------- 276 + 277 + /** Information about a loaded document, provided via the onReady callback. */ 278 + export interface DocumentInfo { 279 + /** Detected or overridden format. */ 280 + format: DocumentFormat 281 + /** Total number of pages (or 1 for non-paginated formats). */ 282 + pageCount: number 283 + /** Document title if available from metadata. */ 284 + title?: string 285 + /** Document author if available from metadata. */ 286 + author?: string 287 + /** Original filename if known. */ 288 + filename?: string 289 + /** File size in bytes if known. */ 290 + fileSize?: number 291 + } 292 + 293 + /** Current viewer state, queryable from a DocView instance. */ 294 + export interface DocViewState { 295 + /** Whether the document is currently loading. */ 296 + loading: boolean 297 + /** Current error, if any. */ 298 + error: DocViewError | null 299 + /** Current 1-indexed page number. */ 300 + currentPage: number 301 + /** Total page count. */ 302 + totalPages: number 303 + /** Current zoom scale factor. */ 304 + zoom: number 305 + /** Loaded document info (null until onReady fires). */ 306 + documentInfo: DocumentInfo | null 307 + } 308 + 309 + 310 + // --------------------------------------------------------------------------- 311 + // Errors 312 + // --------------------------------------------------------------------------- 313 + 314 + export type DocViewErrorCode = 315 + | 'FORMAT_UNSUPPORTED' 316 + | 'FORMAT_DETECTION_FAILED' 317 + | 'PEER_DEPENDENCY_MISSING' 318 + | 'SOURCE_LOAD_FAILED' 319 + | 'RENDER_FAILED' 320 + | 'CHUNK_LOAD_FAILED' 321 + | 'PAGE_OUT_OF_RANGE' 322 + | 'UNKNOWN' 323 + 324 + export class DocViewError extends Error { 325 + code: DocViewErrorCode 326 + detail?: unknown 327 + 328 + constructor(code: DocViewErrorCode, message: string, detail?: unknown) { 329 + super(message) 330 + this.name = 'DocViewError' 331 + this.code = code 332 + this.detail = detail 333 + } 334 + } 335 + 336 + 337 + // --------------------------------------------------------------------------- 338 + // Format Types 339 + // --------------------------------------------------------------------------- 340 + 341 + export type DocumentFormat = 342 + | 'pdf' 343 + | 'epub' 344 + | 'docx' 345 + | 'odt' 346 + | 'ods' 347 + | 'csv' 348 + | 'tsv' 349 + | 'code' 350 + | 'text' 351 + | 'markdown' 352 + | 'html' 353 + | 'json' 354 + | 'xml' 355 + | 'pages' // pre-rendered page images (no original document) 356 + | 'chunked-pdf' // chunked PDF streaming 357 + | (string & {}) // open union — consumers can register custom formats 358 + 359 + 360 + // --------------------------------------------------------------------------- 361 + // Renderer Interface — implemented by each format renderer 362 + // --------------------------------------------------------------------------- 363 + 364 + /** 365 + * The contract every format renderer must fulfill. Each renderer manages 366 + * its own DOM subtree within the provided container element. The DocView 367 + * orchestrator calls these methods in response to user actions and source 368 + * changes. 369 + */ 370 + export interface Renderer { 371 + /** Unique format identifier this renderer handles. */ 372 + readonly format: DocumentFormat 373 + 374 + /** 375 + * Initialize and render the document into the container. 376 + * Called once after the renderer is created. The container is an empty div 377 + * scoped to this renderer — the renderer owns its entire DOM subtree. 378 + */ 379 + mount(container: HTMLElement, options: DocViewOptions): Promise<void> 380 + 381 + /** 382 + * React to changed options (theme, zoom, etc.) without full re-mount. 383 + * Only the changed fields will be present. 384 + */ 385 + update(changed: Partial<DocViewOptions>): Promise<void> 386 + 387 + /** Navigate to a specific page (1-indexed). */ 388 + goToPage(page: number): void 389 + 390 + /** Get total page count. Returns 1 for non-paginated formats. */ 391 + getPageCount(): number 392 + 393 + /** Get current page number (1-indexed). */ 394 + getCurrentPage(): number 395 + 396 + /** Set zoom level. Accepts a scale factor or fit mode string. */ 397 + setZoom(zoom: number | 'fit-width' | 'fit-page'): void 398 + 399 + /** Get current zoom as a numeric scale factor. */ 400 + getZoom(): number 401 + 402 + /** Perform a text search within the document. Returns match count. */ 403 + search?(query: string): Promise<number> 404 + 405 + /** Navigate to the next/previous search result. */ 406 + nextSearchResult?(direction: 'forward' | 'backward'): void 407 + 408 + /** Clean up all DOM, event listeners, and resources. */ 409 + destroy(): void 410 + } 411 + 412 + /** 413 + * Factory function that creates a renderer instance. Registered in the 414 + * format registry so DocView can instantiate the right renderer for each 415 + * document format. 416 + */ 417 + export type RendererFactory = () => Renderer 418 + 419 + 420 + // --------------------------------------------------------------------------- 421 + // Events (for vanilla JS event-driven usage) 422 + // --------------------------------------------------------------------------- 423 + 424 + export interface DocViewEventMap { 425 + ready: DocumentInfo 426 + pagechange: { page: number; totalPages: number } 427 + zoomchange: { zoom: number } 428 + error: DocViewError 429 + loadingchange: { loading: boolean } 430 + destroy: void 431 + } 432 + 433 + export type DocViewEventType = keyof DocViewEventMap
+305
packages/core/src/utils.ts
··· 1 + import type { DocumentSource, DocumentFormat, DocViewError } from './types.js' 2 + import { DocViewError as DVError } from './types.js' 3 + 4 + // --------------------------------------------------------------------------- 5 + // DOM Helpers 6 + // --------------------------------------------------------------------------- 7 + 8 + /** Create an element with optional class and attributes. */ 9 + export function el<K extends keyof HTMLElementTagNameMap>( 10 + tag: K, 11 + className?: string, 12 + attrs?: Record<string, string>, 13 + ): HTMLElementTagNameMap[K] { 14 + const element = document.createElement(tag) 15 + if (className) element.className = className 16 + if (attrs) { 17 + for (const [k, v] of Object.entries(attrs)) { 18 + element.setAttribute(k, v) 19 + } 20 + } 21 + return element 22 + } 23 + 24 + /** Create an SVG icon from a path string (16x16 viewBox). */ 25 + export function svgIcon(pathD: string): SVGSVGElement { 26 + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') 27 + svg.setAttribute('viewBox', '0 0 16 16') 28 + svg.setAttribute('fill', 'none') 29 + svg.setAttribute('stroke', 'currentColor') 30 + svg.setAttribute('stroke-width', '1.5') 31 + svg.setAttribute('stroke-linecap', 'round') 32 + svg.setAttribute('stroke-linejoin', 'round') 33 + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path') 34 + path.setAttribute('d', pathD) 35 + svg.appendChild(path) 36 + return svg 37 + } 38 + 39 + /** Common SVG icon paths (16x16 coordinate space). */ 40 + export const icons = { 41 + chevronLeft: 'M10 3 L5 8 L10 13', 42 + chevronRight: 'M6 3 L11 8 L6 13', 43 + zoomIn: 'M7.5 3v9M3 7.5h9M12.5 12.5 L15 15', 44 + zoomOut: 'M3 7.5h9M12.5 12.5 L15 15', 45 + fitWidth: 'M1 4h14M1 12h14M4 1v3M4 12v3M12 1v3M12 12v3', 46 + fullscreen: 'M2 5V2h3M11 2h3v3M14 11v3h-3M5 14H2v-3', 47 + download: 'M8 2v8M4 7l4 4 4-4M3 13h10', 48 + } as const 49 + 50 + /** Remove all child nodes from an element. */ 51 + export function clearElement(el: HTMLElement): void { 52 + while (el.firstChild) el.removeChild(el.firstChild) 53 + } 54 + 55 + 56 + // --------------------------------------------------------------------------- 57 + // Format Detection 58 + // --------------------------------------------------------------------------- 59 + 60 + const EXTENSION_MAP: Record<string, DocumentFormat> = { 61 + pdf: 'pdf', 62 + epub: 'epub', 63 + docx: 'docx', 64 + doc: 'docx', 65 + odt: 'odt', 66 + ods: 'ods', 67 + csv: 'csv', 68 + tsv: 'tsv', 69 + txt: 'text', 70 + text: 'text', 71 + md: 'markdown', 72 + markdown: 'markdown', 73 + html: 'html', 74 + htm: 'html', 75 + json: 'json', 76 + xml: 'xml', 77 + svg: 'xml', 78 + // Common code extensions 79 + js: 'code', jsx: 'code', ts: 'code', tsx: 'code', mjs: 'code', cjs: 'code', 80 + py: 'code', rb: 'code', rs: 'code', go: 'code', java: 'code', kt: 'code', 81 + c: 'code', h: 'code', cpp: 'code', hpp: 'code', cc: 'code', 82 + cs: 'code', swift: 'code', m: 'code', 83 + php: 'code', pl: 'code', r: 'code', lua: 'code', zig: 'code', 84 + sh: 'code', bash: 'code', zsh: 'code', fish: 'code', ps1: 'code', 85 + sql: 'code', graphql: 'code', gql: 'code', 86 + yaml: 'code', yml: 'code', toml: 'code', ini: 'code', env: 'code', 87 + dockerfile: 'code', makefile: 'code', 88 + css: 'code', scss: 'code', sass: 'code', less: 'code', 89 + vue: 'code', svelte: 'code', astro: 'code', 90 + hs: 'code', elm: 'code', ex: 'code', exs: 'code', erl: 'code', 91 + clj: 'code', cljs: 'code', lisp: 'code', scm: 'code', 92 + dart: 'code', scala: 'code', groovy: 'code', 93 + proto: 'code', thrift: 'code', 94 + tf: 'code', hcl: 'code', 95 + sol: 'code', move: 'code', 96 + wasm: 'code', wat: 'code', 97 + } 98 + 99 + const MIME_MAP: Record<string, DocumentFormat> = { 100 + 'application/pdf': 'pdf', 101 + 'application/epub+zip': 'epub', 102 + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx', 103 + 'application/msword': 'docx', 104 + 'application/vnd.oasis.opendocument.text': 'odt', 105 + 'application/vnd.oasis.opendocument.spreadsheet': 'ods', 106 + 'text/csv': 'csv', 107 + 'text/tab-separated-values': 'tsv', 108 + 'text/plain': 'text', 109 + 'text/markdown': 'markdown', 110 + 'text/html': 'html', 111 + 'application/json': 'json', 112 + 'application/xml': 'xml', 113 + 'text/xml': 'xml', 114 + 'image/svg+xml': 'xml', 115 + 'application/javascript': 'code', 116 + 'text/javascript': 'code', 117 + 'application/typescript': 'code', 118 + 'text/x-python': 'code', 119 + 'text/x-rust': 'code', 120 + 'text/x-go': 'code', 121 + 'text/x-java-source': 'code', 122 + 'text/x-c': 'code', 123 + 'text/x-c++src': 'code', 124 + 'text/css': 'code', 125 + 'text/x-yaml': 'code', 126 + 'text/x-toml': 'code', 127 + 'text/x-shellscript': 'code', 128 + 'application/x-sh': 'code', 129 + 'text/x-sql': 'code', 130 + } 131 + 132 + /** Map file extension to highlight.js language identifier. */ 133 + const EXTENSION_TO_LANGUAGE: Record<string, string> = { 134 + js: 'javascript', jsx: 'javascript', mjs: 'javascript', cjs: 'javascript', 135 + ts: 'typescript', tsx: 'typescript', 136 + py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java', 137 + kt: 'kotlin', c: 'c', h: 'c', cpp: 'cpp', hpp: 'cpp', cc: 'cpp', 138 + cs: 'csharp', swift: 'swift', m: 'objectivec', 139 + php: 'php', pl: 'perl', r: 'r', lua: 'lua', 140 + sh: 'bash', bash: 'bash', zsh: 'bash', fish: 'bash', ps1: 'powershell', 141 + sql: 'sql', graphql: 'graphql', 142 + yaml: 'yaml', yml: 'yaml', toml: 'toml', ini: 'ini', 143 + dockerfile: 'dockerfile', makefile: 'makefile', 144 + css: 'css', scss: 'scss', sass: 'scss', less: 'less', 145 + html: 'html', htm: 'html', xml: 'xml', svg: 'xml', 146 + json: 'json', md: 'markdown', markdown: 'markdown', 147 + vue: 'html', svelte: 'html', astro: 'html', 148 + hs: 'haskell', elm: 'elm', ex: 'elixir', exs: 'elixir', erl: 'erlang', 149 + clj: 'clojure', cljs: 'clojure', lisp: 'lisp', scm: 'scheme', 150 + dart: 'dart', scala: 'scala', groovy: 'groovy', 151 + proto: 'protobuf', tf: 'hcl', sol: 'solidity', 152 + } 153 + 154 + /** Extract file extension from a filename or URL path. */ 155 + export function getExtension(filenameOrUrl: string): string { 156 + const clean = filenameOrUrl.split('?')[0].split('#')[0] 157 + const lastSlash = clean.lastIndexOf('/') 158 + const basename = lastSlash >= 0 ? clean.slice(lastSlash + 1) : clean 159 + const dot = basename.lastIndexOf('.') 160 + if (dot < 0) return basename.toLowerCase() // no extension, use whole name (e.g., "Makefile") 161 + return basename.slice(dot + 1).toLowerCase() 162 + } 163 + 164 + /** Detect document format from a source descriptor. */ 165 + export function detectFormat(source: DocumentSource): DocumentFormat | null { 166 + // Pages and chunked sources have explicit types 167 + if (source.type === 'pages') return 'pages' 168 + if (source.type === 'chunked') return 'chunked-pdf' 169 + 170 + // Try MIME type first 171 + const mime = 'mimeType' in source ? source.mimeType : undefined 172 + if (mime && MIME_MAP[mime]) return MIME_MAP[mime] 173 + 174 + // Try filename / URL extension 175 + let name: string | undefined 176 + if ('filename' in source) name = source.filename 177 + if (!name && source.type === 'url') name = source.url 178 + 179 + if (name) { 180 + const ext = getExtension(name) 181 + if (EXTENSION_MAP[ext]) return EXTENSION_MAP[ext] 182 + } 183 + 184 + return null 185 + } 186 + 187 + /** Get highlight.js language from file extension. */ 188 + export function getLanguageFromExtension(filename: string): string | undefined { 189 + const ext = getExtension(filename) 190 + return EXTENSION_TO_LANGUAGE[ext] 191 + } 192 + 193 + /** Determine which underlying renderer format to use. Some formats alias to the same renderer. */ 194 + export function getRendererFormat(format: DocumentFormat): DocumentFormat { 195 + switch (format) { 196 + case 'markdown': 197 + case 'html': 198 + case 'json': 199 + case 'xml': 200 + // These are rendered as code with language-specific highlighting 201 + return 'code' 202 + case 'tsv': 203 + return 'csv' 204 + case 'chunked-pdf': 205 + return 'chunked-pdf' 206 + case 'pages': 207 + return 'pages' 208 + default: 209 + return format 210 + } 211 + } 212 + 213 + 214 + // --------------------------------------------------------------------------- 215 + // Data Conversion 216 + // --------------------------------------------------------------------------- 217 + 218 + /** Convert various binary types to ArrayBuffer. */ 219 + export async function toArrayBuffer( 220 + data: Blob | ArrayBuffer | Uint8Array, 221 + ): Promise<ArrayBuffer> { 222 + if (data instanceof ArrayBuffer) return data 223 + if (data instanceof Uint8Array) return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer 224 + return data.arrayBuffer() 225 + } 226 + 227 + /** Convert various binary types to Blob. */ 228 + export function toBlob( 229 + data: Blob | ArrayBuffer | Uint8Array, 230 + mimeType = 'application/octet-stream', 231 + ): Blob { 232 + if (data instanceof Blob) return data 233 + return new Blob([data as BlobPart], { type: mimeType }) 234 + } 235 + 236 + /** Read binary data as UTF-8 text. */ 237 + export async function toText(data: Blob | ArrayBuffer | Uint8Array): Promise<string> { 238 + if (data instanceof Blob) return data.text() 239 + const decoder = new TextDecoder('utf-8') 240 + return decoder.decode(data instanceof ArrayBuffer ? data : data.buffer) 241 + } 242 + 243 + /** Fetch a URL as ArrayBuffer with optional custom fetch options. */ 244 + export async function fetchAsBuffer( 245 + url: string, 246 + options?: RequestInit, 247 + ): Promise<ArrayBuffer> { 248 + const response = await fetch(url, options) 249 + if (!response.ok) { 250 + throw new DVError( 251 + 'SOURCE_LOAD_FAILED', 252 + `Failed to fetch ${url}: ${response.status} ${response.statusText}`, 253 + ) 254 + } 255 + return response.arrayBuffer() 256 + } 257 + 258 + 259 + // --------------------------------------------------------------------------- 260 + // Peer Dependency Loading 261 + // --------------------------------------------------------------------------- 262 + 263 + /** 264 + * Attempt a dynamic import and throw a helpful error if the module is missing. 265 + * Each renderer calls this for its peer dependency. 266 + */ 267 + export async function requirePeerDep<T>( 268 + moduleName: string, 269 + formatName: string, 270 + ): Promise<T> { 271 + try { 272 + const mod = await import(/* @vite-ignore */ moduleName) 273 + return mod as T 274 + } catch { 275 + throw new DVError( 276 + 'PEER_DEPENDENCY_MISSING', 277 + `The "${moduleName}" package is required to render ${formatName} files. ` + 278 + `Install it with: npm install ${moduleName}`, 279 + ) 280 + } 281 + } 282 + 283 + 284 + // --------------------------------------------------------------------------- 285 + // Misc 286 + // --------------------------------------------------------------------------- 287 + 288 + /** Clamp a number between min and max. */ 289 + export function clamp(value: number, min: number, max: number): number { 290 + return Math.max(min, Math.min(max, value)) 291 + } 292 + 293 + /** Debounce a function. */ 294 + export function debounce<T extends (...args: unknown[]) => void>( 295 + fn: T, 296 + ms: number, 297 + ): T & { cancel(): void } { 298 + let timer: ReturnType<typeof setTimeout> 299 + const debounced = ((...args: unknown[]) => { 300 + clearTimeout(timer) 301 + timer = setTimeout(() => fn(...args), ms) 302 + }) as T & { cancel(): void } 303 + debounced.cancel = () => clearTimeout(timer) 304 + return debounced 305 + }
+8
packages/core/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "outDir": "./dist", 5 + "rootDir": "./src" 6 + }, 7 + "include": ["src/**/*"] 8 + }
+21
packages/core/tsup.config.ts
··· 1 + import { defineConfig } from 'tsup' 2 + 3 + export default defineConfig({ 4 + entry: ['src/index.ts'], 5 + format: ['esm', 'cjs'], 6 + dts: true, 7 + sourcemap: true, 8 + clean: true, 9 + external: [ 10 + 'pdfjs-dist', 11 + 'epubjs', 12 + 'docx-preview', 13 + 'papaparse', 14 + 'highlight.js', 15 + 'jszip', 16 + 'xlsx', 17 + ], 18 + esbuildOptions(options) { 19 + options.loader = { ...options.loader, '.css': 'copy' } 20 + }, 21 + })
+20
packages/react/LICENSE
··· 1 + Copyright (c) 2026 Aria Quinlan 2 + 3 + This software is provided ‘as-is’, without any express or implied 4 + warranty. In no event will the authors be held liable for any damages 5 + arising from the use of this software. 6 + 7 + Permission is granted to anyone to use this software for any purpose, 8 + including commercial applications, and to alter it and redistribute it 9 + freely, subject to the following restrictions: 10 + 11 + 1. The origin of this software must not be misrepresented; you must not 12 + claim that you wrote the original software. If you use this software 13 + in a product, an acknowledgment in the product documentation would be 14 + appreciated but is not required. 15 + 16 + 2. Altered source versions must be plainly marked as such, and must not be 17 + misrepresented as being the original software. 18 + 19 + 3. This notice may not be removed or altered from any source 20 + distribution.
+57
packages/react/package.json
··· 1 + { 2 + "name": "@docview/react", 3 + "version": "0.1.0", 4 + "description": "React components for the DocView universal document renderer", 5 + "license": "Zlib", 6 + "type": "module", 7 + "main": "./dist/index.cjs", 8 + "module": "./dist/index.js", 9 + "types": "./dist/index.d.ts", 10 + "exports": { 11 + ".": { 12 + "import": "./dist/index.js", 13 + "require": "./dist/index.cjs", 14 + "types": "./dist/index.d.ts" 15 + } 16 + }, 17 + "files": [ 18 + "dist", 19 + "README.md", 20 + "LICENSE" 21 + ], 22 + "scripts": { 23 + "build": "tsup", 24 + "dev": "tsup --watch", 25 + "clean": "rm -rf dist", 26 + "typecheck": "tsc --noEmit" 27 + }, 28 + "dependencies": { 29 + "@docview/core": "workspace:*" 30 + }, 31 + "peerDependencies": { 32 + "react": ">=18.0.0", 33 + "react-dom": ">=18.0.0" 34 + }, 35 + "devDependencies": { 36 + "@types/react": "^18.3.0", 37 + "@types/react-dom": "^18.3.0", 38 + "react": "^18.3.0", 39 + "react-dom": "^18.3.0", 40 + "tsup": "^8.0.0", 41 + "typescript": "^5.5.0" 42 + }, 43 + "keywords": [ 44 + "react", 45 + "pdf", 46 + "epub", 47 + "docx", 48 + "csv", 49 + "odt", 50 + "document", 51 + "viewer", 52 + "component", 53 + "renderer", 54 + "reader", 55 + "chunked" 56 + ] 57 + }
+191
packages/react/src/DocumentViewer.tsx
··· 1 + import { forwardRef, useImperativeHandle, useMemo } from 'react' 2 + import type { 3 + DocumentSource, 4 + DocumentFormat, 5 + DocumentInfo, 6 + DocViewError, 7 + ToolbarConfig, 8 + PdfOptions, 9 + CodeOptions, 10 + CsvOptions, 11 + EpubOptions, 12 + } from '@docview/core' 13 + import { useDocumentRenderer } from './useDocumentRenderer.js' 14 + 15 + export interface DocumentViewerProps { 16 + /** The document to render. Pass null/undefined to show nothing. */ 17 + source: DocumentSource | null | undefined 18 + 19 + /** Explicit format override. If omitted, auto-detected from source. */ 20 + format?: DocumentFormat 21 + 22 + /** Color theme. Defaults to 'dark'. */ 23 + theme?: 'light' | 'dark' | 'system' 24 + 25 + /** Additional CSS class(es) for the root container. */ 26 + className?: string 27 + 28 + /** Inline styles for the outer wrapper div (set width/height here). */ 29 + style?: React.CSSProperties 30 + 31 + /** Page to display initially (1-indexed). Defaults to 1. */ 32 + initialPage?: number 33 + 34 + /** Initial zoom level. */ 35 + zoom?: number | 'fit-width' | 'fit-page' | 'auto' 36 + 37 + /** Toolbar configuration. true = default, false = hidden. */ 38 + toolbar?: boolean | ToolbarConfig 39 + 40 + /** Show page numbers. */ 41 + showPageNumbers?: boolean 42 + 43 + // --- Callbacks --- 44 + onReady?: (info: DocumentInfo) => void 45 + onPageChange?: (page: number, totalPages: number) => void 46 + onZoomChange?: (zoom: number) => void 47 + onError?: (error: DocViewError) => void 48 + onLoadingChange?: (loading: boolean) => void 49 + 50 + // --- Format-specific options --- 51 + pdf?: PdfOptions 52 + code?: CodeOptions 53 + csv?: CsvOptions 54 + epub?: EpubOptions 55 + } 56 + 57 + export interface DocumentViewerRef { 58 + /** Navigate to a specific page (1-indexed). */ 59 + goToPage: (page: number) => void 60 + /** Set zoom level. */ 61 + setZoom: (zoom: number | 'fit-width' | 'fit-page') => void 62 + /** Get current page number. */ 63 + getCurrentPage: () => number 64 + /** Get total page count. */ 65 + getPageCount: () => number 66 + /** Get current zoom level. */ 67 + getZoom: () => number 68 + } 69 + 70 + /** 71 + * React component for rendering documents of any supported format. 72 + * 73 + * Wraps the framework-agnostic `@docview/core` library with React lifecycle 74 + * management, ref-based imperative API, and automatic cleanup. 75 + * 76 + * @example 77 + * ```tsx 78 + * import { DocumentViewer } from '@docview/react' 79 + * import '@docview/core/styles.css' 80 + * 81 + * function App() { 82 + * return ( 83 + * <DocumentViewer 84 + * source={{ type: 'url', url: '/report.pdf' }} 85 + * theme="dark" 86 + * style={{ width: '100%', height: '80vh' }} 87 + * onReady={(info) => console.log(`Loaded ${info.pageCount} pages`)} 88 + * /> 89 + * ) 90 + * } 91 + * ``` 92 + * 93 + * @example Chunked / pre-rendered pages 94 + * ```tsx 95 + * <DocumentViewer 96 + * source={{ 97 + * type: 'pages', 98 + * pages: { 99 + * totalPages: 200, 100 + * fetchPage: async (n) => ({ 101 + * pageNumber: n, 102 + * imageUrl: `/api/pages/${n}.webp`, 103 + * width: 1654, 104 + * height: 2339, 105 + * }), 106 + * }, 107 + * }} 108 + * /> 109 + * ``` 110 + * 111 + * @example Imperative control via ref 112 + * ```tsx 113 + * const viewerRef = useRef<DocumentViewerRef>(null) 114 + * 115 + * <DocumentViewer ref={viewerRef} source={source} /> 116 + * <button onClick={() => viewerRef.current?.goToPage(10)}>Go to page 10</button> 117 + * ``` 118 + */ 119 + export const DocumentViewer = forwardRef<DocumentViewerRef, DocumentViewerProps>( 120 + function DocumentViewer(props, ref) { 121 + const { 122 + source, 123 + format, 124 + theme, 125 + className, 126 + style, 127 + initialPage, 128 + zoom, 129 + toolbar, 130 + showPageNumbers, 131 + onReady, 132 + onPageChange, 133 + onZoomChange, 134 + onError, 135 + onLoadingChange, 136 + pdf, 137 + code, 138 + csv, 139 + epub, 140 + } = props 141 + 142 + const { 143 + containerRef, 144 + state, 145 + goToPage, 146 + setZoom, 147 + } = useDocumentRenderer({ 148 + source: source ?? undefined, 149 + format, 150 + theme, 151 + className, 152 + initialPage, 153 + zoom, 154 + toolbar, 155 + showPageNumbers, 156 + onReady, 157 + onPageChange, 158 + onZoomChange, 159 + onError, 160 + onLoadingChange, 161 + pdf, 162 + code, 163 + csv, 164 + epub, 165 + }) 166 + 167 + // Expose imperative API via ref 168 + useImperativeHandle(ref, () => ({ 169 + goToPage, 170 + setZoom, 171 + getCurrentPage: () => state.currentPage, 172 + getPageCount: () => state.totalPages, 173 + getZoom: () => state.zoom, 174 + }), [goToPage, setZoom, state.currentPage, state.totalPages, state.zoom]) 175 + 176 + const containerStyle = useMemo<React.CSSProperties>(() => ({ 177 + width: '100%', 178 + height: '100%', 179 + minHeight: 200, 180 + ...style, 181 + }), [style]) 182 + 183 + return ( 184 + <div 185 + ref={containerRef as any} 186 + style={containerStyle} 187 + data-docview-wrapper="" 188 + /> 189 + ) 190 + }, 191 + )
+38
packages/react/src/index.ts
··· 1 + // Components 2 + export { DocumentViewer } from './DocumentViewer.js' 3 + export type { DocumentViewerProps, DocumentViewerRef } from './DocumentViewer.js' 4 + 5 + // Hook 6 + export { useDocumentRenderer } from './useDocumentRenderer.js' 7 + export type { UseDocumentRendererOptions, UseDocumentRendererReturn } from './useDocumentRenderer.js' 8 + 9 + // Re-export core types for convenience (so consumers don't need to 10 + // install @docview/core separately just for types) 11 + export type { 12 + DocumentSource, 13 + FileSource, 14 + UrlSource, 15 + PagesSource, 16 + ChunkedSource, 17 + PageData, 18 + ChunkData, 19 + TextLayerData, 20 + TextItem, 21 + PageFetchAdapter, 22 + ChunkFetchAdapter, 23 + TextFetchAdapter, 24 + DocViewOptions, 25 + PdfOptions, 26 + CodeOptions, 27 + CsvOptions, 28 + EpubOptions, 29 + ToolbarConfig, 30 + DocumentInfo, 31 + DocViewState, 32 + DocumentFormat, 33 + DocViewEventMap, 34 + DocViewEventType, 35 + DocViewErrorCode, 36 + } from '@docview/core' 37 + 38 + export { DocViewError, DocView } from '@docview/core'
+168
packages/react/src/useDocumentRenderer.ts
··· 1 + import { useRef, useEffect, useState, useCallback } from 'react' 2 + import type { 3 + DocViewOptions, 4 + DocViewState, 5 + DocumentInfo, 6 + DocViewError, 7 + DocumentSource, 8 + } from '@docview/core' 9 + import { DocView } from '@docview/core' 10 + 11 + export interface UseDocumentRendererOptions 12 + extends Omit<DocViewOptions, 'source' | 'onReady' | 'onPageChange' | 'onZoomChange' | 'onError' | 'onLoadingChange'> { 13 + source: DocumentSource | null | undefined 14 + onReady?: (info: DocumentInfo) => void 15 + onPageChange?: (page: number, totalPages: number) => void 16 + onZoomChange?: (zoom: number) => void 17 + onError?: (error: DocViewError) => void 18 + onLoadingChange?: (loading: boolean) => void 19 + } 20 + 21 + export interface UseDocumentRendererReturn { 22 + /** Ref to attach to the container div. */ 23 + containerRef: React.RefObject<HTMLDivElement | null> 24 + /** Current viewer state. */ 25 + state: DocViewState 26 + /** Navigate to a page. */ 27 + goToPage: (page: number) => void 28 + /** Set zoom level. */ 29 + setZoom: (zoom: number | 'fit-width' | 'fit-page') => void 30 + /** Whether the viewer is mounted and ready. */ 31 + ready: boolean 32 + /** Current error, if any. */ 33 + error: DocViewError | null 34 + } 35 + 36 + /** 37 + * React hook for the DocView document renderer. 38 + * 39 + * Manages the lifecycle of a DocView instance, bridging its imperative API 40 + * to React's declarative model. Handles mounting, updating, and cleanup. 41 + * 42 + * @example 43 + * ```tsx 44 + * function MyViewer({ url }: { url: string }) { 45 + * const { containerRef, state, goToPage } = useDocumentRenderer({ 46 + * source: { type: 'url', url }, 47 + * theme: 'dark', 48 + * }) 49 + * 50 + * return ( 51 + * <div> 52 + * <div ref={containerRef} style={{ width: '100%', height: '600px' }} /> 53 + * <p>Page {state.currentPage} of {state.totalPages}</p> 54 + * </div> 55 + * ) 56 + * } 57 + * ``` 58 + */ 59 + export function useDocumentRenderer( 60 + options: UseDocumentRendererOptions, 61 + ): UseDocumentRendererReturn { 62 + const containerRef = useRef<HTMLDivElement | null>(null) 63 + const instanceRef = useRef<DocView | null>(null) 64 + const optionsRef = useRef(options) 65 + 66 + const [state, setState] = useState<DocViewState>({ 67 + loading: true, 68 + error: null, 69 + currentPage: 1, 70 + totalPages: 0, 71 + zoom: 1, 72 + documentInfo: null, 73 + }) 74 + 75 + const [ready, setReady] = useState(false) 76 + const [error, setError] = useState<DocViewError | null>(null) 77 + 78 + // Keep options ref current 79 + optionsRef.current = options 80 + 81 + // Mount / unmount effect 82 + useEffect(() => { 83 + const container = containerRef.current 84 + if (!container || !options.source) return 85 + 86 + // Clear any previous instance 87 + if (instanceRef.current) { 88 + instanceRef.current.destroy() 89 + instanceRef.current = null 90 + } 91 + 92 + setReady(false) 93 + setError(null) 94 + setState((s) => ({ ...s, loading: true, error: null })) 95 + 96 + const instance = new DocView(container, { 97 + ...options, 98 + source: options.source, 99 + onReady: (info) => { 100 + setReady(true) 101 + setState((s) => ({ 102 + ...s, 103 + loading: false, 104 + totalPages: info.pageCount, 105 + documentInfo: info, 106 + })) 107 + optionsRef.current.onReady?.(info) 108 + }, 109 + onPageChange: (page, totalPages) => { 110 + setState((s) => ({ ...s, currentPage: page, totalPages })) 111 + optionsRef.current.onPageChange?.(page, totalPages) 112 + }, 113 + onZoomChange: (zoom) => { 114 + setState((s) => ({ ...s, zoom })) 115 + optionsRef.current.onZoomChange?.(zoom) 116 + }, 117 + onError: (err) => { 118 + setError(err) 119 + setState((s) => ({ ...s, loading: false, error: err })) 120 + optionsRef.current.onError?.(err) 121 + }, 122 + onLoadingChange: (loading) => { 123 + setState((s) => ({ ...s, loading })) 124 + optionsRef.current.onLoadingChange?.(loading) 125 + }, 126 + }) 127 + 128 + instanceRef.current = instance 129 + 130 + return () => { 131 + instance.destroy() 132 + instanceRef.current = null 133 + } 134 + // Re-mount when source identity changes 135 + // eslint-disable-next-line react-hooks/exhaustive-deps 136 + }, [options.source, options.format, options.theme]) 137 + 138 + // Update non-source options without re-mounting 139 + useEffect(() => { 140 + if (!instanceRef.current) return 141 + 142 + const changed: Partial<DocViewOptions> = {} 143 + if (options.theme) changed.theme = options.theme 144 + if (options.className !== undefined) changed.className = options.className 145 + if (options.zoom !== undefined) changed.zoom = options.zoom 146 + 147 + if (Object.keys(changed).length > 0) { 148 + instanceRef.current.update(changed) 149 + } 150 + }, [options.theme, options.className, options.zoom]) 151 + 152 + const goToPage = useCallback((page: number) => { 153 + instanceRef.current?.goToPage(page) 154 + }, []) 155 + 156 + const setZoom = useCallback((zoom: number | 'fit-width' | 'fit-page') => { 157 + instanceRef.current?.setZoom(zoom) 158 + }, []) 159 + 160 + return { 161 + containerRef, 162 + state, 163 + goToPage, 164 + setZoom, 165 + ready, 166 + error, 167 + } 168 + }
+9
packages/react/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.base.json", 3 + "compilerOptions": { 4 + "outDir": "./dist", 5 + "rootDir": "./src", 6 + "jsx": "react-jsx" 7 + }, 8 + "include": ["src/**/*"] 9 + }
+13
packages/react/tsup.config.ts
··· 1 + import { defineConfig } from 'tsup' 2 + 3 + export default defineConfig({ 4 + entry: ['src/index.ts'], 5 + format: ['esm', 'cjs'], 6 + dts: true, 7 + sourcemap: true, 8 + clean: true, 9 + external: ['react', 'react-dom', '@docview/core'], 10 + esbuildOptions(options) { 11 + options.jsx = 'automatic' 12 + }, 13 + })
+1890
pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + devDependencies: 11 + pdfjs-dist: 12 + specifier: ^5.5.207 13 + version: 5.5.207 14 + typescript: 15 + specifier: ^5.5.0 16 + version: 5.9.3 17 + 18 + examples/basic: 19 + dependencies: 20 + '@docview/core': 21 + specifier: workspace:* 22 + version: link:../../packages/core 23 + docx-preview: 24 + specifier: '>=0.3.0' 25 + version: 0.3.7 26 + epubjs: 27 + specifier: '>=0.3.0' 28 + version: 0.3.93 29 + highlight.js: 30 + specifier: '>=11.0.0' 31 + version: 11.11.1 32 + jszip: 33 + specifier: '>=3.0.0' 34 + version: 3.10.1 35 + papaparse: 36 + specifier: '>=5.0.0' 37 + version: 5.5.3 38 + pdfjs-dist: 39 + specifier: '>=4.0.0' 40 + version: 5.5.207 41 + xlsx: 42 + specifier: '>=0.18.0' 43 + version: 0.18.5 44 + devDependencies: 45 + typescript: 46 + specifier: ^5.5.0 47 + version: 5.9.3 48 + vite: 49 + specifier: ^6.0.0 50 + version: 6.4.1 51 + 52 + examples/vanilla: 53 + dependencies: 54 + '@docview/core': 55 + specifier: workspace:* 56 + version: link:../../packages/core 57 + docx-preview: 58 + specifier: '>=0.3.0' 59 + version: 0.3.7 60 + epubjs: 61 + specifier: '>=0.3.0' 62 + version: 0.3.93 63 + highlight.js: 64 + specifier: '>=11.0.0' 65 + version: 11.11.1 66 + jszip: 67 + specifier: '>=3.0.0' 68 + version: 3.10.1 69 + papaparse: 70 + specifier: '>=5.0.0' 71 + version: 5.5.3 72 + pdfjs-dist: 73 + specifier: '>=4.0.0' 74 + version: 5.5.207 75 + xlsx: 76 + specifier: '>=0.18.0' 77 + version: 0.18.5 78 + devDependencies: 79 + esbuild: 80 + specifier: ^0.25.0 81 + version: 0.25.12 82 + typescript: 83 + specifier: ^5.5.0 84 + version: 5.9.3 85 + 86 + packages/core: 87 + dependencies: 88 + docx-preview: 89 + specifier: '>=0.3.0' 90 + version: 0.3.7 91 + epubjs: 92 + specifier: '>=0.3.0' 93 + version: 0.3.93 94 + highlight.js: 95 + specifier: '>=11.0.0' 96 + version: 11.11.1 97 + jszip: 98 + specifier: '>=3.0.0' 99 + version: 3.10.1 100 + papaparse: 101 + specifier: '>=5.0.0' 102 + version: 5.5.3 103 + pdfjs-dist: 104 + specifier: '>=4.0.0' 105 + version: 5.5.207 106 + xlsx: 107 + specifier: '>=0.18.0' 108 + version: 0.18.5 109 + devDependencies: 110 + tsup: 111 + specifier: ^8.0.0 112 + version: 8.5.1(postcss@8.5.8)(typescript@5.9.3) 113 + typescript: 114 + specifier: ^5.5.0 115 + version: 5.9.3 116 + 117 + packages/react: 118 + dependencies: 119 + '@docview/core': 120 + specifier: workspace:* 121 + version: link:../core 122 + devDependencies: 123 + '@types/react': 124 + specifier: ^18.3.0 125 + version: 18.3.28 126 + '@types/react-dom': 127 + specifier: ^18.3.0 128 + version: 18.3.7(@types/react@18.3.28) 129 + react: 130 + specifier: ^18.3.0 131 + version: 18.3.1 132 + react-dom: 133 + specifier: ^18.3.0 134 + version: 18.3.1(react@18.3.1) 135 + tsup: 136 + specifier: ^8.0.0 137 + version: 8.5.1(postcss@8.5.8)(typescript@5.9.3) 138 + typescript: 139 + specifier: ^5.5.0 140 + version: 5.9.3 141 + 142 + packages: 143 + 144 + '@esbuild/aix-ppc64@0.25.12': 145 + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} 146 + engines: {node: '>=18'} 147 + cpu: [ppc64] 148 + os: [aix] 149 + 150 + '@esbuild/aix-ppc64@0.27.3': 151 + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} 152 + engines: {node: '>=18'} 153 + cpu: [ppc64] 154 + os: [aix] 155 + 156 + '@esbuild/android-arm64@0.25.12': 157 + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} 158 + engines: {node: '>=18'} 159 + cpu: [arm64] 160 + os: [android] 161 + 162 + '@esbuild/android-arm64@0.27.3': 163 + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} 164 + engines: {node: '>=18'} 165 + cpu: [arm64] 166 + os: [android] 167 + 168 + '@esbuild/android-arm@0.25.12': 169 + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} 170 + engines: {node: '>=18'} 171 + cpu: [arm] 172 + os: [android] 173 + 174 + '@esbuild/android-arm@0.27.3': 175 + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} 176 + engines: {node: '>=18'} 177 + cpu: [arm] 178 + os: [android] 179 + 180 + '@esbuild/android-x64@0.25.12': 181 + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} 182 + engines: {node: '>=18'} 183 + cpu: [x64] 184 + os: [android] 185 + 186 + '@esbuild/android-x64@0.27.3': 187 + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} 188 + engines: {node: '>=18'} 189 + cpu: [x64] 190 + os: [android] 191 + 192 + '@esbuild/darwin-arm64@0.25.12': 193 + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} 194 + engines: {node: '>=18'} 195 + cpu: [arm64] 196 + os: [darwin] 197 + 198 + '@esbuild/darwin-arm64@0.27.3': 199 + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} 200 + engines: {node: '>=18'} 201 + cpu: [arm64] 202 + os: [darwin] 203 + 204 + '@esbuild/darwin-x64@0.25.12': 205 + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} 206 + engines: {node: '>=18'} 207 + cpu: [x64] 208 + os: [darwin] 209 + 210 + '@esbuild/darwin-x64@0.27.3': 211 + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} 212 + engines: {node: '>=18'} 213 + cpu: [x64] 214 + os: [darwin] 215 + 216 + '@esbuild/freebsd-arm64@0.25.12': 217 + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} 218 + engines: {node: '>=18'} 219 + cpu: [arm64] 220 + os: [freebsd] 221 + 222 + '@esbuild/freebsd-arm64@0.27.3': 223 + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} 224 + engines: {node: '>=18'} 225 + cpu: [arm64] 226 + os: [freebsd] 227 + 228 + '@esbuild/freebsd-x64@0.25.12': 229 + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} 230 + engines: {node: '>=18'} 231 + cpu: [x64] 232 + os: [freebsd] 233 + 234 + '@esbuild/freebsd-x64@0.27.3': 235 + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} 236 + engines: {node: '>=18'} 237 + cpu: [x64] 238 + os: [freebsd] 239 + 240 + '@esbuild/linux-arm64@0.25.12': 241 + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} 242 + engines: {node: '>=18'} 243 + cpu: [arm64] 244 + os: [linux] 245 + 246 + '@esbuild/linux-arm64@0.27.3': 247 + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} 248 + engines: {node: '>=18'} 249 + cpu: [arm64] 250 + os: [linux] 251 + 252 + '@esbuild/linux-arm@0.25.12': 253 + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} 254 + engines: {node: '>=18'} 255 + cpu: [arm] 256 + os: [linux] 257 + 258 + '@esbuild/linux-arm@0.27.3': 259 + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} 260 + engines: {node: '>=18'} 261 + cpu: [arm] 262 + os: [linux] 263 + 264 + '@esbuild/linux-ia32@0.25.12': 265 + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} 266 + engines: {node: '>=18'} 267 + cpu: [ia32] 268 + os: [linux] 269 + 270 + '@esbuild/linux-ia32@0.27.3': 271 + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} 272 + engines: {node: '>=18'} 273 + cpu: [ia32] 274 + os: [linux] 275 + 276 + '@esbuild/linux-loong64@0.25.12': 277 + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} 278 + engines: {node: '>=18'} 279 + cpu: [loong64] 280 + os: [linux] 281 + 282 + '@esbuild/linux-loong64@0.27.3': 283 + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} 284 + engines: {node: '>=18'} 285 + cpu: [loong64] 286 + os: [linux] 287 + 288 + '@esbuild/linux-mips64el@0.25.12': 289 + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} 290 + engines: {node: '>=18'} 291 + cpu: [mips64el] 292 + os: [linux] 293 + 294 + '@esbuild/linux-mips64el@0.27.3': 295 + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} 296 + engines: {node: '>=18'} 297 + cpu: [mips64el] 298 + os: [linux] 299 + 300 + '@esbuild/linux-ppc64@0.25.12': 301 + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} 302 + engines: {node: '>=18'} 303 + cpu: [ppc64] 304 + os: [linux] 305 + 306 + '@esbuild/linux-ppc64@0.27.3': 307 + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} 308 + engines: {node: '>=18'} 309 + cpu: [ppc64] 310 + os: [linux] 311 + 312 + '@esbuild/linux-riscv64@0.25.12': 313 + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} 314 + engines: {node: '>=18'} 315 + cpu: [riscv64] 316 + os: [linux] 317 + 318 + '@esbuild/linux-riscv64@0.27.3': 319 + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} 320 + engines: {node: '>=18'} 321 + cpu: [riscv64] 322 + os: [linux] 323 + 324 + '@esbuild/linux-s390x@0.25.12': 325 + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} 326 + engines: {node: '>=18'} 327 + cpu: [s390x] 328 + os: [linux] 329 + 330 + '@esbuild/linux-s390x@0.27.3': 331 + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} 332 + engines: {node: '>=18'} 333 + cpu: [s390x] 334 + os: [linux] 335 + 336 + '@esbuild/linux-x64@0.25.12': 337 + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} 338 + engines: {node: '>=18'} 339 + cpu: [x64] 340 + os: [linux] 341 + 342 + '@esbuild/linux-x64@0.27.3': 343 + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} 344 + engines: {node: '>=18'} 345 + cpu: [x64] 346 + os: [linux] 347 + 348 + '@esbuild/netbsd-arm64@0.25.12': 349 + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} 350 + engines: {node: '>=18'} 351 + cpu: [arm64] 352 + os: [netbsd] 353 + 354 + '@esbuild/netbsd-arm64@0.27.3': 355 + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} 356 + engines: {node: '>=18'} 357 + cpu: [arm64] 358 + os: [netbsd] 359 + 360 + '@esbuild/netbsd-x64@0.25.12': 361 + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} 362 + engines: {node: '>=18'} 363 + cpu: [x64] 364 + os: [netbsd] 365 + 366 + '@esbuild/netbsd-x64@0.27.3': 367 + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} 368 + engines: {node: '>=18'} 369 + cpu: [x64] 370 + os: [netbsd] 371 + 372 + '@esbuild/openbsd-arm64@0.25.12': 373 + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} 374 + engines: {node: '>=18'} 375 + cpu: [arm64] 376 + os: [openbsd] 377 + 378 + '@esbuild/openbsd-arm64@0.27.3': 379 + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} 380 + engines: {node: '>=18'} 381 + cpu: [arm64] 382 + os: [openbsd] 383 + 384 + '@esbuild/openbsd-x64@0.25.12': 385 + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} 386 + engines: {node: '>=18'} 387 + cpu: [x64] 388 + os: [openbsd] 389 + 390 + '@esbuild/openbsd-x64@0.27.3': 391 + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} 392 + engines: {node: '>=18'} 393 + cpu: [x64] 394 + os: [openbsd] 395 + 396 + '@esbuild/openharmony-arm64@0.25.12': 397 + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} 398 + engines: {node: '>=18'} 399 + cpu: [arm64] 400 + os: [openharmony] 401 + 402 + '@esbuild/openharmony-arm64@0.27.3': 403 + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} 404 + engines: {node: '>=18'} 405 + cpu: [arm64] 406 + os: [openharmony] 407 + 408 + '@esbuild/sunos-x64@0.25.12': 409 + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} 410 + engines: {node: '>=18'} 411 + cpu: [x64] 412 + os: [sunos] 413 + 414 + '@esbuild/sunos-x64@0.27.3': 415 + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} 416 + engines: {node: '>=18'} 417 + cpu: [x64] 418 + os: [sunos] 419 + 420 + '@esbuild/win32-arm64@0.25.12': 421 + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} 422 + engines: {node: '>=18'} 423 + cpu: [arm64] 424 + os: [win32] 425 + 426 + '@esbuild/win32-arm64@0.27.3': 427 + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} 428 + engines: {node: '>=18'} 429 + cpu: [arm64] 430 + os: [win32] 431 + 432 + '@esbuild/win32-ia32@0.25.12': 433 + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} 434 + engines: {node: '>=18'} 435 + cpu: [ia32] 436 + os: [win32] 437 + 438 + '@esbuild/win32-ia32@0.27.3': 439 + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} 440 + engines: {node: '>=18'} 441 + cpu: [ia32] 442 + os: [win32] 443 + 444 + '@esbuild/win32-x64@0.25.12': 445 + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} 446 + engines: {node: '>=18'} 447 + cpu: [x64] 448 + os: [win32] 449 + 450 + '@esbuild/win32-x64@0.27.3': 451 + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} 452 + engines: {node: '>=18'} 453 + cpu: [x64] 454 + os: [win32] 455 + 456 + '@jridgewell/gen-mapping@0.3.13': 457 + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} 458 + 459 + '@jridgewell/resolve-uri@3.1.2': 460 + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 461 + engines: {node: '>=6.0.0'} 462 + 463 + '@jridgewell/sourcemap-codec@1.5.5': 464 + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} 465 + 466 + '@jridgewell/trace-mapping@0.3.31': 467 + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} 468 + 469 + '@napi-rs/canvas-android-arm64@0.1.96': 470 + resolution: {integrity: sha512-ew1sPrN3dGdZ3L4FoohPfnjq0f9/Jk7o+wP7HkQZokcXgIUD6FIyICEWGhMYzv53j63wUcPvZeAwgewX58/egg==} 471 + engines: {node: '>= 10'} 472 + cpu: [arm64] 473 + os: [android] 474 + 475 + '@napi-rs/canvas-darwin-arm64@0.1.96': 476 + resolution: {integrity: sha512-Q/wOXZ5PzTqpdmA5eUOcegCf4Go/zz3aZ5DlzSeDpOjFmfwMKh8EzLAoweQ+mJVagcHQyzoJhaTEnrO68TNyNg==} 477 + engines: {node: '>= 10'} 478 + cpu: [arm64] 479 + os: [darwin] 480 + 481 + '@napi-rs/canvas-darwin-x64@0.1.96': 482 + resolution: {integrity: sha512-UrXiQz28tQEvGM1qvyptewOAfmUrrd5+wvi6Rzjj2VprZI8iZ2KIvBD2lTTG1bVF95AbeDeG7PJA0D9sLKaOFA==} 483 + engines: {node: '>= 10'} 484 + cpu: [x64] 485 + os: [darwin] 486 + 487 + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.96': 488 + resolution: {integrity: sha512-I90ODxweD8aEP6XKU/NU+biso95MwCtQ2F46dUvhec1HesFi0tq/tAJkYic/1aBSiO/1kGKmSeD1B0duOHhEHQ==} 489 + engines: {node: '>= 10'} 490 + cpu: [arm] 491 + os: [linux] 492 + 493 + '@napi-rs/canvas-linux-arm64-gnu@0.1.96': 494 + resolution: {integrity: sha512-Dx/0+RFV++w3PcRy+4xNXkghhXjA5d0Mw1bs95emn5Llinp1vihMaA6WJt3oYv2LAHc36+gnrhIBsPhUyI2SGw==} 495 + engines: {node: '>= 10'} 496 + cpu: [arm64] 497 + os: [linux] 498 + libc: [glibc] 499 + 500 + '@napi-rs/canvas-linux-arm64-musl@0.1.96': 501 + resolution: {integrity: sha512-UvOi7fii3IE2KDfEfhh8m+LpzSRvhGK7o1eho99M2M0HTik11k3GX+2qgVx9EtujN3/bhFFS1kSO3+vPMaJ0Mg==} 502 + engines: {node: '>= 10'} 503 + cpu: [arm64] 504 + os: [linux] 505 + libc: [musl] 506 + 507 + '@napi-rs/canvas-linux-riscv64-gnu@0.1.96': 508 + resolution: {integrity: sha512-MBSukhGCQ5nRtf9NbFYWOU080yqkZU1PbuH4o1ROvB4CbPl12fchDR35tU83Wz8gWIM9JTn99lBn9DenPIv7Ig==} 509 + engines: {node: '>= 10'} 510 + cpu: [riscv64] 511 + os: [linux] 512 + libc: [glibc] 513 + 514 + '@napi-rs/canvas-linux-x64-gnu@0.1.96': 515 + resolution: {integrity: sha512-I/ccu2SstyKiV3HIeVzyBIWfrJo8cN7+MSQZPnabewWV6hfJ2nY7Df2WqOHmobBRUw84uGR6zfQHsUEio/m5Vg==} 516 + engines: {node: '>= 10'} 517 + cpu: [x64] 518 + os: [linux] 519 + libc: [glibc] 520 + 521 + '@napi-rs/canvas-linux-x64-musl@0.1.96': 522 + resolution: {integrity: sha512-H3uov7qnTl73GDT4h52lAqpJPsl1tIUyNPWJyhQ6gHakohNqqRq3uf80+NEpzcytKGEOENP1wX3yGwZxhjiWEQ==} 523 + engines: {node: '>= 10'} 524 + cpu: [x64] 525 + os: [linux] 526 + libc: [musl] 527 + 528 + '@napi-rs/canvas-win32-arm64-msvc@0.1.96': 529 + resolution: {integrity: sha512-ATp6Y+djOjYtkfV/VRH7CZ8I1MEtkUQBmKUbuWw5zWEHHqfL0cEcInE4Cxgx7zkNAhEdBbnH8HMVrqNp+/gwxA==} 530 + engines: {node: '>= 10'} 531 + cpu: [arm64] 532 + os: [win32] 533 + 534 + '@napi-rs/canvas-win32-x64-msvc@0.1.96': 535 + resolution: {integrity: sha512-UYGdTltVd+Z8mcIuoqGmAXXUvwH5CLf2M6mIB5B0/JmX5J041jETjqtSYl7gN+aj3k1by/SG6sS0hAwCqyK7zw==} 536 + engines: {node: '>= 10'} 537 + cpu: [x64] 538 + os: [win32] 539 + 540 + '@napi-rs/canvas@0.1.96': 541 + resolution: {integrity: sha512-6NNmNxvoJKeucVjxaaRUt3La2i5jShgiAbaY3G/72s1Vp3U06XPrAIxkAjBxpDcamEn/t+WJ4OOlGmvILo4/Ew==} 542 + engines: {node: '>= 10'} 543 + 544 + '@rollup/rollup-android-arm-eabi@4.59.0': 545 + resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} 546 + cpu: [arm] 547 + os: [android] 548 + 549 + '@rollup/rollup-android-arm64@4.59.0': 550 + resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} 551 + cpu: [arm64] 552 + os: [android] 553 + 554 + '@rollup/rollup-darwin-arm64@4.59.0': 555 + resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} 556 + cpu: [arm64] 557 + os: [darwin] 558 + 559 + '@rollup/rollup-darwin-x64@4.59.0': 560 + resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} 561 + cpu: [x64] 562 + os: [darwin] 563 + 564 + '@rollup/rollup-freebsd-arm64@4.59.0': 565 + resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} 566 + cpu: [arm64] 567 + os: [freebsd] 568 + 569 + '@rollup/rollup-freebsd-x64@4.59.0': 570 + resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} 571 + cpu: [x64] 572 + os: [freebsd] 573 + 574 + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': 575 + resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} 576 + cpu: [arm] 577 + os: [linux] 578 + libc: [glibc] 579 + 580 + '@rollup/rollup-linux-arm-musleabihf@4.59.0': 581 + resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} 582 + cpu: [arm] 583 + os: [linux] 584 + libc: [musl] 585 + 586 + '@rollup/rollup-linux-arm64-gnu@4.59.0': 587 + resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} 588 + cpu: [arm64] 589 + os: [linux] 590 + libc: [glibc] 591 + 592 + '@rollup/rollup-linux-arm64-musl@4.59.0': 593 + resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} 594 + cpu: [arm64] 595 + os: [linux] 596 + libc: [musl] 597 + 598 + '@rollup/rollup-linux-loong64-gnu@4.59.0': 599 + resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} 600 + cpu: [loong64] 601 + os: [linux] 602 + libc: [glibc] 603 + 604 + '@rollup/rollup-linux-loong64-musl@4.59.0': 605 + resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} 606 + cpu: [loong64] 607 + os: [linux] 608 + libc: [musl] 609 + 610 + '@rollup/rollup-linux-ppc64-gnu@4.59.0': 611 + resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} 612 + cpu: [ppc64] 613 + os: [linux] 614 + libc: [glibc] 615 + 616 + '@rollup/rollup-linux-ppc64-musl@4.59.0': 617 + resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} 618 + cpu: [ppc64] 619 + os: [linux] 620 + libc: [musl] 621 + 622 + '@rollup/rollup-linux-riscv64-gnu@4.59.0': 623 + resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} 624 + cpu: [riscv64] 625 + os: [linux] 626 + libc: [glibc] 627 + 628 + '@rollup/rollup-linux-riscv64-musl@4.59.0': 629 + resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} 630 + cpu: [riscv64] 631 + os: [linux] 632 + libc: [musl] 633 + 634 + '@rollup/rollup-linux-s390x-gnu@4.59.0': 635 + resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} 636 + cpu: [s390x] 637 + os: [linux] 638 + libc: [glibc] 639 + 640 + '@rollup/rollup-linux-x64-gnu@4.59.0': 641 + resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} 642 + cpu: [x64] 643 + os: [linux] 644 + libc: [glibc] 645 + 646 + '@rollup/rollup-linux-x64-musl@4.59.0': 647 + resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} 648 + cpu: [x64] 649 + os: [linux] 650 + libc: [musl] 651 + 652 + '@rollup/rollup-openbsd-x64@4.59.0': 653 + resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} 654 + cpu: [x64] 655 + os: [openbsd] 656 + 657 + '@rollup/rollup-openharmony-arm64@4.59.0': 658 + resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} 659 + cpu: [arm64] 660 + os: [openharmony] 661 + 662 + '@rollup/rollup-win32-arm64-msvc@4.59.0': 663 + resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} 664 + cpu: [arm64] 665 + os: [win32] 666 + 667 + '@rollup/rollup-win32-ia32-msvc@4.59.0': 668 + resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} 669 + cpu: [ia32] 670 + os: [win32] 671 + 672 + '@rollup/rollup-win32-x64-gnu@4.59.0': 673 + resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} 674 + cpu: [x64] 675 + os: [win32] 676 + 677 + '@rollup/rollup-win32-x64-msvc@4.59.0': 678 + resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} 679 + cpu: [x64] 680 + os: [win32] 681 + 682 + '@types/estree@1.0.8': 683 + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 684 + 685 + '@types/localforage@0.0.34': 686 + resolution: {integrity: sha512-tJxahnjm9dEI1X+hQSC5f2BSd/coZaqbIl1m3TCl0q9SVuC52XcXfV0XmoCU1+PmjyucuVITwoTnN8OlTbEXXA==} 687 + deprecated: This is a stub types definition for localforage (https://github.com/localForage/localForage). localforage provides its own type definitions, so you don't need @types/localforage installed! 688 + 689 + '@types/prop-types@15.7.15': 690 + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} 691 + 692 + '@types/react-dom@18.3.7': 693 + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} 694 + peerDependencies: 695 + '@types/react': ^18.0.0 696 + 697 + '@types/react@18.3.28': 698 + resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} 699 + 700 + '@xmldom/xmldom@0.7.13': 701 + resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==} 702 + engines: {node: '>=10.0.0'} 703 + deprecated: this version is no longer supported, please update to at least 0.8.* 704 + 705 + acorn@8.16.0: 706 + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} 707 + engines: {node: '>=0.4.0'} 708 + hasBin: true 709 + 710 + adler-32@1.3.1: 711 + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} 712 + engines: {node: '>=0.8'} 713 + 714 + any-promise@1.3.0: 715 + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 716 + 717 + bundle-require@5.1.0: 718 + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} 719 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 720 + peerDependencies: 721 + esbuild: '>=0.18' 722 + 723 + cac@6.7.14: 724 + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 725 + engines: {node: '>=8'} 726 + 727 + cfb@1.2.2: 728 + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} 729 + engines: {node: '>=0.8'} 730 + 731 + chokidar@4.0.3: 732 + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} 733 + engines: {node: '>= 14.16.0'} 734 + 735 + codepage@1.15.0: 736 + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} 737 + engines: {node: '>=0.8'} 738 + 739 + commander@4.1.1: 740 + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 741 + engines: {node: '>= 6'} 742 + 743 + confbox@0.1.8: 744 + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} 745 + 746 + consola@3.4.2: 747 + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} 748 + engines: {node: ^14.18.0 || >=16.10.0} 749 + 750 + core-js@3.48.0: 751 + resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} 752 + 753 + core-util-is@1.0.3: 754 + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} 755 + 756 + crc-32@1.2.2: 757 + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} 758 + engines: {node: '>=0.8'} 759 + hasBin: true 760 + 761 + csstype@3.2.3: 762 + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} 763 + 764 + d@1.0.2: 765 + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} 766 + engines: {node: '>=0.12'} 767 + 768 + debug@4.4.3: 769 + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 770 + engines: {node: '>=6.0'} 771 + peerDependencies: 772 + supports-color: '*' 773 + peerDependenciesMeta: 774 + supports-color: 775 + optional: true 776 + 777 + docx-preview@0.3.7: 778 + resolution: {integrity: sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==} 779 + 780 + epubjs@0.3.93: 781 + resolution: {integrity: sha512-c06pNSdBxcXv3dZSbXAVLE1/pmleRhOT6mXNZo6INKmvuKpYB65MwU/lO7830czCtjIiK9i+KR+3S+p0wtljrw==} 782 + 783 + es5-ext@0.10.64: 784 + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} 785 + engines: {node: '>=0.10'} 786 + 787 + es6-iterator@2.0.3: 788 + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} 789 + 790 + es6-symbol@3.1.4: 791 + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} 792 + engines: {node: '>=0.12'} 793 + 794 + esbuild@0.25.12: 795 + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} 796 + engines: {node: '>=18'} 797 + hasBin: true 798 + 799 + esbuild@0.27.3: 800 + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} 801 + engines: {node: '>=18'} 802 + hasBin: true 803 + 804 + esniff@2.0.1: 805 + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} 806 + engines: {node: '>=0.10'} 807 + 808 + event-emitter@0.3.5: 809 + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} 810 + 811 + ext@1.7.0: 812 + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} 813 + 814 + fdir@6.5.0: 815 + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} 816 + engines: {node: '>=12.0.0'} 817 + peerDependencies: 818 + picomatch: ^3 || ^4 819 + peerDependenciesMeta: 820 + picomatch: 821 + optional: true 822 + 823 + fix-dts-default-cjs-exports@1.0.1: 824 + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} 825 + 826 + frac@1.1.2: 827 + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} 828 + engines: {node: '>=0.8'} 829 + 830 + fsevents@2.3.3: 831 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 832 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 833 + os: [darwin] 834 + 835 + highlight.js@11.11.1: 836 + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} 837 + engines: {node: '>=12.0.0'} 838 + 839 + immediate@3.0.6: 840 + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} 841 + 842 + inherits@2.0.4: 843 + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 844 + 845 + isarray@1.0.0: 846 + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} 847 + 848 + joycon@3.1.1: 849 + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} 850 + engines: {node: '>=10'} 851 + 852 + js-tokens@4.0.0: 853 + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 854 + 855 + jszip@3.10.1: 856 + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} 857 + 858 + lie@3.1.1: 859 + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} 860 + 861 + lie@3.3.0: 862 + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} 863 + 864 + lilconfig@3.1.3: 865 + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 866 + engines: {node: '>=14'} 867 + 868 + lines-and-columns@1.2.4: 869 + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 870 + 871 + load-tsconfig@0.2.5: 872 + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} 873 + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 874 + 875 + localforage@1.10.0: 876 + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} 877 + 878 + lodash@4.17.23: 879 + resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} 880 + 881 + loose-envify@1.4.0: 882 + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 883 + hasBin: true 884 + 885 + magic-string@0.30.21: 886 + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} 887 + 888 + marks-pane@1.0.9: 889 + resolution: {integrity: sha512-Ahs4oeG90tbdPWwAJkAAoHg2lRR8lAs9mZXETNPO9hYg3AkjUJBKi1NQ4aaIQZVGrig7c/3NUV1jANl8rFTeMg==} 890 + 891 + mlly@1.8.1: 892 + resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} 893 + 894 + ms@2.1.3: 895 + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 896 + 897 + mz@2.7.0: 898 + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 899 + 900 + nanoid@3.3.11: 901 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 902 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 903 + hasBin: true 904 + 905 + next-tick@1.1.0: 906 + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} 907 + 908 + node-readable-to-web-readable-stream@0.4.2: 909 + resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==} 910 + 911 + object-assign@4.1.1: 912 + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 913 + engines: {node: '>=0.10.0'} 914 + 915 + pako@1.0.11: 916 + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} 917 + 918 + papaparse@5.5.3: 919 + resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} 920 + 921 + path-webpack@0.0.3: 922 + resolution: {integrity: sha512-AmeDxedoo5svf7aB3FYqSAKqMxys014lVKBzy1o/5vv9CtU7U4wgGWL1dA2o6MOzcD53ScN4Jmiq6VbtLz1vIQ==} 923 + 924 + pathe@2.0.3: 925 + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} 926 + 927 + pdfjs-dist@5.5.207: 928 + resolution: {integrity: sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==} 929 + engines: {node: '>=20.19.0 || >=22.13.0 || >=24'} 930 + 931 + picocolors@1.1.1: 932 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 933 + 934 + picomatch@4.0.3: 935 + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} 936 + engines: {node: '>=12'} 937 + 938 + pirates@4.0.7: 939 + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} 940 + engines: {node: '>= 6'} 941 + 942 + pkg-types@1.3.1: 943 + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} 944 + 945 + postcss-load-config@6.0.1: 946 + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} 947 + engines: {node: '>= 18'} 948 + peerDependencies: 949 + jiti: '>=1.21.0' 950 + postcss: '>=8.0.9' 951 + tsx: ^4.8.1 952 + yaml: ^2.4.2 953 + peerDependenciesMeta: 954 + jiti: 955 + optional: true 956 + postcss: 957 + optional: true 958 + tsx: 959 + optional: true 960 + yaml: 961 + optional: true 962 + 963 + postcss@8.5.8: 964 + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} 965 + engines: {node: ^10 || ^12 || >=14} 966 + 967 + process-nextick-args@2.0.1: 968 + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} 969 + 970 + react-dom@18.3.1: 971 + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} 972 + peerDependencies: 973 + react: ^18.3.1 974 + 975 + react@18.3.1: 976 + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 977 + engines: {node: '>=0.10.0'} 978 + 979 + readable-stream@2.3.8: 980 + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} 981 + 982 + readdirp@4.1.2: 983 + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} 984 + engines: {node: '>= 14.18.0'} 985 + 986 + resolve-from@5.0.0: 987 + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} 988 + engines: {node: '>=8'} 989 + 990 + rollup@4.59.0: 991 + resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} 992 + engines: {node: '>=18.0.0', npm: '>=8.0.0'} 993 + hasBin: true 994 + 995 + safe-buffer@5.1.2: 996 + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} 997 + 998 + scheduler@0.23.2: 999 + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} 1000 + 1001 + setimmediate@1.0.5: 1002 + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} 1003 + 1004 + source-map-js@1.2.1: 1005 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1006 + engines: {node: '>=0.10.0'} 1007 + 1008 + source-map@0.7.6: 1009 + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} 1010 + engines: {node: '>= 12'} 1011 + 1012 + ssf@0.11.2: 1013 + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} 1014 + engines: {node: '>=0.8'} 1015 + 1016 + string_decoder@1.1.1: 1017 + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} 1018 + 1019 + sucrase@3.35.1: 1020 + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} 1021 + engines: {node: '>=16 || 14 >=14.17'} 1022 + hasBin: true 1023 + 1024 + thenify-all@1.6.0: 1025 + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 1026 + engines: {node: '>=0.8'} 1027 + 1028 + thenify@3.3.1: 1029 + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 1030 + 1031 + tinyexec@0.3.2: 1032 + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} 1033 + 1034 + tinyglobby@0.2.15: 1035 + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 1036 + engines: {node: '>=12.0.0'} 1037 + 1038 + tree-kill@1.2.2: 1039 + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 1040 + hasBin: true 1041 + 1042 + ts-interface-checker@0.1.13: 1043 + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 1044 + 1045 + tsup@8.5.1: 1046 + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} 1047 + engines: {node: '>=18'} 1048 + hasBin: true 1049 + peerDependencies: 1050 + '@microsoft/api-extractor': ^7.36.0 1051 + '@swc/core': ^1 1052 + postcss: ^8.4.12 1053 + typescript: '>=4.5.0' 1054 + peerDependenciesMeta: 1055 + '@microsoft/api-extractor': 1056 + optional: true 1057 + '@swc/core': 1058 + optional: true 1059 + postcss: 1060 + optional: true 1061 + typescript: 1062 + optional: true 1063 + 1064 + type@2.7.3: 1065 + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} 1066 + 1067 + typescript@5.9.3: 1068 + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} 1069 + engines: {node: '>=14.17'} 1070 + hasBin: true 1071 + 1072 + ufo@1.6.3: 1073 + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} 1074 + 1075 + util-deprecate@1.0.2: 1076 + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1077 + 1078 + vite@6.4.1: 1079 + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} 1080 + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} 1081 + hasBin: true 1082 + peerDependencies: 1083 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 1084 + jiti: '>=1.21.0' 1085 + less: '*' 1086 + lightningcss: ^1.21.0 1087 + sass: '*' 1088 + sass-embedded: '*' 1089 + stylus: '*' 1090 + sugarss: '*' 1091 + terser: ^5.16.0 1092 + tsx: ^4.8.1 1093 + yaml: ^2.4.2 1094 + peerDependenciesMeta: 1095 + '@types/node': 1096 + optional: true 1097 + jiti: 1098 + optional: true 1099 + less: 1100 + optional: true 1101 + lightningcss: 1102 + optional: true 1103 + sass: 1104 + optional: true 1105 + sass-embedded: 1106 + optional: true 1107 + stylus: 1108 + optional: true 1109 + sugarss: 1110 + optional: true 1111 + terser: 1112 + optional: true 1113 + tsx: 1114 + optional: true 1115 + yaml: 1116 + optional: true 1117 + 1118 + wmf@1.0.2: 1119 + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} 1120 + engines: {node: '>=0.8'} 1121 + 1122 + word@0.3.0: 1123 + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} 1124 + engines: {node: '>=0.8'} 1125 + 1126 + xlsx@0.18.5: 1127 + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} 1128 + engines: {node: '>=0.8'} 1129 + hasBin: true 1130 + 1131 + snapshots: 1132 + 1133 + '@esbuild/aix-ppc64@0.25.12': 1134 + optional: true 1135 + 1136 + '@esbuild/aix-ppc64@0.27.3': 1137 + optional: true 1138 + 1139 + '@esbuild/android-arm64@0.25.12': 1140 + optional: true 1141 + 1142 + '@esbuild/android-arm64@0.27.3': 1143 + optional: true 1144 + 1145 + '@esbuild/android-arm@0.25.12': 1146 + optional: true 1147 + 1148 + '@esbuild/android-arm@0.27.3': 1149 + optional: true 1150 + 1151 + '@esbuild/android-x64@0.25.12': 1152 + optional: true 1153 + 1154 + '@esbuild/android-x64@0.27.3': 1155 + optional: true 1156 + 1157 + '@esbuild/darwin-arm64@0.25.12': 1158 + optional: true 1159 + 1160 + '@esbuild/darwin-arm64@0.27.3': 1161 + optional: true 1162 + 1163 + '@esbuild/darwin-x64@0.25.12': 1164 + optional: true 1165 + 1166 + '@esbuild/darwin-x64@0.27.3': 1167 + optional: true 1168 + 1169 + '@esbuild/freebsd-arm64@0.25.12': 1170 + optional: true 1171 + 1172 + '@esbuild/freebsd-arm64@0.27.3': 1173 + optional: true 1174 + 1175 + '@esbuild/freebsd-x64@0.25.12': 1176 + optional: true 1177 + 1178 + '@esbuild/freebsd-x64@0.27.3': 1179 + optional: true 1180 + 1181 + '@esbuild/linux-arm64@0.25.12': 1182 + optional: true 1183 + 1184 + '@esbuild/linux-arm64@0.27.3': 1185 + optional: true 1186 + 1187 + '@esbuild/linux-arm@0.25.12': 1188 + optional: true 1189 + 1190 + '@esbuild/linux-arm@0.27.3': 1191 + optional: true 1192 + 1193 + '@esbuild/linux-ia32@0.25.12': 1194 + optional: true 1195 + 1196 + '@esbuild/linux-ia32@0.27.3': 1197 + optional: true 1198 + 1199 + '@esbuild/linux-loong64@0.25.12': 1200 + optional: true 1201 + 1202 + '@esbuild/linux-loong64@0.27.3': 1203 + optional: true 1204 + 1205 + '@esbuild/linux-mips64el@0.25.12': 1206 + optional: true 1207 + 1208 + '@esbuild/linux-mips64el@0.27.3': 1209 + optional: true 1210 + 1211 + '@esbuild/linux-ppc64@0.25.12': 1212 + optional: true 1213 + 1214 + '@esbuild/linux-ppc64@0.27.3': 1215 + optional: true 1216 + 1217 + '@esbuild/linux-riscv64@0.25.12': 1218 + optional: true 1219 + 1220 + '@esbuild/linux-riscv64@0.27.3': 1221 + optional: true 1222 + 1223 + '@esbuild/linux-s390x@0.25.12': 1224 + optional: true 1225 + 1226 + '@esbuild/linux-s390x@0.27.3': 1227 + optional: true 1228 + 1229 + '@esbuild/linux-x64@0.25.12': 1230 + optional: true 1231 + 1232 + '@esbuild/linux-x64@0.27.3': 1233 + optional: true 1234 + 1235 + '@esbuild/netbsd-arm64@0.25.12': 1236 + optional: true 1237 + 1238 + '@esbuild/netbsd-arm64@0.27.3': 1239 + optional: true 1240 + 1241 + '@esbuild/netbsd-x64@0.25.12': 1242 + optional: true 1243 + 1244 + '@esbuild/netbsd-x64@0.27.3': 1245 + optional: true 1246 + 1247 + '@esbuild/openbsd-arm64@0.25.12': 1248 + optional: true 1249 + 1250 + '@esbuild/openbsd-arm64@0.27.3': 1251 + optional: true 1252 + 1253 + '@esbuild/openbsd-x64@0.25.12': 1254 + optional: true 1255 + 1256 + '@esbuild/openbsd-x64@0.27.3': 1257 + optional: true 1258 + 1259 + '@esbuild/openharmony-arm64@0.25.12': 1260 + optional: true 1261 + 1262 + '@esbuild/openharmony-arm64@0.27.3': 1263 + optional: true 1264 + 1265 + '@esbuild/sunos-x64@0.25.12': 1266 + optional: true 1267 + 1268 + '@esbuild/sunos-x64@0.27.3': 1269 + optional: true 1270 + 1271 + '@esbuild/win32-arm64@0.25.12': 1272 + optional: true 1273 + 1274 + '@esbuild/win32-arm64@0.27.3': 1275 + optional: true 1276 + 1277 + '@esbuild/win32-ia32@0.25.12': 1278 + optional: true 1279 + 1280 + '@esbuild/win32-ia32@0.27.3': 1281 + optional: true 1282 + 1283 + '@esbuild/win32-x64@0.25.12': 1284 + optional: true 1285 + 1286 + '@esbuild/win32-x64@0.27.3': 1287 + optional: true 1288 + 1289 + '@jridgewell/gen-mapping@0.3.13': 1290 + dependencies: 1291 + '@jridgewell/sourcemap-codec': 1.5.5 1292 + '@jridgewell/trace-mapping': 0.3.31 1293 + 1294 + '@jridgewell/resolve-uri@3.1.2': {} 1295 + 1296 + '@jridgewell/sourcemap-codec@1.5.5': {} 1297 + 1298 + '@jridgewell/trace-mapping@0.3.31': 1299 + dependencies: 1300 + '@jridgewell/resolve-uri': 3.1.2 1301 + '@jridgewell/sourcemap-codec': 1.5.5 1302 + 1303 + '@napi-rs/canvas-android-arm64@0.1.96': 1304 + optional: true 1305 + 1306 + '@napi-rs/canvas-darwin-arm64@0.1.96': 1307 + optional: true 1308 + 1309 + '@napi-rs/canvas-darwin-x64@0.1.96': 1310 + optional: true 1311 + 1312 + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.96': 1313 + optional: true 1314 + 1315 + '@napi-rs/canvas-linux-arm64-gnu@0.1.96': 1316 + optional: true 1317 + 1318 + '@napi-rs/canvas-linux-arm64-musl@0.1.96': 1319 + optional: true 1320 + 1321 + '@napi-rs/canvas-linux-riscv64-gnu@0.1.96': 1322 + optional: true 1323 + 1324 + '@napi-rs/canvas-linux-x64-gnu@0.1.96': 1325 + optional: true 1326 + 1327 + '@napi-rs/canvas-linux-x64-musl@0.1.96': 1328 + optional: true 1329 + 1330 + '@napi-rs/canvas-win32-arm64-msvc@0.1.96': 1331 + optional: true 1332 + 1333 + '@napi-rs/canvas-win32-x64-msvc@0.1.96': 1334 + optional: true 1335 + 1336 + '@napi-rs/canvas@0.1.96': 1337 + optionalDependencies: 1338 + '@napi-rs/canvas-android-arm64': 0.1.96 1339 + '@napi-rs/canvas-darwin-arm64': 0.1.96 1340 + '@napi-rs/canvas-darwin-x64': 0.1.96 1341 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.96 1342 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.96 1343 + '@napi-rs/canvas-linux-arm64-musl': 0.1.96 1344 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.96 1345 + '@napi-rs/canvas-linux-x64-gnu': 0.1.96 1346 + '@napi-rs/canvas-linux-x64-musl': 0.1.96 1347 + '@napi-rs/canvas-win32-arm64-msvc': 0.1.96 1348 + '@napi-rs/canvas-win32-x64-msvc': 0.1.96 1349 + optional: true 1350 + 1351 + '@rollup/rollup-android-arm-eabi@4.59.0': 1352 + optional: true 1353 + 1354 + '@rollup/rollup-android-arm64@4.59.0': 1355 + optional: true 1356 + 1357 + '@rollup/rollup-darwin-arm64@4.59.0': 1358 + optional: true 1359 + 1360 + '@rollup/rollup-darwin-x64@4.59.0': 1361 + optional: true 1362 + 1363 + '@rollup/rollup-freebsd-arm64@4.59.0': 1364 + optional: true 1365 + 1366 + '@rollup/rollup-freebsd-x64@4.59.0': 1367 + optional: true 1368 + 1369 + '@rollup/rollup-linux-arm-gnueabihf@4.59.0': 1370 + optional: true 1371 + 1372 + '@rollup/rollup-linux-arm-musleabihf@4.59.0': 1373 + optional: true 1374 + 1375 + '@rollup/rollup-linux-arm64-gnu@4.59.0': 1376 + optional: true 1377 + 1378 + '@rollup/rollup-linux-arm64-musl@4.59.0': 1379 + optional: true 1380 + 1381 + '@rollup/rollup-linux-loong64-gnu@4.59.0': 1382 + optional: true 1383 + 1384 + '@rollup/rollup-linux-loong64-musl@4.59.0': 1385 + optional: true 1386 + 1387 + '@rollup/rollup-linux-ppc64-gnu@4.59.0': 1388 + optional: true 1389 + 1390 + '@rollup/rollup-linux-ppc64-musl@4.59.0': 1391 + optional: true 1392 + 1393 + '@rollup/rollup-linux-riscv64-gnu@4.59.0': 1394 + optional: true 1395 + 1396 + '@rollup/rollup-linux-riscv64-musl@4.59.0': 1397 + optional: true 1398 + 1399 + '@rollup/rollup-linux-s390x-gnu@4.59.0': 1400 + optional: true 1401 + 1402 + '@rollup/rollup-linux-x64-gnu@4.59.0': 1403 + optional: true 1404 + 1405 + '@rollup/rollup-linux-x64-musl@4.59.0': 1406 + optional: true 1407 + 1408 + '@rollup/rollup-openbsd-x64@4.59.0': 1409 + optional: true 1410 + 1411 + '@rollup/rollup-openharmony-arm64@4.59.0': 1412 + optional: true 1413 + 1414 + '@rollup/rollup-win32-arm64-msvc@4.59.0': 1415 + optional: true 1416 + 1417 + '@rollup/rollup-win32-ia32-msvc@4.59.0': 1418 + optional: true 1419 + 1420 + '@rollup/rollup-win32-x64-gnu@4.59.0': 1421 + optional: true 1422 + 1423 + '@rollup/rollup-win32-x64-msvc@4.59.0': 1424 + optional: true 1425 + 1426 + '@types/estree@1.0.8': {} 1427 + 1428 + '@types/localforage@0.0.34': 1429 + dependencies: 1430 + localforage: 1.10.0 1431 + 1432 + '@types/prop-types@15.7.15': {} 1433 + 1434 + '@types/react-dom@18.3.7(@types/react@18.3.28)': 1435 + dependencies: 1436 + '@types/react': 18.3.28 1437 + 1438 + '@types/react@18.3.28': 1439 + dependencies: 1440 + '@types/prop-types': 15.7.15 1441 + csstype: 3.2.3 1442 + 1443 + '@xmldom/xmldom@0.7.13': {} 1444 + 1445 + acorn@8.16.0: {} 1446 + 1447 + adler-32@1.3.1: {} 1448 + 1449 + any-promise@1.3.0: {} 1450 + 1451 + bundle-require@5.1.0(esbuild@0.27.3): 1452 + dependencies: 1453 + esbuild: 0.27.3 1454 + load-tsconfig: 0.2.5 1455 + 1456 + cac@6.7.14: {} 1457 + 1458 + cfb@1.2.2: 1459 + dependencies: 1460 + adler-32: 1.3.1 1461 + crc-32: 1.2.2 1462 + 1463 + chokidar@4.0.3: 1464 + dependencies: 1465 + readdirp: 4.1.2 1466 + 1467 + codepage@1.15.0: {} 1468 + 1469 + commander@4.1.1: {} 1470 + 1471 + confbox@0.1.8: {} 1472 + 1473 + consola@3.4.2: {} 1474 + 1475 + core-js@3.48.0: {} 1476 + 1477 + core-util-is@1.0.3: {} 1478 + 1479 + crc-32@1.2.2: {} 1480 + 1481 + csstype@3.2.3: {} 1482 + 1483 + d@1.0.2: 1484 + dependencies: 1485 + es5-ext: 0.10.64 1486 + type: 2.7.3 1487 + 1488 + debug@4.4.3: 1489 + dependencies: 1490 + ms: 2.1.3 1491 + 1492 + docx-preview@0.3.7: 1493 + dependencies: 1494 + jszip: 3.10.1 1495 + 1496 + epubjs@0.3.93: 1497 + dependencies: 1498 + '@types/localforage': 0.0.34 1499 + '@xmldom/xmldom': 0.7.13 1500 + core-js: 3.48.0 1501 + event-emitter: 0.3.5 1502 + jszip: 3.10.1 1503 + localforage: 1.10.0 1504 + lodash: 4.17.23 1505 + marks-pane: 1.0.9 1506 + path-webpack: 0.0.3 1507 + 1508 + es5-ext@0.10.64: 1509 + dependencies: 1510 + es6-iterator: 2.0.3 1511 + es6-symbol: 3.1.4 1512 + esniff: 2.0.1 1513 + next-tick: 1.1.0 1514 + 1515 + es6-iterator@2.0.3: 1516 + dependencies: 1517 + d: 1.0.2 1518 + es5-ext: 0.10.64 1519 + es6-symbol: 3.1.4 1520 + 1521 + es6-symbol@3.1.4: 1522 + dependencies: 1523 + d: 1.0.2 1524 + ext: 1.7.0 1525 + 1526 + esbuild@0.25.12: 1527 + optionalDependencies: 1528 + '@esbuild/aix-ppc64': 0.25.12 1529 + '@esbuild/android-arm': 0.25.12 1530 + '@esbuild/android-arm64': 0.25.12 1531 + '@esbuild/android-x64': 0.25.12 1532 + '@esbuild/darwin-arm64': 0.25.12 1533 + '@esbuild/darwin-x64': 0.25.12 1534 + '@esbuild/freebsd-arm64': 0.25.12 1535 + '@esbuild/freebsd-x64': 0.25.12 1536 + '@esbuild/linux-arm': 0.25.12 1537 + '@esbuild/linux-arm64': 0.25.12 1538 + '@esbuild/linux-ia32': 0.25.12 1539 + '@esbuild/linux-loong64': 0.25.12 1540 + '@esbuild/linux-mips64el': 0.25.12 1541 + '@esbuild/linux-ppc64': 0.25.12 1542 + '@esbuild/linux-riscv64': 0.25.12 1543 + '@esbuild/linux-s390x': 0.25.12 1544 + '@esbuild/linux-x64': 0.25.12 1545 + '@esbuild/netbsd-arm64': 0.25.12 1546 + '@esbuild/netbsd-x64': 0.25.12 1547 + '@esbuild/openbsd-arm64': 0.25.12 1548 + '@esbuild/openbsd-x64': 0.25.12 1549 + '@esbuild/openharmony-arm64': 0.25.12 1550 + '@esbuild/sunos-x64': 0.25.12 1551 + '@esbuild/win32-arm64': 0.25.12 1552 + '@esbuild/win32-ia32': 0.25.12 1553 + '@esbuild/win32-x64': 0.25.12 1554 + 1555 + esbuild@0.27.3: 1556 + optionalDependencies: 1557 + '@esbuild/aix-ppc64': 0.27.3 1558 + '@esbuild/android-arm': 0.27.3 1559 + '@esbuild/android-arm64': 0.27.3 1560 + '@esbuild/android-x64': 0.27.3 1561 + '@esbuild/darwin-arm64': 0.27.3 1562 + '@esbuild/darwin-x64': 0.27.3 1563 + '@esbuild/freebsd-arm64': 0.27.3 1564 + '@esbuild/freebsd-x64': 0.27.3 1565 + '@esbuild/linux-arm': 0.27.3 1566 + '@esbuild/linux-arm64': 0.27.3 1567 + '@esbuild/linux-ia32': 0.27.3 1568 + '@esbuild/linux-loong64': 0.27.3 1569 + '@esbuild/linux-mips64el': 0.27.3 1570 + '@esbuild/linux-ppc64': 0.27.3 1571 + '@esbuild/linux-riscv64': 0.27.3 1572 + '@esbuild/linux-s390x': 0.27.3 1573 + '@esbuild/linux-x64': 0.27.3 1574 + '@esbuild/netbsd-arm64': 0.27.3 1575 + '@esbuild/netbsd-x64': 0.27.3 1576 + '@esbuild/openbsd-arm64': 0.27.3 1577 + '@esbuild/openbsd-x64': 0.27.3 1578 + '@esbuild/openharmony-arm64': 0.27.3 1579 + '@esbuild/sunos-x64': 0.27.3 1580 + '@esbuild/win32-arm64': 0.27.3 1581 + '@esbuild/win32-ia32': 0.27.3 1582 + '@esbuild/win32-x64': 0.27.3 1583 + 1584 + esniff@2.0.1: 1585 + dependencies: 1586 + d: 1.0.2 1587 + es5-ext: 0.10.64 1588 + event-emitter: 0.3.5 1589 + type: 2.7.3 1590 + 1591 + event-emitter@0.3.5: 1592 + dependencies: 1593 + d: 1.0.2 1594 + es5-ext: 0.10.64 1595 + 1596 + ext@1.7.0: 1597 + dependencies: 1598 + type: 2.7.3 1599 + 1600 + fdir@6.5.0(picomatch@4.0.3): 1601 + optionalDependencies: 1602 + picomatch: 4.0.3 1603 + 1604 + fix-dts-default-cjs-exports@1.0.1: 1605 + dependencies: 1606 + magic-string: 0.30.21 1607 + mlly: 1.8.1 1608 + rollup: 4.59.0 1609 + 1610 + frac@1.1.2: {} 1611 + 1612 + fsevents@2.3.3: 1613 + optional: true 1614 + 1615 + highlight.js@11.11.1: {} 1616 + 1617 + immediate@3.0.6: {} 1618 + 1619 + inherits@2.0.4: {} 1620 + 1621 + isarray@1.0.0: {} 1622 + 1623 + joycon@3.1.1: {} 1624 + 1625 + js-tokens@4.0.0: {} 1626 + 1627 + jszip@3.10.1: 1628 + dependencies: 1629 + lie: 3.3.0 1630 + pako: 1.0.11 1631 + readable-stream: 2.3.8 1632 + setimmediate: 1.0.5 1633 + 1634 + lie@3.1.1: 1635 + dependencies: 1636 + immediate: 3.0.6 1637 + 1638 + lie@3.3.0: 1639 + dependencies: 1640 + immediate: 3.0.6 1641 + 1642 + lilconfig@3.1.3: {} 1643 + 1644 + lines-and-columns@1.2.4: {} 1645 + 1646 + load-tsconfig@0.2.5: {} 1647 + 1648 + localforage@1.10.0: 1649 + dependencies: 1650 + lie: 3.1.1 1651 + 1652 + lodash@4.17.23: {} 1653 + 1654 + loose-envify@1.4.0: 1655 + dependencies: 1656 + js-tokens: 4.0.0 1657 + 1658 + magic-string@0.30.21: 1659 + dependencies: 1660 + '@jridgewell/sourcemap-codec': 1.5.5 1661 + 1662 + marks-pane@1.0.9: {} 1663 + 1664 + mlly@1.8.1: 1665 + dependencies: 1666 + acorn: 8.16.0 1667 + pathe: 2.0.3 1668 + pkg-types: 1.3.1 1669 + ufo: 1.6.3 1670 + 1671 + ms@2.1.3: {} 1672 + 1673 + mz@2.7.0: 1674 + dependencies: 1675 + any-promise: 1.3.0 1676 + object-assign: 4.1.1 1677 + thenify-all: 1.6.0 1678 + 1679 + nanoid@3.3.11: {} 1680 + 1681 + next-tick@1.1.0: {} 1682 + 1683 + node-readable-to-web-readable-stream@0.4.2: 1684 + optional: true 1685 + 1686 + object-assign@4.1.1: {} 1687 + 1688 + pako@1.0.11: {} 1689 + 1690 + papaparse@5.5.3: {} 1691 + 1692 + path-webpack@0.0.3: {} 1693 + 1694 + pathe@2.0.3: {} 1695 + 1696 + pdfjs-dist@5.5.207: 1697 + optionalDependencies: 1698 + '@napi-rs/canvas': 0.1.96 1699 + node-readable-to-web-readable-stream: 0.4.2 1700 + 1701 + picocolors@1.1.1: {} 1702 + 1703 + picomatch@4.0.3: {} 1704 + 1705 + pirates@4.0.7: {} 1706 + 1707 + pkg-types@1.3.1: 1708 + dependencies: 1709 + confbox: 0.1.8 1710 + mlly: 1.8.1 1711 + pathe: 2.0.3 1712 + 1713 + postcss-load-config@6.0.1(postcss@8.5.8): 1714 + dependencies: 1715 + lilconfig: 3.1.3 1716 + optionalDependencies: 1717 + postcss: 8.5.8 1718 + 1719 + postcss@8.5.8: 1720 + dependencies: 1721 + nanoid: 3.3.11 1722 + picocolors: 1.1.1 1723 + source-map-js: 1.2.1 1724 + 1725 + process-nextick-args@2.0.1: {} 1726 + 1727 + react-dom@18.3.1(react@18.3.1): 1728 + dependencies: 1729 + loose-envify: 1.4.0 1730 + react: 18.3.1 1731 + scheduler: 0.23.2 1732 + 1733 + react@18.3.1: 1734 + dependencies: 1735 + loose-envify: 1.4.0 1736 + 1737 + readable-stream@2.3.8: 1738 + dependencies: 1739 + core-util-is: 1.0.3 1740 + inherits: 2.0.4 1741 + isarray: 1.0.0 1742 + process-nextick-args: 2.0.1 1743 + safe-buffer: 5.1.2 1744 + string_decoder: 1.1.1 1745 + util-deprecate: 1.0.2 1746 + 1747 + readdirp@4.1.2: {} 1748 + 1749 + resolve-from@5.0.0: {} 1750 + 1751 + rollup@4.59.0: 1752 + dependencies: 1753 + '@types/estree': 1.0.8 1754 + optionalDependencies: 1755 + '@rollup/rollup-android-arm-eabi': 4.59.0 1756 + '@rollup/rollup-android-arm64': 4.59.0 1757 + '@rollup/rollup-darwin-arm64': 4.59.0 1758 + '@rollup/rollup-darwin-x64': 4.59.0 1759 + '@rollup/rollup-freebsd-arm64': 4.59.0 1760 + '@rollup/rollup-freebsd-x64': 4.59.0 1761 + '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 1762 + '@rollup/rollup-linux-arm-musleabihf': 4.59.0 1763 + '@rollup/rollup-linux-arm64-gnu': 4.59.0 1764 + '@rollup/rollup-linux-arm64-musl': 4.59.0 1765 + '@rollup/rollup-linux-loong64-gnu': 4.59.0 1766 + '@rollup/rollup-linux-loong64-musl': 4.59.0 1767 + '@rollup/rollup-linux-ppc64-gnu': 4.59.0 1768 + '@rollup/rollup-linux-ppc64-musl': 4.59.0 1769 + '@rollup/rollup-linux-riscv64-gnu': 4.59.0 1770 + '@rollup/rollup-linux-riscv64-musl': 4.59.0 1771 + '@rollup/rollup-linux-s390x-gnu': 4.59.0 1772 + '@rollup/rollup-linux-x64-gnu': 4.59.0 1773 + '@rollup/rollup-linux-x64-musl': 4.59.0 1774 + '@rollup/rollup-openbsd-x64': 4.59.0 1775 + '@rollup/rollup-openharmony-arm64': 4.59.0 1776 + '@rollup/rollup-win32-arm64-msvc': 4.59.0 1777 + '@rollup/rollup-win32-ia32-msvc': 4.59.0 1778 + '@rollup/rollup-win32-x64-gnu': 4.59.0 1779 + '@rollup/rollup-win32-x64-msvc': 4.59.0 1780 + fsevents: 2.3.3 1781 + 1782 + safe-buffer@5.1.2: {} 1783 + 1784 + scheduler@0.23.2: 1785 + dependencies: 1786 + loose-envify: 1.4.0 1787 + 1788 + setimmediate@1.0.5: {} 1789 + 1790 + source-map-js@1.2.1: {} 1791 + 1792 + source-map@0.7.6: {} 1793 + 1794 + ssf@0.11.2: 1795 + dependencies: 1796 + frac: 1.1.2 1797 + 1798 + string_decoder@1.1.1: 1799 + dependencies: 1800 + safe-buffer: 5.1.2 1801 + 1802 + sucrase@3.35.1: 1803 + dependencies: 1804 + '@jridgewell/gen-mapping': 0.3.13 1805 + commander: 4.1.1 1806 + lines-and-columns: 1.2.4 1807 + mz: 2.7.0 1808 + pirates: 4.0.7 1809 + tinyglobby: 0.2.15 1810 + ts-interface-checker: 0.1.13 1811 + 1812 + thenify-all@1.6.0: 1813 + dependencies: 1814 + thenify: 3.3.1 1815 + 1816 + thenify@3.3.1: 1817 + dependencies: 1818 + any-promise: 1.3.0 1819 + 1820 + tinyexec@0.3.2: {} 1821 + 1822 + tinyglobby@0.2.15: 1823 + dependencies: 1824 + fdir: 6.5.0(picomatch@4.0.3) 1825 + picomatch: 4.0.3 1826 + 1827 + tree-kill@1.2.2: {} 1828 + 1829 + ts-interface-checker@0.1.13: {} 1830 + 1831 + tsup@8.5.1(postcss@8.5.8)(typescript@5.9.3): 1832 + dependencies: 1833 + bundle-require: 5.1.0(esbuild@0.27.3) 1834 + cac: 6.7.14 1835 + chokidar: 4.0.3 1836 + consola: 3.4.2 1837 + debug: 4.4.3 1838 + esbuild: 0.27.3 1839 + fix-dts-default-cjs-exports: 1.0.1 1840 + joycon: 3.1.1 1841 + picocolors: 1.1.1 1842 + postcss-load-config: 6.0.1(postcss@8.5.8) 1843 + resolve-from: 5.0.0 1844 + rollup: 4.59.0 1845 + source-map: 0.7.6 1846 + sucrase: 3.35.1 1847 + tinyexec: 0.3.2 1848 + tinyglobby: 0.2.15 1849 + tree-kill: 1.2.2 1850 + optionalDependencies: 1851 + postcss: 8.5.8 1852 + typescript: 5.9.3 1853 + transitivePeerDependencies: 1854 + - jiti 1855 + - supports-color 1856 + - tsx 1857 + - yaml 1858 + 1859 + type@2.7.3: {} 1860 + 1861 + typescript@5.9.3: {} 1862 + 1863 + ufo@1.6.3: {} 1864 + 1865 + util-deprecate@1.0.2: {} 1866 + 1867 + vite@6.4.1: 1868 + dependencies: 1869 + esbuild: 0.25.12 1870 + fdir: 6.5.0(picomatch@4.0.3) 1871 + picomatch: 4.0.3 1872 + postcss: 8.5.8 1873 + rollup: 4.59.0 1874 + tinyglobby: 0.2.15 1875 + optionalDependencies: 1876 + fsevents: 2.3.3 1877 + 1878 + wmf@1.0.2: {} 1879 + 1880 + word@0.3.0: {} 1881 + 1882 + xlsx@0.18.5: 1883 + dependencies: 1884 + adler-32: 1.3.1 1885 + cfb: 1.2.2 1886 + codepage: 1.15.0 1887 + crc-32: 1.2.2 1888 + ssf: 0.11.2 1889 + wmf: 1.0.2 1890 + word: 0.3.0
+3
pnpm-workspace.yaml
··· 1 + packages: 2 + - "packages/*" 3 + - "examples/*"
+17
tsconfig.base.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ESNext", 5 + "moduleResolution": "bundler", 6 + "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 + "strict": true, 8 + "esModuleInterop": true, 9 + "skipLibCheck": true, 10 + "forceConsistentCasingInFileNames": true, 11 + "resolveJsonModule": true, 12 + "isolatedModules": true, 13 + "declaration": true, 14 + "declarationMap": true, 15 + "sourceMap": true 16 + } 17 + }