@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,413 +0,0 @@
1
- /**
2
- * Built-in format definitions.
3
- *
4
- * Each definition loads its converter module lazily via `import()` inside the
5
- * method body, so importing the registry never eagerly bundles a heavy
6
- * converter (pdf-lib, jszip, the OOXML writers, …). The capability of each
7
- * format — which of import/export it supports — is expressed purely by which
8
- * methods are present.
9
- */
10
-
11
- import type { ContentContainer } from '@bendyline/squisq/storage';
12
- import type { MarkdownDocument } from '@bendyline/squisq/markdown';
13
- import { ConversionError } from './errors.js';
14
- import { markdownFidelityWarnings } from '../shared/fidelity.js';
15
- import type {
16
- ConversionResult,
17
- BuiltinFormatOptions,
18
- ConvertOptions,
19
- FormatDefinition,
20
- NormalizedInput,
21
- } from './types.js';
22
-
23
- // ── MIME constants ──────────────────────────────────────────────────
24
-
25
- const MIME = {
26
- md: 'text/markdown',
27
- docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
28
- pdf: 'application/pdf',
29
- pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
30
- xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
31
- csv: 'text/csv',
32
- html: 'text/html',
33
- zip: 'application/zip',
34
- epub: 'application/epub+zip',
35
- } as const;
36
-
37
- // ── Small shared helpers ────────────────────────────────────────────
38
-
39
- /** Normalize any binary export return shape to Uint8Array. */
40
- async function toBytes(data: ArrayBuffer | Uint8Array | Blob): Promise<Uint8Array> {
41
- if (data instanceof Uint8Array) return data;
42
- if (data instanceof ArrayBuffer) return new Uint8Array(data);
43
- // Blob. Prefer the modern arrayBuffer() method; fall back to FileReader for
44
- // environments (e.g. some jsdom builds) whose Blob lacks it.
45
- if (typeof data.arrayBuffer === 'function') {
46
- return new Uint8Array(await data.arrayBuffer());
47
- }
48
- const buffer = await new Promise<ArrayBuffer>((resolve, reject) => {
49
- const reader = new FileReader();
50
- reader.onload = () => resolve(reader.result as ArrayBuffer);
51
- reader.onerror = () => reject(reader.error);
52
- reader.readAsArrayBuffer(data);
53
- });
54
- return new Uint8Array(buffer);
55
- }
56
-
57
- /** The theme to apply: explicit option wins, then the doc's own theme. */
58
- function resolveThemeId(input: NormalizedInput, options: ConvertOptions): string | undefined {
59
- return options.themeId ?? input.doc.themeId;
60
- }
61
-
62
- function optionsFor<K extends keyof BuiltinFormatOptions>(
63
- options: ConvertOptions,
64
- id: K,
65
- ): BuiltinFormatOptions[K] {
66
- return (options.formatOptions?.[id] ?? {}) as BuiltinFormatOptions[K];
67
- }
68
-
69
- /** The markdown shape an exporter needs — reuse the source's, else derive it. */
70
- async function markdownOf(input: NormalizedInput): Promise<MarkdownDocument> {
71
- if (input.markdownDoc) return input.markdownDoc;
72
- const { docToMarkdown } = await import('@bendyline/squisq/doc');
73
- return docToMarkdown(input.doc);
74
- }
75
-
76
- /**
77
- * Collect image files from a container, keyed by both their container path and
78
- * their bare filename. Exporters that resolve by url (PPTX) and the standalone
79
- * player (HTML) both match against one of those two keys.
80
- */
81
- async function collectContainerImages(
82
- container: ContentContainer,
83
- ): Promise<Map<string, ArrayBuffer>> {
84
- const images = new Map<string, ArrayBuffer>();
85
- const files = await container.listFiles();
86
- for (const file of files) {
87
- if (!/\.(jpg|jpeg|png|gif|webp|svg|bmp|avif)$/i.test(file.path)) continue;
88
- const data = await container.readFile(file.path);
89
- if (!data) continue;
90
- images.set(file.path, data);
91
- const slash = file.path.lastIndexOf('/');
92
- if (slash !== -1) images.set(file.path.slice(slash + 1), data);
93
- }
94
- return images;
95
- }
96
-
97
- async function collectContainerDocxImages(
98
- container: ContentContainer,
99
- ): Promise<Map<string, { data: ArrayBuffer; contentType: string }>> {
100
- const images = await collectContainerImages(container);
101
- const { extToMime } = await import('../shared/images.js');
102
- return new Map(
103
- [...images].map(([path, data]) => [
104
- path,
105
- { data, contentType: extToMime(path.split('.').pop() ?? '') },
106
- ]),
107
- );
108
- }
109
-
110
- /** Copy a container and make its primary document match the normalized source. */
111
- async function snapshotContainerWithDocument(input: NormalizedInput): Promise<ContentContainer> {
112
- const { MemoryContentContainer } = await import('@bendyline/squisq/storage');
113
- const snapshot = new MemoryContentContainer();
114
- const entries = await input.container.listFiles();
115
- for (const entry of entries) {
116
- const data = await input.container.readFile(entry.path);
117
- if (data) await snapshot.writeFile(entry.path, new Uint8Array(data), entry.mimeType);
118
- }
119
- const { stringifyMarkdown } = await import('@bendyline/squisq/markdown');
120
- const documentPath = (await input.container.getDocumentPath()) ?? 'index.md';
121
- await snapshot.writeDocument(stringifyMarkdown(await markdownOf(input)), documentPath);
122
- return snapshot;
123
- }
124
-
125
- /** Resolve the player IIFE bundle or throw a helpful missing-dependency error. */
126
- async function requirePlayerScript(options: ConvertOptions, format: string): Promise<string> {
127
- const script = await options.resolvePlayerScript?.();
128
- if (!script) {
129
- throw new ConversionError('missing-dependency', 'HTML export needs a player script.', {
130
- format,
131
- hint: "pass resolvePlayerScript, e.g. () => import('@bendyline/squisq-react/standalone-source').then(m => m.PLAYER_BUNDLE)",
132
- });
133
- }
134
- return script;
135
- }
136
-
137
- function ok(bytes: Uint8Array, mimeType: string, warnings: string[] = []): ConversionResult {
138
- // suggestedFilename is filled in by convert() once the baseName is known.
139
- return { bytes, mimeType, suggestedFilename: '', warnings };
140
- }
141
-
142
- /**
143
- * Narrow the untyped per-format escape hatch (`options.formatOptions.pptx`)
144
- * into the PPTX importer's options. Theme + layout inference default ON;
145
- * only an explicit `false` disables them.
146
- */
147
- function pptxImportOptionsFrom(options: ConvertOptions): BuiltinFormatOptions['pptx'] {
148
- const raw = optionsFor(options, 'pptx');
149
- return {
150
- ...raw,
151
- inferTheme: raw?.inferTheme !== false,
152
- inferLayouts: raw?.inferLayouts !== false,
153
- };
154
- }
155
-
156
- // ── Definitions ─────────────────────────────────────────────────────
157
-
158
- export function defaultFormats(): FormatDefinition[] {
159
- const md: FormatDefinition = {
160
- id: 'md',
161
- label: 'Markdown',
162
- mimeType: MIME.md,
163
- extensions: ['.md', '.markdown'],
164
- async importDoc(data, options): Promise<MarkdownDocument> {
165
- const { parseMarkdown } = await import('@bendyline/squisq/markdown');
166
- return parseMarkdown(new TextDecoder().decode(data), optionsFor(options, 'md').parse);
167
- },
168
- async exportDoc(input, options): Promise<ConversionResult> {
169
- const { stringifyMarkdown } = await import('@bendyline/squisq/markdown');
170
- const text = stringifyMarkdown(await markdownOf(input), optionsFor(options, 'md').stringify);
171
- return ok(new TextEncoder().encode(text), MIME.md);
172
- },
173
- };
174
-
175
- const docx: FormatDefinition = {
176
- id: 'docx',
177
- label: 'Word (DOCX)',
178
- mimeType: MIME.docx,
179
- extensions: ['.docx'],
180
- async importContainer(data, options): Promise<ContentContainer> {
181
- const { docxToContainer } = await import('../docx/index.js');
182
- return docxToContainer(data, optionsFor(options, 'docx'));
183
- },
184
- async importDoc(data, options): Promise<MarkdownDocument> {
185
- const { docxToMarkdownDoc } = await import('../docx/index.js');
186
- return docxToMarkdownDoc(data, optionsFor(options, 'docx'));
187
- },
188
- async exportDoc(input, options): Promise<ConversionResult> {
189
- const { markdownDocToDocx } = await import('../docx/index.js');
190
- const raw = optionsFor(options, 'docx');
191
- const markdownDoc = await markdownOf(input);
192
- const containerImages = await collectContainerDocxImages(input.container);
193
- const images = new Map([...containerImages, ...(raw.images ?? new Map())]);
194
- const buf = await markdownDocToDocx(markdownDoc, {
195
- ...raw,
196
- ...(options.title !== undefined ? { title: options.title } : {}),
197
- themeId: resolveThemeId(input, options),
198
- themeRegistry: options.themeRegistry ?? raw.themeRegistry,
199
- images,
200
- });
201
- return ok(await toBytes(buf), MIME.docx, markdownFidelityWarnings(markdownDoc, 'docx'));
202
- },
203
- };
204
-
205
- const pdf: FormatDefinition = {
206
- id: 'pdf',
207
- label: 'PDF',
208
- mimeType: MIME.pdf,
209
- extensions: ['.pdf'],
210
- async importContainer(data, options): Promise<ContentContainer> {
211
- const { pdfToContainer } = await import('../pdf/index.js');
212
- return pdfToContainer(data, optionsFor(options, 'pdf'));
213
- },
214
- async importDoc(data, options): Promise<MarkdownDocument> {
215
- const { pdfToMarkdownDoc } = await import('../pdf/index.js');
216
- return pdfToMarkdownDoc(data, optionsFor(options, 'pdf'));
217
- },
218
- async exportDoc(input, options): Promise<ConversionResult> {
219
- const { markdownDocToPdf } = await import('../pdf/index.js');
220
- const raw = optionsFor(options, 'pdf');
221
- const markdownDoc = await markdownOf(input);
222
- const buf = await markdownDocToPdf(markdownDoc, {
223
- ...raw,
224
- ...(options.title !== undefined ? { title: options.title } : {}),
225
- themeId: resolveThemeId(input, options),
226
- themeRegistry: options.themeRegistry ?? raw.themeRegistry,
227
- });
228
- return ok(await toBytes(buf), MIME.pdf, markdownFidelityWarnings(markdownDoc, 'pdf'));
229
- },
230
- };
231
-
232
- const pptx: FormatDefinition = {
233
- id: 'pptx',
234
- label: 'PowerPoint (PPTX)',
235
- mimeType: MIME.pptx,
236
- extensions: ['.pptx'],
237
- async importContainer(data, options): Promise<ContentContainer> {
238
- const { pptxToContainer } = await import('../pptx/index.js');
239
- return pptxToContainer(data, pptxImportOptionsFrom(options));
240
- },
241
- async importDoc(data, options): Promise<MarkdownDocument> {
242
- const { pptxToMarkdownDoc } = await import('../pptx/index.js');
243
- return pptxToMarkdownDoc(data, pptxImportOptionsFrom(options));
244
- },
245
- async exportDoc(input, options): Promise<ConversionResult> {
246
- const { markdownDocToPptx } = await import('../pptx/index.js');
247
- const raw = optionsFor(options, 'pptx');
248
- const markdownDoc = await markdownOf(input);
249
- const containerImages = await collectContainerImages(input.container);
250
- const images = new Map([...containerImages, ...(raw.images ?? new Map())]);
251
- const buf = await markdownDocToPptx(markdownDoc, {
252
- ...raw,
253
- ...(options.title !== undefined ? { title: options.title } : {}),
254
- themeId: resolveThemeId(input, options),
255
- themeRegistry: options.themeRegistry ?? raw.themeRegistry,
256
- images,
257
- });
258
- return ok(await toBytes(buf), MIME.pptx, markdownFidelityWarnings(markdownDoc, 'pptx'));
259
- },
260
- };
261
-
262
- const xlsx: FormatDefinition = {
263
- id: 'xlsx',
264
- label: 'Excel (XLSX)',
265
- mimeType: MIME.xlsx,
266
- extensions: ['.xlsx'],
267
- async importDoc(data, options): Promise<MarkdownDocument> {
268
- const { xlsxToMarkdownDoc } = await import('../xlsx/index.js');
269
- return xlsxToMarkdownDoc(data, optionsFor(options, 'xlsx'));
270
- },
271
- async exportDoc(input, options): Promise<ConversionResult> {
272
- const { markdownDocToXlsx } = await import('../xlsx/index.js');
273
- const markdownDoc = await markdownOf(input);
274
- // XLSX fidelity is tables-only: `table` nodes become worksheets and
275
- // headings name them, but every other top-level block (prose, lists,
276
- // images, code, …) is dropped. Count those and warn honestly.
277
- const omitted = markdownDoc.children.filter(
278
- (n) => n.type !== 'table' && n.type !== 'heading',
279
- ).length;
280
- const warnings =
281
- omitted > 0
282
- ? [`XLSX export is tables-only; ${omitted} non-table block(s) were omitted.`]
283
- : [];
284
- const blob = await markdownDocToXlsx(markdownDoc, {
285
- ...optionsFor(options, 'xlsx'),
286
- ...(options.title !== undefined ? { title: options.title } : {}),
287
- });
288
- return ok(await toBytes(blob), MIME.xlsx, warnings);
289
- },
290
- };
291
-
292
- const csv: FormatDefinition = {
293
- id: 'csv',
294
- label: 'CSV',
295
- mimeType: MIME.csv,
296
- extensions: ['.csv'],
297
- async importDoc(data, options): Promise<MarkdownDocument> {
298
- const { csvToMarkdownDoc } = await import('../csv/index.js');
299
- return csvToMarkdownDoc(data, optionsFor(options, 'csv'));
300
- },
301
- async exportDoc(input, options): Promise<ConversionResult> {
302
- const { markdownDocToCsv } = await import('../csv/index.js');
303
- const markdownDoc = await markdownOf(input);
304
- const raw = optionsFor(options, 'csv');
305
- const tableCount = markdownDoc.children.filter((n) => n.type === 'table').length;
306
- const warnings: string[] = [];
307
- if (tableCount > 1 && raw.tableIndex === undefined) {
308
- warnings.push(
309
- `Document has ${tableCount} tables; CSV export emitted only the first. Use the csv converter's tableIndex option to select another.`,
310
- );
311
- }
312
- const text = markdownDocToCsv(markdownDoc, raw);
313
- return ok(new TextEncoder().encode(text), MIME.csv, warnings);
314
- },
315
- };
316
-
317
- const html: FormatDefinition = {
318
- id: 'html',
319
- label: 'HTML (single file)',
320
- mimeType: MIME.html,
321
- extensions: ['.html', '.htm'],
322
- async importDoc(data, options): Promise<MarkdownDocument> {
323
- const { htmlToMarkdownDoc } = await import('../html/index.js');
324
- return htmlToMarkdownDoc(data, optionsFor(options, 'html'));
325
- },
326
- // importContainer omitted: HTML import already inlines images as data URIs,
327
- // so there is nothing to extract into a container this wave.
328
- async exportDoc(input, options): Promise<ConversionResult> {
329
- const playerScript = await requirePlayerScript(options, 'html');
330
- const { docToHtml } = await import('../html/index.js');
331
- const raw = optionsFor(options, 'html');
332
- const containerImages = await collectContainerImages(input.container);
333
- const images = new Map([...containerImages, ...(raw.images ?? new Map())]);
334
- const htmlText = docToHtml(input.doc, {
335
- ...raw,
336
- playerScript,
337
- images,
338
- title: options.title ?? input.baseName,
339
- mode: raw.mode ?? 'static',
340
- themeId: resolveThemeId(input, options),
341
- themeRegistry: options.themeRegistry ?? raw.themeRegistry,
342
- });
343
- return ok(new TextEncoder().encode(htmlText), MIME.html);
344
- },
345
- };
346
-
347
- const htmlzip: FormatDefinition = {
348
- id: 'htmlzip',
349
- label: 'HTML (ZIP archive)',
350
- mimeType: MIME.zip,
351
- extensions: ['.html.zip'],
352
- async exportDoc(input, options): Promise<ConversionResult> {
353
- const playerScript = await requirePlayerScript(options, 'htmlzip');
354
- const { docToHtmlZip } = await import('../html/index.js');
355
- const raw = optionsFor(options, 'htmlzip');
356
- const containerImages = await collectContainerImages(input.container);
357
- const images = new Map([...containerImages, ...(raw.images ?? new Map())]);
358
- const blob = await docToHtmlZip(input.doc, {
359
- ...raw,
360
- playerScript,
361
- images,
362
- title: options.title ?? input.baseName,
363
- mode: raw.mode ?? 'static',
364
- themeId: resolveThemeId(input, options),
365
- themeRegistry: options.themeRegistry ?? raw.themeRegistry,
366
- });
367
- return ok(await toBytes(blob), MIME.zip);
368
- },
369
- };
370
-
371
- const epub: FormatDefinition = {
372
- id: 'epub',
373
- label: 'EPUB',
374
- mimeType: MIME.epub,
375
- extensions: ['.epub'],
376
- async exportDoc(input, options): Promise<ConversionResult> {
377
- const { markdownDocToEpub } = await import('../epub/index.js');
378
- const raw = optionsFor(options, 'epub');
379
- const markdownDoc = await markdownOf(input);
380
- const containerImages = await collectContainerImages(input.container);
381
- const images = new Map([...containerImages, ...(raw.images ?? new Map())]);
382
- const buf = await markdownDocToEpub(markdownDoc, {
383
- ...raw,
384
- title: options.title ?? input.baseName,
385
- themeId: resolveThemeId(input, options),
386
- themeRegistry: options.themeRegistry ?? raw.themeRegistry,
387
- images,
388
- });
389
- return ok(await toBytes(buf), MIME.epub, markdownFidelityWarnings(markdownDoc, 'epub'));
390
- },
391
- };
392
-
393
- const dbk: FormatDefinition = {
394
- id: 'dbk',
395
- label: 'Squisq container (DBK)',
396
- mimeType: MIME.zip,
397
- extensions: ['.dbk', '.zip'],
398
- async importContainer(data, options): Promise<ContentContainer> {
399
- const { zipToContainer } = await import('../container/index.js');
400
- return zipToContainer(data, optionsFor(options, 'dbk'));
401
- },
402
- async exportDoc(input, options): Promise<ConversionResult> {
403
- const { containerToZip } = await import('../container/index.js');
404
- const blob = await containerToZip(
405
- await snapshotContainerWithDocument(input),
406
- optionsFor(options, 'dbk'),
407
- );
408
- return ok(await toBytes(blob), MIME.zip);
409
- },
410
- };
411
-
412
- return [md, docx, pdf, pptx, xlsx, csv, html, htmlzip, epub, dbk];
413
- }
@@ -1,46 +0,0 @@
1
- /**
2
- * Structured error for the format registry / `convert()` pipeline.
3
- *
4
- * Every failure path in the registry throws a `ConversionError` with a stable
5
- * machine-readable `code`, so callers can branch on the failure kind rather
6
- * than string-matching messages.
7
- */
8
-
9
- import type { FormatId } from './types.js';
10
-
11
- /** Machine-readable failure kinds surfaced by the registry / `convert()`. */
12
- export type ConversionErrorCode =
13
- | 'unknown-format'
14
- | 'unsupported-input'
15
- | 'unsupported-output'
16
- | 'invalid-input'
17
- | 'missing-dependency'
18
- | 'conversion-failed';
19
-
20
- /** Options for constructing a {@link ConversionError}. */
21
- export interface ConversionErrorOptions {
22
- /** The format id the error relates to, if any. */
23
- format?: FormatId;
24
- /** A human-oriented remediation hint. */
25
- hint?: string;
26
- /** The underlying error, preserved on `error.cause`. */
27
- cause?: unknown;
28
- }
29
-
30
- /** An error raised by the format registry or `convert()`. */
31
- export class ConversionError extends Error {
32
- readonly code: ConversionErrorCode;
33
- readonly format?: FormatId;
34
- readonly hint?: string;
35
-
36
- constructor(code: ConversionErrorCode, message: string, opts: ConversionErrorOptions = {}) {
37
- super(message);
38
- this.name = 'ConversionError';
39
- this.code = code;
40
- this.format = opts.format;
41
- this.hint = opts.hint;
42
- if (opts.cause !== undefined) {
43
- (this as Error & { cause?: unknown }).cause = opts.cause;
44
- }
45
- }
46
- }
@@ -1,43 +0,0 @@
1
- /**
2
- * Format registry + programmatic `convert()` — @bendyline/squisq-formats/registry
3
- *
4
- * A small registry maps format ids (`md`, `docx`, `pdf`, `pptx`, `xlsx`, `csv`,
5
- * `html`, `htmlzip`, `epub`, `dbk`) to `FormatDefinition`s that know how to
6
- * import to / export from squisq's markdown + Doc model. `convert()` is the
7
- * front door: give it a source and a target format id and it returns the
8
- * encoded bytes plus a suggested filename.
9
- *
10
- * @example
11
- * ```ts
12
- * import { convert } from '@bendyline/squisq-formats/registry';
13
- *
14
- * const result = await convert(
15
- * { kind: 'markdown', markdown: '# Hello', baseName: 'greeting' },
16
- * 'docx',
17
- * );
18
- * // result.bytes, result.mimeType, result.suggestedFilename ('greeting.docx')
19
- * ```
20
- */
21
-
22
- export type {
23
- FormatId,
24
- ConversionResult,
25
- NormalizedInput,
26
- ConvertOptions,
27
- FormatDefinition,
28
- FormatRegistry,
29
- ConvertSource,
30
- BuiltinFormatOptions,
31
- MarkdownFormatOptions,
32
- DbkFormatOptions,
33
- PreparedConversion,
34
- PreparedExportOptions,
35
- } from './types.js';
36
- export { BUILTIN_FORMAT_IDS } from './types.js';
37
-
38
- export { ConversionError } from './errors.js';
39
- export type { ConversionErrorCode, ConversionErrorOptions } from './errors.js';
40
-
41
- export { createRegistry, defaultRegistry } from './registry.js';
42
- export { defaultFormats } from './defaultFormats.js';
43
- export { convert, prepareConversion } from './convert.js';
@@ -1,48 +0,0 @@
1
- /**
2
- * Format registry implementation.
3
- *
4
- * `createRegistry()` returns an empty, mutable registry (last-write-wins by id);
5
- * `defaultRegistry()` returns one preloaded with every built-in format.
6
- */
7
-
8
- import type { FormatDefinition, FormatId, FormatRegistry } from './types.js';
9
- import { defaultFormats } from './defaultFormats.js';
10
-
11
- /** Normalize an extension to lowercase, without a leading dot. */
12
- function normalizeExt(ext: string): string {
13
- return ext.replace(/^\.+/, '').toLowerCase();
14
- }
15
-
16
- /** Create an empty registry. Registering an existing id overwrites it. */
17
- export function createRegistry(): FormatRegistry {
18
- const byId = new Map<FormatId, FormatDefinition>();
19
-
20
- return {
21
- register(def: FormatDefinition): void {
22
- byId.set(def.id, def);
23
- },
24
- get(id: FormatId): FormatDefinition | undefined {
25
- return byId.get(id);
26
- },
27
- byExtension(ext: string): FormatDefinition | undefined {
28
- const target = normalizeExt(ext);
29
- if (!target) return undefined;
30
- for (const def of byId.values()) {
31
- if (def.extensions.some((e) => normalizeExt(e) === target)) return def;
32
- }
33
- return undefined;
34
- },
35
- list(): FormatDefinition[] {
36
- return [...byId.values()];
37
- },
38
- };
39
- }
40
-
41
- /** Create a registry preloaded with all built-in formats. */
42
- export function defaultRegistry(): FormatRegistry {
43
- const registry = createRegistry();
44
- for (const def of defaultFormats()) {
45
- registry.register(def);
46
- }
47
- return registry;
48
- }