@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
|
@@ -1,495 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the programmatic convert() entry point: normalization across the
|
|
3
|
-
* three source shapes, byte sniffing, transform threading, round-trips, and
|
|
4
|
-
* suggested-filename correctness.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect } from 'vitest';
|
|
8
|
-
import JSZip from 'jszip';
|
|
9
|
-
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
10
|
-
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
11
|
-
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
12
|
-
import { createTransformStyleRegistry, resolveTransformStyle } from '@bendyline/squisq/transform';
|
|
13
|
-
import { convert, ConversionError, defaultRegistry, prepareConversion } from '../registry/index';
|
|
14
|
-
import { zipToContainer } from '../container/index';
|
|
15
|
-
import { markdownDocToPptx } from '../pptx/export';
|
|
16
|
-
import { buildThemedPptx } from './pptxInferFixtures';
|
|
17
|
-
|
|
18
|
-
const SAMPLE_MD = `# Round Trip
|
|
19
|
-
|
|
20
|
-
This is a paragraph that should survive a docx round trip.
|
|
21
|
-
`;
|
|
22
|
-
|
|
23
|
-
// ── unknown-format / unsupported-output ─────────────────────────────
|
|
24
|
-
|
|
25
|
-
describe('convert error paths', () => {
|
|
26
|
-
it('throws unknown-format for an unregistered target', async () => {
|
|
27
|
-
await expect(convert({ kind: 'markdown', markdown: '# hi' }, 'nope')).rejects.toMatchObject({
|
|
28
|
-
name: 'ConversionError',
|
|
29
|
-
code: 'unknown-format',
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('throws unsupported-output when exporting to an import-only format', async () => {
|
|
34
|
-
// Register a definition with only importDoc, then export to it.
|
|
35
|
-
const registry = defaultRegistry();
|
|
36
|
-
registry.register({
|
|
37
|
-
id: 'importonly',
|
|
38
|
-
label: 'Import Only',
|
|
39
|
-
mimeType: 'application/x-import',
|
|
40
|
-
extensions: ['.imp'],
|
|
41
|
-
async importDoc() {
|
|
42
|
-
return parseMarkdown('# x');
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
await expect(
|
|
46
|
-
convert({ kind: 'markdown', markdown: '# hi' }, 'importonly', { registry }),
|
|
47
|
-
).rejects.toMatchObject({ name: 'ConversionError', code: 'unsupported-output' });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('normalizes importer failures to ConversionError', async () => {
|
|
51
|
-
const promise = convert({ kind: 'bytes', data: new TextEncoder().encode('not a zip') }, 'md', {
|
|
52
|
-
from: 'docx',
|
|
53
|
-
});
|
|
54
|
-
await expect(promise).rejects.toMatchObject({
|
|
55
|
-
name: 'ConversionError',
|
|
56
|
-
code: 'invalid-input',
|
|
57
|
-
format: 'docx',
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('threads cancellation to exporters and preserves the abort reason', async () => {
|
|
62
|
-
const controller = new AbortController();
|
|
63
|
-
const reason = new Error('caller cancelled conversion');
|
|
64
|
-
reason.name = 'AbortError';
|
|
65
|
-
const registry = defaultRegistry();
|
|
66
|
-
registry.register({
|
|
67
|
-
id: 'abortable',
|
|
68
|
-
label: 'Abortable',
|
|
69
|
-
mimeType: 'application/x-abortable',
|
|
70
|
-
extensions: ['.abortable'],
|
|
71
|
-
async exportDoc(_input, options) {
|
|
72
|
-
expect(options.signal).toBe(controller.signal);
|
|
73
|
-
controller.abort(reason);
|
|
74
|
-
throw new Error('must not replace the abort reason');
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
await expect(
|
|
79
|
-
convert({ kind: 'markdown', markdown: '# Cancel me' }, 'abortable', {
|
|
80
|
-
registry,
|
|
81
|
-
signal: controller.signal,
|
|
82
|
-
}),
|
|
83
|
-
).rejects.toBe(reason);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('format option threading', () => {
|
|
88
|
-
it('threads CSV tableIndex and delimiter through the conversion facade', async () => {
|
|
89
|
-
const markdown =
|
|
90
|
-
'| A | B |\n|---|---|\n| first | row |\n\n| C | D |\n|---|---|\n| second | row |\n';
|
|
91
|
-
const result = await convert({ kind: 'markdown', markdown }, 'csv', {
|
|
92
|
-
formatOptions: { csv: { tableIndex: 1, delimiter: ';' } },
|
|
93
|
-
});
|
|
94
|
-
expect(new TextDecoder().decode(result.bytes)).toBe('C;D\r\nsecond;row');
|
|
95
|
-
expect(result.warnings).toEqual([]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('honors Markdown parse options for direct Markdown sources', async () => {
|
|
99
|
-
let captured: import('../registry/index').NormalizedInput | undefined;
|
|
100
|
-
const registry = defaultRegistry();
|
|
101
|
-
registry.register({
|
|
102
|
-
id: 'capture-md-options',
|
|
103
|
-
label: 'Capture Markdown Options',
|
|
104
|
-
mimeType: 'application/x-capture',
|
|
105
|
-
extensions: ['.capture'],
|
|
106
|
-
async exportDoc(input) {
|
|
107
|
-
captured = input;
|
|
108
|
-
return {
|
|
109
|
-
bytes: new Uint8Array(),
|
|
110
|
-
mimeType: 'application/x-capture',
|
|
111
|
-
suggestedFilename: '',
|
|
112
|
-
warnings: [],
|
|
113
|
-
};
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
await convert(
|
|
118
|
-
{ kind: 'markdown', markdown: '---\ntitle: Hidden metadata\n---\n\n# Body' },
|
|
119
|
-
'capture-md-options',
|
|
120
|
-
{
|
|
121
|
-
registry,
|
|
122
|
-
formatOptions: { md: { parse: { frontmatter: false } } },
|
|
123
|
-
},
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
expect(captured?.markdownDoc?.frontmatter).toBeUndefined();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('threads archive safety limits into OOXML importers', async () => {
|
|
130
|
-
const docx = await convert({ kind: 'markdown', markdown: '# Limited' }, 'docx');
|
|
131
|
-
await expect(
|
|
132
|
-
convert({ kind: 'bytes', data: docx.bytes }, 'md', {
|
|
133
|
-
from: 'docx',
|
|
134
|
-
formatOptions: { docx: { maxEntries: 1 } },
|
|
135
|
-
}),
|
|
136
|
-
).rejects.toMatchObject({ code: 'invalid-input', format: 'docx' });
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('can emit DBK with STORE so strict import compression limits accept its own output', async () => {
|
|
140
|
-
const result = await convert(
|
|
141
|
-
{ kind: 'markdown', markdown: `# Self-compatible DBK\n\n${'x'.repeat(16 * 1024)}` },
|
|
142
|
-
'dbk',
|
|
143
|
-
{ formatOptions: { dbk: { compression: 'STORE' } } },
|
|
144
|
-
);
|
|
145
|
-
const restored = await zipToContainer(result.bytes, { maxCompressionRatio: 1.01 });
|
|
146
|
-
expect(await restored.readDocument()).toContain('Self-compatible DBK');
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// ── round trip: md → docx → md ──────────────────────────────────────
|
|
151
|
-
|
|
152
|
-
describe('md → docx → md round trip', () => {
|
|
153
|
-
it('preserves paragraph text through a docx round trip', async () => {
|
|
154
|
-
const toDocx = await convert(
|
|
155
|
-
{ kind: 'markdown', markdown: SAMPLE_MD, baseName: 'doc' },
|
|
156
|
-
'docx',
|
|
157
|
-
);
|
|
158
|
-
expect(toDocx.mimeType).toContain('wordprocessingml');
|
|
159
|
-
expect(toDocx.bytes.byteLength).toBeGreaterThan(0);
|
|
160
|
-
|
|
161
|
-
// Feed the docx bytes back in with an explicit `from`.
|
|
162
|
-
const backToMd = await convert(
|
|
163
|
-
{ kind: 'bytes', data: toDocx.bytes, filename: 'doc.docx' },
|
|
164
|
-
'md',
|
|
165
|
-
{ from: 'docx' },
|
|
166
|
-
);
|
|
167
|
-
const text = new TextDecoder().decode(backToMd.bytes);
|
|
168
|
-
expect(text).toContain('survive a docx round trip');
|
|
169
|
-
expect(backToMd.suggestedFilename).toBe('doc.md');
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// ── byte sniffing ───────────────────────────────────────────────────
|
|
174
|
-
|
|
175
|
-
describe('byte sniffing', () => {
|
|
176
|
-
it('sniffs a PDF by its %PDF magic', async () => {
|
|
177
|
-
// Stub the pdf importer so the test exercises the sniff, not pdfjs (which
|
|
178
|
-
// can't run under jsdom). Any %PDF-prefixed bytes must route to 'pdf'.
|
|
179
|
-
const registry = defaultRegistry();
|
|
180
|
-
registry.register({
|
|
181
|
-
...registry.get('pdf')!,
|
|
182
|
-
importContainer: undefined, // force the importDoc path (skip pdfjs)
|
|
183
|
-
async importDoc() {
|
|
184
|
-
return parseMarkdown('# sniffed as pdf');
|
|
185
|
-
},
|
|
186
|
-
});
|
|
187
|
-
const pdfBytes = new TextEncoder().encode('%PDF-1.4\n%mock pdf body\n');
|
|
188
|
-
const back = await convert({ kind: 'bytes', data: pdfBytes }, 'md', { registry });
|
|
189
|
-
expect(new TextDecoder().decode(back.bytes)).toContain('sniffed as pdf');
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('sniffs a squisq container (plain zip, no [Content_Types].xml) as dbk', async () => {
|
|
193
|
-
// A dbk is a ZIP with a markdown doc and no OOXML content-types part.
|
|
194
|
-
const dbk = await convert({ kind: 'markdown', markdown: '# In a container\n\nBody.' }, 'dbk');
|
|
195
|
-
expect(dbk.mimeType).toBe('application/zip');
|
|
196
|
-
|
|
197
|
-
const back = await convert({ kind: 'bytes', data: dbk.bytes }, 'md');
|
|
198
|
-
const text = new TextDecoder().decode(back.bytes);
|
|
199
|
-
expect(text).toContain('In a container');
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('DBK export snapshots the supplied source instead of stale container markdown', async () => {
|
|
203
|
-
const container = new MemoryContentContainer();
|
|
204
|
-
await container.writeDocument('# Old');
|
|
205
|
-
await container.writeFile('asset.bin', new Uint8Array([1, 2, 3]));
|
|
206
|
-
|
|
207
|
-
const result = await convert({ kind: 'markdown', markdown: '# New', container }, 'dbk');
|
|
208
|
-
const restored = await zipToContainer(result.bytes);
|
|
209
|
-
expect(await restored.readDocument()).toContain('# New');
|
|
210
|
-
expect(new Uint8Array((await restored.readFile('asset.bin'))!)).toEqual(
|
|
211
|
-
new Uint8Array([1, 2, 3]),
|
|
212
|
-
);
|
|
213
|
-
// Snapshotting must not mutate the caller's container.
|
|
214
|
-
expect(await container.readDocument()).toBe('# Old');
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('disambiguates a docx zip via its content-types part', async () => {
|
|
218
|
-
const docx = await convert({ kind: 'markdown', markdown: '# Word doc' }, 'docx');
|
|
219
|
-
// No filename, no `from` — must read [Content_Types].xml to pick docx.
|
|
220
|
-
const back = await convert({ kind: 'bytes', data: docx.bytes }, 'md');
|
|
221
|
-
const text = new TextDecoder().decode(back.bytes);
|
|
222
|
-
expect(text).toContain('Word doc');
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
// ── transform threading ─────────────────────────────────────────────
|
|
227
|
-
|
|
228
|
-
describe('transform threading', () => {
|
|
229
|
-
it('normalizes and transforms once before exporting multiple targets', async () => {
|
|
230
|
-
let importCount = 0;
|
|
231
|
-
let transformLookupCount = 0;
|
|
232
|
-
const exportCounts = { first: 0, second: 0 };
|
|
233
|
-
const markers: string[] = [];
|
|
234
|
-
const exportSignals: Array<AbortSignal | undefined> = [];
|
|
235
|
-
const formats = defaultRegistry();
|
|
236
|
-
formats.register({
|
|
237
|
-
id: 'counted-input',
|
|
238
|
-
label: 'Counted input',
|
|
239
|
-
mimeType: 'application/x-counted-input',
|
|
240
|
-
extensions: ['.counted'],
|
|
241
|
-
async importDoc() {
|
|
242
|
-
importCount += 1;
|
|
243
|
-
return parseMarkdown(SAMPLE_MD);
|
|
244
|
-
},
|
|
245
|
-
});
|
|
246
|
-
for (const [id, key] of [
|
|
247
|
-
['prepared-first', 'first'],
|
|
248
|
-
['prepared-second', 'second'],
|
|
249
|
-
] as const) {
|
|
250
|
-
formats.register({
|
|
251
|
-
id,
|
|
252
|
-
label: id,
|
|
253
|
-
mimeType: `application/x-${id}`,
|
|
254
|
-
extensions: [`.${key}`],
|
|
255
|
-
async exportDoc(input, options) {
|
|
256
|
-
exportCounts[key] += 1;
|
|
257
|
-
exportSignals.push(options.signal);
|
|
258
|
-
const targetOptions = options.formatOptions?.[id] as { marker?: string } | undefined;
|
|
259
|
-
markers.push(targetOptions?.marker ?? 'missing');
|
|
260
|
-
expect(input.doc.blocks.length).toBeGreaterThan(0);
|
|
261
|
-
return {
|
|
262
|
-
bytes: new TextEncoder().encode(key),
|
|
263
|
-
mimeType: `application/x-${id}`,
|
|
264
|
-
suggestedFilename: '',
|
|
265
|
-
warnings: [`${key} warning`],
|
|
266
|
-
};
|
|
267
|
-
},
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const style = { ...resolveTransformStyle('magazine'), id: 'counted-transform' };
|
|
272
|
-
const backingTransforms = createTransformStyleRegistry([style]);
|
|
273
|
-
const transformRegistry = {
|
|
274
|
-
register: (candidate: typeof style) => backingTransforms.register(candidate),
|
|
275
|
-
unregister: (id: string) => backingTransforms.unregister(id),
|
|
276
|
-
get: (id: string) => {
|
|
277
|
-
transformLookupCount += 1;
|
|
278
|
-
return backingTransforms.get(id);
|
|
279
|
-
},
|
|
280
|
-
list: () => backingTransforms.list(),
|
|
281
|
-
};
|
|
282
|
-
const overall = new AbortController();
|
|
283
|
-
const prepared = await prepareConversion(
|
|
284
|
-
{
|
|
285
|
-
kind: 'bytes',
|
|
286
|
-
data: new TextEncoder().encode('count me once'),
|
|
287
|
-
filename: 'source.counted',
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
registry: formats,
|
|
291
|
-
from: 'counted-input',
|
|
292
|
-
transformStyle: 'counted-transform',
|
|
293
|
-
transformRegistry,
|
|
294
|
-
signal: overall.signal,
|
|
295
|
-
},
|
|
296
|
-
);
|
|
297
|
-
const first = await prepared.convert('prepared-first', {
|
|
298
|
-
formatOptions: { 'prepared-first': { marker: 'one' } },
|
|
299
|
-
});
|
|
300
|
-
const target = new AbortController();
|
|
301
|
-
const second = await prepared.convert('prepared-second', {
|
|
302
|
-
signal: target.signal,
|
|
303
|
-
formatOptions: { 'prepared-second': { marker: 'two' } },
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
expect(importCount).toBe(1);
|
|
307
|
-
expect(transformLookupCount).toBe(1);
|
|
308
|
-
expect(exportCounts).toEqual({ first: 1, second: 1 });
|
|
309
|
-
expect(markers).toEqual(['one', 'two']);
|
|
310
|
-
expect(exportSignals[0]).toBe(overall.signal);
|
|
311
|
-
expect(exportSignals[1]?.aborted).toBe(false);
|
|
312
|
-
expect(first.suggestedFilename).toBe('source.first');
|
|
313
|
-
expect(second.suggestedFilename).toBe('source.second');
|
|
314
|
-
expect(first.warnings).toContain('first warning');
|
|
315
|
-
expect(second.warnings).toContain('second warning');
|
|
316
|
-
|
|
317
|
-
await expect(prepared.convert('missing-target')).rejects.toMatchObject({
|
|
318
|
-
name: 'ConversionError',
|
|
319
|
-
code: 'unknown-format',
|
|
320
|
-
format: 'missing-target',
|
|
321
|
-
});
|
|
322
|
-
const targetReason = new Error('cancel target export');
|
|
323
|
-
target.abort(targetReason);
|
|
324
|
-
expect(exportSignals[1]?.reason).toBe(targetReason);
|
|
325
|
-
await expect(prepared.convert('prepared-first', { signal: target.signal })).rejects.toBe(
|
|
326
|
-
targetReason,
|
|
327
|
-
);
|
|
328
|
-
const overallReason = new Error('cancel prepared conversion');
|
|
329
|
-
overall.abort(overallReason);
|
|
330
|
-
await expect(
|
|
331
|
-
prepared.convert('prepared-first', { signal: new AbortController().signal }),
|
|
332
|
-
).rejects.toBe(overallReason);
|
|
333
|
-
expect(exportCounts.first).toBe(1);
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it('applies a transform style before handing off to the exporter', async () => {
|
|
337
|
-
// A capture format records the NormalizedInput it receives, letting us
|
|
338
|
-
// observe that the transform ran without depending on any converter.
|
|
339
|
-
let captured: import('../registry/index').NormalizedInput | undefined;
|
|
340
|
-
const registry = defaultRegistry();
|
|
341
|
-
registry.register({
|
|
342
|
-
id: 'capture',
|
|
343
|
-
label: 'Capture',
|
|
344
|
-
mimeType: 'application/x-capture',
|
|
345
|
-
extensions: ['.cap'],
|
|
346
|
-
async exportDoc(input) {
|
|
347
|
-
captured = input;
|
|
348
|
-
return {
|
|
349
|
-
bytes: new Uint8Array([1]),
|
|
350
|
-
mimeType: 'application/x-capture',
|
|
351
|
-
suggestedFilename: '',
|
|
352
|
-
warnings: [],
|
|
353
|
-
};
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
const plainDoc = markdownToDoc(parseMarkdown(SAMPLE_MD));
|
|
358
|
-
await convert({ kind: 'markdown', markdown: SAMPLE_MD }, 'capture', {
|
|
359
|
-
registry,
|
|
360
|
-
transformStyle: 'magazine',
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
expect(captured).toBeDefined();
|
|
364
|
-
// The transform promotes/reshapes blocks, so the exporter sees a different
|
|
365
|
-
// Doc than a plain markdownToDoc of the same source.
|
|
366
|
-
expect(captured!.doc.blocks.length).not.toBe(plainDoc.blocks.length);
|
|
367
|
-
// markdownDoc is re-derived from the transformed Doc.
|
|
368
|
-
expect(captured!.markdownDoc).toBeDefined();
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('resolves transform ids only from the explicitly supplied registry', async () => {
|
|
372
|
-
let captured: import('../registry/index').NormalizedInput | undefined;
|
|
373
|
-
const formats = defaultRegistry();
|
|
374
|
-
formats.register({
|
|
375
|
-
id: 'capture-custom-transform',
|
|
376
|
-
label: 'Capture Custom Transform',
|
|
377
|
-
mimeType: 'application/x-capture',
|
|
378
|
-
extensions: ['.cap'],
|
|
379
|
-
async exportDoc(input) {
|
|
380
|
-
captured = input;
|
|
381
|
-
return {
|
|
382
|
-
bytes: new Uint8Array([1]),
|
|
383
|
-
mimeType: 'application/x-capture',
|
|
384
|
-
suggestedFilename: '',
|
|
385
|
-
warnings: [],
|
|
386
|
-
};
|
|
387
|
-
},
|
|
388
|
-
});
|
|
389
|
-
const custom = {
|
|
390
|
-
...resolveTransformStyle('minimal'),
|
|
391
|
-
id: 'tenant-transform',
|
|
392
|
-
suggestedThemeId: 'cinematic',
|
|
393
|
-
};
|
|
394
|
-
const transformRegistry = createTransformStyleRegistry([custom]);
|
|
395
|
-
|
|
396
|
-
await convert({ kind: 'markdown', markdown: SAMPLE_MD }, 'capture-custom-transform', {
|
|
397
|
-
registry: formats,
|
|
398
|
-
transformStyle: 'tenant-transform',
|
|
399
|
-
transformRegistry,
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
expect(captured?.doc.themeId).toBe('cinematic');
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
// ── doc-kind source ─────────────────────────────────────────────────
|
|
407
|
-
|
|
408
|
-
describe('doc-kind source', () => {
|
|
409
|
-
it('exports a Doc source', async () => {
|
|
410
|
-
const doc = markdownToDoc(parseMarkdown('# From a Doc\n\nContent.'));
|
|
411
|
-
const result = await convert({ kind: 'doc', doc, baseName: 'fromdoc' }, 'md');
|
|
412
|
-
const text = new TextDecoder().decode(result.bytes);
|
|
413
|
-
expect(text).toContain('From a Doc');
|
|
414
|
-
expect(result.suggestedFilename).toBe('fromdoc.md');
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// ── suggested filename ──────────────────────────────────────────────
|
|
419
|
-
|
|
420
|
-
describe('suggestedFilename', () => {
|
|
421
|
-
it('uses the primary extension per format', async () => {
|
|
422
|
-
const docx = await convert({ kind: 'markdown', markdown: '# t', baseName: 'report' }, 'docx');
|
|
423
|
-
expect(docx.suggestedFilename).toBe('report.docx');
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it('htmlzip suggests <base>.html.zip', async () => {
|
|
427
|
-
const zip = await convert(
|
|
428
|
-
{ kind: 'markdown', markdown: '# t', baseName: 'slides' },
|
|
429
|
-
'htmlzip',
|
|
430
|
-
{
|
|
431
|
-
resolvePlayerScript: async () => '/* stub */',
|
|
432
|
-
},
|
|
433
|
-
);
|
|
434
|
-
expect(zip.suggestedFilename).toBe('slides.html.zip');
|
|
435
|
-
expect(zip.mimeType).toBe('application/zip');
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('defaults baseName to "document" when none is given', async () => {
|
|
439
|
-
const out = await convert({ kind: 'markdown', markdown: '# t' }, 'md');
|
|
440
|
-
expect(out.suggestedFilename).toBe('document.md');
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it('uses only the basename for Windows-style input paths', async () => {
|
|
444
|
-
const out = await convert(
|
|
445
|
-
{
|
|
446
|
-
kind: 'bytes',
|
|
447
|
-
data: new TextEncoder().encode('# Windows'),
|
|
448
|
-
filename: 'C:\\docs\\report.md',
|
|
449
|
-
},
|
|
450
|
-
'md',
|
|
451
|
-
);
|
|
452
|
-
expect(out.suggestedFilename).toBe('report.md');
|
|
453
|
-
});
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
// sanity: ConversionError is exported and usable
|
|
457
|
-
// ── pptx theme/layout inference threading ───────────────────────────
|
|
458
|
-
|
|
459
|
-
describe('pptx import inference threading (default ON)', () => {
|
|
460
|
-
it('carries an inferred theme through convert() by default', async () => {
|
|
461
|
-
const deck = await markdownDocToPptx(parseMarkdown('# Hi\n\nBody.\n'), {});
|
|
462
|
-
const result = await convert({ kind: 'bytes', data: deck, filename: 'deck.pptx' }, 'md');
|
|
463
|
-
const md = new TextDecoder().decode(result.bytes);
|
|
464
|
-
expect(md).toContain('squisq-theme: custom-office-theme');
|
|
465
|
-
expect(md).toContain('squisq-custom-themes');
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it('omits inference with formatOptions.pptx.inferTheme=false', async () => {
|
|
469
|
-
const deck = await markdownDocToPptx(parseMarkdown('# Hi\n\nBody.\n'), {});
|
|
470
|
-
const result = await convert({ kind: 'bytes', data: deck, filename: 'deck.pptx' }, 'md', {
|
|
471
|
-
formatOptions: { pptx: { inferTheme: false, inferLayouts: false } },
|
|
472
|
-
});
|
|
473
|
-
const md = new TextDecoder().decode(result.bytes);
|
|
474
|
-
expect(md).not.toContain('squisq-theme');
|
|
475
|
-
expect(md).not.toContain('squisq-custom-themes');
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
it('end-to-end: inferred theme colors reach a downstream docx export', async () => {
|
|
479
|
-
// Distinctive fixture theme (bg #fdfdf8, text #1a1a2e) → pptx → docx.
|
|
480
|
-
const result = await convert(
|
|
481
|
-
{ kind: 'bytes', data: await buildThemedPptx(), filename: 'deck.pptx' },
|
|
482
|
-
'docx',
|
|
483
|
-
);
|
|
484
|
-
const zip = await JSZip.loadAsync(result.bytes);
|
|
485
|
-
let text = '';
|
|
486
|
-
for (const name of Object.keys(zip.files)) {
|
|
487
|
-
if (/\.(xml|rels)$/i.test(name)) text += await zip.files[name]!.async('string');
|
|
488
|
-
}
|
|
489
|
-
expect(text.toLowerCase()).toContain('1a1a2e');
|
|
490
|
-
});
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
it('exports ConversionError', () => {
|
|
494
|
-
expect(new ConversionError('conversion-failed', 'x')).toBeInstanceOf(Error);
|
|
495
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for CSV import/export: csvToMarkdownDoc, markdownDocToCsv, parseCsv.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { describe, expect, it } from 'vitest';
|
|
6
|
-
import type { MarkdownDocument, MarkdownTable, MarkdownText } from '@bendyline/squisq/markdown';
|
|
7
|
-
import { csvToMarkdownDoc, markdownDocToCsv, parseCsv } from '../csv/index';
|
|
8
|
-
|
|
9
|
-
function cellValue(table: MarkdownTable, row: number, col: number): string {
|
|
10
|
-
const cell = table.children[row]?.children[col];
|
|
11
|
-
const first = cell?.children[0] as MarkdownText | undefined;
|
|
12
|
-
return first?.value ?? '';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('parseCsv', () => {
|
|
16
|
-
it('parses quoted fields, escaped quotes, and embedded delimiters', () => {
|
|
17
|
-
const rows = parseCsv('a,b,c\n"x,y","he said ""hi""",z\n');
|
|
18
|
-
expect(rows).toEqual([
|
|
19
|
-
['a', 'b', 'c'],
|
|
20
|
-
['x,y', 'he said "hi"', 'z'],
|
|
21
|
-
]);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('handles CRLF line endings', () => {
|
|
25
|
-
expect(parseCsv('a,b\r\n1,2\r\n')).toEqual([
|
|
26
|
-
['a', 'b'],
|
|
27
|
-
['1', '2'],
|
|
28
|
-
]);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('csvToMarkdownDoc', () => {
|
|
33
|
-
it('builds a table with a header row by default', async () => {
|
|
34
|
-
const doc = await csvToMarkdownDoc('Name,Age\nAlice,30\n"Bob, Jr.",40\n');
|
|
35
|
-
expect(doc.type).toBe('document');
|
|
36
|
-
const table = doc.children[0] as MarkdownTable;
|
|
37
|
-
expect(table.type).toBe('table');
|
|
38
|
-
expect(table.children).toHaveLength(3);
|
|
39
|
-
expect(table.children[0]!.children[0]!.isHeader).toBe(true);
|
|
40
|
-
expect(cellValue(table, 0, 0)).toBe('Name');
|
|
41
|
-
expect(cellValue(table, 0, 1)).toBe('Age');
|
|
42
|
-
expect(cellValue(table, 2, 0)).toBe('Bob, Jr.');
|
|
43
|
-
expect(cellValue(table, 2, 1)).toBe('40');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('round-trips through markdownDocToCsv', async () => {
|
|
47
|
-
const csv = 'Name,Age\r\nAlice,30\r\n"Bob, Jr.",40';
|
|
48
|
-
const doc = await csvToMarkdownDoc(csv);
|
|
49
|
-
expect(markdownDocToCsv(doc)).toBe(csv);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('markdownDocToCsv tableIndex', () => {
|
|
54
|
-
async function docWithTwoTables(): Promise<MarkdownDocument> {
|
|
55
|
-
const first = await csvToMarkdownDoc('a,b\r\n1,2');
|
|
56
|
-
const second = await csvToMarkdownDoc('c,d\r\n3,4');
|
|
57
|
-
return { type: 'document', children: [...first.children, ...second.children] };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
it('exports the first table by default', async () => {
|
|
61
|
-
const doc = await docWithTwoTables();
|
|
62
|
-
expect(markdownDocToCsv(doc)).toBe('a,b\r\n1,2');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('picks the Nth table via tableIndex', async () => {
|
|
66
|
-
const doc = await docWithTwoTables();
|
|
67
|
-
expect(markdownDocToCsv(doc, { tableIndex: 0 })).toBe('a,b\r\n1,2');
|
|
68
|
-
expect(markdownDocToCsv(doc, { tableIndex: 1 })).toBe('c,d\r\n3,4');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('throws a clear error for an out-of-range tableIndex', async () => {
|
|
72
|
-
const doc = await docWithTwoTables();
|
|
73
|
-
expect(() => markdownDocToCsv(doc, { tableIndex: 2 })).toThrow(
|
|
74
|
-
'CSV export: tableIndex 2 is out of range — the document contains 2 table(s).',
|
|
75
|
-
);
|
|
76
|
-
expect(() => markdownDocToCsv(doc, { tableIndex: -1 })).toThrow('out of range');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('returns an empty string for a table-less document with the implicit default', () => {
|
|
80
|
-
const doc: MarkdownDocument = { type: 'document', children: [] };
|
|
81
|
-
expect(markdownDocToCsv(doc)).toBe('');
|
|
82
|
-
expect(() => markdownDocToCsv(doc, { tableIndex: 0 })).toThrow('out of range');
|
|
83
|
-
});
|
|
84
|
-
});
|