@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.
- package/README.md +48 -2
- package/dist/{chunk-WWBIKRNP.js → chunk-2CSZJEAL.js} +13 -11
- package/dist/chunk-2CSZJEAL.js.map +1 -0
- package/dist/{chunk-RTS5XBZ7.js → chunk-2EFDWKC6.js} +262 -84
- package/dist/chunk-2EFDWKC6.js.map +1 -0
- package/dist/chunk-2PRVLAU3.js +740 -0
- package/dist/chunk-2PRVLAU3.js.map +1 -0
- package/dist/{chunk-SHLPSADL.js → chunk-4HJ6AK2Q.js} +54 -193
- package/dist/chunk-4HJ6AK2Q.js.map +1 -0
- package/dist/{chunk-5LSSWZPU.js → chunk-4VUWTSGM.js} +72 -25
- package/dist/chunk-4VUWTSGM.js.map +1 -0
- package/dist/{chunk-YESS7VY3.js → chunk-CVNVD7VH.js} +154 -39
- package/dist/chunk-CVNVD7VH.js.map +1 -0
- package/dist/chunk-CX3LH47J.js +99 -0
- package/dist/chunk-CX3LH47J.js.map +1 -0
- package/dist/{chunk-D7IFPWNZ.js → chunk-GVS2XXV6.js} +27 -26
- package/dist/chunk-GVS2XXV6.js.map +1 -0
- package/dist/chunk-H7T5DXUU.js +479 -0
- package/dist/chunk-H7T5DXUU.js.map +1 -0
- package/dist/{chunk-EHLGMSTE.js → chunk-HBSCRS5P.js} +10 -8
- package/dist/chunk-HBSCRS5P.js.map +1 -0
- package/dist/{chunk-46FMDOWZ.js → chunk-LXYLOOST.js} +49 -17
- package/dist/chunk-LXYLOOST.js.map +1 -0
- package/dist/{chunk-MJGRI6XR.js → chunk-MLX2BOJC.js} +12 -7
- package/dist/chunk-MLX2BOJC.js.map +1 -0
- package/dist/chunk-NKAJPJ4G.js +106 -0
- package/dist/chunk-NKAJPJ4G.js.map +1 -0
- package/dist/{chunk-DQAZR57U.js → chunk-O3GVVND4.js} +1 -1
- package/dist/chunk-ODL3SSPT.js +18 -0
- package/dist/chunk-ODL3SSPT.js.map +1 -0
- package/dist/chunk-X4USWOQ2.js +131 -0
- package/dist/chunk-X4USWOQ2.js.map +1 -0
- package/dist/chunk-XTLQXR4U.js +195 -0
- package/dist/chunk-XTLQXR4U.js.map +1 -0
- package/dist/chunk-YRT7GQ5Y.js +28 -0
- package/dist/chunk-YRT7GQ5Y.js.map +1 -0
- package/dist/chunk-ZZ7D4DN6.js +97 -0
- package/dist/chunk-ZZ7D4DN6.js.map +1 -0
- package/dist/container/index.d.ts +6 -2
- package/dist/container/index.js +15 -17
- package/dist/container/index.js.map +1 -1
- package/dist/docx/index.d.ts +6 -2
- package/dist/docx/index.js +5 -3
- package/dist/epub/index.d.ts +3 -1
- package/dist/epub/index.js +3 -3
- package/dist/export-Djev9BLU.d.ts +69 -0
- package/dist/extract-LNOA7DBK.js +15 -0
- package/dist/extract-LNOA7DBK.js.map +1 -0
- package/dist/html/index.d.ts +15 -85
- package/dist/html/index.js +3 -3
- package/dist/images-7FBWPKE3.js +7 -0
- package/dist/images-7FBWPKE3.js.map +1 -0
- package/dist/import-DTkDxHmZ.d.ts +84 -0
- package/dist/import-Yivc3kZM.d.ts +136 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +30 -11
- package/dist/infer/index.d.ts +138 -0
- package/dist/infer/index.js +28 -0
- package/dist/infer/index.js.map +1 -0
- package/dist/layouts-7OUUVAGU.js +12 -0
- package/dist/layouts-7OUUVAGU.js.map +1 -0
- package/dist/layouts-CZ0bdsNL.d.ts +150 -0
- package/dist/mapTheme-IR27S6IV.js +11 -0
- package/dist/mapTheme-IR27S6IV.js.map +1 -0
- package/dist/ooxml/index.d.ts +21 -127
- package/dist/ooxml/index.js +31 -9
- package/dist/pdf/index.d.ts +3 -1
- package/dist/pdf/index.js +1 -1
- package/dist/pptx/index.d.ts +8 -108
- package/dist/pptx/index.js +13 -4
- package/dist/reader-Gof6OrJX.d.ts +140 -0
- package/dist/registry/index.d.ts +46 -8
- package/dist/registry/index.js +5 -2
- package/dist/themeReader-DPw4J5Ss.d.ts +56 -0
- package/dist/xlsx/index.d.ts +6 -66
- package/dist/xlsx/index.js +4 -2
- package/dist/zipLimits-bGYMS9cI.d.ts +29 -0
- package/package.json +8 -2
- package/src/__tests__/container.test.ts +41 -0
- package/src/__tests__/convert.test.ts +167 -0
- package/src/__tests__/epub.test.ts +102 -1
- package/src/__tests__/html.test.ts +62 -0
- package/src/__tests__/inferTheme.test.ts +135 -0
- package/src/__tests__/lossyWarnings.test.ts +41 -0
- package/src/__tests__/ooxml.test.ts +59 -2
- package/src/__tests__/ooxmlThemeReader.test.ts +92 -0
- package/src/__tests__/plainHtml.test.ts +15 -1
- package/src/__tests__/plainHtmlBundle.test.ts +18 -0
- package/src/__tests__/pptxImport.test.ts +30 -0
- package/src/__tests__/pptxInferFixtures.ts +314 -0
- package/src/__tests__/pptxLayoutInfer.test.ts +395 -0
- package/src/__tests__/zipSafety.test.ts +317 -0
- package/src/container/index.ts +20 -29
- package/src/docx/export.ts +4 -2
- package/src/docx/import.ts +4 -3
- package/src/epub/export.ts +94 -25
- package/src/html/docsHtmlBundle.ts +6 -2
- package/src/html/htmlTemplate.ts +31 -6
- package/src/html/imageUtils.ts +14 -5
- package/src/html/index.ts +12 -1
- package/src/html/plainHtml.ts +7 -3
- package/src/html/plainHtmlBundle.ts +11 -9
- package/src/index.ts +20 -0
- package/src/infer/extract.ts +127 -0
- package/src/infer/index.ts +168 -0
- package/src/infer/mapTheme.ts +176 -0
- package/src/infer/types.ts +27 -0
- package/src/ooxml/index.ts +14 -0
- package/src/ooxml/readUtils.ts +44 -0
- package/src/ooxml/reader.ts +87 -28
- package/src/ooxml/themeReader.ts +197 -0
- package/src/ooxml/types.ts +8 -9
- package/src/ooxml/writer.ts +28 -10
- package/src/pdf/export.ts +4 -2
- package/src/pdf/import.ts +40 -34
- package/src/pptx/export.ts +4 -2
- package/src/pptx/import.ts +214 -32
- package/src/pptx/index.ts +14 -0
- package/src/pptx/layouts.ts +1168 -0
- package/src/registry/convert.ts +54 -23
- package/src/registry/defaultFormats.ts +131 -44
- package/src/registry/index.ts +3 -0
- package/src/registry/types.ts +52 -6
- package/src/shared/boundedZipArchive.ts +342 -0
- package/src/shared/fidelity.ts +130 -0
- package/src/shared/zipEntryCount.ts +97 -0
- package/src/shared/zipLimits.ts +286 -0
- package/src/shared/zipSafety.ts +19 -0
- package/src/xlsx/import.ts +3 -2
- package/dist/chunk-46FMDOWZ.js.map +0 -1
- package/dist/chunk-5LSSWZPU.js.map +0 -1
- package/dist/chunk-D7IFPWNZ.js.map +0 -1
- package/dist/chunk-EHLGMSTE.js.map +0 -1
- package/dist/chunk-MJGRI6XR.js.map +0 -1
- package/dist/chunk-RTS5XBZ7.js.map +0 -1
- package/dist/chunk-SHLPSADL.js.map +0 -1
- package/dist/chunk-WWBIKRNP.js.map +0 -1
- package/dist/chunk-YESS7VY3.js.map +0 -1
- /package/dist/{chunk-DQAZR57U.js.map → chunk-O3GVVND4.js.map} +0 -0
package/src/registry/convert.ts
CHANGED
|
@@ -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
|
|
45
|
-
|
|
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 =
|
|
55
|
+
const contentTypes = archive.entries.find((entry) => entry.path === '[Content_Types].xml');
|
|
49
56
|
if (!contentTypes) return 'dbk';
|
|
50
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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'
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
184
|
-
const
|
|
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
|
|
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
|
|
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
|
|
297
|
-
const
|
|
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
|
|
404
|
+
const blob = await containerToZip(await snapshotContainerWithDocument(input));
|
|
318
405
|
return ok(await toBytes(blob), MIME.zip);
|
|
319
406
|
},
|
|
320
407
|
};
|
package/src/registry/index.ts
CHANGED
package/src/registry/types.ts
CHANGED
|
@@ -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
|
-
/**
|
|
65
|
-
|
|
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
|
|
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
|
-
/**
|
|
73
|
-
formatOptions?:
|
|
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. */
|