@bendyline/squisq-formats 2.0.1 → 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.
- package/LICENSE +21 -0
- package/NOTICE.md +20 -0
- package/README.md +1 -1
- package/dist/{chunk-CRAVSMPZ.js → chunk-26ISNJ7Y.js} +356 -114
- package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
- package/dist/{chunk-HTW2M27H.js → chunk-3NKXBZSR.js} +193 -42
- package/dist/{chunk-U32AG3G3.js → chunk-4V3KCHAP.js} +3 -4
- package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
- package/dist/{chunk-QRVN6A6E.js → chunk-6S6GU3ZG.js} +5 -6
- package/dist/{chunk-FE6OJV6O.js → chunk-7AWFHP5U.js} +1 -1
- package/dist/{chunk-RFAPOKHJ.js → chunk-AD2WT564.js} +59 -9
- package/dist/{chunk-O3GVVND4.js → chunk-AONELFLA.js} +0 -1
- package/dist/{chunk-XKUMNGBW.js → chunk-EJTNGKEA.js} +5 -8
- package/dist/chunk-GX7RAUME.js +121 -0
- package/dist/{chunk-SSUPBUF5.js → chunk-IIQYS2YH.js} +0 -1
- package/dist/{chunk-ABVI556T.js → chunk-IPN56VLW.js} +83 -58
- package/dist/{chunk-LXYLOOST.js → chunk-JE6LSIHE.js} +83 -22
- package/dist/{chunk-U4MRIFKL.js → chunk-JU2RHXUB.js} +0 -1
- package/dist/{chunk-4VUWTSGM.js → chunk-K6XRMVPW.js} +64 -31
- package/dist/{chunk-ODL3SSPT.js → chunk-KXOZMWBS.js} +0 -1
- package/dist/chunk-OGS5VCGJ.js +446 -0
- package/dist/{chunk-GVS2XXV6.js → chunk-PJXJI2LY.js} +449 -57
- package/dist/{chunk-2KPARF2P.js → chunk-PU7REGWV.js} +5 -8
- package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
- package/dist/{chunk-VSYHZECT.js → chunk-TAAENIRB.js} +5 -8
- package/dist/{chunk-WC7WULGV.js → chunk-X2DEAXNK.js} +62 -2
- package/dist/container/index.js +1 -2
- package/dist/csv/index.d.ts +27 -2
- package/dist/csv/index.js +1 -2
- package/dist/docx/index.d.ts +5 -1
- package/dist/docx/index.js +9 -10
- package/dist/epub/index.d.ts +2 -0
- package/dist/epub/index.js +5 -6
- package/dist/{export-D2NkylDT.d.ts → export-D9msROJS.d.ts} +18 -6
- package/dist/extract-MN7LA3NL.js +13 -0
- package/dist/html/index.d.ts +11 -4
- package/dist/html/index.js +3 -4
- package/dist/images-ESPQKVTW.js +6 -0
- package/dist/{import-K8mfc0fz.d.ts → import-C3htUTss.d.ts} +5 -1
- package/dist/{import-DTkDxHmZ.d.ts → import-C8whCC7_.d.ts} +6 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +28 -26
- package/dist/infer/index.d.ts +3 -3
- package/dist/infer/index.js +7 -9
- package/dist/{layouts-BHrgZ5FS.d.ts → layouts-CTdPlB-u.d.ts} +1 -1
- package/dist/layouts-DRWZGSPD.js +10 -0
- package/dist/{mapTheme-IR27S6IV.js → mapTheme-4TWH25FT.js} +1 -2
- package/dist/ooxml/index.d.ts +3 -3
- package/dist/ooxml/index.js +14 -13
- package/dist/pdf/index.d.ts +18 -0
- package/dist/pdf/index.js +2 -3
- package/dist/pptx/index.d.ts +4 -4
- package/dist/pptx/index.js +11 -13
- package/dist/{reader-B9L8Ucbj.d.ts → reader-B_m1aKZC.d.ts} +30 -1
- package/dist/registry/index.d.ts +21 -5
- package/dist/registry/index.js +9 -6
- package/dist/{themeReader-DJKErl_j.d.ts → themeReader-DCtwC83Q.d.ts} +1 -1
- package/dist/xlsx/index.d.ts +3 -3
- package/dist/xlsx/index.js +6 -7
- package/package.json +6 -3
- package/dist/chunk-2KPARF2P.js.map +0 -1
- package/dist/chunk-4VUWTSGM.js.map +0 -1
- package/dist/chunk-6M7Z25LA.js +0 -46
- package/dist/chunk-6M7Z25LA.js.map +0 -1
- package/dist/chunk-ABVI556T.js.map +0 -1
- package/dist/chunk-CRAVSMPZ.js.map +0 -1
- package/dist/chunk-FE6OJV6O.js.map +0 -1
- package/dist/chunk-GVS2XXV6.js.map +0 -1
- package/dist/chunk-HTW2M27H.js.map +0 -1
- package/dist/chunk-LXYLOOST.js.map +0 -1
- package/dist/chunk-MLX2BOJC.js.map +0 -1
- package/dist/chunk-NKAJPJ4G.js.map +0 -1
- package/dist/chunk-O3GVVND4.js.map +0 -1
- package/dist/chunk-ODL3SSPT.js.map +0 -1
- package/dist/chunk-PN52A5AA.js.map +0 -1
- package/dist/chunk-QRVN6A6E.js.map +0 -1
- package/dist/chunk-RFAPOKHJ.js.map +0 -1
- package/dist/chunk-SSUPBUF5.js.map +0 -1
- package/dist/chunk-U32AG3G3.js.map +0 -1
- package/dist/chunk-U4MRIFKL.js.map +0 -1
- package/dist/chunk-VJJM2SSH.js +0 -275
- package/dist/chunk-VJJM2SSH.js.map +0 -1
- package/dist/chunk-VSYHZECT.js.map +0 -1
- package/dist/chunk-WC7WULGV.js.map +0 -1
- package/dist/chunk-XKUMNGBW.js.map +0 -1
- package/dist/chunk-YRT7GQ5Y.js +0 -28
- package/dist/chunk-YRT7GQ5Y.js.map +0 -1
- package/dist/container/index.js.map +0 -1
- package/dist/csv/index.js.map +0 -1
- package/dist/docx/index.js.map +0 -1
- package/dist/epub/index.js.map +0 -1
- package/dist/extract-H6RXJMHP.js +0 -15
- package/dist/extract-H6RXJMHP.js.map +0 -1
- package/dist/html/index.js.map +0 -1
- package/dist/images-7FBWPKE3.js +0 -7
- package/dist/images-7FBWPKE3.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infer/index.js.map +0 -1
- package/dist/layouts-QVPK3ZCU.js +0 -12
- package/dist/layouts-QVPK3ZCU.js.map +0 -1
- package/dist/mapTheme-IR27S6IV.js.map +0 -1
- package/dist/ooxml/index.js.map +0 -1
- package/dist/pdf/index.js.map +0 -1
- package/dist/pptx/index.js.map +0 -1
- package/dist/registry/index.js.map +0 -1
- package/dist/xlsx/index.js.map +0 -1
- package/src/__tests__/container.test.ts +0 -230
- package/src/__tests__/convert.test.ts +0 -495
- package/src/__tests__/csvImport.test.ts +0 -84
- package/src/__tests__/docxExport.test.ts +0 -457
- package/src/__tests__/docxImport.test.ts +0 -410
- package/src/__tests__/epub.test.ts +0 -649
- package/src/__tests__/exportThemeReconciliation.test.ts +0 -87
- package/src/__tests__/formatRegistry.test.ts +0 -174
- package/src/__tests__/html.test.ts +0 -435
- package/src/__tests__/htmlImport.test.ts +0 -57
- package/src/__tests__/inferTheme.test.ts +0 -135
- package/src/__tests__/lossyWarnings.test.ts +0 -146
- package/src/__tests__/ooxml.test.ts +0 -271
- package/src/__tests__/ooxmlCancellation.test.ts +0 -113
- package/src/__tests__/ooxmlThemeReader.test.ts +0 -92
- package/src/__tests__/pdfExport.test.ts +0 -322
- package/src/__tests__/pdfImport.test.ts +0 -384
- package/src/__tests__/plainHtml.test.ts +0 -417
- package/src/__tests__/plainHtmlBundle.test.ts +0 -253
- package/src/__tests__/pptxExport.test.ts +0 -138
- package/src/__tests__/pptxImport.test.ts +0 -145
- package/src/__tests__/pptxInferFixtures.ts +0 -314
- package/src/__tests__/pptxLayoutInfer.test.ts +0 -395
- package/src/__tests__/roundTrip.test.ts +0 -201
- package/src/__tests__/roundTripAssets.test.ts +0 -50
- package/src/__tests__/roundTripMatrix.fixtures.ts +0 -86
- package/src/__tests__/roundTripMatrix.helpers.ts +0 -154
- package/src/__tests__/roundTripMatrix.test.ts +0 -142
- package/src/__tests__/sharedContainer.test.ts +0 -41
- package/src/__tests__/sharedImages.test.ts +0 -61
- package/src/__tests__/xlsxExport.test.ts +0 -164
- package/src/__tests__/xlsxImport.test.ts +0 -80
- package/src/__tests__/zipSafety.test.ts +0 -317
- package/src/container/index.ts +0 -94
- package/src/csv/index.ts +0 -188
- package/src/docx/export.ts +0 -1267
- package/src/docx/import.ts +0 -995
- package/src/docx/index.ts +0 -26
- package/src/docx/styles.ts +0 -145
- package/src/epub/export.ts +0 -968
- package/src/epub/index.ts +0 -20
- package/src/html/docsHtmlBundle.ts +0 -373
- package/src/html/htmlTemplate.ts +0 -385
- package/src/html/imageUtils.ts +0 -61
- package/src/html/import.ts +0 -297
- package/src/html/index.ts +0 -212
- package/src/html/plainHtml.ts +0 -790
- package/src/html/plainHtmlBundle.ts +0 -421
- package/src/index.ts +0 -109
- package/src/infer/extract.ts +0 -127
- package/src/infer/index.ts +0 -199
- package/src/infer/mapTheme.ts +0 -176
- package/src/infer/types.ts +0 -27
- package/src/ooxml/index.ts +0 -111
- package/src/ooxml/namespaces.ts +0 -196
- package/src/ooxml/readUtils.ts +0 -44
- package/src/ooxml/reader.ts +0 -318
- package/src/ooxml/themeReader.ts +0 -197
- package/src/ooxml/types.ts +0 -103
- package/src/ooxml/writer.ts +0 -339
- package/src/ooxml/xmlUtils.ts +0 -123
- package/src/pdf/export.ts +0 -1084
- package/src/pdf/import.ts +0 -1164
- package/src/pdf/index.ts +0 -29
- package/src/pdf/styles.ts +0 -180
- package/src/pptx/export.ts +0 -1184
- package/src/pptx/import.ts +0 -455
- package/src/pptx/index.ts +0 -52
- package/src/pptx/layouts.ts +0 -1222
- package/src/pptx/styles.ts +0 -96
- package/src/pptx/templates.ts +0 -187
- package/src/registry/convert.ts +0 -433
- package/src/registry/defaultFormats.ts +0 -413
- package/src/registry/errors.ts +0 -46
- package/src/registry/index.ts +0 -43
- package/src/registry/registry.ts +0 -48
- package/src/registry/types.ts +0 -170
- package/src/shared/boundedZipArchive.ts +0 -383
- package/src/shared/container.ts +0 -28
- package/src/shared/fidelity.ts +0 -130
- package/src/shared/images.ts +0 -44
- package/src/shared/inlineRuns.ts +0 -99
- package/src/shared/text.ts +0 -41
- package/src/shared/zipEntryCount.ts +0 -151
- package/src/shared/zipLimits.ts +0 -296
- package/src/shared/zipSafety.ts +0 -19
- package/src/xlsx/export.ts +0 -253
- package/src/xlsx/import.ts +0 -160
- package/src/xlsx/index.ts +0 -35
package/src/registry/convert.ts
DELETED
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Programmatic `convert()` — the format-registry front door.
|
|
3
|
-
*
|
|
4
|
-
* Normalizes any {@link ConvertSource} into a {@link NormalizedInput} (always
|
|
5
|
-
* yielding a `Doc`, keeping the `MarkdownDocument` when the source was
|
|
6
|
-
* markdown-shaped, and always carrying a `ContentContainer` for media), applies
|
|
7
|
-
* an optional transform, then hands off to the target format's exporter.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { MarkdownDocument } from '@bendyline/squisq/markdown';
|
|
11
|
-
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
12
|
-
import type { TransformStyleInput, TransformStyleRegistry } from '@bendyline/squisq/transform';
|
|
13
|
-
import { ConversionError } from './errors.js';
|
|
14
|
-
import { defaultRegistry } from './registry.js';
|
|
15
|
-
import { openBoundedZipArchive, ZipSafetyError } from '../shared/zipSafety.js';
|
|
16
|
-
import type {
|
|
17
|
-
BuiltinFormatOptions,
|
|
18
|
-
ConversionResult,
|
|
19
|
-
ConvertOptions,
|
|
20
|
-
ConvertSource,
|
|
21
|
-
FormatDefinition,
|
|
22
|
-
FormatId,
|
|
23
|
-
FormatRegistry,
|
|
24
|
-
NormalizedInput,
|
|
25
|
-
PreparedConversion,
|
|
26
|
-
PreparedExportOptions,
|
|
27
|
-
} from './types.js';
|
|
28
|
-
|
|
29
|
-
function markdownFormatOptions(options: ConvertOptions): BuiltinFormatOptions['md'] {
|
|
30
|
-
return (options.formatOptions?.md ?? {}) as BuiltinFormatOptions['md'];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function throwIfAborted(signal?: AbortSignal): void {
|
|
34
|
-
if (!signal?.aborted) return;
|
|
35
|
-
if (signal.reason !== undefined) throw signal.reason;
|
|
36
|
-
const error = new Error('Document conversion was cancelled.');
|
|
37
|
-
error.name = 'AbortError';
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ── Byte sniffing ───────────────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
function hasPrefix(bytes: Uint8Array, prefix: number[]): boolean {
|
|
44
|
-
if (bytes.length < prefix.length) return false;
|
|
45
|
-
for (let i = 0; i < prefix.length; i++) {
|
|
46
|
-
if (bytes[i] !== prefix[i]) return false;
|
|
47
|
-
}
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const ZIP_MAGIC = [0x50, 0x4b, 0x03, 0x04]; // "PK\x03\x04"
|
|
52
|
-
const PDF_MAGIC = [0x25, 0x50, 0x44, 0x46]; // "%PDF"
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Disambiguate a ZIP-based file. OOXML formats (docx/pptx/xlsx) all carry a
|
|
56
|
-
* `[Content_Types].xml` whose content-type strings name the flavor; a squisq
|
|
57
|
-
* container (.dbk) is a plain ZIP with no such part, so absence of the OOXML
|
|
58
|
-
* marker means `dbk`.
|
|
59
|
-
*/
|
|
60
|
-
async function sniffZip(bytes: Uint8Array): Promise<FormatId> {
|
|
61
|
-
const archive = await openBoundedZipArchive(bytes).catch((cause: unknown) => {
|
|
62
|
-
if (cause instanceof ZipSafetyError && cause.code !== 'invalid-archive') throw cause;
|
|
63
|
-
throw new ConversionError('invalid-input', 'Input is not a readable ZIP archive.', { cause });
|
|
64
|
-
});
|
|
65
|
-
const contentTypes = archive.entries.find((entry) => entry.path === '[Content_Types].xml');
|
|
66
|
-
if (!contentTypes) return 'dbk';
|
|
67
|
-
const maxContentTypesBytes = 1024 * 1024;
|
|
68
|
-
if ((contentTypes.declaredSize ?? 0) > maxContentTypesBytes) {
|
|
69
|
-
throw new ConversionError('invalid-input', 'OOXML content-types metadata is too large.');
|
|
70
|
-
}
|
|
71
|
-
const xmlBytes = await archive.read(contentTypes.path, maxContentTypesBytes);
|
|
72
|
-
if (!xmlBytes) return 'dbk';
|
|
73
|
-
const xml = new TextDecoder().decode(xmlBytes);
|
|
74
|
-
if (xml.includes('wordprocessingml')) return 'docx';
|
|
75
|
-
if (xml.includes('presentationml')) return 'pptx';
|
|
76
|
-
if (xml.includes('spreadsheetml')) return 'xlsx';
|
|
77
|
-
return 'dbk';
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Determine the source format for raw bytes. */
|
|
81
|
-
async function detectByteFormat(
|
|
82
|
-
bytes: Uint8Array,
|
|
83
|
-
filename: string | undefined,
|
|
84
|
-
options: ConvertOptions,
|
|
85
|
-
registry: FormatRegistry,
|
|
86
|
-
): Promise<FormatId> {
|
|
87
|
-
if (options.from) return options.from;
|
|
88
|
-
|
|
89
|
-
const ext = filename ? extractExt(filename) : undefined;
|
|
90
|
-
if (ext) {
|
|
91
|
-
const byExt = registry.byExtension(ext);
|
|
92
|
-
if (byExt) return byExt.id;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (hasPrefix(bytes, PDF_MAGIC)) return 'pdf';
|
|
96
|
-
if (hasPrefix(bytes, ZIP_MAGIC)) return sniffZip(bytes);
|
|
97
|
-
|
|
98
|
-
// No magic and no recognized extension → assume UTF-8 markdown.
|
|
99
|
-
return 'md';
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ── Filename helpers ────────────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
/** Lowercased extension without the leading dot (`report.DocX` → `docx`). */
|
|
105
|
-
function extractExt(filename: string): string {
|
|
106
|
-
const normalized = filename.replace(/\\/g, '/');
|
|
107
|
-
const base = normalized.split('/').pop() ?? normalized;
|
|
108
|
-
const dot = base.lastIndexOf('.');
|
|
109
|
-
if (dot <= 0) return '';
|
|
110
|
-
return base.slice(dot + 1).toLowerCase();
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Basename without its final extension (`a/b/report.docx` → `report`). */
|
|
114
|
-
function baseNameOf(filename: string): string {
|
|
115
|
-
const normalized = filename.replace(/\\/g, '/');
|
|
116
|
-
const base = normalized.split('/').pop() ?? normalized;
|
|
117
|
-
const dot = base.lastIndexOf('.');
|
|
118
|
-
return dot > 0 ? base.slice(0, dot) : base;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ── Normalization ───────────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
interface Normalized {
|
|
124
|
-
input: NormalizedInput;
|
|
125
|
-
warnings: string[];
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function normalizeBytes(
|
|
129
|
-
source: Extract<ConvertSource, { kind: 'bytes' }>,
|
|
130
|
-
options: ConvertOptions,
|
|
131
|
-
registry: FormatRegistry,
|
|
132
|
-
): Promise<Normalized> {
|
|
133
|
-
const bytes = source.data instanceof Uint8Array ? source.data : new Uint8Array(source.data);
|
|
134
|
-
const buffer = bytes.buffer.slice(
|
|
135
|
-
bytes.byteOffset,
|
|
136
|
-
bytes.byteOffset + bytes.byteLength,
|
|
137
|
-
) as ArrayBuffer;
|
|
138
|
-
|
|
139
|
-
const fromId = await detectByteFormat(bytes, source.filename, options, registry);
|
|
140
|
-
const fromDef = registry.get(fromId);
|
|
141
|
-
if (!fromDef) {
|
|
142
|
-
throw new ConversionError('unknown-format', `Unknown source format "${fromId}".`, {
|
|
143
|
-
format: fromId,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
if (!fromDef.importContainer && !fromDef.importDoc) {
|
|
147
|
-
throw new ConversionError(
|
|
148
|
-
'unsupported-input',
|
|
149
|
-
`Format "${fromDef.label}" cannot be used as a conversion source.`,
|
|
150
|
-
{
|
|
151
|
-
format: fromId,
|
|
152
|
-
hint: 'This format is export-only.',
|
|
153
|
-
},
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const { markdownToDoc } = await import('@bendyline/squisq/doc');
|
|
158
|
-
const { parseMarkdown, stringifyMarkdown } = await import('@bendyline/squisq/markdown');
|
|
159
|
-
|
|
160
|
-
const warnings: string[] = [];
|
|
161
|
-
let markdownDoc: MarkdownDocument;
|
|
162
|
-
let container: ContentContainer;
|
|
163
|
-
|
|
164
|
-
if (fromDef.importContainer) {
|
|
165
|
-
container = await fromDef.importContainer(buffer, options);
|
|
166
|
-
// PDF embedded-image extraction needs a browser canvas; under Node it's
|
|
167
|
-
// skipped inside pdfToContainer. importContainer has no warnings channel of
|
|
168
|
-
// its own, so surface that degraded path here where warnings are collected.
|
|
169
|
-
if (fromDef.id === 'pdf' && typeof document === 'undefined') {
|
|
170
|
-
warnings.push(
|
|
171
|
-
'PDF embedded images were skipped — image decoding requires a browser canvas (running under Node).',
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
const text = await container.readDocument();
|
|
175
|
-
markdownDoc = text
|
|
176
|
-
? parseMarkdown(text, markdownFormatOptions(options).parse)
|
|
177
|
-
: { type: 'document', children: [] };
|
|
178
|
-
} else {
|
|
179
|
-
// importDoc is guaranteed present by the guard above.
|
|
180
|
-
markdownDoc = await fromDef.importDoc!(buffer, options);
|
|
181
|
-
const { MemoryContentContainer } = await import('@bendyline/squisq/storage');
|
|
182
|
-
const mem = new MemoryContentContainer();
|
|
183
|
-
await mem.writeDocument(stringifyMarkdown(markdownDoc));
|
|
184
|
-
container = mem;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const doc = markdownToDoc(markdownDoc, { autoTemplates: options.autoTemplates });
|
|
188
|
-
const baseName = source.filename ? baseNameOf(source.filename) : 'document';
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
input: { doc, markdownDoc, container, baseName },
|
|
192
|
-
warnings,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function normalizeMarkdown(
|
|
197
|
-
source: Extract<ConvertSource, { kind: 'markdown' }>,
|
|
198
|
-
options: ConvertOptions,
|
|
199
|
-
): Promise<Normalized> {
|
|
200
|
-
const { markdownToDoc } = await import('@bendyline/squisq/doc');
|
|
201
|
-
const { parseMarkdown } = await import('@bendyline/squisq/markdown');
|
|
202
|
-
|
|
203
|
-
const { stringifyMarkdown } = await import('@bendyline/squisq/markdown');
|
|
204
|
-
|
|
205
|
-
const markdownDoc =
|
|
206
|
-
typeof source.markdown === 'string'
|
|
207
|
-
? parseMarkdown(source.markdown, markdownFormatOptions(options).parse)
|
|
208
|
-
: source.markdown;
|
|
209
|
-
|
|
210
|
-
let container = source.container;
|
|
211
|
-
if (!container) {
|
|
212
|
-
// Seed a fresh container with the document so container-backed exports
|
|
213
|
-
// (dbk) and media resolution have the markdown to serialize.
|
|
214
|
-
const { MemoryContentContainer } = await import('@bendyline/squisq/storage');
|
|
215
|
-
const mem = new MemoryContentContainer();
|
|
216
|
-
await mem.writeDocument(stringifyMarkdown(markdownDoc));
|
|
217
|
-
container = mem;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const doc = markdownToDoc(markdownDoc, { autoTemplates: options.autoTemplates });
|
|
221
|
-
const baseName = source.baseName ?? 'document';
|
|
222
|
-
|
|
223
|
-
return { input: { doc, markdownDoc, container, baseName }, warnings: [] };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function normalizeDoc(source: Extract<ConvertSource, { kind: 'doc' }>): Promise<Normalized> {
|
|
227
|
-
let container = source.container;
|
|
228
|
-
if (!container) {
|
|
229
|
-
const { MemoryContentContainer } = await import('@bendyline/squisq/storage');
|
|
230
|
-
const { docToMarkdown } = await import('@bendyline/squisq/doc');
|
|
231
|
-
const { stringifyMarkdown } = await import('@bendyline/squisq/markdown');
|
|
232
|
-
const mem = new MemoryContentContainer();
|
|
233
|
-
await mem.writeDocument(stringifyMarkdown(docToMarkdown(source.doc)));
|
|
234
|
-
container = mem;
|
|
235
|
-
}
|
|
236
|
-
const baseName = source.baseName ?? 'document';
|
|
237
|
-
// markdownDoc is intentionally omitted — exporters derive it lazily via docToMarkdown.
|
|
238
|
-
return { input: { doc: source.doc, container, baseName }, warnings: [] };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function normalize(
|
|
242
|
-
source: ConvertSource,
|
|
243
|
-
options: ConvertOptions,
|
|
244
|
-
registry: FormatRegistry,
|
|
245
|
-
): Promise<Normalized> {
|
|
246
|
-
switch (source.kind) {
|
|
247
|
-
case 'bytes':
|
|
248
|
-
return normalizeBytes(source, options, registry);
|
|
249
|
-
case 'markdown':
|
|
250
|
-
return normalizeMarkdown(source, options);
|
|
251
|
-
case 'doc':
|
|
252
|
-
return normalizeDoc(source);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// ── Transform ───────────────────────────────────────────────────────
|
|
257
|
-
|
|
258
|
-
async function applyTransformStyle(
|
|
259
|
-
input: NormalizedInput,
|
|
260
|
-
transformStyle: TransformStyleInput,
|
|
261
|
-
themeId?: string,
|
|
262
|
-
registry?: TransformStyleRegistry,
|
|
263
|
-
): Promise<string[]> {
|
|
264
|
-
const { applyTransform, extractDocImages } = await import('@bendyline/squisq/transform');
|
|
265
|
-
const { docToMarkdown, markdownToDoc } = await import('@bendyline/squisq/doc');
|
|
266
|
-
const images = extractDocImages(input.doc.blocks);
|
|
267
|
-
const result = applyTransform(input.doc, transformStyle, { themeId, images, registry });
|
|
268
|
-
input.doc = result.doc;
|
|
269
|
-
const markdownDoc = docToMarkdown(result.doc);
|
|
270
|
-
input.markdownDoc = markdownDoc;
|
|
271
|
-
|
|
272
|
-
// Lossy-path detection: transforms can emit template blocks whose data lives
|
|
273
|
-
// in template inputs rather than in round-trippable markdown `contents`, so
|
|
274
|
-
// `docToMarkdown` silently drops them. Re-parse with auto-templating off (so
|
|
275
|
-
// the comparison stays ~1:1 and isn't confused by content-aware promotion)
|
|
276
|
-
// and compare block counts. Best-effort — most blocks carry `contents` and
|
|
277
|
-
// round-trip fine, so this only fires on a genuine collapse.
|
|
278
|
-
const roundTripped = markdownToDoc(markdownDoc, { autoTemplates: false });
|
|
279
|
-
if (roundTripped.blocks.length < result.doc.blocks.length) {
|
|
280
|
-
return [
|
|
281
|
-
'Transform produced blocks that don’t round-trip to markdown; export may be incomplete.',
|
|
282
|
-
];
|
|
283
|
-
}
|
|
284
|
-
return [];
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ── prepare / convert ───────────────────────────────────────────────
|
|
288
|
-
|
|
289
|
-
/** Normalize and transform a source once, then safely export that snapshot many times. */
|
|
290
|
-
export async function prepareConversion(
|
|
291
|
-
source: ConvertSource,
|
|
292
|
-
options: ConvertOptions = {},
|
|
293
|
-
): Promise<PreparedConversion> {
|
|
294
|
-
return prepareConversionInternal(source, options);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
async function prepareConversionInternal(
|
|
298
|
-
source: ConvertSource,
|
|
299
|
-
options: ConvertOptions,
|
|
300
|
-
failureFormat?: FormatId,
|
|
301
|
-
): Promise<PreparedConversion> {
|
|
302
|
-
throwIfAborted(options.signal);
|
|
303
|
-
const registry = options.registry ?? defaultRegistry();
|
|
304
|
-
|
|
305
|
-
let normalized: Normalized;
|
|
306
|
-
try {
|
|
307
|
-
normalized = await normalize(source, options, registry);
|
|
308
|
-
throwIfAborted(options.signal);
|
|
309
|
-
} catch (err: unknown) {
|
|
310
|
-
throwIfAborted(options.signal);
|
|
311
|
-
if (err instanceof ConversionError) throw err;
|
|
312
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
313
|
-
throw new ConversionError('invalid-input', message, {
|
|
314
|
-
format: options.from,
|
|
315
|
-
cause: err,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
const { input, warnings } = normalized;
|
|
319
|
-
|
|
320
|
-
// Respect an existing doc theme; only fill it in from options when absent.
|
|
321
|
-
if (options.themeId && !input.doc.themeId) {
|
|
322
|
-
input.doc = { ...input.doc, themeId: options.themeId };
|
|
323
|
-
}
|
|
324
|
-
|
|
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
|
-
throwIfAborted(options.signal);
|
|
336
|
-
}
|
|
337
|
-
} catch (err: unknown) {
|
|
338
|
-
throwIfAborted(options.signal);
|
|
339
|
-
if (err instanceof ConversionError) throw err;
|
|
340
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
341
|
-
throw new ConversionError('conversion-failed', message, {
|
|
342
|
-
format: failureFormat,
|
|
343
|
-
cause: err,
|
|
344
|
-
});
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const preparedWarnings = Object.freeze([...warnings]);
|
|
348
|
-
const preparedInput = { ...input };
|
|
349
|
-
return Object.freeze({
|
|
350
|
-
async convert(to: FormatId, targetOptions: PreparedExportOptions = {}) {
|
|
351
|
-
const target = requireExportTarget(registry, to);
|
|
352
|
-
const exportOptions = mergePreparedExportOptions(options, targetOptions);
|
|
353
|
-
throwIfAborted(exportOptions.signal);
|
|
354
|
-
let result: ConversionResult;
|
|
355
|
-
try {
|
|
356
|
-
result = await target.exportDoc!({ ...preparedInput }, exportOptions);
|
|
357
|
-
throwIfAborted(exportOptions.signal);
|
|
358
|
-
} catch (err: unknown) {
|
|
359
|
-
throwIfAborted(exportOptions.signal);
|
|
360
|
-
if (err instanceof ConversionError) throw err;
|
|
361
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
362
|
-
throw new ConversionError('conversion-failed', message, { format: to, cause: err });
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const ext = target.extensions[0]?.replace(/^\.+/, '') ?? to;
|
|
366
|
-
return {
|
|
367
|
-
...result,
|
|
368
|
-
suggestedFilename: `${preparedInput.baseName}.${ext}`,
|
|
369
|
-
warnings: [...preparedWarnings, ...result.warnings],
|
|
370
|
-
};
|
|
371
|
-
},
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function requireExportTarget(registry: FormatRegistry, to: FormatId): FormatDefinition {
|
|
376
|
-
const target = registry.get(to);
|
|
377
|
-
if (!target) {
|
|
378
|
-
throw new ConversionError('unknown-format', `Unknown target format "${to}".`, { format: to });
|
|
379
|
-
}
|
|
380
|
-
if (!target.exportDoc) {
|
|
381
|
-
throw new ConversionError(
|
|
382
|
-
'unsupported-output',
|
|
383
|
-
`Format "${target.label}" does not support export.`,
|
|
384
|
-
{ format: to, hint: 'This format is import-only.' },
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
return target;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function mergePreparedExportOptions(
|
|
391
|
-
prepared: ConvertOptions,
|
|
392
|
-
target: PreparedExportOptions,
|
|
393
|
-
): ConvertOptions {
|
|
394
|
-
return {
|
|
395
|
-
...prepared,
|
|
396
|
-
signal: mergeAbortSignals(prepared.signal, target.signal),
|
|
397
|
-
title: target.title ?? prepared.title,
|
|
398
|
-
resolvePlayerScript: target.resolvePlayerScript ?? prepared.resolvePlayerScript,
|
|
399
|
-
formatOptions:
|
|
400
|
-
target.formatOptions === undefined
|
|
401
|
-
? prepared.formatOptions
|
|
402
|
-
: { ...(prepared.formatOptions ?? {}), ...target.formatOptions },
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function mergeAbortSignals(
|
|
407
|
-
preparedSignal?: AbortSignal,
|
|
408
|
-
targetSignal?: AbortSignal,
|
|
409
|
-
): AbortSignal | undefined {
|
|
410
|
-
if (!preparedSignal) return targetSignal;
|
|
411
|
-
if (!targetSignal || preparedSignal === targetSignal) return preparedSignal;
|
|
412
|
-
return AbortSignal.any([preparedSignal, targetSignal]);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Convert a source document to a target format.
|
|
417
|
-
*
|
|
418
|
-
* @param source - A bytes / markdown / doc source.
|
|
419
|
-
* @param to - The target format id (must be registered and support export).
|
|
420
|
-
* @param options - Conversion options (registry, theme, transform, …).
|
|
421
|
-
* @throws {@link ConversionError} on any failure, with a stable `code`.
|
|
422
|
-
*/
|
|
423
|
-
export async function convert(
|
|
424
|
-
source: ConvertSource,
|
|
425
|
-
to: FormatId,
|
|
426
|
-
options: ConvertOptions = {},
|
|
427
|
-
): Promise<ConversionResult> {
|
|
428
|
-
throwIfAborted(options.signal);
|
|
429
|
-
const registry = options.registry ?? defaultRegistry();
|
|
430
|
-
requireExportTarget(registry, to);
|
|
431
|
-
const prepared = await prepareConversionInternal(source, { ...options, registry }, to);
|
|
432
|
-
return prepared.convert(to);
|
|
433
|
-
}
|