@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.
- package/README.md +6 -0
- package/dist/LargeFileGate.d.ts +23 -3
- package/dist/LargeFileGate.js +44 -40
- package/dist/LargeFileGate.js.map +1 -1
- package/dist/PluginPreviewRenderer.d.ts +20 -1
- package/dist/PluginPreviewRenderer.js +51 -10
- package/dist/PluginPreviewRenderer.js.map +1 -1
- package/dist/PreviewErrorBoundary.d.ts +2 -0
- package/dist/PreviewErrorBoundary.js +11 -2
- package/dist/PreviewErrorBoundary.js.map +1 -1
- package/dist/core/detect-meta.d.ts +73 -0
- package/dist/core/detect-meta.js +81 -0
- package/dist/core/detect-meta.js.map +1 -0
- package/dist/core/magic-bytes.d.ts +56 -0
- package/dist/core/magic-bytes.js +97 -0
- package/dist/core/magic-bytes.js.map +1 -0
- package/dist/core/plugin.d.ts +2 -2
- package/dist/core/plugin.js +5 -3
- package/dist/core/plugin.js.map +1 -1
- package/dist/core/preview-error.d.ts +35 -0
- package/dist/core/preview-error.js +39 -0
- package/dist/core/preview-error.js.map +1 -0
- package/dist/core/registry.d.ts +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/audio-plugin.d.ts +1 -0
- package/dist/plugins/builtin-plugins.d.ts +1 -0
- package/dist/plugins/csv-plugin.d.ts +1 -0
- package/dist/plugins/docx-plugin.d.ts +1 -0
- package/dist/plugins/epub-plugin.d.ts +1 -0
- package/dist/plugins/html-plugin.d.ts +1 -0
- package/dist/plugins/image-plugin.d.ts +1 -0
- package/dist/plugins/markdown-plugin.d.ts +1 -0
- package/dist/plugins/pdf-plugin.d.ts +1 -0
- package/dist/plugins/pptx-plugin.d.ts +1 -0
- package/dist/plugins/rtf-plugin.d.ts +1 -0
- package/dist/plugins/source-code-plugin.d.ts +1 -0
- package/dist/plugins/svg-plugin.d.ts +1 -0
- package/dist/plugins/text-plugin.d.ts +1 -0
- package/dist/plugins/video-plugin.d.ts +1 -0
- package/dist/plugins/xlsx-plugin.d.ts +1 -0
- package/dist/plugins/zip-plugin.d.ts +1 -0
- package/dist/remote-url.d.ts +20 -5
- package/dist/remote-url.js +52 -128
- package/dist/remote-url.js.map +1 -1
- package/docs/supported-formats.md +143 -0
- package/package.json +12 -11
package/dist/remote-url.d.ts
CHANGED
|
@@ -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
|
|
5
|
-
|
|
6
|
-
url?: string
|
|
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 };
|
package/dist/remote-url.js
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
import { detectFileType, generateId } from "./utils";
|
|
2
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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(
|
|
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
|
};
|
package/dist/remote-url.js.map
CHANGED
|
@@ -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":"AAAA,SAAS,gBAAgB,kBAAkB;AAWpC,MAAM,uBAAuB,MAAM;AAAA,EACxC,YACS,MACP,SACO,KACP;AACA,UAAM,OAAO;AAJN;AAEA;AAGP,SAAK,OAAO;AAAA,EACd;AACF;AA8CA,MAAM,2BAAmD;AAAA,EACvD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EAEN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EAEL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EAEN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EAEL,KAAK;AAAA,EACL,KAAK;AAAA,EAEL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EAEN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EAEL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AACP;AAEA,MAAM,qBAAqB,oBAAI,IAAI;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,MAAM,wBAAwB,oBAAI,IAAI;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,sBAAsB,UAAkC;AAC/D,SAAO,QAAQ,YAAY,CAAC,sBAAsB,IAAI,QAAQ,CAAC;AACjE;AAEA,SAAS,uBAAuB,UAA0B;AACxD,QAAM,YAAY,SACf,QAAQ,iBAAiB,GAAG,EAC5B,QAAQ,QAAQ,GAAG,EACnB,KAAK;AAER,SAAO,aAAa;AACtB;AAEA,SAAS,sBAAsB,OAAuB;AACpD,MAAI;AACF,WAAO,mBAAmB,KAAK;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,YAAY,OAAuB;AAC1C,QAAM,UAAU,MAAM,KAAK;AAE3B,MACG,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,KAC/C,QAAQ,WAAW,GAAG,KAAK,QAAQ,SAAS,GAAG,GAChD;AACA,WAAO,QAAQ,MAAM,GAAG,EAAE;AAAA,EAC5B;AAEA,SAAO;AACT;AAEA,SAAS,aAAa,UAA0B;AAC9C,QAAM,YAAY,SAAS,MAAM,GAAG,EAAE,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC;AACrD,QAAM,QAAQ,UAAU,YAAY,EAAE,MAAM,GAAG;AAE/C,SAAO,MAAM,SAAS,IAAI,MAAM,MAAM,SAAS,CAAC,IAAI;AACtD;AAEA,SAAS,wBAAwB,gBAAgC;AAC/D,QAAM,WAAW,eAAe,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,YAAY,KAAK;AAEvE,SAAO,mBAAmB,IAAI,QAAQ,IAAI,KAAK;AACjD;AAEA,SAAS,kBAAkB,UAA4B;AACrD,SAAO,SAAS,QAAQ,IAAI,cAAc,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,KAAK;AACxE;AAEA,SAAS,kCACP,oBACe;AACf,MAAI,CAAC,mBAAoB,QAAO;AAEhC,QAAM,oBAAoB,mBAAmB;AAAA,IAC3C;AAAA,EACF;AAEA,MAAI,oBAAoB,CAAC,GAAG;AAC1B,UAAM,WAAW,YAAY,kBAAkB,CAAC,CAAC;AAEjD,UAAM,eAAe,SAAS,MAAM,oBAAoB;AACxD,UAAM,eAAe,eAAe,CAAC,KAAK;AAE1C,WAAO,uBAAuB,sBAAsB,YAAY,CAAC;AAAA,EACnE;AAEA,QAAM,gBAAgB,mBAAmB,MAAM,yBAAyB;AAExE,MAAI,gBAAgB,CAAC,GAAG;AACtB,WAAO;AAAA,MACL,sBAAsB,YAAY,cAAc,CAAC,CAAC,CAAC;AAAA,IACrD;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,kBACP,QACA,oBACgB;AAChB,QAAM,kBAAkB,kCAAkC,kBAAkB;AAE5E,MAAI,iBAAiB;AACnB,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,IACV;AAAA,EACF;AAEA,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,MAAM;AAC1B,UAAM,SAAS,IAAI;AAEnB,UAAM,YAAY;AAAA,MAChB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,QAAI,sBAAqC;AAEzC,eAAW,OAAO,WAAW;AAC3B,YAAM,QAAQ,OAAO,IAAI,GAAG,GAAG,KAAK;AAEpC,UAAI,CAAC,SAAS,MAAM,YAAY,MAAM,QAAQ;AAC5C;AAAA,MACF;AAEA,YAAM,YAAY,uBAAuB,KAAK;AAE9C,UAAI,CAAC,qBAAqB;AACxB,8BAAsB;AAAA,MACxB;AAEA,UAAI,aAAa,SAAS,GAAG;AAC3B,eAAO;AAAA,UACL,UAAU;AAAA,UACV,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAEA,UAAM,WAAW,mBAAmB,IAAI,QAAQ;AAChD,UAAM,eAAe,SAAS,MAAM,GAAG,EAAE,OAAO,OAAO,EAAE,IAAI;AAE7D,QAAI,cAAc,KAAK,GAAG;AACxB,YAAM,YAAY,uBAAuB,aAAa,KAAK,CAAC;AAE5D,UAAI,aAAa,SAAS,GAAG;AAC3B,eAAO;AAAA,UACL,UAAU;AAAA,UACV,QAAQ;AAAA,QACV;AAAA,MACF;AAAA,IACF;AAEA,QAAI,qBAAqB;AACvB,aAAO;AAAA,QACL,UAAU;AAAA,QACV,QAAQ;AAAA,MACV;AAAA,IACF;AAEA,QAAI,cAAc,KAAK,GAAG;AACxB,aAAO;AAAA,QACL,UAAU,uBAAuB,aAAa,KAAK,CAAC;AAAA,QACpD,QAAQ;AAAA,MACV;AAAA,IACF;AAEA,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,IACV;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,IACV;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,OAAmB,WAA8B;AACxE,MAAI,MAAM,SAAS,UAAU,OAAQ,QAAO;AAE5C,SAAO,UAAU,MAAM,CAAC,OAAO,UAAU,MAAM,KAAK,MAAM,KAAK;AACjE;AAEA,SAAS,UAAU,OAAmB,OAAe,QAAwB;AAC3E,SAAO,OAAO,aAAa,GAAG,MAAM,SAAS,OAAO,QAAQ,MAAM,CAAC;AACrE;AAEA,SAAS,WAAW,QAAuC;AACzD,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM,GAAG,EAAE,CAAC;AAEhD,MAAI,UAAU,OAAO,GAAG,CAAC,MAAM,SAAS;AACtC,WAAO;AAAA,MACL,KAAK;AAAA,MACL,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,MACE,gBAAgB,OAAO,CAAC,IAAM,IAAM,GAAM,CAAI,CAAC,KAC/C,gBAAgB,OAAO,CAAC,IAAM,IAAM,GAAM,CAAI,CAAC,KAC/C,gBAAgB,OAAO,CAAC,IAAM,IAAM,GAAM,CAAI,CAAC,GAC/C;AACA,WAAO;AAAA,MACL,KAAK;AAAA,MACL,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,MACE,gBAAgB,OAAO;AAAA,IACrB;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,EAC5C,CAAC,GACD;AACA,WAAO;AAAA,MACL,KAAK;AAAA,MACL,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,MAAI,gBAAgB,OAAO,CAAC,KAAM,KAAM,GAAI,CAAC,GAAG;AAC9C,WAAO;AAAA,MACL,KAAK;AAAA,MACL,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,QAAM,YAAY,UAAU,OAAO,GAAG,CAAC;AAEvC,MAAI,cAAc,YAAY,cAAc,UAAU;AACpD,WAAO;AAAA,MACL,KAAK;AAAA,MACL,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,MAAI,UAAU,OAAO,GAAG,CAAC,MAAM,UAAU,UAAU,OAAO,GAAG,CAAC,MAAM,QAAQ;AAC1E,WAAO;AAAA,MACL,KAAK;AAAA,MACL,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,MAAI,MAAM,UAAU,MAAM,UAAU,OAAO,GAAG,CAAC,MAAM,QAAQ;AAC3D,WAAO;AAAA,MACL,KAAK;AAAA,MACL,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,MACE,gBAAgB,OAAO;AAAA,IACrB;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,EAC5C,CAAC,GACD;AACA,WAAO;AAAA,MACL,KAAK;AAAA,MACL,UAAU;AAAA,IACZ;AAAA,EACF;AAEA,SAAO;AAAA,IACL,KAAK;AAAA,IACL,UAAU;AAAA,EACZ;AACF;AAEA,eAAe,kBACb,QACsC;AACtC,MAAI;AACF,UAAM,EAAE,SAAS,MAAM,IAAI,MAAM,OAAO,OAAO;AAC/C,UAAM,MAAM,MAAM,MAAM,UAAU,MAAM;AAExC,UAAM,YAAY,OAAO,KAAK,IAAI,KAAK,EAAE;AAAA,MAAI,CAAC,SAC5C,KAAK,QAAQ,OAAO,GAAG,EAAE,YAAY;AAAA,IACvC;AAEA,UAAM,UAAU,CAAC,WAAmB,UAAU,SAAS,MAAM;AAE7D,QAAI,QAAQ,mBAAmB,GAAG;AAChC,aAAO;AAAA,QACL,KAAK;AAAA,QACL,UACE;AAAA,MACJ;AAAA,IACF;AAEA,QAAI,QAAQ,sBAAsB,GAAG;AACnC,aAAO;AAAA,QACL,KAAK;AAAA,QACL,UACE;AAAA,MACJ;AAAA,IACF;AAEA,QAAI,QAAQ,iBAAiB,GAAG;AAC9B,aAAO;AAAA,QACL,KAAK;AAAA,QACL,UACE;AAAA,MACJ;AAAA,IACF;AAEA,UAAM,eAAe,IAAI,KAAK,UAAU;AAExC,QAAI,cAAc;AAChB,YAAM,YAAY,MAAM,aAAa,MAAM,QAAQ,GAAG,KAAK;AAE3D,UAAI,aAAa,wBAAwB;AACvC,eAAO;AAAA,UACL,KAAK;AAAA,UACL,UAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,sBAAsB,OAKhB;AACb,QAAM,MAAM,aAAa,MAAM,QAAQ;AACvC,QAAM,oBAAoB,yBAAyB,GAAG;AACtD,QAAM,iBAAiB,wBAAwB,MAAM,cAAc;AAGnE,MAAI,MAAM,mBAAmB;AAC3B,WAAO;AAAA,MACL,UAAU,MAAM;AAAA,MAChB,QAAQ;AAAA,IACV;AAAA,EACF;AAGA,MAAI,sBAAsB,MAAM,aAAa,GAAG;AAC9C,WAAO;AAAA,MACL,UAAU,MAAM;AAAA,MAChB,QAAQ;AAAA,IACV;AAAA,EACF;AAGA,MAAI,mBAAmB;AACrB,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,IACV;AAAA,EACF;AAGA,MAAI,gBAAgB;AAClB,WAAO;AAAA,MACL,UAAU;AAAA,MACV,QAAQ;AAAA,IACV;AAAA,EACF;AAGA,MAAI,MAAM,eAAe;AACvB,WAAO;AAAA,MACL,UAAU,MAAM;AAAA,MAChB,QAAQ;AAAA,IACV;AAAA,EACF;AAEA,SAAO;AAAA,IACL,UAAU;AAAA,IACV,QAAQ;AAAA,EACV;AACF;AAEA,SAAS,iBAAiB,UAAmC;AAC3D,QAAM,QAAQ,SAAS,QAAQ,IAAI,gBAAgB;AACnD,MAAI,CAAC,MAAO,QAAO;AAEnB,QAAM,SAAS,OAAO,KAAK;AAC3B,SAAO,OAAO,SAAS,MAAM,KAAK,UAAU,IAAI,SAAS;AAC3D;AAEA,eAAe,sCACb,UACA,SACsB;AACtB,QAAM,QAAQ,iBAAiB,QAAQ;AAEvC,MAAI,CAAC,SAAS,MAAM;AAClB,UAAM,SAAS,MAAM,SAAS,YAAY;AAC1C,YAAQ,aAAa;AAAA,MACnB,UAAU,OAAO;AAAA,MACjB;AAAA,MACA,SAAS,QAAQ,OAAO,aAAa,QAAQ;AAAA,IAC/C,CAAC;AACD,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,SAAS,KAAK,UAAU;AACvC,QAAM,SAAuB,CAAC;AAC9B,MAAI,WAAW;AAEf,MAAI;AACF,WAAO,MAAM;AACX,UAAI,QAAQ,QAAQ,SAAS;AAC3B,YAAI;AACF,gBAAM,OAAO,OAAO;AAAA,QACtB,QAAQ;AAAA,QAER;AACA,cAAM,IAAI,aAAa,8BAA8B,YAAY;AAAA,MACnE;AAEA,YAAM,EAAE,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AAE1C,UAAI,KAAM;AAEV,UAAI,OAAO;AACT,eAAO,KAAK,KAAK;AACjB,oBAAY,MAAM;AAElB,gBAAQ,aAAa;AAAA,UACnB;AAAA,UACA;AAAA,UACA,SAAS,QAAQ,WAAW,QAAQ;AAAA,QACtC,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,UAAE;AACA,QAAI;AACF,aAAO,YAAY;AAAA,IACrB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,SAAS,IAAI,WAAW,QAAQ;AACtC,MAAI,SAAS;AAEb,aAAW,SAAS,QAAQ;AAC1B,WAAO,IAAI,OAAO,MAAM;AACxB,cAAU,MAAM;AAAA,EAClB;AAEA,SAAO,OAAO;AAChB;AAEA,eAAsB,iBACpB,QACA,UAAmC,CAAC,GACjB;AACnB,QAAM,aAAa,OAAO,KAAK;AAE/B,MAAI,CAAC,YAAY;AACf,UAAM,IAAI,eAAe,eAAe,qBAAqB;AAAA,EAC/D;AAEA,MAAI;AAEJ,MAAI;AACF,gBAAY,IAAI,IAAI,UAAU;AAAA,EAChC,QAAQ;AACN,UAAM,IAAI,eAAe,eAAe,0BAA0B;AAAA,EACpE;AAEA,MAAI,CAAC,CAAC,SAAS,QAAQ,EAAE,SAAS,UAAU,QAAQ,GAAG;AACrD,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,UAAU,SAAS;AAAA,IACrB;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AAEJ,MAAI;AACF,eAAW,MAAM,MAAM,UAAU,SAAS,GAAG;AAAA,MAC3C,QAAQ,QAAQ;AAAA,IAClB,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI;AAAA,QACR;AAAA,QACA,8DAAiB,SAAS,MAAM;AAAA,QAChC,UAAU,SAAS;AAAA,MACrB;AAAA,IACF;AAEA,UAAM,iBAAiB,kBAAkB,QAAQ;AACjD,UAAM,qBAAqB,SAAS,QAAQ,IAAI,qBAAqB;AAErE,UAAM,iBAAiB;AAAA,MACrB,UAAU,SAAS;AAAA,MACnB;AAAA,IACF;AAEA,aAAS,MAAM,sCAAsC,UAAU,OAAO;AAEtE,UAAM,cAAc,WAAW,MAAM;AAErC,UAAM,kBACJ,YAAY,QAAQ,QAAQ,MAAM,kBAAkB,MAAM,IAAI;AAEhE,UAAM,aAAa,sBAAsB;AAAA,MACvC,UAAU,eAAe;AAAA,MACzB;AAAA,MACA,eAAe,YAAY;AAAA,MAC3B,mBAAmB,iBAAiB,YAAY;AAAA,IAClD,CAAC;AAED,UAAM,WAAW,eAAe,eAAe,UAAU,WAAW,QAAQ;AAE5E,WAAO;AAAA,MACL,IAAI,WAAW;AAAA,MACf,MAAM,eAAe;AAAA,MACrB,MAAM,OAAO;AAAA,MACb,MAAM,WAAW;AAAA,MACjB;AAAA,MACA,QAAQ;AAAA,QACN,MAAM;AAAA,QACN;AAAA,QACA,MAAM,eAAe;AAAA,QACrB,UAAU,WAAW;AAAA,MACvB;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,QAAI,iBAAiB,gBAAgB;AACnC,YAAM;AAAA,IACR;AAEA,QAAI,iBAAiB,gBAAgB,MAAM,SAAS,cAAc;AAChE,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,QACA,UAAU,SAAS;AAAA,MACrB;AAAA,IACF;AAEA,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,UAAU,SAAS;AAAA,IACrB;AAAA,EACF;AACF;","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.
|
|
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
|
+
}
|