vwr.js 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,432 @@
1
+ # vwr.js
2
+
3
+ A lightweight, embeddable JavaScript library for rendering documents from URLs directly in the browser. Supports multiple document formats through a plugin architecture with a minimal, self-contained UI that requires no external CSS.
4
+
5
+ ## Important Notice
6
+
7
+ **This library is currently in early alpha phase and was created with significant AI assistance to solve a specific problem.**
8
+
9
+ - **Status**: Early alpha - needs improvement and full review
10
+ - **AI-Assisted Development**: This library was built with AI assistance and requires thorough review
11
+ - **Production Use**: Not recommended for production use without thorough testing and review
12
+ - **Contributions**: Feedback, bug reports, and contributions are especially welcome during this alpha phase
13
+ - **Future Plans**: Active development planned to improve stability, performance, and features
14
+
15
+ This library was built to address a specific document viewing need and will be improved over time. Please use with caution and report any issues you encounter.
16
+
17
+ ## Features
18
+
19
+ - **Multiple Formats**: PDF, Images (PNG, JPG, GIF, WebP, BMP, ICO), SVG, DOCX, Markdown, CSV, Plain Text
20
+ - **Zero Dependencies**: No external CSS required - all styles are injected automatically
21
+ - **Zoom Controls**: Zoom in/out with toolbar buttons, mouse wheel, or pinch gestures
22
+ - **Pagination**: Multi-page PDF support with page navigation
23
+ - **Download**: Built-in download button for original files
24
+ - **Fullscreen**: Fullscreen mode support (when available)
25
+ - **TypeScript**: Full TypeScript support with type definitions
26
+ - **Single Bundle**: All dependencies bundled - just include one file
27
+
28
+ ## Installation
29
+
30
+ ### npm
31
+
32
+ ```bash
33
+ npm install vwr.js
34
+ ```
35
+
36
+ ### ES Module
37
+
38
+ ```javascript
39
+ import { render } from 'vwr.js';
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ### Basic Usage
45
+
46
+ ```html
47
+ <!DOCTYPE html>
48
+ <html>
49
+ <head>
50
+ <title>Document Viewer</title>
51
+ </head>
52
+ <body>
53
+ <div id="viewer"></div>
54
+
55
+ <script src="vwr.min.js"></script>
56
+ <script>
57
+ const container = document.getElementById('viewer');
58
+ const viewer = vwr.render(container, 'https://example.com/document.pdf');
59
+ </script>
60
+ </body>
61
+ </html>
62
+ ```
63
+
64
+ ### With ES Modules
65
+
66
+ ```javascript
67
+ import { render } from 'vwr.js';
68
+
69
+ const container = document.getElementById('viewer');
70
+ const viewer = render(container, 'https://example.com/document.pdf');
71
+ ```
72
+
73
+ ### With Options and Callbacks
74
+
75
+ ```javascript
76
+ import { render } from 'vwr.js';
77
+
78
+ const container = document.getElementById('viewer');
79
+
80
+ const viewer = render(container, 'https://example.com/document.pdf', {
81
+ onLoad: (event) => {
82
+ console.log('Document loaded:', event);
83
+ console.log('Format:', event.mimeType);
84
+ console.log('Renderer:', event.renderer);
85
+ if (event.pageCount) {
86
+ console.log('Pages:', event.pageCount);
87
+ }
88
+ },
89
+ onError: (error) => {
90
+ console.error('Error loading document:', error.message);
91
+ console.error('Error code:', error.code);
92
+ },
93
+ onPageChange: (currentPage, totalPages) => {
94
+ console.log(`Page ${currentPage} of ${totalPages}`);
95
+ },
96
+ onZoomChange: (zoomLevel) => {
97
+ console.log(`Zoom: ${zoomLevel * 100}%`);
98
+ }
99
+ });
100
+
101
+ // Clean up when done
102
+ // viewer.destroy();
103
+ ```
104
+
105
+ ## API Reference
106
+
107
+ ### `render(element, url, options?)`
108
+
109
+ Renders a document from a URL into the specified container element.
110
+
111
+ #### Parameters
112
+
113
+ - `element` (HTMLElement, required): Container element to render the viewer into
114
+ - `url` (string, required): URL of the document to load
115
+ - `options` (VwrOptions, optional): Configuration options
116
+
117
+ #### Returns
118
+
119
+ Returns a `VwrInstance` object with a `destroy()` method for cleanup.
120
+
121
+ ### Options
122
+
123
+ ```typescript
124
+ interface VwrOptions {
125
+ onLoad?: (event: LoadEvent) => void;
126
+ onError?: (error: ErrorEvent) => void;
127
+ onPageChange?: (currentPage: number, totalPages: number) => void;
128
+ onZoomChange?: (zoomLevel: number) => void;
129
+ }
130
+ ```
131
+
132
+ #### `onLoad`
133
+
134
+ Called when the document is successfully loaded.
135
+
136
+ ```typescript
137
+ interface LoadEvent {
138
+ url: string;
139
+ mimeType: string;
140
+ renderer: string;
141
+ pageCount?: number; // Only for paginated documents (PDF)
142
+ }
143
+ ```
144
+
145
+ #### `onError`
146
+
147
+ Called when an error occurs while loading or rendering the document.
148
+
149
+ ```typescript
150
+ interface ErrorEvent {
151
+ url: string;
152
+ message: string;
153
+ code: 'NETWORK_ERROR' | 'CORS_ERROR' | 'UNSUPPORTED_FORMAT' | 'PARSE_ERROR' | 'UNKNOWN';
154
+ originalError?: Error;
155
+ }
156
+ ```
157
+
158
+ #### `onPageChange`
159
+
160
+ Called when the user navigates to a different page (PDF only).
161
+
162
+ ```typescript
163
+ onPageChange: (currentPage: number, totalPages: number) => void
164
+ ```
165
+
166
+ #### `onZoomChange`
167
+
168
+ Called when the zoom level changes.
169
+
170
+ ```typescript
171
+ onZoomChange: (zoomLevel: number) => void
172
+ // zoomLevel is a number between 0.25 and 4.0 (25% to 400%)
173
+ ```
174
+
175
+ ### Instance Methods
176
+
177
+ #### `destroy()`
178
+
179
+ Removes the viewer and cleans up all resources.
180
+
181
+ ```javascript
182
+ const viewer = render(container, url);
183
+ // ... later
184
+ viewer.destroy();
185
+ ```
186
+
187
+ ## Supported Formats
188
+
189
+ | Format | Extensions | Features |
190
+ |--------|------------|----------|
191
+ | PDF | `.pdf` | Pagination, zoom, page navigation |
192
+ | Images | `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.bmp`, `.ico` | Zoom, pan when zoomed |
193
+ | SVG | `.svg` | Vector scaling, sanitized for security |
194
+ | Word Documents | `.docx` | Formatted HTML output |
195
+ | Markdown | `.md`, `.markdown` | GitHub Flavored Markdown support |
196
+ | CSV | `.csv` | Table view with header row (max 1000 rows) |
197
+ | Plain Text | `.txt` | Monospace formatting |
198
+
199
+ ## Examples
200
+
201
+ ### Viewing Different Document Types
202
+
203
+ ```javascript
204
+ // PDF
205
+ render(container, 'https://example.com/document.pdf');
206
+
207
+ // Image
208
+ render(container, 'https://example.com/image.png');
209
+
210
+ // SVG
211
+ render(container, 'https://example.com/diagram.svg');
212
+
213
+ // DOCX
214
+ render(container, 'https://example.com/document.docx');
215
+
216
+ // Markdown
217
+ render(container, 'https://example.com/README.md');
218
+
219
+ // CSV
220
+ render(container, 'https://example.com/data.csv');
221
+
222
+ // Plain Text
223
+ render(container, 'https://example.com/notes.txt');
224
+ ```
225
+
226
+ ### Handling Errors
227
+
228
+ ```javascript
229
+ render(container, url, {
230
+ onError: (error) => {
231
+ switch (error.code) {
232
+ case 'NETWORK_ERROR':
233
+ alert('Unable to load document. Please check your connection.');
234
+ break;
235
+ case 'CORS_ERROR':
236
+ alert('Unable to load document due to security restrictions.');
237
+ break;
238
+ case 'UNSUPPORTED_FORMAT':
239
+ alert('This document format is not supported.');
240
+ break;
241
+ case 'PARSE_ERROR':
242
+ alert('Unable to read this document. The file may be corrupted.');
243
+ break;
244
+ default:
245
+ alert('An unexpected error occurred.');
246
+ }
247
+ }
248
+ });
249
+ ```
250
+
251
+ ### Tracking Page Changes (PDF)
252
+
253
+ ```javascript
254
+ render(container, 'https://example.com/document.pdf', {
255
+ onPageChange: (currentPage, totalPages) => {
256
+ // Update your UI with page information
257
+ document.getElementById('page-info').textContent =
258
+ `Page ${currentPage} of ${totalPages}`;
259
+ }
260
+ });
261
+ ```
262
+
263
+ ### Dynamic Document Loading
264
+
265
+ ```javascript
266
+ const container = document.getElementById('viewer');
267
+ let currentViewer = null;
268
+
269
+ function loadDocument(url) {
270
+ // Clean up previous viewer
271
+ if (currentViewer) {
272
+ currentViewer.destroy();
273
+ }
274
+
275
+ // Load new document
276
+ currentViewer = render(container, url, {
277
+ onLoad: (event) => {
278
+ console.log('Loaded:', event.renderer);
279
+ }
280
+ });
281
+ }
282
+
283
+ // Load different documents
284
+ loadDocument('https://example.com/doc1.pdf');
285
+ // Later...
286
+ loadDocument('https://example.com/doc2.docx');
287
+ ```
288
+
289
+ ### Styling the Container
290
+
291
+ ```html
292
+ <style>
293
+ #viewer {
294
+ width: 100%;
295
+ height: 600px;
296
+ border: 1px solid #ccc;
297
+ border-radius: 4px;
298
+ }
299
+ </style>
300
+
301
+ <div id="viewer"></div>
302
+ ```
303
+
304
+ ## Format Detection
305
+
306
+ The library automatically detects document formats using:
307
+
308
+ 1. **MIME type** from the HTTP response `Content-Type` header (primary)
309
+ 2. **File extension** as a fallback when MIME type is ambiguous or missing
310
+
311
+ Common MIME types:
312
+ - `application/pdf` → PDF
313
+ - `image/png`, `image/jpeg`, etc. → Images
314
+ - `image/svg+xml` → SVG
315
+ - `application/vnd.openxmlformats-officedocument.wordprocessingml.document` → DOCX
316
+ - `text/markdown` → Markdown
317
+ - `text/csv` → CSV
318
+ - `text/plain` → Plain Text (or Markdown if `.md` extension)
319
+
320
+ ## Browser Support
321
+
322
+ - Chrome (last 2 versions)
323
+ - Firefox (last 2 versions)
324
+ - Safari (last 2 versions)
325
+ - Edge (last 2 versions)
326
+ - iOS Safari (last 2 versions)
327
+ - Chrome for Android (last 2 versions)
328
+
329
+ **Note**: Fullscreen mode may not be available on all platforms (e.g., iOS Safari on iPhone).
330
+
331
+ ## Keyboard and Mouse Controls
332
+
333
+ ### Desktop
334
+
335
+ - **Zoom**: `Ctrl/Cmd + Scroll` (mouse wheel)
336
+ - **Zoom**: Toolbar buttons (`+` / `-`)
337
+ - **Pan**: Click and drag when zoomed beyond fit-to-width
338
+ - **Page Navigation**: Toolbar buttons (`<` / `>`)
339
+
340
+ ### Mobile/Touch
341
+
342
+ - **Zoom**: Pinch to zoom
343
+ - **Pan**: Drag when zoomed beyond fit-to-width
344
+
345
+ ## CORS Requirements
346
+
347
+ When loading documents from a different origin, the server must send appropriate CORS headers:
348
+
349
+ ```
350
+ Access-Control-Allow-Origin: *
351
+ ```
352
+
353
+ Or specify your domain:
354
+
355
+ ```
356
+ Access-Control-Allow-Origin: https://yourdomain.com
357
+ ```
358
+
359
+ If CORS headers are missing, you'll see a `CORS_ERROR` in the `onError` callback.
360
+
361
+ ## Size
362
+
363
+ - **Minified**: ~3.2MB
364
+
365
+ The library includes PDF.js, mammoth.js, marked, DOMPurify, and PapaParse as bundled dependencies.
366
+
367
+ ## Development
368
+
369
+ ```bash
370
+ # Install dependencies
371
+ npm install
372
+
373
+ # Run tests
374
+ npm test
375
+
376
+ # Run tests with coverage
377
+ npm run test:coverage
378
+
379
+ # Build
380
+ npm run build
381
+
382
+ # Lint
383
+ npm run lint
384
+
385
+ # Type check
386
+ npm run typecheck
387
+ ```
388
+
389
+ ## AI Acknowledgement & Development Status
390
+
391
+ **This library was created with significant AI assistance and is currently in early alpha phase.**
392
+
393
+ ### Current Status
394
+
395
+ - **Phase**: Early Alpha
396
+ - **Purpose**: Built to solve a specific document viewing problem
397
+ - **Review Status**: Needs full code review and testing
398
+ - **Production Readiness**: Not recommended for production use without thorough testing
399
+
400
+ ### What This Means
401
+
402
+ This library was developed quickly to address a specific need, and while it includes comprehensive features and test coverage, it has not yet undergone:
403
+
404
+ - Full security audit
405
+ - Performance optimization review
406
+ - Edge case testing across all supported formats
407
+ - Production deployment validation
408
+ - Community review and feedback
409
+
410
+ ### Future Plans
411
+
412
+ Active development is planned to:
413
+ - Improve stability and error handling
414
+ - Optimize performance, especially for large documents
415
+ - Add more comprehensive test coverage
416
+ - Address edge cases and browser compatibility issues
417
+ - Refine the API based on real-world usage
418
+ - Complete security review
419
+
420
+ ### Contributing
421
+
422
+ **Contributions, bug reports, and feedback are especially welcome** during this alpha phase. Your input will help shape the future of this library.
423
+
424
+ Please report any issues, suggest improvements, or submit pull requests. All contributions are appreciated!
425
+
426
+ ## License
427
+
428
+ MIT
429
+
430
+ ## Contributing
431
+
432
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare const icons: {
2
+ download: string;
3
+ fullscreen: string;
4
+ warning: string;
5
+ };
@@ -0,0 +1 @@
1
+ export declare function ensureStyles(): void;
@@ -0,0 +1,22 @@
1
+ export interface ToolbarRefs {
2
+ element: HTMLElement;
3
+ pageGroup: HTMLElement;
4
+ pageLabel: HTMLElement;
5
+ zoomLabel: HTMLElement;
6
+ zoomIn: HTMLButtonElement;
7
+ zoomOut: HTMLButtonElement;
8
+ prevPage: HTMLButtonElement;
9
+ nextPage: HTMLButtonElement;
10
+ download: HTMLButtonElement;
11
+ fullscreen: HTMLButtonElement | null;
12
+ }
13
+ export interface ToolbarOptions {
14
+ onZoomChange: (delta: number) => void;
15
+ onDownload: () => void;
16
+ onPrevPage: () => void;
17
+ onNextPage: () => void;
18
+ supportsPagination: boolean;
19
+ supportsFullscreen: boolean;
20
+ }
21
+ export declare function createToolbar(options: ToolbarOptions): ToolbarRefs;
22
+ export declare function updateToolbar(toolbar: ToolbarRefs, zoom: number, current: number, total: number): void;
@@ -0,0 +1,42 @@
1
+ export interface VwrOptions {
2
+ onLoad?: (event: LoadEvent) => void;
3
+ onError?: (error: ErrorEvent) => void;
4
+ onPageChange?: (currentPage: number, totalPages: number) => void;
5
+ onZoomChange?: (zoomLevel: number) => void;
6
+ }
7
+ export interface LoadEvent {
8
+ url: string;
9
+ mimeType: string;
10
+ renderer: string;
11
+ pageCount?: number;
12
+ }
13
+ export interface ErrorEvent {
14
+ url: string;
15
+ message: string;
16
+ code: "NETWORK_ERROR" | "CORS_ERROR" | "UNSUPPORTED_FORMAT" | "PARSE_ERROR" | "UNKNOWN";
17
+ originalError?: Error;
18
+ }
19
+ export interface RenderOptions {
20
+ zoom: number;
21
+ container: HTMLElement;
22
+ content: HTMLElement;
23
+ onPageCount?: (count: number) => void;
24
+ }
25
+ export interface RenderResult {
26
+ pageCount?: number;
27
+ getCurrentPage?: () => number;
28
+ scrollToPage?: (page: number) => void;
29
+ setZoom?: (zoom: number) => void;
30
+ getFitZoom?: () => number;
31
+ destroy?: () => void;
32
+ }
33
+ export interface Renderer {
34
+ supportsPagination: boolean;
35
+ supportsFullscreen: boolean;
36
+ canRender(mimeType: string, extension: string): boolean;
37
+ render(container: HTMLElement, data: ArrayBuffer, options: RenderOptions): Promise<RenderResult>;
38
+ destroy(): void;
39
+ }
40
+ export interface VwrInstance {
41
+ destroy(): void;
42
+ }
@@ -0,0 +1,43 @@
1
+ import type { VwrInstance, VwrOptions } from "./types";
2
+ export declare class Viewer implements VwrInstance {
3
+ private container;
4
+ private url;
5
+ private options;
6
+ private root;
7
+ private content;
8
+ private doc;
9
+ private docInner;
10
+ private loading;
11
+ private errorOverlay;
12
+ private toolbar;
13
+ private renderer;
14
+ private renderResult;
15
+ private zoom;
16
+ private fitZoom;
17
+ private pageCount;
18
+ private currentPage;
19
+ private lastNotifiedPage;
20
+ private scrollTimeout;
21
+ private pageIndicators;
22
+ private cleanupFns;
23
+ constructor(container: HTMLElement, url: string, options?: VwrOptions);
24
+ destroy(): void;
25
+ private load;
26
+ private pickRenderer;
27
+ private setLoading;
28
+ private showError;
29
+ private cleanupRenderer;
30
+ private adjustZoom;
31
+ private setZoom;
32
+ private applyZoom;
33
+ private setupPaging;
34
+ private handleScrollIndicator;
35
+ private showPageIndicator;
36
+ private getCurrentPage;
37
+ private scrollToPage;
38
+ private download;
39
+ private computeFitZoom;
40
+ private setupInteractions;
41
+ private touchDistance;
42
+ private touchMidpoint;
43
+ }
@@ -0,0 +1,6 @@
1
+ import type { VwrInstance, VwrOptions } from "./core/types";
2
+ export declare function render(element: HTMLElement, url: string, options?: VwrOptions): VwrInstance;
3
+ export declare const vwr: {
4
+ render: typeof render;
5
+ };
6
+ export type { VwrInstance, VwrOptions } from "./core/types";
@@ -0,0 +1,8 @@
1
+ import type { Renderer, RenderOptions, RenderResult } from "../core/types";
2
+ export declare class CSVRenderer implements Renderer {
3
+ supportsPagination: boolean;
4
+ supportsFullscreen: boolean;
5
+ canRender(mimeType: string, extension: string): boolean;
6
+ render(container: HTMLElement, data: ArrayBuffer, _options: RenderOptions): Promise<RenderResult>;
7
+ destroy(): void;
8
+ }
@@ -0,0 +1,8 @@
1
+ import type { Renderer, RenderOptions, RenderResult } from "../core/types";
2
+ export declare class DocxRenderer implements Renderer {
3
+ supportsPagination: boolean;
4
+ supportsFullscreen: boolean;
5
+ canRender(mimeType: string, extension: string): boolean;
6
+ render(container: HTMLElement, data: ArrayBuffer, _options: RenderOptions): Promise<RenderResult>;
7
+ destroy(): void;
8
+ }
@@ -0,0 +1,10 @@
1
+ import type { Renderer, RenderOptions, RenderResult } from "../core/types";
2
+ export declare class ImageRenderer implements Renderer {
3
+ supportsPagination: boolean;
4
+ supportsFullscreen: boolean;
5
+ private blobUrl;
6
+ canRender(mimeType: string, extension: string): boolean;
7
+ render(container: HTMLElement, data: ArrayBuffer, options: RenderOptions): Promise<RenderResult>;
8
+ destroy(): void;
9
+ private getFitZoom;
10
+ }
@@ -0,0 +1,8 @@
1
+ import type { Renderer, RenderOptions, RenderResult } from "../core/types";
2
+ export declare class MarkdownRenderer implements Renderer {
3
+ supportsPagination: boolean;
4
+ supportsFullscreen: boolean;
5
+ canRender(mimeType: string, extension: string): boolean;
6
+ render(container: HTMLElement, data: ArrayBuffer, _options: RenderOptions): Promise<RenderResult>;
7
+ destroy(): void;
8
+ }
@@ -0,0 +1,16 @@
1
+ import type { Renderer, RenderOptions, RenderResult } from "../core/types";
2
+ export declare class PDFRenderer implements Renderer {
3
+ supportsPagination: boolean;
4
+ supportsFullscreen: boolean;
5
+ private pdfDoc;
6
+ private observer;
7
+ private pages;
8
+ private workerUrl;
9
+ canRender(mimeType: string, extension: string): boolean;
10
+ render(container: HTMLElement, data: ArrayBuffer, options: RenderOptions): Promise<RenderResult>;
11
+ destroy(): void;
12
+ private getFitZoom;
13
+ private applyZoom;
14
+ private renderPage;
15
+ private getCurrentPage;
16
+ }
@@ -0,0 +1 @@
1
+ export declare function createPdfWorkerUrl(): string;
@@ -0,0 +1,2 @@
1
+ declare const _default: "// Mock PDF.js worker code";
2
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import type { Renderer, RenderOptions, RenderResult } from "../core/types";
2
+ export declare class SVGRenderer implements Renderer {
3
+ supportsPagination: boolean;
4
+ supportsFullscreen: boolean;
5
+ canRender(mimeType: string, extension: string): boolean;
6
+ render(container: HTMLElement, data: ArrayBuffer, options: RenderOptions): Promise<RenderResult>;
7
+ destroy(): void;
8
+ private getFitZoom;
9
+ }
@@ -0,0 +1,8 @@
1
+ import type { Renderer, RenderOptions, RenderResult } from "../core/types";
2
+ export declare class TextRenderer implements Renderer {
3
+ supportsPagination: boolean;
4
+ supportsFullscreen: boolean;
5
+ canRender(mimeType: string, extension: string): boolean;
6
+ render(container: HTMLElement, data: ArrayBuffer, _options: RenderOptions): Promise<RenderResult>;
7
+ destroy(): void;
8
+ }
@@ -0,0 +1,9 @@
1
+ import type { ErrorEvent } from "../core/types";
2
+ export interface FetchResult {
3
+ data: ArrayBuffer;
4
+ mimeType: string;
5
+ filename: string | null;
6
+ }
7
+ export declare function fetchDocument(url: string): Promise<FetchResult>;
8
+ export declare function buildError(url: string, code: ErrorEvent["code"], originalError?: Error): ErrorEvent;
9
+ export declare function errorMessage(code: ErrorEvent["code"]): string;
@@ -0,0 +1,4 @@
1
+ export declare function isFullscreenEnabled(): boolean;
2
+ export declare function requestFullscreen(element: HTMLElement): Promise<void>;
3
+ export declare function exitFullscreen(): Promise<void>;
4
+ export declare function isFullscreenActive(): boolean;