@bendyline/squisq-formats 2.1.0 → 2.2.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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +1 -1
  4. package/dist/{chunk-NNHKUXKA.js → chunk-26ISNJ7Y.js} +85 -65
  5. package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
  6. package/dist/{chunk-WQSHGBLN.js → chunk-3NKXBZSR.js} +193 -42
  7. package/dist/{chunk-KURGXM4I.js → chunk-4V3KCHAP.js} +3 -4
  8. package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
  9. package/dist/{chunk-EW54IRRS.js → chunk-6S6GU3ZG.js} +5 -6
  10. package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
  11. package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
  12. package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
  13. package/dist/{chunk-SC67HYQJ.js → chunk-EJTNGKEA.js} +5 -8
  14. package/dist/chunk-GX7RAUME.js +121 -0
  15. package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
  16. package/dist/{chunk-RLU7UFYU.js → chunk-IPN56VLW.js} +83 -58
  17. package/dist/{chunk-DTDF6QDP.js → chunk-JE6LSIHE.js} +81 -20
  18. package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
  19. package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
  20. package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
  21. package/dist/chunk-OGS5VCGJ.js +446 -0
  22. package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
  23. package/dist/{chunk-74GO3FVS.js → chunk-PU7REGWV.js} +5 -8
  24. package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
  25. package/dist/{chunk-QFLDYKCR.js → chunk-TAAENIRB.js} +5 -8
  26. package/dist/{chunk-7ARKUCQT.js → chunk-X2DEAXNK.js} +54 -2
  27. package/dist/container/index.js +1 -2
  28. package/dist/csv/index.d.ts +27 -2
  29. package/dist/csv/index.js +1 -2
  30. package/dist/docx/index.d.ts +5 -1
  31. package/dist/docx/index.js +9 -11
  32. package/dist/epub/index.d.ts +2 -0
  33. package/dist/epub/index.js +5 -6
  34. package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
  35. package/dist/extract-MN7LA3NL.js +13 -0
  36. package/dist/html/index.d.ts +11 -4
  37. package/dist/html/index.js +3 -4
  38. package/dist/images-ESPQKVTW.js +6 -0
  39. package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
  40. package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
  41. package/dist/index.d.ts +7 -7
  42. package/dist/index.js +28 -26
  43. package/dist/infer/index.d.ts +3 -3
  44. package/dist/infer/index.js +7 -9
  45. package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
  46. package/dist/layouts-DRWZGSPD.js +10 -0
  47. package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
  48. package/dist/ooxml/index.d.ts +3 -3
  49. package/dist/ooxml/index.js +14 -13
  50. package/dist/pdf/index.d.ts +18 -0
  51. package/dist/pdf/index.js +2 -3
  52. package/dist/pptx/index.d.ts +4 -4
  53. package/dist/pptx/index.js +11 -13
  54. package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
  55. package/dist/registry/index.d.ts +21 -5
  56. package/dist/registry/index.js +9 -6
  57. package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
  58. package/dist/xlsx/index.d.ts +3 -3
  59. package/dist/xlsx/index.js +6 -7
  60. package/package.json +6 -3
  61. package/dist/chunk-4VUWTSGM.js.map +0 -1
  62. package/dist/chunk-6M7Z25LA.js +0 -46
  63. package/dist/chunk-6M7Z25LA.js.map +0 -1
  64. package/dist/chunk-74GO3FVS.js.map +0 -1
  65. package/dist/chunk-7ARKUCQT.js.map +0 -1
  66. package/dist/chunk-DTDF6QDP.js.map +0 -1
  67. package/dist/chunk-EW54IRRS.js.map +0 -1
  68. package/dist/chunk-FE6OJV6O.js.map +0 -1
  69. package/dist/chunk-GVS2XXV6.js.map +0 -1
  70. package/dist/chunk-KURGXM4I.js.map +0 -1
  71. package/dist/chunk-MLX2BOJC.js.map +0 -1
  72. package/dist/chunk-NKAJPJ4G.js.map +0 -1
  73. package/dist/chunk-NNHKUXKA.js.map +0 -1
  74. package/dist/chunk-O3GVVND4.js.map +0 -1
  75. package/dist/chunk-ODL3SSPT.js.map +0 -1
  76. package/dist/chunk-PN52A5AA.js.map +0 -1
  77. package/dist/chunk-QFLDYKCR.js.map +0 -1
  78. package/dist/chunk-RFAPOKHJ.js.map +0 -1
  79. package/dist/chunk-RLU7UFYU.js.map +0 -1
  80. package/dist/chunk-SC67HYQJ.js.map +0 -1
  81. package/dist/chunk-SSUPBUF5.js.map +0 -1
  82. package/dist/chunk-U4MRIFKL.js.map +0 -1
  83. package/dist/chunk-UGYF5AZE.js +0 -275
  84. package/dist/chunk-UGYF5AZE.js.map +0 -1
  85. package/dist/chunk-WQSHGBLN.js.map +0 -1
  86. package/dist/chunk-YRT7GQ5Y.js +0 -28
  87. package/dist/chunk-YRT7GQ5Y.js.map +0 -1
  88. package/dist/container/index.js.map +0 -1
  89. package/dist/csv/index.js.map +0 -1
  90. package/dist/docx/index.js.map +0 -1
  91. package/dist/epub/index.js.map +0 -1
  92. package/dist/extract-OJ7ZQV6P.js +0 -15
  93. package/dist/extract-OJ7ZQV6P.js.map +0 -1
  94. package/dist/html/index.js.map +0 -1
  95. package/dist/images-7FBWPKE3.js +0 -7
  96. package/dist/images-7FBWPKE3.js.map +0 -1
  97. package/dist/index.js.map +0 -1
  98. package/dist/infer/index.js.map +0 -1
  99. package/dist/layouts-5VDIRPIJ.js +0 -12
  100. package/dist/layouts-5VDIRPIJ.js.map +0 -1
  101. package/dist/mapTheme-IR27S6IV.js.map +0 -1
  102. package/dist/ooxml/index.js.map +0 -1
  103. package/dist/pdf/index.js.map +0 -1
  104. package/dist/pptx/index.js.map +0 -1
  105. package/dist/registry/index.js.map +0 -1
  106. package/dist/xlsx/index.js.map +0 -1
  107. package/src/__tests__/container.test.ts +0 -230
  108. package/src/__tests__/convert.test.ts +0 -495
  109. package/src/__tests__/csvImport.test.ts +0 -84
  110. package/src/__tests__/docxExport.test.ts +0 -491
  111. package/src/__tests__/docxImport.test.ts +0 -531
  112. package/src/__tests__/epub.test.ts +0 -649
  113. package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
  114. package/src/__tests__/formatRegistry.test.ts +0 -174
  115. package/src/__tests__/html.test.ts +0 -439
  116. package/src/__tests__/htmlImport.test.ts +0 -57
  117. package/src/__tests__/inferTheme.test.ts +0 -135
  118. package/src/__tests__/lossyWarnings.test.ts +0 -146
  119. package/src/__tests__/ooxml.test.ts +0 -271
  120. package/src/__tests__/ooxmlCancellation.test.ts +0 -113
  121. package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
  122. package/src/__tests__/pdfExport.test.ts +0 -322
  123. package/src/__tests__/pdfImport.test.ts +0 -384
  124. package/src/__tests__/plainHtml.test.ts +0 -417
  125. package/src/__tests__/plainHtmlBundle.test.ts +0 -253
  126. package/src/__tests__/pptxExport.test.ts +0 -138
  127. package/src/__tests__/pptxImport.test.ts +0 -145
  128. package/src/__tests__/pptxInferFixtures.ts +0 -314
  129. package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
  130. package/src/__tests__/roundTrip.test.ts +0 -201
  131. package/src/__tests__/roundTripAssets.test.ts +0 -50
  132. package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
  133. package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
  134. package/src/__tests__/roundTripMatrix.test.ts +0 -142
  135. package/src/__tests__/sharedContainer.test.ts +0 -41
  136. package/src/__tests__/sharedImages.test.ts +0 -61
  137. package/src/__tests__/xlsxExport.test.ts +0 -164
  138. package/src/__tests__/xlsxImport.test.ts +0 -80
  139. package/src/__tests__/zipSafety.test.ts +0 -317
  140. package/src/container/index.ts +0 -94
  141. package/src/csv/index.ts +0 -188
  142. package/src/docx/export.ts +0 -1375
  143. package/src/docx/import.ts +0 -1250
  144. package/src/docx/index.ts +0 -26
  145. package/src/docx/styles.ts +0 -145
  146. package/src/epub/export.ts +0 -968
  147. package/src/epub/index.ts +0 -20
  148. package/src/html/docsHtmlBundle.ts +0 -373
  149. package/src/html/htmlTemplate.ts +0 -385
  150. package/src/html/imageUtils.ts +0 -61
  151. package/src/html/import.ts +0 -297
  152. package/src/html/index.ts +0 -212
  153. package/src/html/plainHtml.ts +0 -790
  154. package/src/html/plainHtmlBundle.ts +0 -421
  155. package/src/index.ts +0 -109
  156. package/src/infer/extract.ts +0 -127
  157. package/src/infer/index.ts +0 -199
  158. package/src/infer/mapTheme.ts +0 -176
  159. package/src/infer/types.ts +0 -27
  160. package/src/ooxml/index.ts +0 -111
  161. package/src/ooxml/namespaces.ts +0 -217
  162. package/src/ooxml/readUtils.ts +0 -44
  163. package/src/ooxml/reader.ts +0 -318
  164. package/src/ooxml/themeReader.ts +0 -197
  165. package/src/ooxml/types.ts +0 -103
  166. package/src/ooxml/writer.ts +0 -339
  167. package/src/ooxml/xmlUtils.ts +0 -123
  168. package/src/pdf/export.ts +0 -1084
  169. package/src/pdf/import.ts +0 -1164
  170. package/src/pdf/index.ts +0 -29
  171. package/src/pdf/styles.ts +0 -180
  172. package/src/pptx/export.ts +0 -1184
  173. package/src/pptx/import.ts +0 -455
  174. package/src/pptx/index.ts +0 -52
  175. package/src/pptx/layouts.ts +0 -1222
  176. package/src/pptx/styles.ts +0 -96
  177. package/src/pptx/templates.ts +0 -187
  178. package/src/registry/convert.ts +0 -433
  179. package/src/registry/defaultFormats.ts +0 -413
  180. package/src/registry/errors.ts +0 -46
  181. package/src/registry/index.ts +0 -43
  182. package/src/registry/registry.ts +0 -48
  183. package/src/registry/types.ts +0 -170
  184. package/src/shared/boundedZipArchive.ts +0 -383
  185. package/src/shared/container.ts +0 -28
  186. package/src/shared/fidelity.ts +0 -130
  187. package/src/shared/images.ts +0 -44
  188. package/src/shared/inlineRuns.ts +0 -99
  189. package/src/shared/text.ts +0 -41
  190. package/src/shared/zipEntryCount.ts +0 -151
  191. package/src/shared/zipLimits.ts +0 -296
  192. package/src/shared/zipSafety.ts +0 -19
  193. package/src/xlsx/export.ts +0 -253
  194. package/src/xlsx/import.ts +0 -160
  195. package/src/xlsx/index.ts +0 -35
@@ -1,170 +0,0 @@
1
- /**
2
- * Format registry — shared types.
3
- *
4
- * The registry is the programmatic heart of `convert()`: it maps a small set of
5
- * format ids (`md`, `docx`, `pdf`, …) to `FormatDefinition`s, each of which
6
- * knows how to import to / export from squisq's markdown + Doc model. Every
7
- * converter module is loaded lazily via `import()` inside the definition
8
- * methods, so pulling in the registry never eagerly bundles heavy converters.
9
- */
10
-
11
- import type { Doc, ThemeRegistry } from '@bendyline/squisq/schemas';
12
- import type { TransformStyleInput, TransformStyleRegistry } from '@bendyline/squisq/transform';
13
- import type { MarkdownDocument } from '@bendyline/squisq/markdown';
14
- import type { ContentContainer } from '@bendyline/squisq/storage';
15
- import type { ParseOptions, StringifyOptions } from '@bendyline/squisq/markdown';
16
- import type { DocxExportOptions } from '../docx/export.js';
17
- import type { DocxImportOptions } from '../docx/import.js';
18
- import type { PptxExportOptions } from '../pptx/export.js';
19
- import type { PptxImportOptions } from '../pptx/import.js';
20
- import type { XlsxExportOptions } from '../xlsx/export.js';
21
- import type { XlsxImportOptions } from '../xlsx/import.js';
22
- import type { CsvExportOptions, CsvImportOptions } from '../csv/index.js';
23
- import type { PdfExportOptions } from '../pdf/export.js';
24
- import type { PdfImportOptions } from '../pdf/import.js';
25
- import type { HtmlExportOptions } from '../html/htmlTemplate.js';
26
- import type { HtmlImportOptions } from '../html/import.js';
27
- import type { EpubExportOptions } from '../epub/export.js';
28
- import type { ContainerToZipOptions } from '../container/index.js';
29
- import type { ZipSafetyLimits } from '../shared/zipSafety.js';
30
-
31
- /** A format identifier (e.g. `'docx'`). Strings so hosts can register their own. */
32
- export type FormatId = string;
33
-
34
- /** The built-in formats the default registry ships with. */
35
- export const BUILTIN_FORMAT_IDS = [
36
- 'md',
37
- 'docx',
38
- 'pdf',
39
- 'pptx',
40
- 'xlsx',
41
- 'csv',
42
- 'html',
43
- 'htmlzip',
44
- 'epub',
45
- 'dbk',
46
- ] as const;
47
-
48
- /** The bytes + metadata produced by a successful export. */
49
- export interface ConversionResult {
50
- /** Encoded output bytes. */
51
- bytes: Uint8Array;
52
- /** MIME type of the output. */
53
- mimeType: string;
54
- /** A sensible download filename (`<baseName>.<ext>`). */
55
- suggestedFilename: string;
56
- /** Non-fatal notes accumulated during conversion (may be empty). */
57
- warnings: string[];
58
- }
59
-
60
- export interface MarkdownFormatOptions {
61
- parse?: ParseOptions;
62
- stringify?: StringifyOptions;
63
- }
64
-
65
- /** Resource limits applied when importing a DBK/ZIP container. */
66
- export type DbkFormatOptions = ZipSafetyLimits & ContainerToZipOptions;
67
-
68
- /**
69
- * Strongly typed option bags for built-in formats. Import and export options
70
- * share a bag because a single conversion may use the format on either side.
71
- * Custom registries may add arbitrary keys through the intersection used by
72
- * {@link ConvertOptions.formatOptions}.
73
- */
74
- export interface BuiltinFormatOptions {
75
- md: MarkdownFormatOptions;
76
- docx: DocxImportOptions & DocxExportOptions;
77
- pptx: PptxImportOptions & PptxExportOptions;
78
- xlsx: XlsxImportOptions & XlsxExportOptions;
79
- csv: CsvImportOptions & CsvExportOptions;
80
- pdf: PdfImportOptions & PdfExportOptions;
81
- html: HtmlImportOptions & Partial<Omit<HtmlExportOptions, 'playerScript'>>;
82
- htmlzip: Partial<Omit<HtmlExportOptions, 'playerScript'>>;
83
- epub: EpubExportOptions;
84
- dbk: DbkFormatOptions;
85
- }
86
-
87
- /**
88
- * A source normalized into every shape an exporter might need. `doc` is always
89
- * present; `markdownDoc` is present when the source was markdown-shaped (an
90
- * exporter that wants markdown but finds none derives it from `doc`).
91
- */
92
- export interface NormalizedInput {
93
- doc: Doc;
94
- markdownDoc?: MarkdownDocument;
95
- container: ContentContainer;
96
- baseName: string;
97
- }
98
-
99
- /** Options threaded through `convert()` and into every format method. */
100
- export interface ConvertOptions {
101
- /** Cancel normalization, transformation, or export at the next bounded work boundary. */
102
- signal?: AbortSignal;
103
- /** Registry to resolve formats against. Defaults to `defaultRegistry()`. */
104
- registry?: FormatRegistry;
105
- /** Explicit source format id (skips extension/byte sniffing). */
106
- from?: FormatId;
107
- /** Theme id to apply to the exported document. */
108
- themeId?: string;
109
- /** Explicit caller-owned registry for non-document custom themes. */
110
- themeRegistry?: ThemeRegistry;
111
- /** Built-in/registry id or call-scoped style to apply before export. */
112
- transformStyle?: TransformStyleInput;
113
- /** Explicit caller-owned registry used to resolve transform style ids. */
114
- transformRegistry?: TransformStyleRegistry;
115
- /** Content-aware auto-templating when deriving a Doc from markdown. */
116
- autoTemplates?: boolean;
117
- /** Title hint for exporters that expose document metadata. */
118
- title?: string;
119
- /** Lazily resolve the standalone player IIFE bundle (required for HTML export). */
120
- resolvePlayerScript?: () => Promise<string>;
121
- /** Typed per-format options for built-ins; custom format ids remain extensible. */
122
- formatOptions?: Partial<BuiltinFormatOptions> & Record<FormatId, unknown>;
123
- }
124
-
125
- /** Target-only options accepted after a source has already been normalized and transformed. */
126
- export type PreparedExportOptions = Pick<
127
- ConvertOptions,
128
- 'signal' | 'title' | 'resolvePlayerScript' | 'formatOptions'
129
- >;
130
-
131
- /**
132
- * An opaque normalized conversion snapshot. The source and transform pipeline
133
- * has already completed; each call exports that same snapshot to one target.
134
- */
135
- export interface PreparedConversion {
136
- convert(to: FormatId, options?: PreparedExportOptions): Promise<ConversionResult>;
137
- }
138
-
139
- /** Describes how a single format imports to / exports from the squisq model. */
140
- export interface FormatDefinition {
141
- id: FormatId;
142
- label: string;
143
- mimeType: string;
144
- extensions: readonly string[];
145
- /** Import raw bytes to a MarkdownDocument. */
146
- importDoc?(data: ArrayBuffer, options: ConvertOptions): Promise<MarkdownDocument>;
147
- /** Import raw bytes to a ContentContainer (markdown + extracted media). */
148
- importContainer?(data: ArrayBuffer, options: ConvertOptions): Promise<ContentContainer>;
149
- /** Export a normalized input to bytes. */
150
- exportDoc?(input: NormalizedInput, options: ConvertOptions): Promise<ConversionResult>;
151
- }
152
-
153
- /** A mutable collection of format definitions keyed by id. */
154
- export interface FormatRegistry {
155
- register(def: FormatDefinition): void;
156
- get(id: FormatId): FormatDefinition | undefined;
157
- byExtension(ext: string): FormatDefinition | undefined;
158
- list(): FormatDefinition[];
159
- }
160
-
161
- /** The three shapes `convert()` accepts as a source. */
162
- export type ConvertSource =
163
- | { kind: 'bytes'; data: ArrayBuffer | Uint8Array; filename?: string }
164
- | {
165
- kind: 'markdown';
166
- markdown: string | MarkdownDocument;
167
- container?: ContentContainer;
168
- baseName?: string;
169
- }
170
- | { kind: 'doc'; doc: Doc; container?: ContentContainer; baseName?: string };
@@ -1,383 +0,0 @@
1
- /** Incremental JSZip member reads governed by one shared archive budget. */
2
-
3
- import JSZip from 'jszip';
4
- import {
5
- archiveTooLargeError,
6
- assertNonNegativeFinite,
7
- compressionRatio,
8
- compressionRatioError,
9
- entryTooLargeError,
10
- resolveZipSafetyLimits,
11
- throwIfZipAborted,
12
- validateZipArchive,
13
- tooManyEntriesError,
14
- ZipSafetyError,
15
- type ResolvedZipSafetyLimits,
16
- type ValidatedZipEntry,
17
- type ZipSafetyErrorCode,
18
- type ZipSafetyLimits,
19
- } from './zipLimits.js';
20
- import { declaredZipEntryCount, prepareZipInput, type ZipInput } from './zipEntryCount.js';
21
-
22
- interface ActiveZipRead {
23
- stop(error: unknown): void;
24
- }
25
-
26
- interface ZipChunkStream {
27
- on(event: 'data', callback: (chunk: Uint8Array) => void): ZipChunkStream;
28
- on(event: 'error', callback: (error: unknown) => void): ZipChunkStream;
29
- on(event: 'end', callback: () => void): ZipChunkStream;
30
- pause(): ZipChunkStream;
31
- resume(): ZipChunkStream;
32
- }
33
-
34
- interface StreamableZipObject {
35
- internalStream(type: 'uint8array'): ZipChunkStream;
36
- }
37
-
38
- /**
39
- * A JSZip archive whose member reads share one actual-byte budget.
40
- *
41
- * JSZip's promise API accumulates a complete inflated member before returning.
42
- * This wrapper instead consumes `internalStream('uint8array')`, checks every
43
- * emitted chunk, and pauses every active stream as soon as any limit fails.
44
- * Completed reads are cached so repeated OOXML part access is neither inflated
45
- * nor charged twice.
46
- */
47
- export interface BoundedZipArchive {
48
- readonly zip: JSZip;
49
- readonly entries: readonly ValidatedZipEntry[];
50
- readonly emittedUncompressedBytes: number;
51
- read(path: string, maxBytes?: number): Promise<Uint8Array | null>;
52
- readText(path: string, maxBytes?: number): Promise<string | null>;
53
- release(path: string): void;
54
- }
55
-
56
- class JsZipBoundedArchive implements BoundedZipArchive {
57
- readonly zip: JSZip;
58
- readonly entries: readonly ValidatedZipEntry[];
59
-
60
- private readonly limits: ResolvedZipSafetyLimits;
61
- private readonly entriesByPath: ReadonlyMap<string, ValidatedZipEntry>;
62
- private readonly signal?: AbortSignal;
63
- private readonly reads = new Map<string, Promise<Uint8Array>>();
64
- private readonly activeReads = new Set<ActiveZipRead>();
65
- private terminalError?: ZipSafetyError;
66
- private emittedBytes = 0;
67
-
68
- constructor(
69
- zip: JSZip,
70
- entries: readonly ValidatedZipEntry[],
71
- limits: ResolvedZipSafetyLimits,
72
- signal?: AbortSignal,
73
- ) {
74
- this.zip = zip;
75
- this.entries = entries;
76
- this.limits = limits;
77
- this.signal = signal;
78
- this.entriesByPath = new Map(entries.map((entry) => [entry.path, entry]));
79
- }
80
-
81
- /** Actual uncompressed bytes accepted from JSZip streams so far. */
82
- get emittedUncompressedBytes(): number {
83
- return this.emittedBytes;
84
- }
85
-
86
- /** Read and cache one member, returning null when it does not exist. */
87
- async read(path: string, maxBytes?: number): Promise<Uint8Array | null> {
88
- throwIfZipAborted(this.signal);
89
- this.throwIfFailed();
90
- if (maxBytes !== undefined) assertNonNegativeFinite('member read limit', maxBytes);
91
- const entry = this.entriesByPath.get(path);
92
- if (!entry) return null;
93
-
94
- let pending = this.reads.get(path);
95
- if (!pending) {
96
- pending = this.streamEntry(entry, maxBytes);
97
- this.reads.set(path, pending);
98
- }
99
- let bytes: Uint8Array;
100
- try {
101
- bytes = await pending;
102
- } catch (error: unknown) {
103
- // Cancellation is scoped to the current operation, not archive
104
- // corruption. Do not leave a cancelled read cached for a later caller.
105
- if (this.reads.get(path) === pending && !this.terminalError) this.reads.delete(path);
106
- throw error;
107
- }
108
- throwIfZipAborted(this.signal);
109
- if (maxBytes !== undefined && bytes.byteLength > maxBytes) {
110
- const error = entryTooLargeError(path, bytes.byteLength, maxBytes);
111
- this.failArchive(error);
112
- throw error;
113
- }
114
- return bytes;
115
- }
116
-
117
- /** Read one UTF-8 text member, returning null when it does not exist. */
118
- async readText(path: string, maxBytes?: number): Promise<string | null> {
119
- const bytes = await this.read(path, maxBytes);
120
- return bytes ? new TextDecoder().decode(bytes) : null;
121
- }
122
-
123
- /** Release a cached member when a sequential consumer has taken ownership. */
124
- release(path: string): void {
125
- this.reads.delete(path);
126
- }
127
-
128
- private streamEntry(metadata: ValidatedZipEntry, readLimit?: number): Promise<Uint8Array> {
129
- throwIfZipAborted(this.signal);
130
- const { path, entry, declaredSize, crc32: expectedCrc } = metadata;
131
- const effectiveEntryLimit = Math.min(
132
- this.limits.maxEntryUncompressedBytes,
133
- readLimit ?? Number.POSITIVE_INFINITY,
134
- );
135
- // Never trust a declaration enough to allocate it up front. A forged
136
- // 511 MiB size must not force 511 MiB of memory for a tiny member.
137
- let output: Uint8Array = new Uint8Array(0);
138
- let stream: ZipChunkStream;
139
- try {
140
- stream = (entry as unknown as StreamableZipObject).internalStream('uint8array');
141
- } catch (error: unknown) {
142
- const safeError = asZipSafetyError(error, path);
143
- this.failArchive(safeError);
144
- return Promise.reject(safeError);
145
- }
146
-
147
- return new Promise<Uint8Array>((resolve, reject) => {
148
- let settled = false;
149
- let written = 0;
150
- let crc = 0xffffffff;
151
- const removeAbortListener = (): void =>
152
- this.signal?.removeEventListener('abort', handleAbort);
153
- const handleAbort = (): void =>
154
- controller.stop(this.signal?.reason ?? new Error('ZIP operation was cancelled'));
155
-
156
- const controller: ActiveZipRead = {
157
- stop: (error) => {
158
- if (settled) return;
159
- settled = true;
160
- this.activeReads.delete(controller);
161
- removeAbortListener();
162
- // Public JSZip cancellation is cooperative. It prevents another
163
- // compressed-input tick; data already emitted synchronously by the
164
- // current tick is ignored because `settled` is now true.
165
- try {
166
- stream.pause();
167
- } catch {
168
- // Reject with the original safety failure even if a malformed
169
- // third-party stream cannot be paused cleanly.
170
- }
171
- reject(error);
172
- },
173
- };
174
- this.activeReads.add(controller);
175
- if (this.signal) {
176
- this.signal.addEventListener('abort', handleAbort, { once: true });
177
- if (this.signal.aborted) {
178
- handleAbort();
179
- return;
180
- }
181
- }
182
-
183
- stream
184
- .on('data', (chunk: Uint8Array) => {
185
- if (settled) return;
186
- const nextWritten = written + chunk.byteLength;
187
- try {
188
- throwIfZipAborted(this.signal);
189
- this.assertChunkAllowed(metadata, nextWritten, chunk.byteLength, effectiveEntryLimit);
190
- output = growOutput(output, nextWritten, declaredSize, effectiveEntryLimit);
191
- output.set(chunk, written);
192
- written = nextWritten;
193
- crc = updateCrc32(crc, chunk);
194
- } catch (error: unknown) {
195
- if (this.signal?.aborted) controller.stop(error);
196
- else this.failArchive(asZipSafetyError(error, path));
197
- }
198
- })
199
- .on('error', (error: unknown) => {
200
- if (settled) return;
201
- const message = error instanceof Error ? error.message : String(error);
202
- const code: ZipSafetyErrorCode = message.includes('uncompressed data size mismatch')
203
- ? 'size-mismatch'
204
- : 'decompression-failed';
205
- this.failArchive(
206
- new ZipSafetyError(code, `ZIP import: could not decompress ${path}: ${message}`, {
207
- path,
208
- cause: error,
209
- }),
210
- );
211
- })
212
- .on('end', () => {
213
- if (settled) return;
214
- if (declaredSize !== undefined && written !== declaredSize) {
215
- this.failArchive(
216
- new ZipSafetyError(
217
- 'size-mismatch',
218
- `ZIP import: archive member size does not match its metadata: ${path}.`,
219
- { path, actual: written },
220
- ),
221
- );
222
- return;
223
- }
224
- const actualCrc = (crc ^ 0xffffffff) >>> 0;
225
- if (expectedCrc !== undefined && actualCrc !== expectedCrc >>> 0) {
226
- this.failArchive(
227
- new ZipSafetyError('crc-mismatch', `ZIP import: CRC32 mismatch for ${path}.`, {
228
- path,
229
- }),
230
- );
231
- return;
232
- }
233
-
234
- settled = true;
235
- this.activeReads.delete(controller);
236
- removeAbortListener();
237
- resolve(output.byteLength === written ? output : output.slice(0, written));
238
- })
239
- .resume();
240
- });
241
- }
242
-
243
- private assertChunkAllowed(
244
- entry: ValidatedZipEntry,
245
- nextEntryBytes: number,
246
- chunkBytes: number,
247
- effectiveEntryLimit: number,
248
- ): void {
249
- if (entry.declaredSize !== undefined && nextEntryBytes > entry.declaredSize) {
250
- throw new ZipSafetyError(
251
- 'size-mismatch',
252
- `ZIP import: archive member size does not match its metadata: ${entry.path}.`,
253
- { path: entry.path, actual: nextEntryBytes },
254
- );
255
- }
256
- if (
257
- effectiveEntryLimit < this.limits.maxUncompressedBytes &&
258
- nextEntryBytes > effectiveEntryLimit
259
- ) {
260
- throw entryTooLargeError(entry.path, nextEntryBytes, effectiveEntryLimit);
261
- }
262
-
263
- const nextTotal = this.emittedBytes + chunkBytes;
264
- if (nextTotal > this.limits.maxUncompressedBytes) {
265
- throw archiveTooLargeError(nextTotal, this.limits.maxUncompressedBytes);
266
- }
267
- if (nextEntryBytes > effectiveEntryLimit) {
268
- throw entryTooLargeError(entry.path, nextEntryBytes, effectiveEntryLimit);
269
- }
270
-
271
- const ratio =
272
- entry.compressedSize === undefined
273
- ? undefined
274
- : compressionRatio(nextEntryBytes, entry.compressedSize);
275
- if (ratio !== undefined && ratio > this.limits.maxCompressionRatio) {
276
- throw compressionRatioError(entry.path, ratio, this.limits.maxCompressionRatio);
277
- }
278
- this.emittedBytes = nextTotal;
279
- }
280
-
281
- private failArchive(error: ZipSafetyError): void {
282
- if (this.terminalError) return;
283
- this.terminalError = error;
284
- for (const active of [...this.activeReads]) active.stop(error);
285
- }
286
-
287
- private throwIfFailed(): void {
288
- if (this.terminalError) throw this.terminalError;
289
- }
290
- }
291
-
292
- /** Load with JSZip, validate metadata, and create a shared bounded read session. */
293
- export async function openBoundedZipArchive(
294
- data: ZipInput,
295
- limits: ZipSafetyLimits = {},
296
- ): Promise<BoundedZipArchive> {
297
- throwIfZipAborted(limits.signal);
298
- const resolved = resolveZipSafetyLimits(limits);
299
- let prepared: { input: ZipInput; bytes?: Uint8Array };
300
- try {
301
- prepared = await prepareZipInput(data, limits.signal);
302
- } catch (error: unknown) {
303
- if (limits.signal?.aborted) throw limits.signal.reason ?? error;
304
- throw invalidArchiveError(error);
305
- }
306
- throwIfZipAborted(limits.signal);
307
- if (prepared.bytes) {
308
- const declaredEntries = declaredZipEntryCount(prepared.bytes);
309
- if (declaredEntries !== undefined && declaredEntries > resolved.maxEntries) {
310
- throw tooManyEntriesError(declaredEntries, resolved.maxEntries);
311
- }
312
- }
313
- let zip: JSZip;
314
- try {
315
- zip = await JSZip.loadAsync(prepared.input);
316
- } catch (error: unknown) {
317
- if (limits.signal?.aborted) throw limits.signal.reason ?? error;
318
- throw invalidArchiveError(error);
319
- }
320
- throwIfZipAborted(limits.signal);
321
- return createBoundedZipArchive(zip, limits);
322
- }
323
-
324
- function invalidArchiveError(cause: unknown): ZipSafetyError {
325
- return new ZipSafetyError('invalid-archive', 'ZIP import: archive is not readable.', { cause });
326
- }
327
-
328
- /** Wrap an existing JSZip instance in the same validation and shared budget. */
329
- export function createBoundedZipArchive(
330
- zip: JSZip,
331
- limits: ZipSafetyLimits = {},
332
- ): BoundedZipArchive {
333
- throwIfZipAborted(limits.signal);
334
- const resolved = resolveZipSafetyLimits(limits);
335
- const entries = validateZipArchive(zip, limits);
336
- throwIfZipAborted(limits.signal);
337
- return new JsZipBoundedArchive(zip, entries, resolved, limits.signal);
338
- }
339
-
340
- function asZipSafetyError(error: unknown, path: string): ZipSafetyError {
341
- if (error instanceof ZipSafetyError) return error;
342
- return new ZipSafetyError(
343
- 'decompression-failed',
344
- `ZIP import: could not decompress ${path}: ${error instanceof Error ? error.message : String(error)}`,
345
- { path, cause: error },
346
- );
347
- }
348
-
349
- function growOutput(
350
- current: Uint8Array,
351
- required: number,
352
- declaredSize: number | undefined,
353
- limit: number,
354
- ): Uint8Array {
355
- if (required <= current.byteLength) return current;
356
- const maximum = Math.min(declaredSize ?? limit, limit);
357
- const grownSize = current.byteLength === 0 ? required : current.byteLength * 2;
358
- const nextSize = Math.min(maximum, Math.max(required, grownSize));
359
- const next = new Uint8Array(nextSize);
360
- next.set(current);
361
- return next;
362
- }
363
-
364
- let crcTable: Uint32Array | undefined;
365
-
366
- function updateCrc32(crc: number, bytes: Uint8Array): number {
367
- const table = crcTable ?? (crcTable = makeCrcTable());
368
- let next = crc >>> 0;
369
- for (const byte of bytes) next = table[(next ^ byte) & 0xff]! ^ (next >>> 8);
370
- return next >>> 0;
371
- }
372
-
373
- function makeCrcTable(): Uint32Array {
374
- const table = new Uint32Array(256);
375
- for (let n = 0; n < table.length; n++) {
376
- let value = n;
377
- for (let bit = 0; bit < 8; bit++) {
378
- value = (value & 1) !== 0 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
379
- }
380
- table[n] = value >>> 0;
381
- }
382
- return table;
383
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * Shared tail for the container-producing importers.
3
- *
4
- * `docxToContainer`, `pptxToContainer`, and `pdfToContainer` all end the same
5
- * way: create a `MemoryContentContainer`, write the primary markdown document,
6
- * then write each extracted image under its path with its MIME type. This
7
- * factors that identical epilogue into one place.
8
- */
9
-
10
- import { MemoryContentContainer } from '@bendyline/squisq/storage';
11
- import type { ContentContainer } from '@bendyline/squisq/storage';
12
-
13
- /**
14
- * Assemble a {@link ContentContainer} from a markdown document plus a set of
15
- * media files. `images` is any iterable of `[path, { data, mimeType }]` pairs
16
- * (a `Map` from the importers' extraction context satisfies this directly).
17
- */
18
- export async function buildContainer(
19
- markdown: string,
20
- images: Iterable<readonly [string, { data: ArrayBuffer; mimeType: string }]>,
21
- ): Promise<ContentContainer> {
22
- const container = new MemoryContentContainer();
23
- await container.writeDocument(markdown);
24
- for (const [path, { data, mimeType }] of images) {
25
- await container.writeFile(path, new Uint8Array(data), mimeType);
26
- }
27
- return container;
28
- }