@nxgt/material-core 0.2.0 → 0.3.1

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 (34) hide show
  1. package/README.md +143 -1
  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/slider-ticks.d.ts +7 -0
  19. package/dist/lib/slider-ticks.d.ts.map +1 -0
  20. package/dist/lib/slider-ticks.js +16 -0
  21. package/dist/lib/slider-ticks.js.map +10 -0
  22. package/dist/pdf-viewer/index.d.ts +1 -0
  23. package/dist/pdf-viewer/index.d.ts.map +1 -1
  24. package/dist/pdf-viewer/index.js +68 -33
  25. package/dist/pdf-viewer/index.js.map +6 -4
  26. package/dist/pdf-viewer/pdf-pin-page.d.ts +16 -0
  27. package/dist/pdf-viewer/pdf-pin-page.d.ts.map +1 -0
  28. package/dist/pdf-viewer/pdf-scroll-spy.d.ts +2 -8
  29. package/dist/pdf-viewer/pdf-scroll-spy.d.ts.map +1 -1
  30. package/dist/pdf-viewer/pdf-user-input.d.ts +7 -0
  31. package/dist/pdf-viewer/pdf-user-input.d.ts.map +1 -0
  32. package/docs/roadmap.md +15 -5
  33. package/docs/troubleshooting.md +65 -0
  34. package/package.json +20 -2
package/README.md CHANGED
@@ -67,10 +67,13 @@ built-in.
67
67
  | `/pdf-source` | `PdfSource` and its helpers (`toPdfjsSource`, `pdfSourceKey`, …) |
68
68
  | `/pdf-file-name` | `resolvePdfFileName`, `DEFAULT_PDF_FILE_NAME` |
69
69
  | `/pdf-download` | `downloadPdfFile`, `printPdfFile` — in a browser |
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`), 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` |
71
71
  | `/currency-digits` | `currencyFractionDigits` — the decimals `CurrencyField` shows |
72
72
  | `/field-blur-layers` | `layerOf`, `openerOf`, `FieldLayers` — whose overlay a layer of the page is, for a field's blur |
73
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 |
74
77
  | `/vite` | `pdfkitStandardFonts()` — see below |
75
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 |
76
79
 
@@ -276,6 +279,37 @@ const page = pageTextFromItems([{ str: 'Café au', hasEOL: true }, { str: 'lait'
276
279
  findTextMatches(page.text, 'cafe au lait'); // [{ start: 0, end: 12 }] — case, accents and line breaks ignored
277
280
  ```
278
281
 
282
+ To open a reader on a page, `pinToPage(node, beforeScroll, onRelease?)`
283
+ keeps that page at the top of the page area (an element inside the
284
+ `[data-slot="scroll-area-viewport"]` of a `[data-slot="scroll-area"]`) while
285
+ the pages above it change size, until a wheel, touch, pointer or key in the
286
+ page area, or the function it returns. `beforeScroll` runs before each
287
+ correction (hold your scroll spy there); `onRelease` runs once when the pin
288
+ lets go — at once when there is nothing to pin, or no `ResizeObserver` (a
289
+ server). `pinHolds(node)` tells whether the view still shows `node` where the
290
+ pin puts it: at the top, or as near as the area can scroll at its end. A
291
+ scroll that leaves it elsewhere came from find in page, a screen reader's
292
+ cursor or your own `scrollTop`, none of which the pin hears: treat it as the
293
+ user's.
294
+
295
+ ```ts
296
+ import { pinHolds, pinToPage } from '@nxgt/material-core/pdf-viewer';
297
+
298
+ // pages: Map<number, HTMLElement>; spy: from createScrollSpy
299
+ let pinned = true;
300
+ const unpin = pinToPage(
301
+ () => pages.get(3), // the page's element, looked up again on each correction
302
+ () => spy.hold(), // the corrections are not the user's scrolling
303
+ () => { pinned = false; }, // the pin let go
304
+ );
305
+
306
+ function onScroll(page: number) {
307
+ if (pinned && pinHolds(pages.get(3))) return; // still the reader's own scroll
308
+ unpin(); // the view moved elsewhere: let go, then report
309
+ reportPage(page);
310
+ }
311
+ ```
312
+
279
313
  ### A currency's decimals: `/currency-digits`
280
314
 
281
315
  ```ts
@@ -304,6 +338,112 @@ formatFileSize(5 * 1024 ** 2); // '5.0 MB'
304
338
  formatFileSize(-1); // '' — as for NaN and Infinity
305
339
  ```
306
340
 
341
+ ### A slider's tick marks: `/slider-ticks`
342
+
343
+ The values both libraries' `SliderField` (`ticks`) draws its marks at, from
344
+ `min` to `max` inclusive and evenly spaced: `false` gives none, a number that
345
+ many, `true` one per `step` (five without one). Any count is kept between 2
346
+ and 21 marks, except when `min === max`: then there is one mark, at `min`.
347
+
348
+ ```ts
349
+ import { tickValues } from '@nxgt/material-core/slider-ticks';
350
+
351
+ tickValues(0, 100, false); // []
352
+ tickValues(0, 100, 5); // [0, 25, 50, 75, 100]
353
+ tickValues(0, 10, true, 5); // [0, 5, 10]
354
+ tickValues(0, 100, true, 1).length; // 21 — capped
355
+ tickValues(5, 5, true); // [5] — min === max
356
+ ```
357
+
358
+ ### A date range's presets and times: `/date-range`
359
+
360
+ The pure half of both libraries' `DateRangeField` (`presets`, `withTime` /
361
+ `with-time`): the built-in presets' ranges, whether a field's rules rule a
362
+ range out, and the time of day on each end. Plain `Date` maths in the local
363
+ time zone; nothing reads the clock unless you leave `now` out. Both libraries
364
+ re-export `DATE_RANGE_PRESET_IDS` and the preset types.
365
+
366
+ ```ts
367
+ import {
368
+ clampRangeEnd,
369
+ isRangeDisabled,
370
+ presetRange,
371
+ resolvePresetRange,
372
+ } from '@nxgt/material-core/date-range';
373
+
374
+ const now = new Date(2026, 2, 18, 15, 30); // Wednesday 18 March 2026
375
+
376
+ presetRange('last-7-days', now); // 12 → 18 March, both at 00:00 (today counts)
377
+ presetRange('this-month', now); // 1 → 18 March
378
+ presetRange('last-month', now); // 1 → 28 February
379
+ resolvePresetRange('today', now, { withTime: true }); // 18 March 00:00 → 23:59
380
+ resolvePresetRange({ label: 'Q1', range: (n) => ({ from: new Date(n.getFullYear(), 0, 1), to: new Date(n.getFullYear(), 2, 31) }) }, now);
381
+
382
+ // Ruled out when its end comes before its start, it starts before `min`,
383
+ // ends after `max`, or either end is a disabled day; the days between are
384
+ // not checked.
385
+ isRangeDisabled(presetRange('yesterday', now), { min: now }); // true
386
+
387
+ // On the same day, an end before the start becomes the start.
388
+ clampRangeEnd({ from: new Date(2026, 2, 18, 14), to: new Date(2026, 2, 18, 9) }); // to: 14:00
389
+ ```
390
+
391
+ `keepRangeTimes(next, previous)` gives days picked on a calendar the times
392
+ `previous` had at each end (a new start 00:00, a new end 23:59), then clamps
393
+ the end;
394
+ `withTimeOf(day, time)` and `endOfDayTime(day)` set one end's time;
395
+ `isSameDayRange(a, b)` compares two ranges by their days.
396
+
397
+ ### A file's preview: `/file-preview`
398
+
399
+ The pure half of both libraries' `FilePreview`. `fileKind` picks the kind of
400
+ preview — `image`, `pdf`, `video`, `audio`, `text`, `json`, `csv`,
401
+ `markdown` or `other` — from the MIME type when it names one (parameters and
402
+ case ignored, a `data:` URL's own type read), else from the extension of the
403
+ name, then of the URL. `describeFile` does the same for a `File` / `Blob` or a
404
+ URL, with `type` and `name` overriding what the file says (a URL
405
+ `safePreviewUrl` refuses lends no name). `safePreviewUrl` is the allow-list
406
+ for what reaches an `href` or a `src`: `http(s):`, a relative URL, `blob:`,
407
+ and a `data:` URL of an image, video, audio file, plain text, CSV, Markdown,
408
+ JSON or PDF; it returns `null` for anything else (`javascript:`,
409
+ `vbscript:`, `data:text/html`, …), after removing what the browser ignores
410
+ (spaces and control characters at the ends, tabs and newlines anywhere), and
411
+ `FilePreviewUnsafeUrlError` ("Refused to preview a URL with an unsafe
412
+ scheme") is what `FilePreview` reports for one it refused.
413
+ `readTextPreview` reads the first bytes of a `Blob`, or of what a URL answers
414
+ (it stops downloading past the cap), decoded with the `charset` of the
415
+ response's `Content-Type` (or of the `Blob`'s `type`) when `TextDecoder`
416
+ supports it, else as UTF-8, and says whether there was more; an HTTP error
417
+ throws `FilePreviewHttpError` (`Could not read <url>: HTTP <status>`) with
418
+ its `status`, and an aborted `signal` rejects with its reason, for a `Blob`
419
+ too. Nothing runs at import, so it is safe on a server; only
420
+ `readTextPreview` touches `fetch`.
421
+
422
+ ```ts
423
+ import {
424
+ describeFile,
425
+ fileKind,
426
+ readTextPreview,
427
+ safePreviewUrl,
428
+ } from '@nxgt/material-core/file-preview';
429
+
430
+ fileKind({ type: 'application/pdf' }); // 'pdf'
431
+ fileKind({ type: 'application/octet-stream', name: 'notes.MD' }); // 'markdown' — the extension decides
432
+ fileKind({ url: 'https://example.com/clip.webm?sig=1' }); // 'video'
433
+ fileKind({ name: 'archive.zip' }); // 'other'
434
+
435
+ safePreviewUrl('/files/report.pdf'); // '/files/report.pdf'
436
+ safePreviewUrl('javascript:alert(1)'); // null
437
+ safePreviewUrl('data:text/html,<b>hi</b>'); // null
438
+
439
+ describeFile(new File(['{}'], 'data.json', { type: 'application/json' }));
440
+ // { kind: 'json', name: 'data.json', type: 'application/json', size: 2, blob: File }
441
+
442
+ const { text, truncated } = await readTextPreview('/logs/today.log', {
443
+ maxBytes: 64 * 1024, // 256 KB (TEXT_PREVIEW_MAX_BYTES) by default
444
+ });
445
+ ```
446
+
307
447
  ### A field's blur across overlays: `/field-blur-layers`
308
448
 
309
449
  A field whose popover, listbox or dialog renders into `<body>` must not count
@@ -389,6 +529,8 @@ of `@nxgt/material-vue` adds the files to `nitro.externals.traceInclude`.
389
529
  | `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 |
390
530
  | `Cannot find module '#standard-fonts/Helvetica'` on every request of a bundled server | `pdfkitStandardFonts()` from `/vite`, or keep the renderer external |
391
531
  | `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 |
532
+ | `Could not read <url>: HTTP 401` (`FilePreviewHttpError`) for a file behind authentication | fetch it with your credentials and pass the `Blob` to `readTextPreview` / `FilePreview` |
533
+ | `Refused to preview a URL with an unsafe scheme` (`FilePreviewUnsafeUrlError`) | pass an `https:` or relative URL, or the content as a `Blob` |
392
534
 
393
535
  Every message, with its cause, is in [troubleshooting](https://cdn.jsdelivr.net/npm/@nxgt/material-core@0/docs/troubleshooting.md).
394
536
 
@@ -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": "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",
11
+ "debugId": "AA628A1137F8DCB664756E2164756E21",
12
+ "names": []
13
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Which URLs `FilePreview` lets reach a link's `href` or a media element's
3
+ * `src`. Pure: nothing runs at import.
4
+ */
5
+ /**
6
+ * `url` when it is safe to put in an `href` or a `src`, else `null`.
7
+ *
8
+ * Allowed: `http:` and `https:`, a relative URL (`/a.pdf`, `files/a.pdf`,
9
+ * `//host/a.pdf`, `?q`, `#x`), `blob:`, and a `data:` URL of an image, a
10
+ * video, an audio file, plain text, CSV, Markdown, JSON or a PDF. Refused:
11
+ * `javascript:`, `vbscript:`, a `data:` URL of anything else (HTML, for
12
+ * one), and every other scheme. What the browser ignores (spaces and
13
+ * control characters at the ends, tabs and newlines anywhere) is removed
14
+ * first, so it cannot hide a scheme.
15
+ */
16
+ export declare function safePreviewUrl(url: string | null | undefined): string | null;
17
+ /** A file given as a URL that `safePreviewUrl` refused. */
18
+ export declare class FilePreviewUnsafeUrlError extends Error {
19
+ name: string;
20
+ constructor();
21
+ }
22
+ //# sourceMappingURL=safe-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-url.d.ts","sourceRoot":"","sources":["../../src/file-preview/safe-url.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAeH;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAmB5E;AAED,2DAA2D;AAC3D,qBAAa,yBAA0B,SAAQ,KAAK;IAC1C,IAAI,SAA+B;;CAK5C"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A text file's beginning, read with a size cap, for `FilePreview`. Nothing
3
+ * runs at import: `fetch` and `TextDecoder` are reached only when
4
+ * `readTextPreview` is called.
5
+ */
6
+ /** How much of a text file `FilePreview` reads by default: 256 KB. */
7
+ export declare const TEXT_PREVIEW_MAX_BYTES: number;
8
+ /** A text file's first bytes, decoded, and whether there was more. */
9
+ export type TextPreview = {
10
+ text: string;
11
+ /** The file is longer than `maxBytes`; `text` is its beginning. */
12
+ truncated: boolean;
13
+ };
14
+ /** A URL that answered with an HTTP error. */
15
+ export declare class FilePreviewHttpError extends Error {
16
+ name: string;
17
+ readonly status: number;
18
+ constructor(url: string, status: number);
19
+ }
20
+ /**
21
+ * The first `maxBytes` of a `Blob` / `File`, or of what a URL answers, as
22
+ * text, decoded with the `charset` of the URL's `Content-Type` (or of the
23
+ * `Blob`'s `type`) when `TextDecoder` supports it, else as UTF-8. A URL is
24
+ * fetched, and stops downloading once past the cap; an HTTP error throws
25
+ * `FilePreviewHttpError`. Pass `signal` to abort: the promise then rejects
26
+ * with the signal's reason, also for a `Blob`.
27
+ */
28
+ export declare function readTextPreview(source: Blob | string, options?: {
29
+ maxBytes?: number;
30
+ signal?: AbortSignal;
31
+ }): Promise<TextPreview>;
32
+ //# sourceMappingURL=text-preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text-preview.d.ts","sourceRoot":"","sources":["../../src/file-preview/text-preview.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,sEAAsE;AACtE,eAAO,MAAM,sBAAsB,QAAa,CAAC;AAEjD,sEAAsE;AACtE,MAAM,MAAM,WAAW,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,SAAS,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,8CAA8C;AAC9C,qBAAa,oBAAqB,SAAQ,KAAK;IACrC,IAAI,SAA0B;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEZ,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAIvC;AAiFD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACpC,MAAM,EAAE,IAAI,GAAG,MAAM,EACrB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GACvD,OAAO,CAAC,WAAW,CAAC,CAatB"}