@nxgt/material-core 0.1.1 → 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 (42) hide show
  1. package/README.md +178 -5
  2. package/dist/file-preview/describe-file.d.ts +30 -0
  3. package/dist/file-preview/describe-file.d.ts.map +1 -0
  4. package/dist/file-preview/file-kind.d.ts +41 -0
  5. package/dist/file-preview/file-kind.d.ts.map +1 -0
  6. package/dist/file-preview/index.d.ts +10 -0
  7. package/dist/file-preview/index.d.ts.map +1 -0
  8. package/dist/file-preview/index.js +260 -0
  9. package/dist/file-preview/index.js.map +13 -0
  10. package/dist/file-preview/safe-url.d.ts +22 -0
  11. package/dist/file-preview/safe-url.d.ts.map +1 -0
  12. package/dist/file-preview/text-preview.d.ts +32 -0
  13. package/dist/file-preview/text-preview.d.ts.map +1 -0
  14. package/dist/lib/date-range.d.ts +84 -0
  15. package/dist/lib/date-range.d.ts.map +1 -0
  16. package/dist/lib/date-range.js +105 -0
  17. package/dist/lib/date-range.js.map +10 -0
  18. package/dist/lib/format-file-size.d.ts +10 -0
  19. package/dist/lib/format-file-size.d.ts.map +1 -0
  20. package/dist/lib/format-file-size.js +17 -0
  21. package/dist/lib/format-file-size.js.map +10 -0
  22. package/dist/lib/slider-ticks.d.ts +7 -0
  23. package/dist/lib/slider-ticks.d.ts.map +1 -0
  24. package/dist/lib/slider-ticks.js +16 -0
  25. package/dist/lib/slider-ticks.js.map +10 -0
  26. package/dist/pdf-viewer/index.d.ts +1 -0
  27. package/dist/pdf-viewer/index.d.ts.map +1 -1
  28. package/dist/pdf-viewer/index.js +68 -33
  29. package/dist/pdf-viewer/index.js.map +6 -4
  30. package/dist/pdf-viewer/pdf-pin-page.d.ts +16 -0
  31. package/dist/pdf-viewer/pdf-pin-page.d.ts.map +1 -0
  32. package/dist/pdf-viewer/pdf-scroll-spy.d.ts +2 -8
  33. package/dist/pdf-viewer/pdf-scroll-spy.d.ts.map +1 -1
  34. package/dist/pdf-viewer/pdf-user-input.d.ts +7 -0
  35. package/dist/pdf-viewer/pdf-user-input.d.ts.map +1 -0
  36. package/dist/pdfkit-fonts/index.d.ts +61 -0
  37. package/dist/pdfkit-fonts/index.d.ts.map +1 -0
  38. package/dist/pdfkit-fonts/index.js +80 -0
  39. package/dist/pdfkit-fonts/index.js.map +10 -0
  40. package/docs/roadmap.md +12 -1
  41. package/docs/troubleshooting.md +65 -0
  42. package/package.json +33 -3
package/README.md CHANGED
@@ -5,13 +5,16 @@ The framework-free core that [`@nxgt/material`](https://www.npmjs.com/package/@n
5
5
  (Vue) share, as one copy: the PDF document model and its react-pdf renderer,
6
6
  the print formatters, the PDF file helpers, the PDF viewer's framework-free
7
7
  half (page geometry, scroll spy, text search), the currency field's decimals,
8
- the fields' blur rule and the Vite plugin that bundles pdfkit's standard
9
- fonts.
8
+ the file-size formatter, the fields' blur rule, the Vite plugin that bundles
9
+ pdfkit's standard fonts, and the Node-only resolution of those fonts that the
10
+ Next.js helper and the Nuxt module run at build time.
10
11
 
11
12
  **You usually do not install it yourself.** Both libraries depend on it and
12
13
  re-export what they always exported, at the same subpaths:
13
14
  `@nxgt/material/print`, `@nxgt/material-vue/pdf`, `/lib` and `/vite` read
14
- from here, and so do their `PdfReader`, `CurrencyField` and form fields. Import it directly when you want the model or a formatter without
15
+ from here, and so do their `PdfReader`, `CurrencyField`, form fields, file
16
+ list and upload field (`formatFileSize`), `@nxgt/material/next` and
17
+ `@nxgt/material-vue/nuxt`. Import it directly when you want the model or a formatter without
15
18
  a UI library.
16
19
 
17
20
  ```ts
@@ -42,7 +45,10 @@ files are plain ES modules and also load in Node without a bundler.
42
45
  | `vite@^6 \|\| ^7 \|\| ^8` | `@nxgt/material-core/vite` — its declarations name Vite's `Plugin` type (types only); the plugin itself runs in your Vite config |
43
46
 
44
47
  Every other subpath imports none of them, and is safe to import on a server
45
- with no DOM.
48
+ with no DOM. One subpath is **Node only**: `/pdfkit-fonts` imports `node:module`
49
+ and `node:path`, for a framework's config at build time — never import it
50
+ from code a browser bundle can reach. No other subpath imports a `node:`
51
+ built-in.
46
52
 
47
53
  ## Subpaths
48
54
 
@@ -61,10 +67,15 @@ with no DOM.
61
67
  | `/pdf-source` | `PdfSource` and its helpers (`toPdfjsSource`, `pdfSourceKey`, …) |
62
68
  | `/pdf-file-name` | `resolvePdfFileName`, `DEFAULT_PDF_FILE_NAME` |
63
69
  | `/pdf-download` | `downloadPdfFile`, `printPdfFile` — in a browser |
64
- | `/pdf-viewer` | The PDF viewer's pure half: page geometry and fit (`readerFitScale`, `placeholderSize`, `rotatedSize`, `PdfPageSize`, `FitMode`), page sizes (`readPageSizes`, `createPageSizesLoader`), the scroll spy (`createScrollSpy`, `revealHit`), text search (`pageTextFromItems`, `findTextMatches`, `searchDocument`), `escapeHtml`, `mapPdfOutline` |
70
+ | `/pdf-viewer` | The PDF viewer's pure half: page geometry and fit (`readerFitScale`, `placeholderSize`, `rotatedSize`, `PdfPageSize`, `FitMode`), page sizes (`readPageSizes`, `createPageSizesLoader`), the scroll spy (`createScrollSpy`, `revealHit`, `scrollIntoArea`), the opening page's pin (`pinToPage`, whose optional `onRelease` runs once when it lets go, and `pinHolds`, whether the view is still where it puts the page), text search (`pageTextFromItems`, `findTextMatches`, `searchDocument`), `escapeHtml`, `mapPdfOutline` |
65
71
  | `/currency-digits` | `currencyFractionDigits` — the decimals `CurrencyField` shows |
66
72
  | `/field-blur-layers` | `layerOf`, `openerOf`, `FieldLayers` — whose overlay a layer of the page is, for a field's blur |
73
+ | `/format-file-size` | `formatFileSize` — a byte count as `'1.5 KB'` |
74
+ | `/slider-ticks` | `tickValues` — where a slider's tick marks sit |
75
+ | `/date-range` | `DATE_RANGE_PRESET_IDS`, `presetRange`, `resolvePresetRange`, `isSameDayRange`, `isRangeDisabled`, `withTimeOf`, `endOfDayTime`, `clampRangeEnd`, `keepRangeTimes` and the types `DayRange`, `PartialDayRange`, `DateRangePreset`, `DateRangePresetId`, `DateRangePresetOption`, `DateRangeRules` — `DateRangeField`'s presets and times |
76
+ | `/file-preview` | `fileKind`, `describeFile`, `safePreviewUrl`, `readTextPreview`, `FilePreviewHttpError`, `FilePreviewUnsafeUrlError`, `TEXT_PREVIEW_MAX_BYTES`, `fileExtension`, `fileNameFromUrl`, `dataUrlType`, `kindOfMimeType`, `normaliseMimeType`, `isTextFileKind`, `TEXT_FILE_KINDS` and the types `FileKind`, `FileKindInput`, `DescribedFile`, `FilePreviewSource`, `TextPreview` — `FilePreview`'s kind detection, URL allow-list and text reader |
67
77
  | `/vite` | `pdfkitStandardFonts()` — see below |
78
+ | `/pdfkit-fonts` | **Node only.** `pdfkitStandardFontDirs`, `pdfkitStandardFontFiles`, `PDFKIT_STANDARD_FONTS`, the types `PdfkitStandardFont` and `PdfkitFontsWarn` — where the app's pdfkit keeps its standard fonts, for a server tracer |
68
79
 
69
80
  ## Usage
70
81
 
@@ -279,6 +290,127 @@ currencyFractionDigits('KWD'); // 3
279
290
  currencyFractionDigits('EUR', 0); // 0 — an explicit value wins, as in formatAmount
280
291
  ```
281
292
 
293
+ ### A file's size: `/format-file-size`
294
+
295
+ 1024-based, one decimal above bytes, capped at GB, locale-free: a server and
296
+ a browser print the same string. What both libraries' `FileList`,
297
+ `NetworkUploadField` (and its size limit message), `FileCard` and chat
298
+ attachments show; both re-export it as `formatFileSize`.
299
+
300
+ ```ts
301
+ import { formatFileSize } from '@nxgt/material-core/format-file-size';
302
+
303
+ formatFileSize(0); // '0 B'
304
+ formatFileSize(512); // '512 B'
305
+ formatFileSize(1536); // '1.5 KB'
306
+ formatFileSize(5 * 1024 ** 2); // '5.0 MB'
307
+ formatFileSize(-1); // '' — as for NaN and Infinity
308
+ ```
309
+
310
+ ### A slider's tick marks: `/slider-ticks`
311
+
312
+ The values both libraries' `SliderField` (`ticks`) draws its marks at, from
313
+ `min` to `max` inclusive and evenly spaced: `false` gives none, a number that
314
+ many, `true` one per `step` (five without one). Any count is kept between 2
315
+ and 21 marks, except when `min === max`: then there is one mark, at `min`.
316
+
317
+ ```ts
318
+ import { tickValues } from '@nxgt/material-core/slider-ticks';
319
+
320
+ tickValues(0, 100, false); // []
321
+ tickValues(0, 100, 5); // [0, 25, 50, 75, 100]
322
+ tickValues(0, 10, true, 5); // [0, 5, 10]
323
+ tickValues(0, 100, true, 1).length; // 21 — capped
324
+ tickValues(5, 5, true); // [5] — min === max
325
+ ```
326
+
327
+ ### A date range's presets and times: `/date-range`
328
+
329
+ The pure half of both libraries' `DateRangeField` (`presets`, `withTime` /
330
+ `with-time`): the built-in presets' ranges, whether a field's rules rule a
331
+ range out, and the time of day on each end. Plain `Date` maths in the local
332
+ time zone; nothing reads the clock unless you leave `now` out. Both libraries
333
+ re-export `DATE_RANGE_PRESET_IDS` and the preset types.
334
+
335
+ ```ts
336
+ import {
337
+ clampRangeEnd,
338
+ isRangeDisabled,
339
+ presetRange,
340
+ resolvePresetRange,
341
+ } from '@nxgt/material-core/date-range';
342
+
343
+ const now = new Date(2026, 2, 18, 15, 30); // Wednesday 18 March 2026
344
+
345
+ presetRange('last-7-days', now); // 12 → 18 March, both at 00:00 (today counts)
346
+ presetRange('this-month', now); // 1 → 18 March
347
+ presetRange('last-month', now); // 1 → 28 February
348
+ resolvePresetRange('today', now, { withTime: true }); // 18 March 00:00 → 23:59
349
+ resolvePresetRange({ label: 'Q1', range: (n) => ({ from: new Date(n.getFullYear(), 0, 1), to: new Date(n.getFullYear(), 2, 31) }) }, now);
350
+
351
+ // Ruled out when its end comes before its start, it starts before `min`,
352
+ // ends after `max`, or either end is a disabled day; the days between are
353
+ // not checked.
354
+ isRangeDisabled(presetRange('yesterday', now), { min: now }); // true
355
+
356
+ // On the same day, an end before the start becomes the start.
357
+ clampRangeEnd({ from: new Date(2026, 2, 18, 14), to: new Date(2026, 2, 18, 9) }); // to: 14:00
358
+ ```
359
+
360
+ `keepRangeTimes(next, previous)` gives days picked on a calendar the times
361
+ `previous` had at each end (a new start 00:00, a new end 23:59), then clamps
362
+ the end;
363
+ `withTimeOf(day, time)` and `endOfDayTime(day)` set one end's time;
364
+ `isSameDayRange(a, b)` compares two ranges by their days.
365
+
366
+ ### A file's preview: `/file-preview`
367
+
368
+ The pure half of both libraries' `FilePreview`. `fileKind` picks the kind of
369
+ preview — `image`, `pdf`, `video`, `audio`, `text`, `json`, `csv`,
370
+ `markdown` or `other` — from the MIME type when it names one (parameters and
371
+ case ignored, a `data:` URL's own type read), else from the extension of the
372
+ name, then of the URL. `describeFile` does the same for a `File` / `Blob` or a
373
+ URL, with `type` and `name` overriding what the file says (a URL
374
+ `safePreviewUrl` refuses lends no name). `safePreviewUrl` is the allow-list
375
+ for what reaches an `href` or a `src`: `http(s):`, a relative URL, `blob:`,
376
+ and a `data:` URL of an image, video, audio file, plain text, CSV, Markdown,
377
+ JSON or PDF; it returns `null` for anything else (`javascript:`,
378
+ `vbscript:`, `data:text/html`, …), after removing what the browser ignores
379
+ (spaces and control characters at the ends, tabs and newlines anywhere), and
380
+ `FilePreviewUnsafeUrlError` is what `FilePreview` reports for one it refused.
381
+ `readTextPreview` reads the first bytes of a `Blob`, or of what a URL answers
382
+ (it stops downloading past the cap), decoded with the `charset` of the
383
+ response's `Content-Type` (or of the `Blob`'s `type`) when `TextDecoder`
384
+ supports it, else as UTF-8, and says whether there was more; an HTTP error
385
+ throws `FilePreviewHttpError` with its `status`, and an aborted `signal`
386
+ rejects with its reason, for a `Blob` too. Nothing runs at import, so it is
387
+ safe on a server; only `readTextPreview` touches `fetch`.
388
+
389
+ ```ts
390
+ import {
391
+ describeFile,
392
+ fileKind,
393
+ readTextPreview,
394
+ safePreviewUrl,
395
+ } from '@nxgt/material-core/file-preview';
396
+
397
+ fileKind({ type: 'application/pdf' }); // 'pdf'
398
+ fileKind({ type: 'application/octet-stream', name: 'notes.MD' }); // 'markdown' — the extension decides
399
+ fileKind({ url: 'https://example.com/clip.webm?sig=1' }); // 'video'
400
+ fileKind({ name: 'archive.zip' }); // 'other'
401
+
402
+ safePreviewUrl('/files/report.pdf'); // '/files/report.pdf'
403
+ safePreviewUrl('javascript:alert(1)'); // null
404
+ safePreviewUrl('data:text/html,<b>hi</b>'); // null
405
+
406
+ describeFile(new File(['{}'], 'data.json', { type: 'application/json' }));
407
+ // { kind: 'json', name: 'data.json', type: 'application/json', size: 2, blob: File }
408
+
409
+ const { text, truncated } = await readTextPreview('/logs/today.log', {
410
+ maxBytes: 64 * 1024, // 256 KB (TEXT_PREVIEW_MAX_BYTES) by default
411
+ });
412
+ ```
413
+
282
414
  ### A field's blur across overlays: `/field-blur-layers`
283
415
 
284
416
  A field whose popover, listbox or dialog renders into `<body>` must not count
@@ -317,12 +449,53 @@ export default defineConfig({ plugins: [pdfkitStandardFonts()] });
317
449
  function. The doc comment on `pdfkitStandardFonts` explains when you need it
318
450
  and when keeping the library external is the lighter answer.
319
451
 
452
+ ### Tracing pdfkit's fonts: `/pdfkit-fonts` (Node only)
453
+
454
+ A server that keeps `node_modules` but copies only what a tracer finds
455
+ (`next build` with `output: 'standalone'`, Nitro's `.output/server`) gets
456
+ pdfkit without its fonts: pdfkit loads them with a runtime
457
+ `require('#standard-fonts/<Name>')` no tracer follows, and every PDF then
458
+ fails with `Cannot find module '…/pdfkit/js/standard-fonts/Helvetica.cjs'`.
459
+ These functions find the real files, as the app at a project directory
460
+ loads them — through `@react-pdf/renderer` and through `@react-pdf/font`,
461
+ each of which may nest its own pdfkit — for you to hand to the tracer.
462
+
463
+ ```ts
464
+ // A build script or a framework config: Node code.
465
+ import { pdfkitStandardFontDirs, pdfkitStandardFontFiles } from '@nxgt/material-core/pdfkit-fonts';
466
+
467
+ const warn = (message: string) => console.warn(`[my-build] ${message}`);
468
+
469
+ pdfkitStandardFontDirs(process.cwd(), warn);
470
+ // ['/app/node_modules/pdfkit/js/standard-fonts'] — one per distinct pdfkit;
471
+ // each also holds the chunks/ the fonts require
472
+
473
+ pdfkitStandardFontFiles(process.cwd(), warn);
474
+ // ['/app/node_modules/pdfkit/js/standard-fonts/Courier.cjs', … 14 per pdfkit]
475
+ ```
476
+
477
+ - **Without `@react-pdf/renderer`** (an optional peer: the app does not
478
+ print), both return `[]` and never call `warn`.
479
+ - **Otherwise, whatever fails** (`@react-pdf/font` or pdfkit not
480
+ resolvable, a font missing) reaches `warn` **once per call**, every problem
481
+ in one line (each error's first line, without Node's `Require stack:`),
482
+ and the rest is still returned. Prefix it with your
483
+ tool's name; a font that cannot be traced is a PDF route that fails in
484
+ production.
485
+ - A relative project directory is taken from the working directory. The
486
+ paths are real paths (symlinks resolved), as pnpm and bun lay them out.
487
+
488
+ You rarely call it yourself: `withNxgtPrint` (`@nxgt/material/next`) turns
489
+ the directories into `outputFileTracingIncludes` globs, and the Nuxt module
490
+ of `@nxgt/material-vue` adds the files to `nitro.externals.traceInclude`.
491
+
320
492
  ## Traps
321
493
 
322
494
  | Symptom | Fix |
323
495
  | --- | --- |
324
496
  | `Cannot find package '@react-pdf/renderer'` (or `'react'`) when importing `/pdf` or `/pdf/render` | `bun add react @react-pdf/renderer@^4.9` — or import from `/pdf/model` where you only build documents |
325
497
  | `Cannot find module '#standard-fonts/Helvetica'` on every request of a bundled server | `pdfkitStandardFonts()` from `/vite`, or keep the renderer external |
498
+ | `Cannot find module '…/pdfkit/js/standard-fonts/Helvetica.cjs'` from a traced server output | add `pdfkitStandardFontFiles()` (or the directories' `**/*.cjs`) to the tracer's includes — `withNxgtPrint` and the Nuxt module do |
326
499
 
327
500
  Every message, with its cause, is in [troubleshooting](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/docs/troubleshooting.md).
328
501
 
@@ -0,0 +1,30 @@
1
+ import { type FileKind } from './file-kind';
2
+ /** What `FilePreview` previews: a `File` or `Blob`, or a URL. */
3
+ export type FilePreviewSource = Blob | string;
4
+ /** A file as `FilePreview` sees it, before anything is read. */
5
+ export type DescribedFile = {
6
+ kind: FileKind;
7
+ /**
8
+ * The given name, else the `File`'s, else the URL's last segment (none
9
+ * for a URL `safePreviewUrl` refuses); may be `''`.
10
+ */
11
+ name: string;
12
+ /** The given MIME type, else the `Blob`'s or the `data:` URL's; may be `''`. */
13
+ type: string;
14
+ /** The size in bytes, known for a `Blob` only. */
15
+ size?: number;
16
+ /** The `File` / `Blob` itself. */
17
+ blob?: Blob;
18
+ /** The URL, when the file was given as one. */
19
+ url?: string;
20
+ };
21
+ /**
22
+ * The kind, name, type and size of a file given as a `File` / `Blob` or a
23
+ * URL, with `type` and `name` overriding what the file says. `null` for no
24
+ * file. Reads nothing: a URL is not fetched.
25
+ */
26
+ export declare function describeFile(file: FilePreviewSource | null | undefined, options?: {
27
+ type?: string;
28
+ name?: string;
29
+ }): DescribedFile | null;
30
+ //# sourceMappingURL=describe-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe-file.d.ts","sourceRoot":"","sources":["../../src/file-preview/describe-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAEN,KAAK,QAAQ,EAIb,MAAM,aAAa,CAAC;AAGrB,iEAAiE;AACjE,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,MAAM,CAAC;AAE9C,gEAAgE;AAChE,MAAM,MAAM,aAAa,GAAG;IAC3B,IAAI,EAAE,QAAQ,CAAC;IACf;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,IAAI,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAC1C,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5C,aAAa,GAAG,IAAI,CAmBtB"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Which kind of preview `FilePreview` gives a file: from its MIME type or,
3
+ * failing that, its extension. Pure string work, safe anywhere.
4
+ */
5
+ /** What `FilePreview` renders a file as. */
6
+ export type FileKind = 'image' | 'pdf' | 'video' | 'audio' | 'text' | 'json' | 'csv' | 'markdown' | 'other';
7
+ /** The kinds whose content is read as text and shown as such. */
8
+ export declare const TEXT_FILE_KINDS: readonly FileKind[];
9
+ /** `'image/png'` from `'Image/PNG; charset=x'`: lower case, parameters dropped. */
10
+ export declare function normaliseMimeType(type: string | undefined): string;
11
+ /** The kind a MIME type names, or `undefined` when it names none. */
12
+ export declare function kindOfMimeType(type: string | undefined): FileKind | undefined;
13
+ /**
14
+ * A file name's or a URL's extension, lower case and without its dot:
15
+ * `'png'` for `'Photo.PNG'` and `'https://x/a.png?v=2#top'`, `''` when there
16
+ * is none (`'README'`, `'.env'`, a `data:` URL).
17
+ */
18
+ export declare function fileExtension(nameOrUrl: string | undefined): string;
19
+ /** The MIME type a `data:` URL declares (`''` for any other URL). */
20
+ export declare function dataUrlType(url: string | undefined): string;
21
+ /**
22
+ * The last segment of a URL's path, decoded: `'report.pdf'` for
23
+ * `'https://x/files/report.pdf?token=1'`; `''` for a `data:` or `blob:` URL
24
+ * or a path ending in `/`.
25
+ */
26
+ export declare function fileNameFromUrl(url: string | undefined): string;
27
+ /** What `fileKind` reads: the MIME type, and a name or URL for the extension. */
28
+ export type FileKindInput = {
29
+ type?: string;
30
+ name?: string;
31
+ url?: string;
32
+ };
33
+ /**
34
+ * The kind of preview a file gets: from its MIME type (or a `data:` URL's)
35
+ * when that names one, else from the extension of its name, then of its URL;
36
+ * `'other'` when neither says.
37
+ */
38
+ export declare function fileKind({ type, name, url }: FileKindInput): FileKind;
39
+ /** Whether a kind is read as text (`text`, `json`, `csv`, `markdown`). */
40
+ export declare function isTextFileKind(kind: FileKind): boolean;
41
+ //# sourceMappingURL=file-kind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-kind.d.ts","sourceRoot":"","sources":["../../src/file-preview/file-kind.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GACjB,OAAO,GACP,KAAK,GACL,OAAO,GACP,OAAO,GACP,MAAM,GACN,MAAM,GACN,KAAK,GACL,UAAU,GACV,OAAO,CAAC;AAEX,iEAAiE;AACjE,eAAO,MAAM,eAAe,EAAE,SAAS,QAAQ,EAK9C,CAAC;AAkDF,mFAAmF;AACnF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAElE;AAED,qEAAqE;AACrE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAsB7E;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAMnE;AAED,qEAAqE;AACrE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAG3D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAS/D;AAED,iFAAiF;AACjF,MAAM,MAAM,aAAa,GAAG;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,aAAa,GAAG,QAAQ,CAQrE;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAEtD"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The pure half of both libraries' `FilePreview`: which kind of preview a
3
+ * file gets (`fileKind`, `describeFile`), which URLs it may link to or load
4
+ * (`safePreviewUrl`), and its text read with a size cap (`readTextPreview`). Safe on a server: nothing runs at import.
5
+ */
6
+ export { type DescribedFile, describeFile, type FilePreviewSource, } from './describe-file';
7
+ export { dataUrlType, type FileKind, type FileKindInput, fileExtension, fileKind, fileNameFromUrl, isTextFileKind, kindOfMimeType, normaliseMimeType, TEXT_FILE_KINDS, } from './file-kind';
8
+ export { FilePreviewUnsafeUrlError, safePreviewUrl } from './safe-url';
9
+ export { FilePreviewHttpError, readTextPreview, TEXT_PREVIEW_MAX_BYTES, type TextPreview, } from './text-preview';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/file-preview/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACN,KAAK,aAAa,EAClB,YAAY,EACZ,KAAK,iBAAiB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,WAAW,EACX,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,aAAa,EACb,QAAQ,EACR,eAAe,EACf,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,eAAe,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,yBAAyB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACvE,OAAO,EACN,oBAAoB,EACpB,eAAe,EACf,sBAAsB,EACtB,KAAK,WAAW,GAChB,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,260 @@
1
+ // src/file-preview/file-kind.ts
2
+ var TEXT_FILE_KINDS = [
3
+ "text",
4
+ "json",
5
+ "csv",
6
+ "markdown"
7
+ ];
8
+ var EXTENSION_KINDS = {
9
+ png: "image",
10
+ jpg: "image",
11
+ jpeg: "image",
12
+ gif: "image",
13
+ webp: "image",
14
+ avif: "image",
15
+ svg: "image",
16
+ bmp: "image",
17
+ ico: "image",
18
+ pdf: "pdf",
19
+ mp4: "video",
20
+ m4v: "video",
21
+ webm: "video",
22
+ ogv: "video",
23
+ mov: "video",
24
+ mp3: "audio",
25
+ wav: "audio",
26
+ ogg: "audio",
27
+ oga: "audio",
28
+ m4a: "audio",
29
+ aac: "audio",
30
+ flac: "audio",
31
+ opus: "audio",
32
+ json: "json",
33
+ csv: "csv",
34
+ tsv: "csv",
35
+ md: "markdown",
36
+ markdown: "markdown",
37
+ txt: "text",
38
+ log: "text",
39
+ xml: "text",
40
+ yaml: "text",
41
+ yml: "text",
42
+ ini: "text",
43
+ toml: "text"
44
+ };
45
+ var TEXT_APPLICATION_TYPES = new Set([
46
+ "application/xml",
47
+ "application/yaml",
48
+ "application/x-yaml",
49
+ "application/toml",
50
+ "application/javascript",
51
+ "application/x-sh",
52
+ "application/sql"
53
+ ]);
54
+ function normaliseMimeType(type) {
55
+ return (type ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
56
+ }
57
+ function kindOfMimeType(type) {
58
+ const mime = normaliseMimeType(type);
59
+ if (!mime)
60
+ return;
61
+ if (mime === "application/pdf")
62
+ return "pdf";
63
+ if (mime === "application/json" || mime === "text/json" || mime.endsWith("+json")) {
64
+ return "json";
65
+ }
66
+ if (mime === "text/csv" || mime === "application/csv")
67
+ return "csv";
68
+ if (mime === "text/tab-separated-values")
69
+ return "csv";
70
+ if (mime === "text/markdown" || mime === "text/x-markdown") {
71
+ return "markdown";
72
+ }
73
+ const [family] = mime.split("/");
74
+ if (family === "image" || family === "video" || family === "audio") {
75
+ return family;
76
+ }
77
+ if (family === "text" || TEXT_APPLICATION_TYPES.has(mime))
78
+ return "text";
79
+ return;
80
+ }
81
+ function fileExtension(nameOrUrl) {
82
+ if (!nameOrUrl || nameOrUrl.startsWith("data:"))
83
+ return "";
84
+ const path = nameOrUrl.split(/[?#]/)[0] ?? "";
85
+ const base = path.slice(path.lastIndexOf("/") + 1);
86
+ const dot = base.lastIndexOf(".");
87
+ return dot > 0 ? base.slice(dot + 1).toLowerCase() : "";
88
+ }
89
+ function dataUrlType(url) {
90
+ const match = /^data:([^;,]*)/i.exec(url ?? "");
91
+ return normaliseMimeType(match?.[1]);
92
+ }
93
+ function fileNameFromUrl(url) {
94
+ if (!url || /^(data|blob):/i.test(url))
95
+ return "";
96
+ const path = url.split(/[?#]/)[0] ?? "";
97
+ const segment = path.slice(path.lastIndexOf("/") + 1);
98
+ try {
99
+ return decodeURIComponent(segment);
100
+ } catch {
101
+ return segment;
102
+ }
103
+ }
104
+ function fileKind({ type, name, url }) {
105
+ return kindOfMimeType(type) ?? kindOfMimeType(dataUrlType(url)) ?? EXTENSION_KINDS[fileExtension(name)] ?? EXTENSION_KINDS[fileExtension(url)] ?? "other";
106
+ }
107
+ function isTextFileKind(kind) {
108
+ return TEXT_FILE_KINDS.includes(kind);
109
+ }
110
+
111
+ // src/file-preview/safe-url.ts
112
+ var DATA_TYPES = /^(image\/[\w.+-]+|video\/[\w.+-]+|audio\/[\w.+-]+|text\/(plain|csv|markdown|x-markdown)|application\/(json|pdf))$/;
113
+ var ENDS = /^[\u0000-\u0020]+|[\u0000-\u0020]+$/g;
114
+ var TAB_OR_NEWLINE = /[\t\n\r]/g;
115
+ var SCHEME = /^([a-z][a-z\d+.-]*):/i;
116
+ function safePreviewUrl(url) {
117
+ if (url == null)
118
+ return null;
119
+ const value = url.replace(ENDS, "").replace(TAB_OR_NEWLINE, "");
120
+ if (!value)
121
+ return null;
122
+ const scheme = SCHEME.exec(value)?.[1]?.toLowerCase();
123
+ if (scheme === undefined) {
124
+ const head = value.split(/[/?#]/, 1)[0] ?? "";
125
+ return head.includes(":") ? null : value;
126
+ }
127
+ if (scheme === "http" || scheme === "https" || scheme === "blob") {
128
+ return value;
129
+ }
130
+ if (scheme === "data") {
131
+ const type = value.slice("data:".length).split(/[;,]/, 1)[0]?.toLowerCase();
132
+ return type && DATA_TYPES.test(type) ? value : null;
133
+ }
134
+ return null;
135
+ }
136
+
137
+ class FilePreviewUnsafeUrlError extends Error {
138
+ constructor() {
139
+ super("Refused to preview a URL with an unsafe scheme");
140
+ this.name = "FilePreviewUnsafeUrlError";
141
+ }
142
+ }
143
+
144
+ // src/file-preview/describe-file.ts
145
+ function describeFile(file, options = {}) {
146
+ if (file == null || file === "")
147
+ return null;
148
+ if (typeof file === "string") {
149
+ const name = options.name ?? (safePreviewUrl(file) === null ? "" : fileNameFromUrl(file));
150
+ const type = normaliseMimeType(options.type ?? dataUrlType(file));
151
+ return { kind: fileKind({ type, name, url: file }), name, type, url: file };
152
+ }
153
+ const own = "name" in file && typeof file.name === "string" ? file.name : "";
154
+ const name = options.name ?? own;
155
+ const type = normaliseMimeType(options.type ?? file.type);
156
+ return {
157
+ kind: fileKind({ type, name }),
158
+ name,
159
+ type,
160
+ size: file.size,
161
+ blob: file
162
+ };
163
+ }
164
+ // src/file-preview/text-preview.ts
165
+ var TEXT_PREVIEW_MAX_BYTES = 256 * 1024;
166
+
167
+ class FilePreviewHttpError extends Error {
168
+ constructor(url, status) {
169
+ super(`Could not read ${url}: HTTP ${status}`);
170
+ this.name = "FilePreviewHttpError";
171
+ this.status = status;
172
+ }
173
+ }
174
+ function decoderFor(contentType) {
175
+ const charset = /;\s*charset\s*=\s*"?([^";\s]+)/i.exec(contentType ?? "")?.[1];
176
+ if (charset) {
177
+ try {
178
+ return new TextDecoder(charset);
179
+ } catch {}
180
+ }
181
+ return new TextDecoder;
182
+ }
183
+ function decode(bytes, truncated, contentType) {
184
+ return decoderFor(contentType).decode(bytes, { stream: truncated });
185
+ }
186
+ async function readBlob(blob, maxBytes, signal, contentType) {
187
+ signal?.throwIfAborted();
188
+ const truncated = blob.size > maxBytes;
189
+ const buffer = await blob.slice(0, maxBytes).arrayBuffer();
190
+ signal?.throwIfAborted();
191
+ return {
192
+ text: decode(new Uint8Array(buffer), truncated, contentType),
193
+ truncated
194
+ };
195
+ }
196
+ async function readStream(body, maxBytes, signal, contentType) {
197
+ const reader = body.getReader();
198
+ const chunks = [];
199
+ let length = 0;
200
+ let truncated = false;
201
+ while (length <= maxBytes) {
202
+ const { done, value } = await reader.read();
203
+ if (signal?.aborted) {
204
+ await reader.cancel().catch(() => {});
205
+ signal.throwIfAborted();
206
+ }
207
+ if (done)
208
+ break;
209
+ chunks.push(value);
210
+ length += value.length;
211
+ }
212
+ if (length > maxBytes) {
213
+ truncated = true;
214
+ await reader.cancel().catch(() => {});
215
+ }
216
+ const bytes = new Uint8Array(Math.min(length, maxBytes));
217
+ let offset = 0;
218
+ for (const chunk of chunks) {
219
+ if (offset >= bytes.length)
220
+ break;
221
+ const part = chunk.subarray(0, bytes.length - offset);
222
+ bytes.set(part, offset);
223
+ offset += part.length;
224
+ }
225
+ return { text: decode(bytes, truncated, contentType), truncated };
226
+ }
227
+ async function readTextPreview(source, options = {}) {
228
+ const maxBytes = Math.max(0, options.maxBytes ?? TEXT_PREVIEW_MAX_BYTES);
229
+ const { signal } = options;
230
+ if (typeof source !== "string") {
231
+ return readBlob(source, maxBytes, signal, source.type);
232
+ }
233
+ const response = await fetch(source, { signal });
234
+ if (!response.ok)
235
+ throw new FilePreviewHttpError(source, response.status);
236
+ const contentType = response.headers.get("content-type");
237
+ if (!response.body) {
238
+ return readBlob(await response.blob(), maxBytes, signal, contentType);
239
+ }
240
+ return readStream(response.body, maxBytes, signal, contentType);
241
+ }
242
+ export {
243
+ FilePreviewHttpError,
244
+ FilePreviewUnsafeUrlError,
245
+ TEXT_FILE_KINDS,
246
+ TEXT_PREVIEW_MAX_BYTES,
247
+ dataUrlType,
248
+ describeFile,
249
+ fileExtension,
250
+ fileKind,
251
+ fileNameFromUrl,
252
+ isTextFileKind,
253
+ kindOfMimeType,
254
+ normaliseMimeType,
255
+ readTextPreview,
256
+ safePreviewUrl
257
+ };
258
+
259
+ //# debugId=AA628A1137F8DCB664756E2164756E21
260
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/file-preview/file-kind.ts", "../src/file-preview/safe-url.ts", "../src/file-preview/describe-file.ts", "../src/file-preview/text-preview.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Which kind of preview `FilePreview` gives a file: from its MIME type or,\n * failing that, its extension. Pure string work, safe anywhere.\n */\n\n/** What `FilePreview` renders a file as. */\nexport type FileKind =\n\t| 'image'\n\t| 'pdf'\n\t| 'video'\n\t| 'audio'\n\t| 'text'\n\t| 'json'\n\t| 'csv'\n\t| 'markdown'\n\t| 'other';\n\n/** The kinds whose content is read as text and shown as such. */\nexport const TEXT_FILE_KINDS: readonly FileKind[] = [\n\t'text',\n\t'json',\n\t'csv',\n\t'markdown',\n];\n\nconst EXTENSION_KINDS: Record<string, FileKind> = {\n\tpng: 'image',\n\tjpg: 'image',\n\tjpeg: 'image',\n\tgif: 'image',\n\twebp: 'image',\n\tavif: 'image',\n\tsvg: 'image',\n\tbmp: 'image',\n\tico: 'image',\n\tpdf: 'pdf',\n\tmp4: 'video',\n\tm4v: 'video',\n\twebm: 'video',\n\togv: 'video',\n\tmov: 'video',\n\tmp3: 'audio',\n\twav: 'audio',\n\togg: 'audio',\n\toga: 'audio',\n\tm4a: 'audio',\n\taac: 'audio',\n\tflac: 'audio',\n\topus: 'audio',\n\tjson: 'json',\n\tcsv: 'csv',\n\ttsv: 'csv',\n\tmd: 'markdown',\n\tmarkdown: 'markdown',\n\ttxt: 'text',\n\tlog: 'text',\n\txml: 'text',\n\tyaml: 'text',\n\tyml: 'text',\n\tini: 'text',\n\ttoml: 'text',\n};\n\nconst TEXT_APPLICATION_TYPES = new Set([\n\t'application/xml',\n\t'application/yaml',\n\t'application/x-yaml',\n\t'application/toml',\n\t'application/javascript',\n\t'application/x-sh',\n\t'application/sql',\n]);\n\n/** `'image/png'` from `'Image/PNG; charset=x'`: lower case, parameters dropped. */\nexport function normaliseMimeType(type: string | undefined): string {\n\treturn (type ?? '').split(';')[0]?.trim().toLowerCase() ?? '';\n}\n\n/** The kind a MIME type names, or `undefined` when it names none. */\nexport function kindOfMimeType(type: string | undefined): FileKind | undefined {\n\tconst mime = normaliseMimeType(type);\n\tif (!mime) return undefined;\n\tif (mime === 'application/pdf') return 'pdf';\n\tif (\n\t\tmime === 'application/json' ||\n\t\tmime === 'text/json' ||\n\t\tmime.endsWith('+json')\n\t) {\n\t\treturn 'json';\n\t}\n\tif (mime === 'text/csv' || mime === 'application/csv') return 'csv';\n\tif (mime === 'text/tab-separated-values') return 'csv';\n\tif (mime === 'text/markdown' || mime === 'text/x-markdown') {\n\t\treturn 'markdown';\n\t}\n\tconst [family] = mime.split('/');\n\tif (family === 'image' || family === 'video' || family === 'audio') {\n\t\treturn family;\n\t}\n\tif (family === 'text' || TEXT_APPLICATION_TYPES.has(mime)) return 'text';\n\treturn undefined;\n}\n\n/**\n * A file name's or a URL's extension, lower case and without its dot:\n * `'png'` for `'Photo.PNG'` and `'https://x/a.png?v=2#top'`, `''` when there\n * is none (`'README'`, `'.env'`, a `data:` URL).\n */\nexport function fileExtension(nameOrUrl: string | undefined): string {\n\tif (!nameOrUrl || nameOrUrl.startsWith('data:')) return '';\n\tconst path = nameOrUrl.split(/[?#]/)[0] ?? '';\n\tconst base = path.slice(path.lastIndexOf('/') + 1);\n\tconst dot = base.lastIndexOf('.');\n\treturn dot > 0 ? base.slice(dot + 1).toLowerCase() : '';\n}\n\n/** The MIME type a `data:` URL declares (`''` for any other URL). */\nexport function dataUrlType(url: string | undefined): string {\n\tconst match = /^data:([^;,]*)/i.exec(url ?? '');\n\treturn normaliseMimeType(match?.[1]);\n}\n\n/**\n * The last segment of a URL's path, decoded: `'report.pdf'` for\n * `'https://x/files/report.pdf?token=1'`; `''` for a `data:` or `blob:` URL\n * or a path ending in `/`.\n */\nexport function fileNameFromUrl(url: string | undefined): string {\n\tif (!url || /^(data|blob):/i.test(url)) return '';\n\tconst path = url.split(/[?#]/)[0] ?? '';\n\tconst segment = path.slice(path.lastIndexOf('/') + 1);\n\ttry {\n\t\treturn decodeURIComponent(segment);\n\t} catch {\n\t\treturn segment;\n\t}\n}\n\n/** What `fileKind` reads: the MIME type, and a name or URL for the extension. */\nexport type FileKindInput = {\n\ttype?: string;\n\tname?: string;\n\turl?: string;\n};\n\n/**\n * The kind of preview a file gets: from its MIME type (or a `data:` URL's)\n * when that names one, else from the extension of its name, then of its URL;\n * `'other'` when neither says.\n */\nexport function fileKind({ type, name, url }: FileKindInput): FileKind {\n\treturn (\n\t\tkindOfMimeType(type) ??\n\t\tkindOfMimeType(dataUrlType(url)) ??\n\t\tEXTENSION_KINDS[fileExtension(name)] ??\n\t\tEXTENSION_KINDS[fileExtension(url)] ??\n\t\t'other'\n\t);\n}\n\n/** Whether a kind is read as text (`text`, `json`, `csv`, `markdown`). */\nexport function isTextFileKind(kind: FileKind): boolean {\n\treturn TEXT_FILE_KINDS.includes(kind);\n}\n",
6
+ "/**\n * Which URLs `FilePreview` lets reach a link's `href` or a media element's\n * `src`. Pure: nothing runs at import.\n */\n\n/** `data:` types that may be previewed or downloaded. */\nconst DATA_TYPES =\n\t/^(image\\/[\\w.+-]+|video\\/[\\w.+-]+|audio\\/[\\w.+-]+|text\\/(plain|csv|markdown|x-markdown)|application\\/(json|pdf))$/;\n\n/** What the URL parser trims from the ends: control characters and spaces. */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: control characters are what this trims\nconst ENDS = /^[\\u0000-\\u0020]+|[\\u0000-\\u0020]+$/g;\n\n/** What the URL parser removes anywhere, which could split a scheme. */\nconst TAB_OR_NEWLINE = /[\\t\\n\\r]/g;\n\nconst SCHEME = /^([a-z][a-z\\d+.-]*):/i;\n\n/**\n * `url` when it is safe to put in an `href` or a `src`, else `null`.\n *\n * Allowed: `http:` and `https:`, a relative URL (`/a.pdf`, `files/a.pdf`,\n * `//host/a.pdf`, `?q`, `#x`), `blob:`, and a `data:` URL of an image, a\n * video, an audio file, plain text, CSV, Markdown, JSON or a PDF. Refused:\n * `javascript:`, `vbscript:`, a `data:` URL of anything else (HTML, for\n * one), and every other scheme. What the browser ignores (spaces and\n * control characters at the ends, tabs and newlines anywhere) is removed\n * first, so it cannot hide a scheme.\n */\nexport function safePreviewUrl(url: string | null | undefined): string | null {\n\tif (url == null) return null;\n\tconst value = url.replace(ENDS, '').replace(TAB_OR_NEWLINE, '');\n\tif (!value) return null;\n\tconst scheme = SCHEME.exec(value)?.[1]?.toLowerCase();\n\tif (scheme === undefined) {\n\t\t// No scheme: relative, unless a colon comes before any `/`, `?` or\n\t\t// `#` (something the pattern does not take for a scheme, like `1x:`).\n\t\tconst head = value.split(/[/?#]/, 1)[0] ?? '';\n\t\treturn head.includes(':') ? null : value;\n\t}\n\tif (scheme === 'http' || scheme === 'https' || scheme === 'blob') {\n\t\treturn value;\n\t}\n\tif (scheme === 'data') {\n\t\tconst type = value.slice('data:'.length).split(/[;,]/, 1)[0]?.toLowerCase();\n\t\treturn type && DATA_TYPES.test(type) ? value : null;\n\t}\n\treturn null;\n}\n\n/** A file given as a URL that `safePreviewUrl` refused. */\nexport class FilePreviewUnsafeUrlError extends Error {\n\toverride name = 'FilePreviewUnsafeUrlError';\n\n\tconstructor() {\n\t\tsuper('Refused to preview a URL with an unsafe scheme');\n\t}\n}\n",
7
+ "import {\n\tdataUrlType,\n\ttype FileKind,\n\tfileKind,\n\tfileNameFromUrl,\n\tnormaliseMimeType,\n} from './file-kind';\nimport { safePreviewUrl } from './safe-url';\n\n/** What `FilePreview` previews: a `File` or `Blob`, or a URL. */\nexport type FilePreviewSource = Blob | string;\n\n/** A file as `FilePreview` sees it, before anything is read. */\nexport type DescribedFile = {\n\tkind: FileKind;\n\t/**\n\t * The given name, else the `File`'s, else the URL's last segment (none\n\t * for a URL `safePreviewUrl` refuses); may be `''`.\n\t */\n\tname: string;\n\t/** The given MIME type, else the `Blob`'s or the `data:` URL's; may be `''`. */\n\ttype: string;\n\t/** The size in bytes, known for a `Blob` only. */\n\tsize?: number;\n\t/** The `File` / `Blob` itself. */\n\tblob?: Blob;\n\t/** The URL, when the file was given as one. */\n\turl?: string;\n};\n\n/**\n * The kind, name, type and size of a file given as a `File` / `Blob` or a\n * URL, with `type` and `name` overriding what the file says. `null` for no\n * file. Reads nothing: a URL is not fetched.\n */\nexport function describeFile(\n\tfile: FilePreviewSource | null | undefined,\n\toptions: { type?: string; name?: string } = {},\n): DescribedFile | null {\n\tif (file == null || file === '') return null;\n\tif (typeof file === 'string') {\n\t\tconst name =\n\t\t\toptions.name ??\n\t\t\t(safePreviewUrl(file) === null ? '' : fileNameFromUrl(file));\n\t\tconst type = normaliseMimeType(options.type ?? dataUrlType(file));\n\t\treturn { kind: fileKind({ type, name, url: file }), name, type, url: file };\n\t}\n\tconst own = 'name' in file && typeof file.name === 'string' ? file.name : '';\n\tconst name = options.name ?? own;\n\tconst type = normaliseMimeType(options.type ?? file.type);\n\treturn {\n\t\tkind: fileKind({ type, name }),\n\t\tname,\n\t\ttype,\n\t\tsize: file.size,\n\t\tblob: file,\n\t};\n}\n",
8
+ "/**\n * A text file's beginning, read with a size cap, for `FilePreview`. Nothing\n * runs at import: `fetch` and `TextDecoder` are reached only when\n * `readTextPreview` is called.\n */\n\n/** How much of a text file `FilePreview` reads by default: 256 KB. */\nexport const TEXT_PREVIEW_MAX_BYTES = 256 * 1024;\n\n/** A text file's first bytes, decoded, and whether there was more. */\nexport type TextPreview = {\n\ttext: string;\n\t/** The file is longer than `maxBytes`; `text` is its beginning. */\n\ttruncated: boolean;\n};\n\n/** A URL that answered with an HTTP error. */\nexport class FilePreviewHttpError extends Error {\n\toverride name = 'FilePreviewHttpError';\n\treadonly status: number;\n\n\tconstructor(url: string, status: number) {\n\t\tsuper(`Could not read ${url}: HTTP ${status}`);\n\t\tthis.status = status;\n\t}\n}\n\n/**\n * A decoder for the `charset` of a `Content-Type`, when `TextDecoder` knows\n * it; else UTF-8.\n */\nfunction decoderFor(contentType: string | null | undefined): TextDecoder {\n\tconst charset = /;\\s*charset\\s*=\\s*\"?([^\";\\s]+)/i.exec(\n\t\tcontentType ?? '',\n\t)?.[1];\n\tif (charset) {\n\t\ttry {\n\t\t\treturn new TextDecoder(charset);\n\t\t} catch {\n\t\t\t// An encoding `TextDecoder` does not support: fall back to UTF-8.\n\t\t}\n\t}\n\treturn new TextDecoder();\n}\n\nfunction decode(\n\tbytes: Uint8Array,\n\ttruncated: boolean,\n\tcontentType: string | null | undefined,\n): string {\n\t// `stream: true` holds back a character cut in half by the cap, rather\n\t// than ending the text on U+FFFD.\n\treturn decoderFor(contentType).decode(bytes, { stream: truncated });\n}\n\nasync function readBlob(\n\tblob: Blob,\n\tmaxBytes: number,\n\tsignal: AbortSignal | undefined,\n\tcontentType: string | null | undefined,\n): Promise<TextPreview> {\n\tsignal?.throwIfAborted();\n\tconst truncated = blob.size > maxBytes;\n\tconst buffer = await blob.slice(0, maxBytes).arrayBuffer();\n\tsignal?.throwIfAborted();\n\treturn {\n\t\ttext: decode(new Uint8Array(buffer), truncated, contentType),\n\t\ttruncated,\n\t};\n}\n\nasync function readStream(\n\tbody: ReadableStream<Uint8Array>,\n\tmaxBytes: number,\n\tsignal: AbortSignal | undefined,\n\tcontentType: string | null,\n): Promise<TextPreview> {\n\tconst reader = body.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet length = 0;\n\tlet truncated = false;\n\twhile (length <= maxBytes) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (signal?.aborted) {\n\t\t\tawait reader.cancel().catch(() => {});\n\t\t\tsignal.throwIfAborted();\n\t\t}\n\t\tif (done) break;\n\t\tchunks.push(value);\n\t\tlength += value.length;\n\t}\n\tif (length > maxBytes) {\n\t\ttruncated = true;\n\t\tawait reader.cancel().catch(() => {});\n\t}\n\tconst bytes = new Uint8Array(Math.min(length, maxBytes));\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tif (offset >= bytes.length) break;\n\t\tconst part = chunk.subarray(0, bytes.length - offset);\n\t\tbytes.set(part, offset);\n\t\toffset += part.length;\n\t}\n\treturn { text: decode(bytes, truncated, contentType), truncated };\n}\n\n/**\n * The first `maxBytes` of a `Blob` / `File`, or of what a URL answers, as\n * text, decoded with the `charset` of the URL's `Content-Type` (or of the\n * `Blob`'s `type`) when `TextDecoder` supports it, else as UTF-8. A URL is\n * fetched, and stops downloading once past the cap; an HTTP error throws\n * `FilePreviewHttpError`. Pass `signal` to abort: the promise then rejects\n * with the signal's reason, also for a `Blob`.\n */\nexport async function readTextPreview(\n\tsource: Blob | string,\n\toptions: { maxBytes?: number; signal?: AbortSignal } = {},\n): Promise<TextPreview> {\n\tconst maxBytes = Math.max(0, options.maxBytes ?? TEXT_PREVIEW_MAX_BYTES);\n\tconst { signal } = options;\n\tif (typeof source !== 'string') {\n\t\treturn readBlob(source, maxBytes, signal, source.type);\n\t}\n\tconst response = await fetch(source, { signal });\n\tif (!response.ok) throw new FilePreviewHttpError(source, response.status);\n\tconst contentType = response.headers.get('content-type');\n\tif (!response.body) {\n\t\treturn readBlob(await response.blob(), maxBytes, signal, contentType);\n\t}\n\treturn readStream(response.body, maxBytes, signal, contentType);\n}\n"
9
+ ],
10
+ "mappings": ";AAkBO,IAAM,kBAAuC;AAAA,EACnD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAEA,IAAM,kBAA4C;AAAA,EACjD,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AACP;AAEA,IAAM,yBAAyB,IAAI,IAAI;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,CAAC;AAGM,SAAS,iBAAiB,CAAC,MAAkC;AAAA,EACnE,QAAQ,QAAQ,IAAI,MAAM,GAAG,EAAE,IAAI,KAAK,EAAE,YAAY,KAAK;AAAA;AAIrD,SAAS,cAAc,CAAC,MAAgD;AAAA,EAC9E,MAAM,OAAO,kBAAkB,IAAI;AAAA,EACnC,IAAI,CAAC;AAAA,IAAM;AAAA,EACX,IAAI,SAAS;AAAA,IAAmB,OAAO;AAAA,EACvC,IACC,SAAS,sBACT,SAAS,eACT,KAAK,SAAS,OAAO,GACpB;AAAA,IACD,OAAO;AAAA,EACR;AAAA,EACA,IAAI,SAAS,cAAc,SAAS;AAAA,IAAmB,OAAO;AAAA,EAC9D,IAAI,SAAS;AAAA,IAA6B,OAAO;AAAA,EACjD,IAAI,SAAS,mBAAmB,SAAS,mBAAmB;AAAA,IAC3D,OAAO;AAAA,EACR;AAAA,EACA,OAAO,UAAU,KAAK,MAAM,GAAG;AAAA,EAC/B,IAAI,WAAW,WAAW,WAAW,WAAW,WAAW,SAAS;AAAA,IACnE,OAAO;AAAA,EACR;AAAA,EACA,IAAI,WAAW,UAAU,uBAAuB,IAAI,IAAI;AAAA,IAAG,OAAO;AAAA,EAClE;AAAA;AAQM,SAAS,aAAa,CAAC,WAAuC;AAAA,EACpE,IAAI,CAAC,aAAa,UAAU,WAAW,OAAO;AAAA,IAAG,OAAO;AAAA,EACxD,MAAM,OAAO,UAAU,MAAM,MAAM,EAAE,MAAM;AAAA,EAC3C,MAAM,OAAO,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI,CAAC;AAAA,EACjD,MAAM,MAAM,KAAK,YAAY,GAAG;AAAA,EAChC,OAAO,MAAM,IAAI,KAAK,MAAM,MAAM,CAAC,EAAE,YAAY,IAAI;AAAA;AAI/C,SAAS,WAAW,CAAC,KAAiC;AAAA,EAC5D,MAAM,QAAQ,kBAAkB,KAAK,OAAO,EAAE;AAAA,EAC9C,OAAO,kBAAkB,QAAQ,EAAE;AAAA;AAQ7B,SAAS,eAAe,CAAC,KAAiC;AAAA,EAChE,IAAI,CAAC,OAAO,iBAAiB,KAAK,GAAG;AAAA,IAAG,OAAO;AAAA,EAC/C,MAAM,OAAO,IAAI,MAAM,MAAM,EAAE,MAAM;AAAA,EACrC,MAAM,UAAU,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI,CAAC;AAAA,EACpD,IAAI;AAAA,IACH,OAAO,mBAAmB,OAAO;AAAA,IAChC,MAAM;AAAA,IACP,OAAO;AAAA;AAAA;AAgBF,SAAS,QAAQ,GAAG,MAAM,MAAM,OAAgC;AAAA,EACtE,OACC,eAAe,IAAI,KACnB,eAAe,YAAY,GAAG,CAAC,KAC/B,gBAAgB,cAAc,IAAI,MAClC,gBAAgB,cAAc,GAAG,MACjC;AAAA;AAKK,SAAS,cAAc,CAAC,MAAyB;AAAA,EACvD,OAAO,gBAAgB,SAAS,IAAI;AAAA;;;AC5JrC,IAAM,aACL;AAID,IAAM,OAAO;AAGb,IAAM,iBAAiB;AAEvB,IAAM,SAAS;AAaR,SAAS,cAAc,CAAC,KAA+C;AAAA,EAC7E,IAAI,OAAO;AAAA,IAAM,OAAO;AAAA,EACxB,MAAM,QAAQ,IAAI,QAAQ,MAAM,EAAE,EAAE,QAAQ,gBAAgB,EAAE;AAAA,EAC9D,IAAI,CAAC;AAAA,IAAO,OAAO;AAAA,EACnB,MAAM,SAAS,OAAO,KAAK,KAAK,IAAI,IAAI,YAAY;AAAA,EACpD,IAAI,WAAW,WAAW;AAAA,IAGzB,MAAM,OAAO,MAAM,MAAM,SAAS,CAAC,EAAE,MAAM;AAAA,IAC3C,OAAO,KAAK,SAAS,GAAG,IAAI,OAAO;AAAA,EACpC;AAAA,EACA,IAAI,WAAW,UAAU,WAAW,WAAW,WAAW,QAAQ;AAAA,IACjE,OAAO;AAAA,EACR;AAAA,EACA,IAAI,WAAW,QAAQ;AAAA,IACtB,MAAM,OAAO,MAAM,MAAM,QAAQ,MAAM,EAAE,MAAM,QAAQ,CAAC,EAAE,IAAI,YAAY;AAAA,IAC1E,OAAO,QAAQ,WAAW,KAAK,IAAI,IAAI,QAAQ;AAAA,EAChD;AAAA,EACA,OAAO;AAAA;AAAA;AAID,MAAM,kCAAkC,MAAM;AAAA,EAGpD,WAAW,GAAG;AAAA,IACb,MAAM,gDAAgD;AAAA,IAH9C,YAAO;AAAA;AAKjB;;;ACtBO,SAAS,YAAY,CAC3B,MACA,UAA4C,CAAC,GACtB;AAAA,EACvB,IAAI,QAAQ,QAAQ,SAAS;AAAA,IAAI,OAAO;AAAA,EACxC,IAAI,OAAO,SAAS,UAAU;AAAA,IAC7B,MAAM,OACL,QAAQ,SACP,eAAe,IAAI,MAAM,OAAO,KAAK,gBAAgB,IAAI;AAAA,IAC3D,MAAM,OAAO,kBAAkB,QAAQ,QAAQ,YAAY,IAAI,CAAC;AAAA,IAChE,OAAO,EAAE,MAAM,SAAS,EAAE,MAAM,MAAM,KAAK,KAAK,CAAC,GAAG,MAAM,MAAM,KAAK,KAAK;AAAA,EAC3E;AAAA,EACA,MAAM,MAAM,UAAU,QAAQ,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;AAAA,EAC1E,MAAM,OAAO,QAAQ,QAAQ;AAAA,EAC7B,MAAM,OAAO,kBAAkB,QAAQ,QAAQ,KAAK,IAAI;AAAA,EACxD,OAAO;AAAA,IACN,MAAM,SAAS,EAAE,MAAM,KAAK,CAAC;AAAA,IAC7B;AAAA,IACA;AAAA,IACA,MAAM,KAAK;AAAA,IACX,MAAM;AAAA,EACP;AAAA;;ACjDM,IAAM,yBAAyB,MAAM;AAAA;AAUrC,MAAM,6BAA6B,MAAM;AAAA,EAI/C,WAAW,CAAC,KAAa,QAAgB;AAAA,IACxC,MAAM,kBAAkB,aAAa,QAAQ;AAAA,IAJrC,YAAO;AAAA,IAKf,KAAK,SAAS;AAAA;AAEhB;AAMA,SAAS,UAAU,CAAC,aAAqD;AAAA,EACxE,MAAM,UAAU,kCAAkC,KACjD,eAAe,EAChB,IAAI;AAAA,EACJ,IAAI,SAAS;AAAA,IACZ,IAAI;AAAA,MACH,OAAO,IAAI,YAAY,OAAO;AAAA,MAC7B,MAAM;AAAA,EAGT;AAAA,EACA,OAAO,IAAI;AAAA;AAGZ,SAAS,MAAM,CACd,OACA,WACA,aACS;AAAA,EAGT,OAAO,WAAW,WAAW,EAAE,OAAO,OAAO,EAAE,QAAQ,UAAU,CAAC;AAAA;AAGnE,eAAe,QAAQ,CACtB,MACA,UACA,QACA,aACuB;AAAA,EACvB,QAAQ,eAAe;AAAA,EACvB,MAAM,YAAY,KAAK,OAAO;AAAA,EAC9B,MAAM,SAAS,MAAM,KAAK,MAAM,GAAG,QAAQ,EAAE,YAAY;AAAA,EACzD,QAAQ,eAAe;AAAA,EACvB,OAAO;AAAA,IACN,MAAM,OAAO,IAAI,WAAW,MAAM,GAAG,WAAW,WAAW;AAAA,IAC3D;AAAA,EACD;AAAA;AAGD,eAAe,UAAU,CACxB,MACA,UACA,QACA,aACuB;AAAA,EACvB,MAAM,SAAS,KAAK,UAAU;AAAA,EAC9B,MAAM,SAAuB,CAAC;AAAA,EAC9B,IAAI,SAAS;AAAA,EACb,IAAI,YAAY;AAAA,EAChB,OAAO,UAAU,UAAU;AAAA,IAC1B,QAAQ,MAAM,UAAU,MAAM,OAAO,KAAK;AAAA,IAC1C,IAAI,QAAQ,SAAS;AAAA,MACpB,MAAM,OAAO,OAAO,EAAE,MAAM,MAAM,EAAE;AAAA,MACpC,OAAO,eAAe;AAAA,IACvB;AAAA,IACA,IAAI;AAAA,MAAM;AAAA,IACV,OAAO,KAAK,KAAK;AAAA,IACjB,UAAU,MAAM;AAAA,EACjB;AAAA,EACA,IAAI,SAAS,UAAU;AAAA,IACtB,YAAY;AAAA,IACZ,MAAM,OAAO,OAAO,EAAE,MAAM,MAAM,EAAE;AAAA,EACrC;AAAA,EACA,MAAM,QAAQ,IAAI,WAAW,KAAK,IAAI,QAAQ,QAAQ,CAAC;AAAA,EACvD,IAAI,SAAS;AAAA,EACb,WAAW,SAAS,QAAQ;AAAA,IAC3B,IAAI,UAAU,MAAM;AAAA,MAAQ;AAAA,IAC5B,MAAM,OAAO,MAAM,SAAS,GAAG,MAAM,SAAS,MAAM;AAAA,IACpD,MAAM,IAAI,MAAM,MAAM;AAAA,IACtB,UAAU,KAAK;AAAA,EAChB;AAAA,EACA,OAAO,EAAE,MAAM,OAAO,OAAO,WAAW,WAAW,GAAG,UAAU;AAAA;AAWjE,eAAsB,eAAe,CACpC,QACA,UAAuD,CAAC,GACjC;AAAA,EACvB,MAAM,WAAW,KAAK,IAAI,GAAG,QAAQ,YAAY,sBAAsB;AAAA,EACvE,QAAQ,WAAW;AAAA,EACnB,IAAI,OAAO,WAAW,UAAU;AAAA,IAC/B,OAAO,SAAS,QAAQ,UAAU,QAAQ,OAAO,IAAI;AAAA,EACtD;AAAA,EACA,MAAM,WAAW,MAAM,MAAM,QAAQ,EAAE,OAAO,CAAC;AAAA,EAC/C,IAAI,CAAC,SAAS;AAAA,IAAI,MAAM,IAAI,qBAAqB,QAAQ,SAAS,MAAM;AAAA,EACxE,MAAM,cAAc,SAAS,QAAQ,IAAI,cAAc;AAAA,EACvD,IAAI,CAAC,SAAS,MAAM;AAAA,IACnB,OAAO,SAAS,MAAM,SAAS,KAAK,GAAG,UAAU,QAAQ,WAAW;AAAA,EACrE;AAAA,EACA,OAAO,WAAW,SAAS,MAAM,UAAU,QAAQ,WAAW;AAAA;",
11
+ "debugId": "AA628A1137F8DCB664756E2164756E21",
12
+ "names": []
13
+ }