@lamberl-lee/file-preview 0.2.0 → 0.3.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.
Files changed (48) hide show
  1. package/README.md +6 -0
  2. package/dist/LargeFileGate.d.ts +23 -3
  3. package/dist/LargeFileGate.js +44 -40
  4. package/dist/LargeFileGate.js.map +1 -1
  5. package/dist/PluginPreviewRenderer.d.ts +20 -1
  6. package/dist/PluginPreviewRenderer.js +51 -10
  7. package/dist/PluginPreviewRenderer.js.map +1 -1
  8. package/dist/PreviewErrorBoundary.d.ts +2 -0
  9. package/dist/PreviewErrorBoundary.js +11 -2
  10. package/dist/PreviewErrorBoundary.js.map +1 -1
  11. package/dist/core/detect-meta.d.ts +73 -0
  12. package/dist/core/detect-meta.js +81 -0
  13. package/dist/core/detect-meta.js.map +1 -0
  14. package/dist/core/magic-bytes.d.ts +56 -0
  15. package/dist/core/magic-bytes.js +97 -0
  16. package/dist/core/magic-bytes.js.map +1 -0
  17. package/dist/core/plugin.d.ts +2 -2
  18. package/dist/core/plugin.js +5 -3
  19. package/dist/core/plugin.js.map +1 -1
  20. package/dist/core/preview-error.d.ts +35 -0
  21. package/dist/core/preview-error.js +39 -0
  22. package/dist/core/preview-error.js.map +1 -0
  23. package/dist/core/registry.d.ts +1 -0
  24. package/dist/index.d.ts +4 -1
  25. package/dist/index.js +21 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/plugins/audio-plugin.d.ts +1 -0
  28. package/dist/plugins/builtin-plugins.d.ts +1 -0
  29. package/dist/plugins/csv-plugin.d.ts +1 -0
  30. package/dist/plugins/docx-plugin.d.ts +1 -0
  31. package/dist/plugins/epub-plugin.d.ts +1 -0
  32. package/dist/plugins/html-plugin.d.ts +1 -0
  33. package/dist/plugins/image-plugin.d.ts +1 -0
  34. package/dist/plugins/markdown-plugin.d.ts +1 -0
  35. package/dist/plugins/pdf-plugin.d.ts +1 -0
  36. package/dist/plugins/pptx-plugin.d.ts +1 -0
  37. package/dist/plugins/rtf-plugin.d.ts +1 -0
  38. package/dist/plugins/source-code-plugin.d.ts +1 -0
  39. package/dist/plugins/svg-plugin.d.ts +1 -0
  40. package/dist/plugins/text-plugin.d.ts +1 -0
  41. package/dist/plugins/video-plugin.d.ts +1 -0
  42. package/dist/plugins/xlsx-plugin.d.ts +1 -0
  43. package/dist/plugins/zip-plugin.d.ts +1 -0
  44. package/dist/remote-url.d.ts +20 -5
  45. package/dist/remote-url.js +52 -128
  46. package/dist/remote-url.js.map +1 -1
  47. package/docs/supported-formats.md +143 -0
  48. package/package.json +12 -11
@@ -1,10 +1,10 @@
1
1
  import { FileInfo } from './core/types.js';
2
+ import { PreviewError } from './core/preview-error.js';
2
3
 
3
4
  type RemoteUrlErrorCode = "INVALID_URL" | "UNSUPPORTED_PROTOCOL" | "NETWORK_OR_CORS" | "HTTP_ERROR" | "ABORTED" | "FILE_TOO_LARGE";
4
- declare class RemoteUrlError extends Error {
5
- code: RemoteUrlErrorCode;
6
- url?: string | undefined;
7
- constructor(code: RemoteUrlErrorCode, message: string, url?: string | undefined);
5
+ declare class RemoteUrlError extends PreviewError {
6
+ readonly remoteCode: RemoteUrlErrorCode;
7
+ constructor(code: RemoteUrlErrorCode, message: string, url?: string);
8
8
  }
9
9
  interface RemoteLoadProgress {
10
10
  received: number;
@@ -14,7 +14,22 @@ interface RemoteLoadProgress {
14
14
  interface ProcessRemoteUrlOptions {
15
15
  signal?: AbortSignal;
16
16
  onProgress?: (progress: RemoteLoadProgress) => void;
17
+ /**
18
+ * Maximum bytes to download from the remote URL.
19
+ *
20
+ * Defaults to 100 MB. If the server advertises a `Content-Length` above
21
+ * this, the download is rejected before any bytes are transferred. If the
22
+ * server omits `Content-Length`, the download is aborted mid-stream the
23
+ * moment `received` crosses the limit — so an unbounded response can never
24
+ * exhaust browser memory.
25
+ *
26
+ * On either path the rejection is a `RemoteUrlError` with code
27
+ * `FILE_TOO_LARGE`. Set to `Infinity` to disable the limit entirely.
28
+ */
29
+ maxBytes?: number;
17
30
  }
31
+ /** Default remote download cap. Overridable via `ProcessRemoteUrlOptions.maxBytes`. */
32
+ declare const DEFAULT_REMOTE_MAX_BYTES: number;
18
33
  declare function processRemoteUrl(rawUrl: string, options?: ProcessRemoteUrlOptions): Promise<FileInfo>;
19
34
 
20
- export { type ProcessRemoteUrlOptions, type RemoteLoadProgress, RemoteUrlError, type RemoteUrlErrorCode, processRemoteUrl };
35
+ export { DEFAULT_REMOTE_MAX_BYTES, type ProcessRemoteUrlOptions, type RemoteLoadProgress, RemoteUrlError, type RemoteUrlErrorCode, processRemoteUrl };
@@ -1,12 +1,27 @@
1
1
  import { detectFileType, generateId } from "./utils";
2
- class RemoteUrlError extends Error {
2
+ import { sniffMagic, sniffZipContainer } from "./core/magic-bytes";
3
+ import { PreviewError } from "./core/preview-error";
4
+ function mapRemoteUrlErrorCode(code) {
5
+ switch (code) {
6
+ case "NETWORK_OR_CORS":
7
+ return "REMOTE_CORS_ERROR";
8
+ case "HTTP_ERROR":
9
+ return "REMOTE_HTTP_ERROR";
10
+ default:
11
+ return code;
12
+ }
13
+ }
14
+ class RemoteUrlError extends PreviewError {
3
15
  constructor(code, message, url) {
4
- super(message);
5
- this.code = code;
6
- this.url = url;
16
+ super(mapRemoteUrlErrorCode(code), message, {
17
+ url,
18
+ details: { remoteCode: code }
19
+ });
7
20
  this.name = "RemoteUrlError";
21
+ this.remoteCode = code;
8
22
  }
9
23
  }
24
+ const DEFAULT_REMOTE_MAX_BYTES = 100 * 1024 * 1024;
10
25
  const REMOTE_MIME_BY_EXTENSION = {
11
26
  docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
12
27
  pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
@@ -177,128 +192,6 @@ function getRemoteFileName(rawUrl, contentDisposition) {
177
192
  };
178
193
  }
179
194
  }
180
- function startsWithBytes(bytes, signature) {
181
- if (bytes.length < signature.length) return false;
182
- return signature.every((value, index) => bytes[index] === value);
183
- }
184
- function readAscii(bytes, start, length) {
185
- return String.fromCharCode(...bytes.subarray(start, start + length));
186
- }
187
- function sniffMagic(buffer) {
188
- const bytes = new Uint8Array(buffer.slice(0, 32));
189
- if (readAscii(bytes, 0, 5) === "%PDF-") {
190
- return {
191
- ext: "pdf",
192
- mimeType: "application/pdf"
193
- };
194
- }
195
- if (startsWithBytes(bytes, [80, 75, 3, 4]) || startsWithBytes(bytes, [80, 75, 5, 6]) || startsWithBytes(bytes, [80, 75, 7, 8])) {
196
- return {
197
- ext: "zip",
198
- mimeType: "application/zip"
199
- };
200
- }
201
- if (startsWithBytes(bytes, [
202
- 137,
203
- 80,
204
- 78,
205
- 71,
206
- 13,
207
- 10,
208
- 26,
209
- 10
210
- ])) {
211
- return {
212
- ext: "png",
213
- mimeType: "image/png"
214
- };
215
- }
216
- if (startsWithBytes(bytes, [255, 216, 255])) {
217
- return {
218
- ext: "jpg",
219
- mimeType: "image/jpeg"
220
- };
221
- }
222
- const gifHeader = readAscii(bytes, 0, 6);
223
- if (gifHeader === "GIF87a" || gifHeader === "GIF89a") {
224
- return {
225
- ext: "gif",
226
- mimeType: "image/gif"
227
- };
228
- }
229
- if (readAscii(bytes, 0, 4) === "RIFF" && readAscii(bytes, 8, 4) === "WEBP") {
230
- return {
231
- ext: "webp",
232
- mimeType: "image/webp"
233
- };
234
- }
235
- if (bytes.length >= 12 && readAscii(bytes, 4, 4) === "ftyp") {
236
- return {
237
- ext: "mp4",
238
- mimeType: "video/mp4"
239
- };
240
- }
241
- if (startsWithBytes(bytes, [
242
- 208,
243
- 207,
244
- 17,
245
- 224,
246
- 161,
247
- 177,
248
- 26,
249
- 225
250
- ])) {
251
- return {
252
- ext: "ole",
253
- mimeType: "application/x-ole-storage"
254
- };
255
- }
256
- return {
257
- ext: null,
258
- mimeType: null
259
- };
260
- }
261
- async function sniffZipContainer(buffer) {
262
- try {
263
- const { default: JSZip } = await import("jszip");
264
- const zip = await JSZip.loadAsync(buffer);
265
- const fileNames = Object.keys(zip.files).map(
266
- (name) => name.replace(/\\/g, "/").toLowerCase()
267
- );
268
- const hasFile = (target) => fileNames.includes(target);
269
- if (hasFile("word/document.xml")) {
270
- return {
271
- ext: "docx",
272
- mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
273
- };
274
- }
275
- if (hasFile("ppt/presentation.xml")) {
276
- return {
277
- ext: "pptx",
278
- mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
279
- };
280
- }
281
- if (hasFile("xl/workbook.xml")) {
282
- return {
283
- ext: "xlsx",
284
- mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
285
- };
286
- }
287
- const mimetypeFile = zip.file("mimetype");
288
- if (mimetypeFile) {
289
- const mimetype = (await mimetypeFile.async("string")).trim();
290
- if (mimetype === "application/epub+zip") {
291
- return {
292
- ext: "epub",
293
- mimeType: "application/epub+zip"
294
- };
295
- }
296
- }
297
- return null;
298
- } catch {
299
- return null;
300
- }
301
- }
302
195
  function resolveRemoteMimeType(input) {
303
196
  const ext = getExtension(input.fileName);
304
197
  const mimeFromExtension = REMOTE_MIME_BY_EXTENSION[ext];
@@ -344,10 +237,25 @@ function getContentLength(response) {
344
237
  const parsed = Number(value);
345
238
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
346
239
  }
347
- async function readResponseAsArrayBufferWithProgress(response, options) {
240
+ async function readResponseAsArrayBufferWithProgress(response, options, url) {
348
241
  const total = getContentLength(response);
242
+ const maxBytes = options.maxBytes ?? DEFAULT_REMOTE_MAX_BYTES;
243
+ if (total !== null && total > maxBytes) {
244
+ throw new RemoteUrlError(
245
+ "FILE_TOO_LARGE",
246
+ `Remote file is ${total} bytes, exceeds the ${maxBytes}-byte limit.`,
247
+ url
248
+ );
249
+ }
349
250
  if (!response.body) {
350
251
  const buffer = await response.arrayBuffer();
252
+ if (buffer.byteLength > maxBytes) {
253
+ throw new RemoteUrlError(
254
+ "FILE_TOO_LARGE",
255
+ `Remote file is ${buffer.byteLength} bytes, exceeds the ${maxBytes}-byte limit.`,
256
+ url
257
+ );
258
+ }
351
259
  options.onProgress?.({
352
260
  received: buffer.byteLength,
353
261
  total,
@@ -372,6 +280,17 @@ async function readResponseAsArrayBufferWithProgress(response, options) {
372
280
  if (value) {
373
281
  chunks.push(value);
374
282
  received += value.byteLength;
283
+ if (received > maxBytes) {
284
+ try {
285
+ await reader.cancel();
286
+ } catch {
287
+ }
288
+ throw new RemoteUrlError(
289
+ "FILE_TOO_LARGE",
290
+ `Remote file exceeded the ${maxBytes}-byte limit after ${received} bytes (no reliable Content-Length).`,
291
+ url
292
+ );
293
+ }
375
294
  options.onProgress?.({
376
295
  received,
377
296
  total,
@@ -430,7 +349,11 @@ async function processRemoteUrl(rawUrl, options = {}) {
430
349
  parsedUrl.toString(),
431
350
  contentDisposition
432
351
  );
433
- buffer = await readResponseAsArrayBufferWithProgress(response, options);
352
+ buffer = await readResponseAsArrayBufferWithProgress(
353
+ response,
354
+ options,
355
+ parsedUrl.toString()
356
+ );
434
357
  const magicResult = sniffMagic(buffer);
435
358
  const containerResult = magicResult.ext === "zip" ? await sniffZipContainer(buffer) : null;
436
359
  const mimeResult = resolveRemoteMimeType({
@@ -472,6 +395,7 @@ async function processRemoteUrl(rawUrl, options = {}) {
472
395
  }
473
396
  }
474
397
  export {
398
+ DEFAULT_REMOTE_MAX_BYTES,
475
399
  RemoteUrlError,
476
400
  processRemoteUrl
477
401
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/remote-url.ts"],"sourcesContent":["import { detectFileType, generateId } from \"./utils\";\nimport type { FileInfo } from \"./utils\";\n\nexport type RemoteUrlErrorCode =\n | \"INVALID_URL\"\n | \"UNSUPPORTED_PROTOCOL\"\n | \"NETWORK_OR_CORS\"\n | \"HTTP_ERROR\"\n | \"ABORTED\"\n | \"FILE_TOO_LARGE\";\n\nexport class RemoteUrlError extends Error {\n constructor(\n public code: RemoteUrlErrorCode,\n message: string,\n public url?: string\n ) {\n super(message);\n this.name = \"RemoteUrlError\";\n }\n}\n\nexport interface RemoteLoadProgress {\n received: number;\n total: number | null;\n percent: number | null;\n}\n\nexport interface ProcessRemoteUrlOptions {\n signal?: AbortSignal;\n onProgress?: (progress: RemoteLoadProgress) => void;\n}\n\ntype FileNameSource =\n | \"content-disposition\"\n | \"query\"\n | \"pathname\"\n | \"fallback\";\n\ntype MimeDetectionSource =\n | \"container\"\n | \"magic\"\n | \"extension\"\n | \"header\"\n | \"fallback\";\n\ninterface FileNameResult {\n fileName: string;\n source: FileNameSource;\n}\n\ninterface MimeResult {\n mimeType: string;\n source: MimeDetectionSource;\n}\n\ninterface MagicSniffResult {\n ext: string | null;\n mimeType: string | null;\n}\n\ninterface ContainerSniffResult {\n ext: string;\n mimeType: string;\n}\n\nconst REMOTE_MIME_BY_EXTENSION: Record<string, string> = {\n docx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n pptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n xlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\n doc: \"application/msword\",\n ppt: \"application/vnd.ms-powerpoint\",\n xls: \"application/vnd.ms-excel\",\n\n pdf: \"application/pdf\",\n zip: \"application/zip\",\n epub: \"application/epub+zip\",\n\n json: \"application/json\",\n csv: \"text/csv\",\n md: \"text/markdown\",\n mdx: \"text/markdown\",\n html: \"text/html\",\n htm: \"text/html\",\n svg: \"image/svg+xml\",\n\n txt: \"text/plain\",\n log: \"text/plain\",\n\n png: \"image/png\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n bmp: \"image/bmp\",\n ico: \"image/x-icon\",\n avif: \"image/avif\",\n\n mp4: \"video/mp4\",\n webm: \"video/webm\",\n mov: \"video/quicktime\",\n\n mp3: \"audio/mpeg\",\n wav: \"audio/wav\",\n ogg: \"audio/ogg\",\n flac: \"audio/flac\",\n aac: \"audio/aac\",\n m4a: \"audio/mp4\",\n};\n\nconst GENERIC_MIME_TYPES = new Set([\n \"application/octet-stream\",\n \"binary/octet-stream\",\n \"application/x-msdownload\",\n \"application/download\",\n]);\n\nconst WEAK_MAGIC_MIME_TYPES = new Set([\n \"application/zip\",\n \"application/x-ole-storage\",\n \"video/mp4\",\n]);\n\nfunction isStrongMagicMimeType(mimeType: string | null): boolean {\n return Boolean(mimeType && !WEAK_MAGIC_MIME_TYPES.has(mimeType));\n}\n\nfunction sanitizeRemoteFileName(fileName: string): string {\n const cleanName = fileName\n .replace(/[\\\\/:*?\"<>|]/g, \"_\")\n .replace(/\\s+/g, \" \")\n .trim();\n\n return cleanName || \"remote-file\";\n}\n\nfunction tryDecodeURIComponent(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction stripQuotes(value: string): string {\n const trimmed = value.trim();\n\n if (\n (trimmed.startsWith('\"') && trimmed.endsWith('\"')) ||\n (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\"))\n ) {\n return trimmed.slice(1, -1);\n }\n\n return trimmed;\n}\n\nfunction getExtension(fileName: string): string {\n const cleanName = fileName.split(\"?\")[0].split(\"#\")[0];\n const parts = cleanName.toLowerCase().split(\".\");\n\n return parts.length > 1 ? parts[parts.length - 1] : \"\";\n}\n\nfunction normalizeHeaderMimeType(headerMimeType: string): string {\n const mimeType = headerMimeType.split(\";\")[0]?.trim().toLowerCase() || \"\";\n\n return GENERIC_MIME_TYPES.has(mimeType) ? \"\" : mimeType;\n}\n\nfunction getHeaderMimeType(response: Response): string {\n return response.headers.get(\"content-type\")?.split(\";\")[0]?.trim() || \"\";\n}\n\nfunction getFileNameFromContentDisposition(\n contentDisposition?: string | null\n): string | null {\n if (!contentDisposition) return null;\n\n const filenameStarMatch = contentDisposition.match(\n /filename\\*\\s*=\\s*([^;]+)/i\n );\n\n if (filenameStarMatch?.[1]) {\n const rawValue = stripQuotes(filenameStarMatch[1]);\n\n const rfc5987Match = rawValue.match(/^[^']*'[^']*'(.+)$/);\n const encodedValue = rfc5987Match?.[1] || rawValue;\n\n return sanitizeRemoteFileName(tryDecodeURIComponent(encodedValue));\n }\n\n const filenameMatch = contentDisposition.match(/filename\\s*=\\s*([^;]+)/i);\n\n if (filenameMatch?.[1]) {\n return sanitizeRemoteFileName(\n tryDecodeURIComponent(stripQuotes(filenameMatch[1]))\n );\n }\n\n return null;\n}\n\nfunction getRemoteFileName(\n rawUrl: string,\n contentDisposition?: string | null\n): FileNameResult {\n const fromDisposition = getFileNameFromContentDisposition(contentDisposition);\n\n if (fromDisposition) {\n return {\n fileName: fromDisposition,\n source: \"content-disposition\",\n };\n }\n\n try {\n const url = new URL(rawUrl);\n const params = url.searchParams;\n\n const queryKeys = [\n \"showname\",\n \"filename\",\n \"fileName\",\n \"name\",\n \"file\",\n \"download\",\n ];\n\n let firstQueryCandidate: string | null = null;\n\n for (const key of queryKeys) {\n const value = params.get(key)?.trim();\n\n if (!value || value.toLowerCase() === \"true\") {\n continue;\n }\n\n const candidate = sanitizeRemoteFileName(value);\n\n if (!firstQueryCandidate) {\n firstQueryCandidate = candidate;\n }\n\n if (getExtension(candidate)) {\n return {\n fileName: candidate,\n source: \"query\",\n };\n }\n }\n\n const pathname = decodeURIComponent(url.pathname);\n const pathnameName = pathname.split(\"/\").filter(Boolean).pop();\n\n if (pathnameName?.trim()) {\n const candidate = sanitizeRemoteFileName(pathnameName.trim());\n\n if (getExtension(candidate)) {\n return {\n fileName: candidate,\n source: \"pathname\",\n };\n }\n }\n\n if (firstQueryCandidate) {\n return {\n fileName: firstQueryCandidate,\n source: \"query\",\n };\n }\n\n if (pathnameName?.trim()) {\n return {\n fileName: sanitizeRemoteFileName(pathnameName.trim()),\n source: \"pathname\",\n };\n }\n\n return {\n fileName: \"remote-file\",\n source: \"fallback\",\n };\n } catch {\n return {\n fileName: \"remote-file\",\n source: \"fallback\",\n };\n }\n}\n\nfunction startsWithBytes(bytes: Uint8Array, signature: number[]): boolean {\n if (bytes.length < signature.length) return false;\n\n return signature.every((value, index) => bytes[index] === value);\n}\n\nfunction readAscii(bytes: Uint8Array, start: number, length: number): string {\n return String.fromCharCode(...bytes.subarray(start, start + length));\n}\n\nfunction sniffMagic(buffer: ArrayBuffer): MagicSniffResult {\n const bytes = new Uint8Array(buffer.slice(0, 32));\n\n if (readAscii(bytes, 0, 5) === \"%PDF-\") {\n return {\n ext: \"pdf\",\n mimeType: \"application/pdf\",\n };\n }\n\n if (\n startsWithBytes(bytes, [0x50, 0x4b, 0x03, 0x04]) ||\n startsWithBytes(bytes, [0x50, 0x4b, 0x05, 0x06]) ||\n startsWithBytes(bytes, [0x50, 0x4b, 0x07, 0x08])\n ) {\n return {\n ext: \"zip\",\n mimeType: \"application/zip\",\n };\n }\n\n if (\n startsWithBytes(bytes, [\n 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,\n ])\n ) {\n return {\n ext: \"png\",\n mimeType: \"image/png\",\n };\n }\n\n if (startsWithBytes(bytes, [0xff, 0xd8, 0xff])) {\n return {\n ext: \"jpg\",\n mimeType: \"image/jpeg\",\n };\n }\n\n const gifHeader = readAscii(bytes, 0, 6);\n\n if (gifHeader === \"GIF87a\" || gifHeader === \"GIF89a\") {\n return {\n ext: \"gif\",\n mimeType: \"image/gif\",\n };\n }\n\n if (readAscii(bytes, 0, 4) === \"RIFF\" && readAscii(bytes, 8, 4) === \"WEBP\") {\n return {\n ext: \"webp\",\n mimeType: \"image/webp\",\n };\n }\n\n if (bytes.length >= 12 && readAscii(bytes, 4, 4) === \"ftyp\") {\n return {\n ext: \"mp4\",\n mimeType: \"video/mp4\",\n };\n }\n\n if (\n startsWithBytes(bytes, [\n 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1,\n ])\n ) {\n return {\n ext: \"ole\",\n mimeType: \"application/x-ole-storage\",\n };\n }\n\n return {\n ext: null,\n mimeType: null,\n };\n}\n\nasync function sniffZipContainer(\n buffer: ArrayBuffer\n): Promise<ContainerSniffResult | null> {\n try {\n const { default: JSZip } = await import(\"jszip\");\n const zip = await JSZip.loadAsync(buffer);\n\n const fileNames = Object.keys(zip.files).map((name) =>\n name.replace(/\\\\/g, \"/\").toLowerCase()\n );\n\n const hasFile = (target: string) => fileNames.includes(target);\n\n if (hasFile(\"word/document.xml\")) {\n return {\n ext: \"docx\",\n mimeType:\n \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n };\n }\n\n if (hasFile(\"ppt/presentation.xml\")) {\n return {\n ext: \"pptx\",\n mimeType:\n \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n };\n }\n\n if (hasFile(\"xl/workbook.xml\")) {\n return {\n ext: \"xlsx\",\n mimeType:\n \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n };\n }\n\n const mimetypeFile = zip.file(\"mimetype\");\n\n if (mimetypeFile) {\n const mimetype = (await mimetypeFile.async(\"string\")).trim();\n\n if (mimetype === \"application/epub+zip\") {\n return {\n ext: \"epub\",\n mimeType: \"application/epub+zip\",\n };\n }\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\nfunction resolveRemoteMimeType(input: {\n fileName: string;\n headerMimeType: string;\n magicMimeType: string | null;\n containerMimeType: string | null;\n}): MimeResult {\n const ext = getExtension(input.fileName);\n const mimeFromExtension = REMOTE_MIME_BY_EXTENSION[ext];\n const mimeFromHeader = normalizeHeaderMimeType(input.headerMimeType);\n\n // 1. ZIP internal container (docx/pptx/xlsx/epub)\n if (input.containerMimeType) {\n return {\n mimeType: input.containerMimeType,\n source: \"container\",\n };\n }\n\n // 2. Strong magic (PDF/PNG/JPG/GIF/WEBP)\n if (isStrongMagicMimeType(input.magicMimeType)) {\n return {\n mimeType: input.magicMimeType!,\n source: \"magic\",\n };\n }\n\n // 3. Explicit extension wins over weak magic (zip/ole/ftyp)\n if (mimeFromExtension) {\n return {\n mimeType: mimeFromExtension,\n source: \"extension\",\n };\n }\n\n // 4. Content-Type header\n if (mimeFromHeader) {\n return {\n mimeType: mimeFromHeader,\n source: \"header\",\n };\n }\n\n // 5. Weak magic as last resort before fallback\n if (input.magicMimeType) {\n return {\n mimeType: input.magicMimeType,\n source: \"magic\",\n };\n }\n\n return {\n mimeType: \"application/octet-stream\",\n source: \"fallback\",\n };\n}\n\nfunction getContentLength(response: Response): number | null {\n const value = response.headers.get(\"content-length\");\n if (!value) return null;\n\n const parsed = Number(value);\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;\n}\n\nasync function readResponseAsArrayBufferWithProgress(\n response: Response,\n options: ProcessRemoteUrlOptions\n): Promise<ArrayBuffer> {\n const total = getContentLength(response);\n\n if (!response.body) {\n const buffer = await response.arrayBuffer();\n options.onProgress?.({\n received: buffer.byteLength,\n total,\n percent: total ? buffer.byteLength / total : null,\n });\n return buffer;\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let received = 0;\n\n try {\n while (true) {\n if (options.signal?.aborted) {\n try {\n await reader.cancel();\n } catch {\n // ignore\n }\n throw new DOMException(\"The operation was aborted.\", \"AbortError\");\n }\n\n const { done, value } = await reader.read();\n\n if (done) break;\n\n if (value) {\n chunks.push(value);\n received += value.byteLength;\n\n options.onProgress?.({\n received,\n total,\n percent: total ? received / total : null,\n });\n }\n }\n } finally {\n try {\n reader.releaseLock();\n } catch {\n // ignore\n }\n }\n\n const merged = new Uint8Array(received);\n let offset = 0;\n\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n return merged.buffer;\n}\n\nexport async function processRemoteUrl(\n rawUrl: string,\n options: ProcessRemoteUrlOptions = {}\n): Promise<FileInfo> {\n const trimmedUrl = rawUrl.trim();\n\n if (!trimmedUrl) {\n throw new RemoteUrlError(\"INVALID_URL\", \"Remote URL is empty\");\n }\n\n let parsedUrl: URL;\n\n try {\n parsedUrl = new URL(trimmedUrl);\n } catch {\n throw new RemoteUrlError(\"INVALID_URL\", \"Please enter a valid URL\");\n }\n\n if (![\"http:\", \"https:\"].includes(parsedUrl.protocol)) {\n throw new RemoteUrlError(\n \"UNSUPPORTED_PROTOCOL\",\n \"Only http/https URLs are supported\",\n parsedUrl.toString()\n );\n }\n\n let response: Response;\n let buffer: ArrayBuffer;\n\n try {\n response = await fetch(parsedUrl.toString(), {\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new RemoteUrlError(\n \"HTTP_ERROR\",\n `远程文件请求失败:HTTP ${response.status}`,\n parsedUrl.toString()\n );\n }\n\n const headerMimeType = getHeaderMimeType(response);\n const contentDisposition = response.headers.get(\"content-disposition\");\n\n const fileNameResult = getRemoteFileName(\n parsedUrl.toString(),\n contentDisposition\n );\n\n buffer = await readResponseAsArrayBufferWithProgress(response, options);\n\n const magicResult = sniffMagic(buffer);\n\n const containerResult =\n magicResult.ext === \"zip\" ? await sniffZipContainer(buffer) : null;\n\n const mimeResult = resolveRemoteMimeType({\n fileName: fileNameResult.fileName,\n headerMimeType,\n magicMimeType: magicResult.mimeType,\n containerMimeType: containerResult?.mimeType ?? null,\n });\n\n const fileType = detectFileType(fileNameResult.fileName, mimeResult.mimeType);\n\n return {\n id: generateId(),\n name: fileNameResult.fileName,\n size: buffer.byteLength,\n type: mimeResult.mimeType,\n fileType,\n source: {\n kind: \"arrayBuffer\",\n buffer,\n name: fileNameResult.fileName,\n mimeType: mimeResult.mimeType,\n },\n };\n } catch (error) {\n if (error instanceof RemoteUrlError) {\n throw error;\n }\n\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw new RemoteUrlError(\n \"ABORTED\",\n \"Remote file loading cancelled\",\n parsedUrl.toString()\n );\n }\n\n throw new RemoteUrlError(\n \"NETWORK_OR_CORS\",\n \"无法加载远程文件。可能是 URL 不可访问,或目标服务器未允许浏览器跨域访问。\",\n parsedUrl.toString()\n );\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/remote-url.ts"],"sourcesContent":["import { detectFileType, generateId } from \"./utils\";\nimport type { FileInfo } from \"./utils\";\nimport { sniffMagic, sniffZipContainer } from \"./core/magic-bytes\";\nimport { PreviewError, type PreviewErrorCode } from \"./core/preview-error\";\n\nexport type RemoteUrlErrorCode =\n | \"INVALID_URL\"\n | \"UNSUPPORTED_PROTOCOL\"\n | \"NETWORK_OR_CORS\"\n | \"HTTP_ERROR\"\n | \"ABORTED\"\n | \"FILE_TOO_LARGE\";\n\nfunction mapRemoteUrlErrorCode(code: RemoteUrlErrorCode): PreviewErrorCode {\n switch (code) {\n case \"NETWORK_OR_CORS\":\n return \"REMOTE_CORS_ERROR\";\n case \"HTTP_ERROR\":\n return \"REMOTE_HTTP_ERROR\";\n default:\n return code;\n }\n}\n\nexport class RemoteUrlError extends PreviewError {\n readonly remoteCode: RemoteUrlErrorCode;\n\n constructor(\n code: RemoteUrlErrorCode,\n message: string,\n url?: string\n ) {\n super(mapRemoteUrlErrorCode(code), message, {\n url,\n details: { remoteCode: code },\n });\n this.name = \"RemoteUrlError\";\n this.remoteCode = code;\n }\n}\n\nexport interface RemoteLoadProgress {\n received: number;\n total: number | null;\n percent: number | null;\n}\n\nexport interface ProcessRemoteUrlOptions {\n signal?: AbortSignal;\n onProgress?: (progress: RemoteLoadProgress) => void;\n /**\n * Maximum bytes to download from the remote URL.\n *\n * Defaults to 100 MB. If the server advertises a `Content-Length` above\n * this, the download is rejected before any bytes are transferred. If the\n * server omits `Content-Length`, the download is aborted mid-stream the\n * moment `received` crosses the limit — so an unbounded response can never\n * exhaust browser memory.\n *\n * On either path the rejection is a `RemoteUrlError` with code\n * `FILE_TOO_LARGE`. Set to `Infinity` to disable the limit entirely.\n */\n maxBytes?: number;\n}\n\n/** Default remote download cap. Overridable via `ProcessRemoteUrlOptions.maxBytes`. */\nexport const DEFAULT_REMOTE_MAX_BYTES = 100 * 1024 * 1024;\n\ntype FileNameSource =\n | \"content-disposition\"\n | \"query\"\n | \"pathname\"\n | \"fallback\";\n\ntype MimeDetectionSource =\n | \"container\"\n | \"magic\"\n | \"extension\"\n | \"header\"\n | \"fallback\";\n\ninterface FileNameResult {\n fileName: string;\n source: FileNameSource;\n}\n\ninterface MimeResult {\n mimeType: string;\n source: MimeDetectionSource;\n}\n\nconst REMOTE_MIME_BY_EXTENSION: Record<string, string> = {\n docx: \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\",\n pptx: \"application/vnd.openxmlformats-officedocument.presentationml.presentation\",\n xlsx: \"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n\n doc: \"application/msword\",\n ppt: \"application/vnd.ms-powerpoint\",\n xls: \"application/vnd.ms-excel\",\n\n pdf: \"application/pdf\",\n zip: \"application/zip\",\n epub: \"application/epub+zip\",\n\n json: \"application/json\",\n csv: \"text/csv\",\n md: \"text/markdown\",\n mdx: \"text/markdown\",\n html: \"text/html\",\n htm: \"text/html\",\n svg: \"image/svg+xml\",\n\n txt: \"text/plain\",\n log: \"text/plain\",\n\n png: \"image/png\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n bmp: \"image/bmp\",\n ico: \"image/x-icon\",\n avif: \"image/avif\",\n\n mp4: \"video/mp4\",\n webm: \"video/webm\",\n mov: \"video/quicktime\",\n\n mp3: \"audio/mpeg\",\n wav: \"audio/wav\",\n ogg: \"audio/ogg\",\n flac: \"audio/flac\",\n aac: \"audio/aac\",\n m4a: \"audio/mp4\",\n};\n\nconst GENERIC_MIME_TYPES = new Set([\n \"application/octet-stream\",\n \"binary/octet-stream\",\n \"application/x-msdownload\",\n \"application/download\",\n]);\n\nconst WEAK_MAGIC_MIME_TYPES = new Set([\n \"application/zip\",\n \"application/x-ole-storage\",\n \"video/mp4\",\n]);\n\nfunction isStrongMagicMimeType(mimeType: string | null): boolean {\n return Boolean(mimeType && !WEAK_MAGIC_MIME_TYPES.has(mimeType));\n}\n\nfunction sanitizeRemoteFileName(fileName: string): string {\n const cleanName = fileName\n .replace(/[\\\\/:*?\"<>|]/g, \"_\")\n .replace(/\\s+/g, \" \")\n .trim();\n\n return cleanName || \"remote-file\";\n}\n\nfunction tryDecodeURIComponent(value: string): string {\n try {\n return decodeURIComponent(value);\n } catch {\n return value;\n }\n}\n\nfunction stripQuotes(value: string): string {\n const trimmed = value.trim();\n\n if (\n (trimmed.startsWith('\"') && trimmed.endsWith('\"')) ||\n (trimmed.startsWith(\"'\") && trimmed.endsWith(\"'\"))\n ) {\n return trimmed.slice(1, -1);\n }\n\n return trimmed;\n}\n\nfunction getExtension(fileName: string): string {\n const cleanName = fileName.split(\"?\")[0].split(\"#\")[0];\n const parts = cleanName.toLowerCase().split(\".\");\n\n return parts.length > 1 ? parts[parts.length - 1] : \"\";\n}\n\nfunction normalizeHeaderMimeType(headerMimeType: string): string {\n const mimeType = headerMimeType.split(\";\")[0]?.trim().toLowerCase() || \"\";\n\n return GENERIC_MIME_TYPES.has(mimeType) ? \"\" : mimeType;\n}\n\nfunction getHeaderMimeType(response: Response): string {\n return response.headers.get(\"content-type\")?.split(\";\")[0]?.trim() || \"\";\n}\n\nfunction getFileNameFromContentDisposition(\n contentDisposition?: string | null\n): string | null {\n if (!contentDisposition) return null;\n\n const filenameStarMatch = contentDisposition.match(\n /filename\\*\\s*=\\s*([^;]+)/i\n );\n\n if (filenameStarMatch?.[1]) {\n const rawValue = stripQuotes(filenameStarMatch[1]);\n\n const rfc5987Match = rawValue.match(/^[^']*'[^']*'(.+)$/);\n const encodedValue = rfc5987Match?.[1] || rawValue;\n\n return sanitizeRemoteFileName(tryDecodeURIComponent(encodedValue));\n }\n\n const filenameMatch = contentDisposition.match(/filename\\s*=\\s*([^;]+)/i);\n\n if (filenameMatch?.[1]) {\n return sanitizeRemoteFileName(\n tryDecodeURIComponent(stripQuotes(filenameMatch[1]))\n );\n }\n\n return null;\n}\n\nfunction getRemoteFileName(\n rawUrl: string,\n contentDisposition?: string | null\n): FileNameResult {\n const fromDisposition = getFileNameFromContentDisposition(contentDisposition);\n\n if (fromDisposition) {\n return {\n fileName: fromDisposition,\n source: \"content-disposition\",\n };\n }\n\n try {\n const url = new URL(rawUrl);\n const params = url.searchParams;\n\n const queryKeys = [\n \"showname\",\n \"filename\",\n \"fileName\",\n \"name\",\n \"file\",\n \"download\",\n ];\n\n let firstQueryCandidate: string | null = null;\n\n for (const key of queryKeys) {\n const value = params.get(key)?.trim();\n\n if (!value || value.toLowerCase() === \"true\") {\n continue;\n }\n\n const candidate = sanitizeRemoteFileName(value);\n\n if (!firstQueryCandidate) {\n firstQueryCandidate = candidate;\n }\n\n if (getExtension(candidate)) {\n return {\n fileName: candidate,\n source: \"query\",\n };\n }\n }\n\n const pathname = decodeURIComponent(url.pathname);\n const pathnameName = pathname.split(\"/\").filter(Boolean).pop();\n\n if (pathnameName?.trim()) {\n const candidate = sanitizeRemoteFileName(pathnameName.trim());\n\n if (getExtension(candidate)) {\n return {\n fileName: candidate,\n source: \"pathname\",\n };\n }\n }\n\n if (firstQueryCandidate) {\n return {\n fileName: firstQueryCandidate,\n source: \"query\",\n };\n }\n\n if (pathnameName?.trim()) {\n return {\n fileName: sanitizeRemoteFileName(pathnameName.trim()),\n source: \"pathname\",\n };\n }\n\n return {\n fileName: \"remote-file\",\n source: \"fallback\",\n };\n } catch {\n return {\n fileName: \"remote-file\",\n source: \"fallback\",\n };\n }\n}\n\n\nfunction resolveRemoteMimeType(input: {\n fileName: string;\n headerMimeType: string;\n magicMimeType: string | null;\n containerMimeType: string | null;\n}): MimeResult {\n const ext = getExtension(input.fileName);\n const mimeFromExtension = REMOTE_MIME_BY_EXTENSION[ext];\n const mimeFromHeader = normalizeHeaderMimeType(input.headerMimeType);\n\n // 1. ZIP internal container (docx/pptx/xlsx/epub)\n if (input.containerMimeType) {\n return {\n mimeType: input.containerMimeType,\n source: \"container\",\n };\n }\n\n // 2. Strong magic (PDF/PNG/JPG/GIF/WEBP)\n if (isStrongMagicMimeType(input.magicMimeType)) {\n return {\n mimeType: input.magicMimeType!,\n source: \"magic\",\n };\n }\n\n // 3. Explicit extension wins over weak magic (zip/ole/ftyp)\n if (mimeFromExtension) {\n return {\n mimeType: mimeFromExtension,\n source: \"extension\",\n };\n }\n\n // 4. Content-Type header\n if (mimeFromHeader) {\n return {\n mimeType: mimeFromHeader,\n source: \"header\",\n };\n }\n\n // 5. Weak magic as last resort before fallback\n if (input.magicMimeType) {\n return {\n mimeType: input.magicMimeType,\n source: \"magic\",\n };\n }\n\n return {\n mimeType: \"application/octet-stream\",\n source: \"fallback\",\n };\n}\n\nfunction getContentLength(response: Response): number | null {\n const value = response.headers.get(\"content-length\");\n if (!value) return null;\n\n const parsed = Number(value);\n return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;\n}\n\nasync function readResponseAsArrayBufferWithProgress(\n response: Response,\n options: ProcessRemoteUrlOptions,\n url: string\n): Promise<ArrayBuffer> {\n const total = getContentLength(response);\n const maxBytes = options.maxBytes ?? DEFAULT_REMOTE_MAX_BYTES;\n\n // Pre-flight: if the server tells us up front the file is too big, refuse\n // before allocating any memory or transferring the body.\n if (total !== null && total > maxBytes) {\n throw new RemoteUrlError(\n \"FILE_TOO_LARGE\",\n `Remote file is ${total} bytes, exceeds the ${maxBytes}-byte limit.`,\n url\n );\n }\n\n if (!response.body) {\n const buffer = await response.arrayBuffer();\n if (buffer.byteLength > maxBytes) {\n throw new RemoteUrlError(\n \"FILE_TOO_LARGE\",\n `Remote file is ${buffer.byteLength} bytes, exceeds the ${maxBytes}-byte limit.`,\n url\n );\n }\n options.onProgress?.({\n received: buffer.byteLength,\n total,\n percent: total ? buffer.byteLength / total : null,\n });\n return buffer;\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let received = 0;\n\n try {\n while (true) {\n if (options.signal?.aborted) {\n try {\n await reader.cancel();\n } catch {\n // ignore\n }\n throw new DOMException(\"The operation was aborted.\", \"AbortError\");\n }\n\n const { done, value } = await reader.read();\n\n if (done) break;\n\n if (value) {\n chunks.push(value);\n received += value.byteLength;\n\n // No Content-Length (or a lying one): abort as soon as we cross the\n // cap so an unbounded stream can't drain memory.\n if (received > maxBytes) {\n try {\n await reader.cancel();\n } catch {\n // ignore\n }\n throw new RemoteUrlError(\n \"FILE_TOO_LARGE\",\n `Remote file exceeded the ${maxBytes}-byte limit after ${received} bytes (no reliable Content-Length).`,\n url\n );\n }\n\n options.onProgress?.({\n received,\n total,\n percent: total ? received / total : null,\n });\n }\n }\n } finally {\n try {\n reader.releaseLock();\n } catch {\n // ignore\n }\n }\n\n const merged = new Uint8Array(received);\n let offset = 0;\n\n for (const chunk of chunks) {\n merged.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n return merged.buffer;\n}\n\nexport async function processRemoteUrl(\n rawUrl: string,\n options: ProcessRemoteUrlOptions = {}\n): Promise<FileInfo> {\n const trimmedUrl = rawUrl.trim();\n\n if (!trimmedUrl) {\n throw new RemoteUrlError(\"INVALID_URL\", \"Remote URL is empty\");\n }\n\n let parsedUrl: URL;\n\n try {\n parsedUrl = new URL(trimmedUrl);\n } catch {\n throw new RemoteUrlError(\"INVALID_URL\", \"Please enter a valid URL\");\n }\n\n if (![\"http:\", \"https:\"].includes(parsedUrl.protocol)) {\n throw new RemoteUrlError(\n \"UNSUPPORTED_PROTOCOL\",\n \"Only http/https URLs are supported\",\n parsedUrl.toString()\n );\n }\n\n let response: Response;\n let buffer: ArrayBuffer;\n\n try {\n response = await fetch(parsedUrl.toString(), {\n signal: options.signal,\n });\n\n if (!response.ok) {\n throw new RemoteUrlError(\n \"HTTP_ERROR\",\n `远程文件请求失败:HTTP ${response.status}`,\n parsedUrl.toString()\n );\n }\n\n const headerMimeType = getHeaderMimeType(response);\n const contentDisposition = response.headers.get(\"content-disposition\");\n\n const fileNameResult = getRemoteFileName(\n parsedUrl.toString(),\n contentDisposition\n );\n\n buffer = await readResponseAsArrayBufferWithProgress(\n response,\n options,\n parsedUrl.toString()\n );\n\n const magicResult = sniffMagic(buffer);\n\n const containerResult =\n magicResult.ext === \"zip\" ? await sniffZipContainer(buffer) : null;\n\n const mimeResult = resolveRemoteMimeType({\n fileName: fileNameResult.fileName,\n headerMimeType,\n magicMimeType: magicResult.mimeType,\n containerMimeType: containerResult?.mimeType ?? null,\n });\n\n const fileType = detectFileType(fileNameResult.fileName, mimeResult.mimeType);\n\n return {\n id: generateId(),\n name: fileNameResult.fileName,\n size: buffer.byteLength,\n type: mimeResult.mimeType,\n fileType,\n source: {\n kind: \"arrayBuffer\",\n buffer,\n name: fileNameResult.fileName,\n mimeType: mimeResult.mimeType,\n },\n };\n } catch (error) {\n if (error instanceof RemoteUrlError) {\n throw error;\n }\n\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw new RemoteUrlError(\n \"ABORTED\",\n \"Remote file loading cancelled\",\n parsedUrl.toString()\n );\n }\n\n throw new RemoteUrlError(\n \"NETWORK_OR_CORS\",\n \"无法加载远程文件。可能是 URL 不可访问,或目标服务器未允许浏览器跨域访问。\",\n parsedUrl.toString()\n );\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,143 @@
1
+ # Supported formats
2
+
3
+ > **Read this before integrating.** This page describes the *boundaries* of
4
+ > what the library can render. The headline list of "20+ formats" only buys
5
+ > you so much — the realistic limits below are what you will actually
6
+ > encounter in production.
7
+ >
8
+ > Source of truth for the per-format status: `PREVIEW_SUPPORT_MATRIX` in
9
+ > `support-status.ts`. Anything here disagreeing with the code is a doc
10
+ > bug.
11
+
12
+ ---
13
+
14
+ ## 1 · Quick reference table
15
+
16
+ | Format | Status | Underlying renderer | What we render | What we **don't** guarantee |
17
+ | --- | --- | --- | --- | --- |
18
+ | **PDF** | ✅ supported | [`pdfjs-dist`](https://www.npmjs.com/package/pdfjs-dist) | Pages, text selection, basic forms | Form submission, annotations editing, embedded JavaScript |
19
+ | **DOCX** | ✅ supported | [`docx-preview`](https://www.npmjs.com/package/docx-preview) | Body text, lists, tables, inline images | Pixel-identical Word rendering, comments, tracked changes, complex section breaks |
20
+ | **XLSX** | ✅ supported | [`exceljs`](https://www.npmjs.com/package/exceljs) | Sheets, cells, basic formatting, **stored** formula results | Formula recalculation, charts, pivot tables, conditional-format rendering, macros |
21
+ | **PPTX** | ✅ supported | [`pptx-preview`](https://www.npmjs.com/package/pptx-preview) | Slide thumbnails, text, layouts | Animations, transitions, embedded video, complex masters, EMF/WMF images |
22
+ | **EPUB** | ✅ supported | [`jszip`](https://www.npmjs.com/package/jszip) + custom | Chapters, table of contents, embedded images | DRM-protected EPUBs, fixed-layout EPUBs, audio/video embeds |
23
+ | **RTF** | ✅ supported | [`rtf.js`](https://www.npmjs.com/package/rtf.js) | Rich text, WMF/EMF vectors via embedded engine | Anything `rtf.js` itself can't parse — falls back to plain-text view automatically |
24
+ | **Markdown** | ✅ supported | `react-markdown` + `remark-gfm` + Shiki | GFM (tables, task lists, fenced code with syntax highlighting), HTML in MD is sanitized | Custom MDX components, raw `<script>` (stripped) |
25
+ | **HTML** | ✅ supported | DOMPurify + `<iframe sandbox>` | Sanitized HTML preview + raw source view | Inline scripts, external resource loads (sandbox blocks them by default) |
26
+ | **SVG** | ✅ supported | DOMPurify + sanitized `<img>` / inline | Scaled vector preview, source view | Inline scripts (stripped), external `<image href="…">` referencing untrusted hosts |
27
+ | **Code** (50+ languages) | ✅ supported | [Shiki](https://shiki.matsu.io/) | Token-accurate syntax highlighting, line numbers | Code execution, language server features (hover, jump-to-def) |
28
+ | **JSON** | ✅ supported | Shiki (routed through source-code plugin) | Syntax highlighting, line numbers | Schema-aware tree view, JSON-Path search |
29
+ | **Plain text** | ✅ supported | built-in | Line numbers, word wrap, large-file streaming view | Encoding auto-detection beyond UTF-8 BOM |
30
+ | **CSV** | ✅ supported | built-in | Sortable table | Multi-million-row datasets (use the streaming text view) |
31
+ | **Image** (png/jpg/gif/webp/avif/…) | ✅ supported | browser-native | Zoom, rotate, fit-to-screen | TIFF, HEIC, RAW formats |
32
+ | **Audio / Video** | ✅ supported | browser-native `<audio>` / `<video>` | Playback for codecs the browser supports | Codec compatibility (MOV/HEVC/HEIF on non-Apple browsers) |
33
+ | **ZIP** | ⚠️ listing only | `jszip` | Entry tree, sizes, contents browsing | Auto-recursive preview of inner files (intentional — would amplify zip-bombs) |
34
+ | **DOC** (legacy) | ⚠️ degraded | none | Plain-text extraction only | Layout, images, formatting. Convert to `.docx` for full preview. |
35
+ | **PPT** (legacy) | ❌ not supported | — | — | Convert to `.pptx`. |
36
+ | **XLS** (legacy) | ❌ not supported | — | — | Convert to `.xlsx`. |
37
+
38
+ > ✅ supported · ⚠️ supported with a documented limit · ❌ not supported
39
+
40
+ ---
41
+
42
+ ## 2 · What "supported" actually means
43
+
44
+ Browser-side parsing of office formats is **best-effort visual approximation**,
45
+ not faithful reproduction of the desktop apps. The library deliberately does
46
+ not promise:
47
+
48
+ - **Pixel-perfect Word/Excel/PowerPoint output.** No browser can fully
49
+ emulate desktop rendering — fonts differ, paginations drift, complex
50
+ layouts simplify.
51
+ - **Formula recomputation in XLSX.** We display whatever values are stored
52
+ in the file. If you need fresh recomputation, run a server-side process.
53
+ - **Macros, scripts, executable content.** Macros in `.xlsm` / `.docm`,
54
+ PDF JavaScript, embedded `.exe` payloads — all ignored or stripped.
55
+ This is a security feature, not a missing feature.
56
+ - **PowerPoint animations / transitions / embedded media playback.**
57
+ - **Identical output across browsers.** Audio/video codec support, color
58
+ management, and font fallback differ.
59
+ - **DRM-protected content** of any kind.
60
+
61
+ Setting consumer expectations early (in your product UI) avoids "your
62
+ preview doesn't match Word!" support tickets. We strongly recommend
63
+ labeling office previews as "preview" rather than "view" and offering a
64
+ download button as a peer to the preview.
65
+
66
+ ---
67
+
68
+ ## 3 · Security posture
69
+
70
+ - **HTML, SVG, RTF**: all run through DOMPurify; HTML additionally renders
71
+ inside `<iframe sandbox>` (no `allow-scripts`, `allow-same-origin`,
72
+ etc.) so embedded scripts can't reach your page.
73
+ - **PDF**: `pdfjs-dist` renders the document content but does not execute
74
+ embedded PDF JavaScript by default.
75
+ - **ZIP**: only the entry list is parsed; inner files aren't auto-decoded.
76
+ This is deliberate — auto-decompressing nested archives is the classic
77
+ zip-bomb vector.
78
+ - **All renderers** read from a `PreviewSource` (file/blob/arrayBuffer/url).
79
+ When the source is `url`, `processRemoteUrl` enforces `maxBytes`
80
+ (default 100 MB) and rejects oversize bodies before they hit memory.
81
+
82
+ ---
83
+
84
+ ## 4 · Performance limits
85
+
86
+ The library applies a built-in `LargeFileGate` (since 0.3.0) to every
87
+ preview rendered via `<PluginPreviewRenderer>`:
88
+
89
+ | File size | Behavior |
90
+ | --- | --- |
91
+ | **< 20 MB** | Renders normally |
92
+ | **20–50 MB** | Renders with a non-blocking "may be slower" banner |
93
+ | **50–100 MB** | Requires explicit confirmation ("Preview anyway") |
94
+ | **≥ 100 MB** | Refuses to preview; offers download only |
95
+
96
+ Disable via `<PluginPreviewRenderer largeFilePolicy="off" />` if you need
97
+ to (e.g. trusted internal-only content with bounded sizes).
98
+
99
+ For remote URLs, `processRemoteUrl({ maxBytes })` enforces a parallel cap
100
+ during download — even if `LargeFileGate` is off, an unbounded URL can't
101
+ exhaust memory:
102
+
103
+ - Server sends `Content-Length` larger than `maxBytes` → rejected
104
+ pre-flight, zero bytes transferred.
105
+ - Server omits `Content-Length` → reader is aborted the moment received
106
+ bytes cross `maxBytes`.
107
+
108
+ ---
109
+
110
+ ## 5 · Optional peer dependencies
111
+
112
+ To preview the heavy formats you must also install the format's peer
113
+ dependency. Without it, the matched plugin throws a
114
+ `MissingPeerDependencyError` with an install hint instead of a cryptic
115
+ bundler error:
116
+
117
+ | Format | Add this peer dep |
118
+ | --- | --- |
119
+ | PDF | `pdfjs-dist` (^4.4.0) |
120
+ | DOCX | `docx-preview` (^0.3.7) |
121
+ | XLSX | `exceljs` (^4.4.0) |
122
+ | PPTX | `pptx-preview` (^1.0.7) |
123
+ | RTF | `rtf.js` (^3.0.9) |
124
+ | ZIP / EPUB | `jszip` (^3.10.1) |
125
+
126
+ The base install only ships dompurify / react-markdown / remark-gfm /
127
+ shiki — enough for Markdown / HTML / SVG / code / image / video / audio /
128
+ CSV / JSON / plain text out of the box.
129
+
130
+ ---
131
+
132
+ ## 6 · Browser support
133
+
134
+ | Browser | Minimum |
135
+ | --- | --- |
136
+ | Chrome / Edge | 90+ |
137
+ | Firefox | 88+ |
138
+ | Safari | 14+ |
139
+
140
+ The library uses `File`/`Blob`/`ArrayBuffer`, `fetch`, `URL.createObjectURL`,
141
+ dynamic `import()`, and `crypto.randomUUID()`. PDF preview additionally
142
+ spawns a Web Worker. No polyfills are shipped — older browsers need the
143
+ relevant polyfills from the consumer.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamberl-lee/file-preview",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Browser-side file preview toolkit for React — 20+ formats (PDF, DOCX, PPTX, XLSX, EPUB, RTF, code, images, video, audio …) with zero server processing.",
5
5
  "keywords": [
6
6
  "react",
@@ -47,19 +47,11 @@
47
47
  "files": [
48
48
  "dist",
49
49
  "scripts",
50
+ "docs",
50
51
  "README.md",
51
52
  "LICENSE",
52
53
  "COPYING"
53
54
  ],
54
- "scripts": {
55
- "build": "tsup",
56
- "dev": "tsup --watch",
57
- "typecheck": "tsc --noEmit",
58
- "test": "vitest run",
59
- "test:watch": "vitest",
60
- "copy:pdf-worker": "node scripts/copy-pdf-worker.mjs",
61
- "copy:rtfjs-bundles": "node scripts/copy-rtfjs-bundles.mjs"
62
- },
63
55
  "peerDependencies": {
64
56
  "react": "^18.2.0 || ^19.0.0",
65
57
  "react-dom": "^18.2.0 || ^19.0.0",
@@ -121,5 +113,14 @@
121
113
  "publishConfig": {
122
114
  "access": "public",
123
115
  "registry": "https://registry.npmjs.org"
116
+ },
117
+ "scripts": {
118
+ "build": "tsup",
119
+ "dev": "tsup --watch",
120
+ "typecheck": "tsc --noEmit",
121
+ "test": "vitest run",
122
+ "test:watch": "vitest",
123
+ "copy:pdf-worker": "node scripts/copy-pdf-worker.mjs",
124
+ "copy:rtfjs-bundles": "node scripts/copy-rtfjs-bundles.mjs"
124
125
  }
125
- }
126
+ }