@bendyline/squisq-formats 1.4.2 → 2.0.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 (139) hide show
  1. package/README.md +48 -2
  2. package/dist/{chunk-WWBIKRNP.js → chunk-2CSZJEAL.js} +13 -11
  3. package/dist/chunk-2CSZJEAL.js.map +1 -0
  4. package/dist/{chunk-RTS5XBZ7.js → chunk-2EFDWKC6.js} +262 -84
  5. package/dist/chunk-2EFDWKC6.js.map +1 -0
  6. package/dist/chunk-2PRVLAU3.js +740 -0
  7. package/dist/chunk-2PRVLAU3.js.map +1 -0
  8. package/dist/{chunk-SHLPSADL.js → chunk-4HJ6AK2Q.js} +54 -193
  9. package/dist/chunk-4HJ6AK2Q.js.map +1 -0
  10. package/dist/{chunk-5LSSWZPU.js → chunk-4VUWTSGM.js} +72 -25
  11. package/dist/chunk-4VUWTSGM.js.map +1 -0
  12. package/dist/{chunk-YESS7VY3.js → chunk-CVNVD7VH.js} +154 -39
  13. package/dist/chunk-CVNVD7VH.js.map +1 -0
  14. package/dist/chunk-CX3LH47J.js +99 -0
  15. package/dist/chunk-CX3LH47J.js.map +1 -0
  16. package/dist/{chunk-D7IFPWNZ.js → chunk-GVS2XXV6.js} +27 -26
  17. package/dist/chunk-GVS2XXV6.js.map +1 -0
  18. package/dist/chunk-H7T5DXUU.js +479 -0
  19. package/dist/chunk-H7T5DXUU.js.map +1 -0
  20. package/dist/{chunk-EHLGMSTE.js → chunk-HBSCRS5P.js} +10 -8
  21. package/dist/chunk-HBSCRS5P.js.map +1 -0
  22. package/dist/{chunk-46FMDOWZ.js → chunk-LXYLOOST.js} +49 -17
  23. package/dist/chunk-LXYLOOST.js.map +1 -0
  24. package/dist/{chunk-MJGRI6XR.js → chunk-MLX2BOJC.js} +12 -7
  25. package/dist/chunk-MLX2BOJC.js.map +1 -0
  26. package/dist/chunk-NKAJPJ4G.js +106 -0
  27. package/dist/chunk-NKAJPJ4G.js.map +1 -0
  28. package/dist/{chunk-DQAZR57U.js → chunk-O3GVVND4.js} +1 -1
  29. package/dist/chunk-ODL3SSPT.js +18 -0
  30. package/dist/chunk-ODL3SSPT.js.map +1 -0
  31. package/dist/chunk-X4USWOQ2.js +131 -0
  32. package/dist/chunk-X4USWOQ2.js.map +1 -0
  33. package/dist/chunk-XTLQXR4U.js +195 -0
  34. package/dist/chunk-XTLQXR4U.js.map +1 -0
  35. package/dist/chunk-YRT7GQ5Y.js +28 -0
  36. package/dist/chunk-YRT7GQ5Y.js.map +1 -0
  37. package/dist/chunk-ZZ7D4DN6.js +97 -0
  38. package/dist/chunk-ZZ7D4DN6.js.map +1 -0
  39. package/dist/container/index.d.ts +6 -2
  40. package/dist/container/index.js +15 -17
  41. package/dist/container/index.js.map +1 -1
  42. package/dist/docx/index.d.ts +6 -2
  43. package/dist/docx/index.js +5 -3
  44. package/dist/epub/index.d.ts +3 -1
  45. package/dist/epub/index.js +3 -3
  46. package/dist/export-Djev9BLU.d.ts +69 -0
  47. package/dist/extract-LNOA7DBK.js +15 -0
  48. package/dist/extract-LNOA7DBK.js.map +1 -0
  49. package/dist/html/index.d.ts +15 -85
  50. package/dist/html/index.js +3 -3
  51. package/dist/images-7FBWPKE3.js +7 -0
  52. package/dist/images-7FBWPKE3.js.map +1 -0
  53. package/dist/import-DTkDxHmZ.d.ts +84 -0
  54. package/dist/import-Yivc3kZM.d.ts +136 -0
  55. package/dist/index.d.ts +13 -4
  56. package/dist/index.js +30 -11
  57. package/dist/infer/index.d.ts +138 -0
  58. package/dist/infer/index.js +28 -0
  59. package/dist/infer/index.js.map +1 -0
  60. package/dist/layouts-7OUUVAGU.js +12 -0
  61. package/dist/layouts-7OUUVAGU.js.map +1 -0
  62. package/dist/layouts-CZ0bdsNL.d.ts +150 -0
  63. package/dist/mapTheme-IR27S6IV.js +11 -0
  64. package/dist/mapTheme-IR27S6IV.js.map +1 -0
  65. package/dist/ooxml/index.d.ts +21 -127
  66. package/dist/ooxml/index.js +31 -9
  67. package/dist/pdf/index.d.ts +3 -1
  68. package/dist/pdf/index.js +1 -1
  69. package/dist/pptx/index.d.ts +8 -108
  70. package/dist/pptx/index.js +13 -4
  71. package/dist/reader-Gof6OrJX.d.ts +140 -0
  72. package/dist/registry/index.d.ts +46 -8
  73. package/dist/registry/index.js +5 -2
  74. package/dist/themeReader-DPw4J5Ss.d.ts +56 -0
  75. package/dist/xlsx/index.d.ts +6 -66
  76. package/dist/xlsx/index.js +4 -2
  77. package/dist/zipLimits-bGYMS9cI.d.ts +29 -0
  78. package/package.json +8 -2
  79. package/src/__tests__/container.test.ts +41 -0
  80. package/src/__tests__/convert.test.ts +167 -0
  81. package/src/__tests__/epub.test.ts +102 -1
  82. package/src/__tests__/html.test.ts +62 -0
  83. package/src/__tests__/inferTheme.test.ts +135 -0
  84. package/src/__tests__/lossyWarnings.test.ts +41 -0
  85. package/src/__tests__/ooxml.test.ts +59 -2
  86. package/src/__tests__/ooxmlThemeReader.test.ts +92 -0
  87. package/src/__tests__/plainHtml.test.ts +15 -1
  88. package/src/__tests__/plainHtmlBundle.test.ts +18 -0
  89. package/src/__tests__/pptxImport.test.ts +30 -0
  90. package/src/__tests__/pptxInferFixtures.ts +314 -0
  91. package/src/__tests__/pptxLayoutInfer.test.ts +395 -0
  92. package/src/__tests__/zipSafety.test.ts +317 -0
  93. package/src/container/index.ts +20 -29
  94. package/src/docx/export.ts +4 -2
  95. package/src/docx/import.ts +4 -3
  96. package/src/epub/export.ts +94 -25
  97. package/src/html/docsHtmlBundle.ts +6 -2
  98. package/src/html/htmlTemplate.ts +31 -6
  99. package/src/html/imageUtils.ts +14 -5
  100. package/src/html/index.ts +12 -1
  101. package/src/html/plainHtml.ts +7 -3
  102. package/src/html/plainHtmlBundle.ts +11 -9
  103. package/src/index.ts +20 -0
  104. package/src/infer/extract.ts +127 -0
  105. package/src/infer/index.ts +168 -0
  106. package/src/infer/mapTheme.ts +176 -0
  107. package/src/infer/types.ts +27 -0
  108. package/src/ooxml/index.ts +14 -0
  109. package/src/ooxml/readUtils.ts +44 -0
  110. package/src/ooxml/reader.ts +87 -28
  111. package/src/ooxml/themeReader.ts +197 -0
  112. package/src/ooxml/types.ts +8 -9
  113. package/src/ooxml/writer.ts +28 -10
  114. package/src/pdf/export.ts +4 -2
  115. package/src/pdf/import.ts +40 -34
  116. package/src/pptx/export.ts +4 -2
  117. package/src/pptx/import.ts +214 -32
  118. package/src/pptx/index.ts +14 -0
  119. package/src/pptx/layouts.ts +1168 -0
  120. package/src/registry/convert.ts +54 -23
  121. package/src/registry/defaultFormats.ts +131 -44
  122. package/src/registry/index.ts +3 -0
  123. package/src/registry/types.ts +52 -6
  124. package/src/shared/boundedZipArchive.ts +342 -0
  125. package/src/shared/fidelity.ts +130 -0
  126. package/src/shared/zipEntryCount.ts +97 -0
  127. package/src/shared/zipLimits.ts +286 -0
  128. package/src/shared/zipSafety.ts +19 -0
  129. package/src/xlsx/import.ts +3 -2
  130. package/dist/chunk-46FMDOWZ.js.map +0 -1
  131. package/dist/chunk-5LSSWZPU.js.map +0 -1
  132. package/dist/chunk-D7IFPWNZ.js.map +0 -1
  133. package/dist/chunk-EHLGMSTE.js.map +0 -1
  134. package/dist/chunk-MJGRI6XR.js.map +0 -1
  135. package/dist/chunk-RTS5XBZ7.js.map +0 -1
  136. package/dist/chunk-SHLPSADL.js.map +0 -1
  137. package/dist/chunk-WWBIKRNP.js.map +0 -1
  138. package/dist/chunk-YESS7VY3.js.map +0 -1
  139. /package/dist/{chunk-DQAZR57U.js.map → chunk-O3GVVND4.js.map} +0 -0
@@ -9,9 +9,12 @@
9
9
 
10
10
  import type { MarkdownDocument } from '@bendyline/squisq/markdown';
11
11
  import type { ContentContainer } from '@bendyline/squisq/storage';
12
+ import type { TransformStyleInput, TransformStyleRegistry } from '@bendyline/squisq/transform';
12
13
  import { ConversionError } from './errors.js';
13
14
  import { defaultRegistry } from './registry.js';
15
+ import { openBoundedZipArchive, ZipSafetyError } from '../shared/zipSafety.js';
14
16
  import type {
17
+ BuiltinFormatOptions,
15
18
  ConversionResult,
16
19
  ConvertOptions,
17
20
  ConvertSource,
@@ -21,6 +24,10 @@ import type {
21
24
  NormalizedInput,
22
25
  } from './types.js';
23
26
 
27
+ function markdownFormatOptions(options: ConvertOptions): BuiltinFormatOptions['md'] {
28
+ return (options.formatOptions?.md ?? {}) as BuiltinFormatOptions['md'];
29
+ }
30
+
24
31
  // ── Byte sniffing ───────────────────────────────────────────────────
25
32
 
26
33
  function hasPrefix(bytes: Uint8Array, prefix: number[]): boolean {
@@ -41,13 +48,19 @@ const PDF_MAGIC = [0x25, 0x50, 0x44, 0x46]; // "%PDF"
41
48
  * marker means `dbk`.
42
49
  */
43
50
  async function sniffZip(bytes: Uint8Array): Promise<FormatId> {
44
- const JSZip = (await import('jszip')).default;
45
- const zip = await JSZip.loadAsync(bytes).catch(() => {
46
- throw new ConversionError('invalid-input', 'Input is not a readable ZIP archive.');
51
+ const archive = await openBoundedZipArchive(bytes).catch((cause: unknown) => {
52
+ if (cause instanceof ZipSafetyError && cause.code !== 'invalid-archive') throw cause;
53
+ throw new ConversionError('invalid-input', 'Input is not a readable ZIP archive.', { cause });
47
54
  });
48
- const contentTypes = zip.file('[Content_Types].xml');
55
+ const contentTypes = archive.entries.find((entry) => entry.path === '[Content_Types].xml');
49
56
  if (!contentTypes) return 'dbk';
50
- const xml = await contentTypes.async('string');
57
+ const maxContentTypesBytes = 1024 * 1024;
58
+ if ((contentTypes.declaredSize ?? 0) > maxContentTypesBytes) {
59
+ throw new ConversionError('invalid-input', 'OOXML content-types metadata is too large.');
60
+ }
61
+ const xmlBytes = await archive.read(contentTypes.path, maxContentTypesBytes);
62
+ if (!xmlBytes) return 'dbk';
63
+ const xml = new TextDecoder().decode(xmlBytes);
51
64
  if (xml.includes('wordprocessingml')) return 'docx';
52
65
  if (xml.includes('presentationml')) return 'pptx';
53
66
  if (xml.includes('spreadsheetml')) return 'xlsx';
@@ -80,7 +93,8 @@ async function detectByteFormat(
80
93
 
81
94
  /** Lowercased extension without the leading dot (`report.DocX` → `docx`). */
82
95
  function extractExt(filename: string): string {
83
- const base = filename.split('/').pop() ?? filename;
96
+ const normalized = filename.replace(/\\/g, '/');
97
+ const base = normalized.split('/').pop() ?? normalized;
84
98
  const dot = base.lastIndexOf('.');
85
99
  if (dot <= 0) return '';
86
100
  return base.slice(dot + 1).toLowerCase();
@@ -88,7 +102,8 @@ function extractExt(filename: string): string {
88
102
 
89
103
  /** Basename without its final extension (`a/b/report.docx` → `report`). */
90
104
  function baseNameOf(filename: string): string {
91
- const base = filename.split('/').pop() ?? filename;
105
+ const normalized = filename.replace(/\\/g, '/');
106
+ const base = normalized.split('/').pop() ?? normalized;
92
107
  const dot = base.lastIndexOf('.');
93
108
  return dot > 0 ? base.slice(0, dot) : base;
94
109
  }
@@ -147,7 +162,9 @@ async function normalizeBytes(
147
162
  );
148
163
  }
149
164
  const text = await container.readDocument();
150
- markdownDoc = text ? parseMarkdown(text) : { type: 'document', children: [] };
165
+ markdownDoc = text
166
+ ? parseMarkdown(text, markdownFormatOptions(options).parse)
167
+ : { type: 'document', children: [] };
151
168
  } else {
152
169
  // importDoc is guaranteed present by the guard above.
153
170
  markdownDoc = await fromDef.importDoc!(buffer, options);
@@ -176,7 +193,9 @@ async function normalizeMarkdown(
176
193
  const { stringifyMarkdown } = await import('@bendyline/squisq/markdown');
177
194
 
178
195
  const markdownDoc =
179
- typeof source.markdown === 'string' ? parseMarkdown(source.markdown) : source.markdown;
196
+ typeof source.markdown === 'string'
197
+ ? parseMarkdown(source.markdown, markdownFormatOptions(options).parse)
198
+ : source.markdown;
180
199
 
181
200
  let container = source.container;
182
201
  if (!container) {
@@ -228,13 +247,14 @@ async function normalize(
228
247
 
229
248
  async function applyTransformStyle(
230
249
  input: NormalizedInput,
231
- transformStyle: string,
250
+ transformStyle: TransformStyleInput,
232
251
  themeId?: string,
252
+ registry?: TransformStyleRegistry,
233
253
  ): Promise<string[]> {
234
254
  const { applyTransform, extractDocImages } = await import('@bendyline/squisq/transform');
235
255
  const { docToMarkdown, markdownToDoc } = await import('@bendyline/squisq/doc');
236
256
  const images = extractDocImages(input.doc.blocks);
237
- const result = applyTransform(input.doc, transformStyle, { themeId, images });
257
+ const result = applyTransform(input.doc, transformStyle, { themeId, images, registry });
238
258
  input.doc = result.doc;
239
259
  const markdownDoc = docToMarkdown(result.doc);
240
260
  input.markdownDoc = markdownDoc;
@@ -283,25 +303,36 @@ export async function convert(
283
303
  );
284
304
  }
285
305
 
286
- const { input, warnings } = await normalize(source, options, registry);
306
+ let normalized: Normalized;
307
+ try {
308
+ normalized = await normalize(source, options, registry);
309
+ } catch (err: unknown) {
310
+ if (err instanceof ConversionError) throw err;
311
+ const message = err instanceof Error ? err.message : String(err);
312
+ throw new ConversionError('invalid-input', message, {
313
+ format: options.from,
314
+ cause: err,
315
+ });
316
+ }
317
+ const { input, warnings } = normalized;
287
318
 
288
319
  // Respect an existing doc theme; only fill it in from options when absent.
289
320
  if (options.themeId && !input.doc.themeId) {
290
- input.doc.themeId = options.themeId;
291
- }
292
-
293
- if (options.transformStyle) {
294
- warnings.push(
295
- ...(await applyTransformStyle(
296
- input,
297
- options.transformStyle,
298
- options.themeId ?? input.doc.themeId,
299
- )),
300
- );
321
+ input.doc = { ...input.doc, themeId: options.themeId };
301
322
  }
302
323
 
303
324
  let result: ConversionResult;
304
325
  try {
326
+ if (options.transformStyle) {
327
+ warnings.push(
328
+ ...(await applyTransformStyle(
329
+ input,
330
+ options.transformStyle,
331
+ options.themeId ?? input.doc.themeId,
332
+ options.transformRegistry,
333
+ )),
334
+ );
335
+ }
305
336
  result = await target.exportDoc(input, options);
306
337
  } catch (err: unknown) {
307
338
  if (err instanceof ConversionError) throw err;
@@ -11,8 +11,10 @@
11
11
  import type { ContentContainer } from '@bendyline/squisq/storage';
12
12
  import type { MarkdownDocument } from '@bendyline/squisq/markdown';
13
13
  import { ConversionError } from './errors.js';
14
+ import { markdownFidelityWarnings } from '../shared/fidelity.js';
14
15
  import type {
15
16
  ConversionResult,
17
+ BuiltinFormatOptions,
16
18
  ConvertOptions,
17
19
  FormatDefinition,
18
20
  NormalizedInput,
@@ -57,6 +59,13 @@ function resolveThemeId(input: NormalizedInput, options: ConvertOptions): string
57
59
  return options.themeId ?? input.doc.themeId;
58
60
  }
59
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
+
60
69
  /** The markdown shape an exporter needs — reuse the source's, else derive it. */
61
70
  async function markdownOf(input: NormalizedInput): Promise<MarkdownDocument> {
62
71
  if (input.markdownDoc) return input.markdownDoc;
@@ -85,6 +94,34 @@ async function collectContainerImages(
85
94
  return images;
86
95
  }
87
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
+
88
125
  /** Resolve the player IIFE bundle or throw a helpful missing-dependency error. */
89
126
  async function requirePlayerScript(options: ConvertOptions, format: string): Promise<string> {
90
127
  const script = await options.resolvePlayerScript?.();
@@ -102,6 +139,20 @@ function ok(bytes: Uint8Array, mimeType: string, warnings: string[] = []): Conve
102
139
  return { bytes, mimeType, suggestedFilename: '', warnings };
103
140
  }
104
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
+
105
156
  // ── Definitions ─────────────────────────────────────────────────────
106
157
 
107
158
  export function defaultFormats(): FormatDefinition[] {
@@ -110,13 +161,13 @@ export function defaultFormats(): FormatDefinition[] {
110
161
  label: 'Markdown',
111
162
  mimeType: MIME.md,
112
163
  extensions: ['.md', '.markdown'],
113
- async importDoc(data): Promise<MarkdownDocument> {
164
+ async importDoc(data, options): Promise<MarkdownDocument> {
114
165
  const { parseMarkdown } = await import('@bendyline/squisq/markdown');
115
- return parseMarkdown(new TextDecoder().decode(data));
166
+ return parseMarkdown(new TextDecoder().decode(data), optionsFor(options, 'md').parse);
116
167
  },
117
- async exportDoc(input): Promise<ConversionResult> {
168
+ async exportDoc(input, options): Promise<ConversionResult> {
118
169
  const { stringifyMarkdown } = await import('@bendyline/squisq/markdown');
119
- const text = stringifyMarkdown(await markdownOf(input));
170
+ const text = stringifyMarkdown(await markdownOf(input), optionsFor(options, 'md').stringify);
120
171
  return ok(new TextEncoder().encode(text), MIME.md);
121
172
  },
122
173
  };
@@ -126,20 +177,28 @@ export function defaultFormats(): FormatDefinition[] {
126
177
  label: 'Word (DOCX)',
127
178
  mimeType: MIME.docx,
128
179
  extensions: ['.docx'],
129
- async importContainer(data): Promise<ContentContainer> {
180
+ async importContainer(data, options): Promise<ContentContainer> {
130
181
  const { docxToContainer } = await import('../docx/index.js');
131
- return docxToContainer(data);
182
+ return docxToContainer(data, optionsFor(options, 'docx'));
132
183
  },
133
- async importDoc(data): Promise<MarkdownDocument> {
184
+ async importDoc(data, options): Promise<MarkdownDocument> {
134
185
  const { docxToMarkdownDoc } = await import('../docx/index.js');
135
- return docxToMarkdownDoc(data);
186
+ return docxToMarkdownDoc(data, optionsFor(options, 'docx'));
136
187
  },
137
188
  async exportDoc(input, options): Promise<ConversionResult> {
138
189
  const { markdownDocToDocx } = await import('../docx/index.js');
139
- const buf = await markdownDocToDocx(await markdownOf(input), {
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 } : {}),
140
197
  themeId: resolveThemeId(input, options),
198
+ themeRegistry: options.themeRegistry ?? raw.themeRegistry,
199
+ images,
141
200
  });
142
- return ok(await toBytes(buf), MIME.docx);
201
+ return ok(await toBytes(buf), MIME.docx, markdownFidelityWarnings(markdownDoc, 'docx'));
143
202
  },
144
203
  };
145
204
 
@@ -148,20 +207,25 @@ export function defaultFormats(): FormatDefinition[] {
148
207
  label: 'PDF',
149
208
  mimeType: MIME.pdf,
150
209
  extensions: ['.pdf'],
151
- async importContainer(data): Promise<ContentContainer> {
210
+ async importContainer(data, options): Promise<ContentContainer> {
152
211
  const { pdfToContainer } = await import('../pdf/index.js');
153
- return pdfToContainer(data);
212
+ return pdfToContainer(data, optionsFor(options, 'pdf'));
154
213
  },
155
- async importDoc(data): Promise<MarkdownDocument> {
214
+ async importDoc(data, options): Promise<MarkdownDocument> {
156
215
  const { pdfToMarkdownDoc } = await import('../pdf/index.js');
157
- return pdfToMarkdownDoc(data);
216
+ return pdfToMarkdownDoc(data, optionsFor(options, 'pdf'));
158
217
  },
159
218
  async exportDoc(input, options): Promise<ConversionResult> {
160
219
  const { markdownDocToPdf } = await import('../pdf/index.js');
161
- const buf = await markdownDocToPdf(await markdownOf(input), {
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 } : {}),
162
225
  themeId: resolveThemeId(input, options),
226
+ themeRegistry: options.themeRegistry ?? raw.themeRegistry,
163
227
  });
164
- return ok(await toBytes(buf), MIME.pdf);
228
+ return ok(await toBytes(buf), MIME.pdf, markdownFidelityWarnings(markdownDoc, 'pdf'));
165
229
  },
166
230
  };
167
231
 
@@ -170,22 +234,28 @@ export function defaultFormats(): FormatDefinition[] {
170
234
  label: 'PowerPoint (PPTX)',
171
235
  mimeType: MIME.pptx,
172
236
  extensions: ['.pptx'],
173
- async importContainer(data): Promise<ContentContainer> {
237
+ async importContainer(data, options): Promise<ContentContainer> {
174
238
  const { pptxToContainer } = await import('../pptx/index.js');
175
- return pptxToContainer(data);
239
+ return pptxToContainer(data, pptxImportOptionsFrom(options));
176
240
  },
177
- async importDoc(data): Promise<MarkdownDocument> {
241
+ async importDoc(data, options): Promise<MarkdownDocument> {
178
242
  const { pptxToMarkdownDoc } = await import('../pptx/index.js');
179
- return pptxToMarkdownDoc(data);
243
+ return pptxToMarkdownDoc(data, pptxImportOptionsFrom(options));
180
244
  },
181
245
  async exportDoc(input, options): Promise<ConversionResult> {
182
246
  const { markdownDocToPptx } = await import('../pptx/index.js');
183
- const images = await collectContainerImages(input.container);
184
- const buf = await markdownDocToPptx(await markdownOf(input), {
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 } : {}),
185
254
  themeId: resolveThemeId(input, options),
255
+ themeRegistry: options.themeRegistry ?? raw.themeRegistry,
186
256
  images,
187
257
  });
188
- return ok(await toBytes(buf), MIME.pptx);
258
+ return ok(await toBytes(buf), MIME.pptx, markdownFidelityWarnings(markdownDoc, 'pptx'));
189
259
  },
190
260
  };
191
261
 
@@ -194,11 +264,11 @@ export function defaultFormats(): FormatDefinition[] {
194
264
  label: 'Excel (XLSX)',
195
265
  mimeType: MIME.xlsx,
196
266
  extensions: ['.xlsx'],
197
- async importDoc(data): Promise<MarkdownDocument> {
267
+ async importDoc(data, options): Promise<MarkdownDocument> {
198
268
  const { xlsxToMarkdownDoc } = await import('../xlsx/index.js');
199
- return xlsxToMarkdownDoc(data);
269
+ return xlsxToMarkdownDoc(data, optionsFor(options, 'xlsx'));
200
270
  },
201
- async exportDoc(input): Promise<ConversionResult> {
271
+ async exportDoc(input, options): Promise<ConversionResult> {
202
272
  const { markdownDocToXlsx } = await import('../xlsx/index.js');
203
273
  const markdownDoc = await markdownOf(input);
204
274
  // XLSX fidelity is tables-only: `table` nodes become worksheets and
@@ -211,7 +281,10 @@ export function defaultFormats(): FormatDefinition[] {
211
281
  omitted > 0
212
282
  ? [`XLSX export is tables-only; ${omitted} non-table block(s) were omitted.`]
213
283
  : [];
214
- const blob = await markdownDocToXlsx(markdownDoc);
284
+ const blob = await markdownDocToXlsx(markdownDoc, {
285
+ ...optionsFor(options, 'xlsx'),
286
+ ...(options.title !== undefined ? { title: options.title } : {}),
287
+ });
215
288
  return ok(await toBytes(blob), MIME.xlsx, warnings);
216
289
  },
217
290
  };
@@ -221,21 +294,22 @@ export function defaultFormats(): FormatDefinition[] {
221
294
  label: 'CSV',
222
295
  mimeType: MIME.csv,
223
296
  extensions: ['.csv'],
224
- async importDoc(data): Promise<MarkdownDocument> {
297
+ async importDoc(data, options): Promise<MarkdownDocument> {
225
298
  const { csvToMarkdownDoc } = await import('../csv/index.js');
226
- return csvToMarkdownDoc(data);
299
+ return csvToMarkdownDoc(data, optionsFor(options, 'csv'));
227
300
  },
228
- async exportDoc(input): Promise<ConversionResult> {
301
+ async exportDoc(input, options): Promise<ConversionResult> {
229
302
  const { markdownDocToCsv } = await import('../csv/index.js');
230
303
  const markdownDoc = await markdownOf(input);
304
+ const raw = optionsFor(options, 'csv');
231
305
  const tableCount = markdownDoc.children.filter((n) => n.type === 'table').length;
232
306
  const warnings: string[] = [];
233
- if (tableCount > 1) {
307
+ if (tableCount > 1 && raw.tableIndex === undefined) {
234
308
  warnings.push(
235
309
  `Document has ${tableCount} tables; CSV export emitted only the first. Use the csv converter's tableIndex option to select another.`,
236
310
  );
237
311
  }
238
- const text = markdownDocToCsv(markdownDoc);
312
+ const text = markdownDocToCsv(markdownDoc, raw);
239
313
  return ok(new TextEncoder().encode(text), MIME.csv, warnings);
240
314
  },
241
315
  };
@@ -245,22 +319,26 @@ export function defaultFormats(): FormatDefinition[] {
245
319
  label: 'HTML (single file)',
246
320
  mimeType: MIME.html,
247
321
  extensions: ['.html', '.htm'],
248
- async importDoc(data): Promise<MarkdownDocument> {
322
+ async importDoc(data, options): Promise<MarkdownDocument> {
249
323
  const { htmlToMarkdownDoc } = await import('../html/index.js');
250
- return htmlToMarkdownDoc(data);
324
+ return htmlToMarkdownDoc(data, optionsFor(options, 'html'));
251
325
  },
252
326
  // importContainer omitted: HTML import already inlines images as data URIs,
253
327
  // so there is nothing to extract into a container this wave.
254
328
  async exportDoc(input, options): Promise<ConversionResult> {
255
329
  const playerScript = await requirePlayerScript(options, 'html');
256
330
  const { docToHtml } = await import('../html/index.js');
257
- const images = await collectContainerImages(input.container);
331
+ const raw = optionsFor(options, 'html');
332
+ const containerImages = await collectContainerImages(input.container);
333
+ const images = new Map([...containerImages, ...(raw.images ?? new Map())]);
258
334
  const htmlText = docToHtml(input.doc, {
335
+ ...raw,
259
336
  playerScript,
260
337
  images,
261
338
  title: options.title ?? input.baseName,
262
- mode: 'static',
339
+ mode: raw.mode ?? 'static',
263
340
  themeId: resolveThemeId(input, options),
341
+ themeRegistry: options.themeRegistry ?? raw.themeRegistry,
264
342
  });
265
343
  return ok(new TextEncoder().encode(htmlText), MIME.html);
266
344
  },
@@ -274,13 +352,17 @@ export function defaultFormats(): FormatDefinition[] {
274
352
  async exportDoc(input, options): Promise<ConversionResult> {
275
353
  const playerScript = await requirePlayerScript(options, 'htmlzip');
276
354
  const { docToHtmlZip } = await import('../html/index.js');
277
- const images = await collectContainerImages(input.container);
355
+ const raw = optionsFor(options, 'htmlzip');
356
+ const containerImages = await collectContainerImages(input.container);
357
+ const images = new Map([...containerImages, ...(raw.images ?? new Map())]);
278
358
  const blob = await docToHtmlZip(input.doc, {
359
+ ...raw,
279
360
  playerScript,
280
361
  images,
281
362
  title: options.title ?? input.baseName,
282
- mode: 'static',
363
+ mode: raw.mode ?? 'static',
283
364
  themeId: resolveThemeId(input, options),
365
+ themeRegistry: options.themeRegistry ?? raw.themeRegistry,
284
366
  });
285
367
  return ok(await toBytes(blob), MIME.zip);
286
368
  },
@@ -293,13 +375,18 @@ export function defaultFormats(): FormatDefinition[] {
293
375
  extensions: ['.epub'],
294
376
  async exportDoc(input, options): Promise<ConversionResult> {
295
377
  const { markdownDocToEpub } = await import('../epub/index.js');
296
- const images = await collectContainerImages(input.container);
297
- const buf = await markdownDocToEpub(await markdownOf(input), {
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,
298
384
  title: options.title ?? input.baseName,
299
385
  themeId: resolveThemeId(input, options),
386
+ themeRegistry: options.themeRegistry ?? raw.themeRegistry,
300
387
  images,
301
388
  });
302
- return ok(await toBytes(buf), MIME.epub);
389
+ return ok(await toBytes(buf), MIME.epub, markdownFidelityWarnings(markdownDoc, 'epub'));
303
390
  },
304
391
  };
305
392
 
@@ -308,13 +395,13 @@ export function defaultFormats(): FormatDefinition[] {
308
395
  label: 'Squisq container (DBK)',
309
396
  mimeType: MIME.zip,
310
397
  extensions: ['.dbk', '.zip'],
311
- async importContainer(data): Promise<ContentContainer> {
398
+ async importContainer(data, options): Promise<ContentContainer> {
312
399
  const { zipToContainer } = await import('../container/index.js');
313
- return zipToContainer(data);
400
+ return zipToContainer(data, optionsFor(options, 'dbk'));
314
401
  },
315
402
  async exportDoc(input): Promise<ConversionResult> {
316
403
  const { containerToZip } = await import('../container/index.js');
317
- const blob = await containerToZip(input.container);
404
+ const blob = await containerToZip(await snapshotContainerWithDocument(input));
318
405
  return ok(await toBytes(blob), MIME.zip);
319
406
  },
320
407
  };
@@ -27,6 +27,9 @@ export type {
27
27
  FormatDefinition,
28
28
  FormatRegistry,
29
29
  ConvertSource,
30
+ BuiltinFormatOptions,
31
+ MarkdownFormatOptions,
32
+ DbkFormatOptions,
30
33
  } from './types.js';
31
34
  export { BUILTIN_FORMAT_IDS } from './types.js';
32
35
 
@@ -8,9 +8,24 @@
8
8
  * methods, so pulling in the registry never eagerly bundles heavy converters.
9
9
  */
10
10
 
11
- import type { Doc } from '@bendyline/squisq/schemas';
11
+ import type { Doc, ThemeRegistry } from '@bendyline/squisq/schemas';
12
+ import type { TransformStyleInput, TransformStyleRegistry } from '@bendyline/squisq/transform';
12
13
  import type { MarkdownDocument } from '@bendyline/squisq/markdown';
13
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 { ZipSafetyLimits } from '../shared/zipSafety.js';
14
29
 
15
30
  /** A format identifier (e.g. `'docx'`). Strings so hosts can register their own. */
16
31
  export type FormatId = string;
@@ -41,6 +56,33 @@ export interface ConversionResult {
41
56
  warnings: string[];
42
57
  }
43
58
 
59
+ export interface MarkdownFormatOptions {
60
+ parse?: ParseOptions;
61
+ stringify?: StringifyOptions;
62
+ }
63
+
64
+ /** Resource limits applied when importing a DBK/ZIP container. */
65
+ export type DbkFormatOptions = ZipSafetyLimits;
66
+
67
+ /**
68
+ * Strongly typed option bags for built-in formats. Import and export options
69
+ * share a bag because a single conversion may use the format on either side.
70
+ * Custom registries may add arbitrary keys through the intersection used by
71
+ * {@link ConvertOptions.formatOptions}.
72
+ */
73
+ export interface BuiltinFormatOptions {
74
+ md: MarkdownFormatOptions;
75
+ docx: DocxImportOptions & DocxExportOptions;
76
+ pptx: PptxImportOptions & PptxExportOptions;
77
+ xlsx: XlsxImportOptions & XlsxExportOptions;
78
+ csv: CsvImportOptions & CsvExportOptions;
79
+ pdf: PdfImportOptions & PdfExportOptions;
80
+ html: HtmlImportOptions & Partial<Omit<HtmlExportOptions, 'playerScript'>>;
81
+ htmlzip: Partial<Omit<HtmlExportOptions, 'playerScript'>>;
82
+ epub: EpubExportOptions;
83
+ dbk: DbkFormatOptions;
84
+ }
85
+
44
86
  /**
45
87
  * A source normalized into every shape an exporter might need. `doc` is always
46
88
  * present; `markdownDoc` is present when the source was markdown-shaped (an
@@ -61,16 +103,20 @@ export interface ConvertOptions {
61
103
  from?: FormatId;
62
104
  /** Theme id to apply to the exported document. */
63
105
  themeId?: string;
64
- /** Transform style id to apply before export. */
65
- transformStyle?: string;
106
+ /** Explicit caller-owned registry for non-document custom themes. */
107
+ themeRegistry?: ThemeRegistry;
108
+ /** Built-in/registry id or call-scoped style to apply before export. */
109
+ transformStyle?: TransformStyleInput;
110
+ /** Explicit caller-owned registry used to resolve transform style ids. */
111
+ transformRegistry?: TransformStyleRegistry;
66
112
  /** Content-aware auto-templating when deriving a Doc from markdown. */
67
113
  autoTemplates?: boolean;
68
- /** Title hint for exporters that support one (epub, html). */
114
+ /** Title hint for exporters that expose document metadata. */
69
115
  title?: string;
70
116
  /** Lazily resolve the standalone player IIFE bundle (required for HTML export). */
71
117
  resolvePlayerScript?: () => Promise<string>;
72
- /** Per-format escape hatch for extra options. */
73
- formatOptions?: Record<FormatId, Record<string, unknown>>;
118
+ /** Typed per-format options for built-ins; custom format ids remain extensible. */
119
+ formatOptions?: Partial<BuiltinFormatOptions> & Record<FormatId, unknown>;
74
120
  }
75
121
 
76
122
  /** Describes how a single format imports to / exports from the squisq model. */