@nxgt/material-core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +108 -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 +7 -1
  33. 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
 
@@ -304,6 +307,110 @@ formatFileSize(5 * 1024 ** 2); // '5.0 MB'
304
307
  formatFileSize(-1); // '' — as for NaN and Infinity
305
308
  ```
306
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
+
307
414
  ### A field's blur across overlays: `/field-blur-layers`
308
415
 
309
416
  A field whose popover, listbox or dialog renders into `<body>` must not count
@@ -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"}