@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,113 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { inferThemeFromFile } from '../infer/index';
|
|
3
|
-
import { getPartXml, openPackage } from '../ooxml/reader';
|
|
4
|
-
import { inspectPptxLayouts } from '../pptx/layouts';
|
|
5
|
-
import { prepareZipInput } from '../shared/zipEntryCount';
|
|
6
|
-
import { buildThemedPptx } from './pptxInferFixtures';
|
|
7
|
-
|
|
8
|
-
function blockingBlob(onRead: () => void): Blob {
|
|
9
|
-
const blob = {
|
|
10
|
-
size: 128,
|
|
11
|
-
slice: () => blob,
|
|
12
|
-
arrayBuffer: () => {
|
|
13
|
-
onRead();
|
|
14
|
-
return new Promise<ArrayBuffer>(() => undefined);
|
|
15
|
-
},
|
|
16
|
-
};
|
|
17
|
-
return blob as unknown as Blob;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe('OOXML cancellation', () => {
|
|
21
|
-
it('preserves the exact reason while theme inference reads Blob input', async () => {
|
|
22
|
-
let markStarted: (() => void) | undefined;
|
|
23
|
-
const started = new Promise<void>((resolve) => {
|
|
24
|
-
markStarted = resolve;
|
|
25
|
-
});
|
|
26
|
-
const controller = new AbortController();
|
|
27
|
-
const reason = new Error('stop inference');
|
|
28
|
-
const pending = inferThemeFromFile(
|
|
29
|
-
blockingBlob(() => markStarted?.()),
|
|
30
|
-
{
|
|
31
|
-
signal: controller.signal,
|
|
32
|
-
},
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
await started;
|
|
36
|
-
controller.abort(reason);
|
|
37
|
-
|
|
38
|
-
await expect(pending).rejects.toBe(reason);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('preserves the exact reason while layout inspection opens Blob input', async () => {
|
|
42
|
-
let markStarted: (() => void) | undefined;
|
|
43
|
-
const started = new Promise<void>((resolve) => {
|
|
44
|
-
markStarted = resolve;
|
|
45
|
-
});
|
|
46
|
-
const controller = new AbortController();
|
|
47
|
-
const reason = new Error('stop layout inspection');
|
|
48
|
-
const pending = inspectPptxLayouts(
|
|
49
|
-
blockingBlob(() => markStarted?.()),
|
|
50
|
-
{
|
|
51
|
-
signal: controller.signal,
|
|
52
|
-
},
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
await started;
|
|
56
|
-
controller.abort(reason);
|
|
57
|
-
|
|
58
|
-
await expect(pending).rejects.toBe(reason);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('propagates package cancellation through later OOXML part traversal', async () => {
|
|
62
|
-
const controller = new AbortController();
|
|
63
|
-
const pkg = await openPackage(await buildThemedPptx(), { signal: controller.signal });
|
|
64
|
-
const reason = new Error('stop OOXML traversal');
|
|
65
|
-
controller.abort(reason);
|
|
66
|
-
|
|
67
|
-
await expect(getPartXml(pkg, 'ppt/presentation.xml')).rejects.toBe(reason);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('handles a synchronously re-entrant FileReader abort exactly once', async () => {
|
|
71
|
-
const previous = globalThis.FileReader;
|
|
72
|
-
let abortCalls = 0;
|
|
73
|
-
class ReentrantFileReader {
|
|
74
|
-
result: ArrayBuffer | null = null;
|
|
75
|
-
error: DOMException | null = null;
|
|
76
|
-
onload: ((event: ProgressEvent<FileReader>) => void) | null = null;
|
|
77
|
-
onerror: ((event: ProgressEvent<FileReader>) => void) | null = null;
|
|
78
|
-
onabort: ((event: ProgressEvent<FileReader>) => void) | null = null;
|
|
79
|
-
readAsArrayBuffer(): void {
|
|
80
|
-
// Remain pending until the caller aborts.
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
abort(): void {
|
|
84
|
-
abortCalls += 1;
|
|
85
|
-
this.onabort?.({} as ProgressEvent<FileReader>);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
Object.defineProperty(globalThis, 'FileReader', {
|
|
89
|
-
configurable: true,
|
|
90
|
-
writable: true,
|
|
91
|
-
value: ReentrantFileReader,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const blobLike = { size: 1, slice: () => blobLike } as unknown as Blob;
|
|
95
|
-
const controller = new AbortController();
|
|
96
|
-
const reason = new Error('stop FileReader');
|
|
97
|
-
try {
|
|
98
|
-
const pending = prepareZipInput(blobLike, controller.signal);
|
|
99
|
-
controller.abort(reason);
|
|
100
|
-
await expect(pending).rejects.toBe(reason);
|
|
101
|
-
expect(abortCalls).toBe(1);
|
|
102
|
-
} finally {
|
|
103
|
-
if (previous === undefined) Reflect.deleteProperty(globalThis, 'FileReader');
|
|
104
|
-
else {
|
|
105
|
-
Object.defineProperty(globalThis, 'FileReader', {
|
|
106
|
-
configurable: true,
|
|
107
|
-
writable: true,
|
|
108
|
-
value: previous,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
});
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the shared OOXML theme reader: clrScheme slot resolution
|
|
3
|
-
* (srgbClr / sysClr / fallbacks) and fontScheme parsing.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, expect, it } from 'vitest';
|
|
7
|
-
import { NS_DRAWINGML } from '../ooxml/namespaces';
|
|
8
|
-
import { parseThemeXml } from '../ooxml/themeReader';
|
|
9
|
-
import { buildThemeXml } from './pptxInferFixtures';
|
|
10
|
-
|
|
11
|
-
function parse(xml: string): Document {
|
|
12
|
-
return new DOMParser().parseFromString(xml, 'application/xml');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe('parseThemeXml', () => {
|
|
16
|
-
it('reads srgbClr slots, theme name, and latin fonts', () => {
|
|
17
|
-
const theme = parseThemeXml(parse(buildThemeXml({ name: 'Ion' })));
|
|
18
|
-
expect(theme.name).toBe('Ion');
|
|
19
|
-
expect(theme.colors).toMatchObject({
|
|
20
|
-
dk1: '#1a1a2e',
|
|
21
|
-
lt1: '#fdfdf8',
|
|
22
|
-
dk2: '#30304a',
|
|
23
|
-
lt2: '#efefe4',
|
|
24
|
-
accent1: '#4472c4',
|
|
25
|
-
accent6: '#70ad47',
|
|
26
|
-
hlink: '#0563c1',
|
|
27
|
-
});
|
|
28
|
-
expect(theme.fonts).toEqual({ majorLatin: 'Playfair Display', minorLatin: 'Aptos' });
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('prefers sysClr lastClr when present', () => {
|
|
32
|
-
const xml = buildThemeXml({
|
|
33
|
-
dk1: '<a:sysClr val="windowText" lastClr="0B0B0B"/>',
|
|
34
|
-
lt1: '<a:sysClr val="window" lastClr="FAFAFA"/>',
|
|
35
|
-
});
|
|
36
|
-
const theme = parseThemeXml(parse(xml));
|
|
37
|
-
expect(theme.colors?.dk1).toBe('#0b0b0b');
|
|
38
|
-
expect(theme.colors?.lt1).toBe('#fafafa');
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('resolves windowText/window sysClr without lastClr', () => {
|
|
42
|
-
const xml = buildThemeXml({
|
|
43
|
-
dk1: '<a:sysClr val="windowText"/>',
|
|
44
|
-
lt1: '<a:sysClr val="window"/>',
|
|
45
|
-
});
|
|
46
|
-
const theme = parseThemeXml(parse(xml));
|
|
47
|
-
expect(theme.colors?.dk1).toBe('#000000');
|
|
48
|
-
expect(theme.colors?.lt1).toBe('#ffffff');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('falls back with a warning for unknown sysClr values', () => {
|
|
52
|
-
const xml = buildThemeXml({ dk1: '<a:sysClr val="btnFace"/>' });
|
|
53
|
-
const theme = parseThemeXml(parse(xml));
|
|
54
|
-
expect(theme.colors?.dk1).toBe('#000000');
|
|
55
|
-
expect(theme.warnings.some((w) => w.includes('dk1'))).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('drops unresolvable accent slots with warnings', () => {
|
|
59
|
-
// accent list of 2 → accent3..6 slots missing entirely.
|
|
60
|
-
const xml = buildThemeXml({ accents: ['112233', '445566'] });
|
|
61
|
-
const theme = parseThemeXml(parse(xml));
|
|
62
|
-
expect(theme.colors?.accent1).toBe('#112233');
|
|
63
|
-
expect(theme.colors?.accent2).toBe('#445566');
|
|
64
|
-
expect(theme.colors?.accent3).toBeUndefined();
|
|
65
|
-
expect(theme.warnings.some((w) => w.includes('accent3'))).toBe(true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('handles a missing fontScheme', () => {
|
|
69
|
-
const theme = parseThemeXml(parse(buildThemeXml({ omitFontScheme: true })));
|
|
70
|
-
expect(theme.fonts).toBeUndefined();
|
|
71
|
-
expect(theme.warnings.some((w) => w.includes('fontScheme'))).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('treats an empty typeface as absent', () => {
|
|
75
|
-
const xml =
|
|
76
|
-
`<a:theme xmlns:a="${NS_DRAWINGML}" name="T"><a:themeElements>` +
|
|
77
|
-
`<a:fontScheme name="T">` +
|
|
78
|
-
`<a:majorFont><a:latin typeface=""/></a:majorFont>` +
|
|
79
|
-
`<a:minorFont><a:latin typeface="Georgia"/></a:minorFont>` +
|
|
80
|
-
`</a:fontScheme></a:themeElements></a:theme>`;
|
|
81
|
-
const theme = parseThemeXml(parse(xml));
|
|
82
|
-
expect(theme.fonts).toEqual({ minorLatin: 'Georgia' });
|
|
83
|
-
expect(theme.warnings.some((w) => w.includes('majorFont'))).toBe(true);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('reports a theme with no clrScheme', () => {
|
|
87
|
-
const xml = `<a:theme xmlns:a="${NS_DRAWINGML}" name="Empty"><a:themeElements/></a:theme>`;
|
|
88
|
-
const theme = parseThemeXml(parse(xml));
|
|
89
|
-
expect(theme.colors).toBeUndefined();
|
|
90
|
-
expect(theme.warnings.some((w) => w.includes('clrScheme'))).toBe(true);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for PDF export: markdownDocToPdf, docToPdf.
|
|
3
|
-
*
|
|
4
|
-
* Exports various MarkdownDocuments to PDF, then re-opens the
|
|
5
|
-
* resulting ArrayBuffers with pdf-lib to verify structure/metadata.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect } from 'vitest';
|
|
9
|
-
import { PDFDocument } from 'pdf-lib';
|
|
10
|
-
import type {
|
|
11
|
-
MarkdownDocument,
|
|
12
|
-
MarkdownHeading,
|
|
13
|
-
MarkdownParagraph,
|
|
14
|
-
MarkdownList,
|
|
15
|
-
MarkdownListItem,
|
|
16
|
-
MarkdownTable,
|
|
17
|
-
MarkdownTableRow,
|
|
18
|
-
MarkdownTableCell,
|
|
19
|
-
MarkdownCodeBlock,
|
|
20
|
-
MarkdownBlockquote,
|
|
21
|
-
MarkdownThematicBreak,
|
|
22
|
-
MarkdownText,
|
|
23
|
-
MarkdownStrong,
|
|
24
|
-
MarkdownEmphasis,
|
|
25
|
-
MarkdownInlineCode,
|
|
26
|
-
MarkdownLink,
|
|
27
|
-
} from '@bendyline/squisq/markdown';
|
|
28
|
-
|
|
29
|
-
import { markdownDocToPdf, docToPdf } from '../pdf/export';
|
|
30
|
-
import type { Doc } from '@bendyline/squisq/schemas';
|
|
31
|
-
|
|
32
|
-
// ============================================
|
|
33
|
-
// Helpers
|
|
34
|
-
// ============================================
|
|
35
|
-
|
|
36
|
-
async function exportAndLoad(
|
|
37
|
-
doc: MarkdownDocument,
|
|
38
|
-
options?: Parameters<typeof markdownDocToPdf>[1],
|
|
39
|
-
) {
|
|
40
|
-
const buffer = await markdownDocToPdf(doc, options);
|
|
41
|
-
expect(buffer).toBeInstanceOf(ArrayBuffer);
|
|
42
|
-
expect(buffer.byteLength).toBeGreaterThan(0);
|
|
43
|
-
return PDFDocument.load(buffer);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function text(value: string): MarkdownText {
|
|
47
|
-
return { type: 'text', value };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ============================================
|
|
51
|
-
// Basic Structure
|
|
52
|
-
// ============================================
|
|
53
|
-
|
|
54
|
-
describe('markdownDocToPdf', () => {
|
|
55
|
-
it('produces a valid PDF for an empty document', async () => {
|
|
56
|
-
const doc: MarkdownDocument = { type: 'document', children: [] };
|
|
57
|
-
const pdf = await exportAndLoad(doc);
|
|
58
|
-
expect(pdf.getPageCount()).toBeGreaterThanOrEqual(1);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('sets title and author metadata', async () => {
|
|
62
|
-
const doc: MarkdownDocument = { type: 'document', children: [] };
|
|
63
|
-
const pdf = await exportAndLoad(doc, { title: 'Test Title', author: 'Test Author' });
|
|
64
|
-
expect(pdf.getTitle()).toBe('Test Title');
|
|
65
|
-
expect(pdf.getAuthor()).toBe('Test Author');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('renders a single paragraph', async () => {
|
|
69
|
-
const doc: MarkdownDocument = {
|
|
70
|
-
type: 'document',
|
|
71
|
-
children: [
|
|
72
|
-
{
|
|
73
|
-
type: 'paragraph',
|
|
74
|
-
children: [text('Hello, World!')],
|
|
75
|
-
} as MarkdownParagraph,
|
|
76
|
-
],
|
|
77
|
-
};
|
|
78
|
-
const pdf = await exportAndLoad(doc);
|
|
79
|
-
expect(pdf.getPageCount()).toBe(1);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('renders headings at various depths', async () => {
|
|
83
|
-
const children: MarkdownHeading[] = [1, 2, 3, 4, 5, 6].map((d) => ({
|
|
84
|
-
type: 'heading',
|
|
85
|
-
depth: d as 1 | 2 | 3 | 4 | 5 | 6,
|
|
86
|
-
children: [text(`Heading Level ${d}`)],
|
|
87
|
-
}));
|
|
88
|
-
const doc: MarkdownDocument = { type: 'document', children };
|
|
89
|
-
const pdf = await exportAndLoad(doc);
|
|
90
|
-
expect(pdf.getPageCount()).toBeGreaterThanOrEqual(1);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('renders inline formatting (bold, italic, code)', async () => {
|
|
94
|
-
const doc: MarkdownDocument = {
|
|
95
|
-
type: 'document',
|
|
96
|
-
children: [
|
|
97
|
-
{
|
|
98
|
-
type: 'paragraph',
|
|
99
|
-
children: [
|
|
100
|
-
text('Normal '),
|
|
101
|
-
{ type: 'strong', children: [text('bold')] } as MarkdownStrong,
|
|
102
|
-
text(' '),
|
|
103
|
-
{ type: 'emphasis', children: [text('italic')] } as MarkdownEmphasis,
|
|
104
|
-
text(' '),
|
|
105
|
-
{ type: 'inlineCode', value: 'code' } as MarkdownInlineCode,
|
|
106
|
-
],
|
|
107
|
-
} as MarkdownParagraph,
|
|
108
|
-
],
|
|
109
|
-
};
|
|
110
|
-
// Should not throw
|
|
111
|
-
const pdf = await exportAndLoad(doc);
|
|
112
|
-
expect(pdf.getPageCount()).toBe(1);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('renders a link', async () => {
|
|
116
|
-
const doc: MarkdownDocument = {
|
|
117
|
-
type: 'document',
|
|
118
|
-
children: [
|
|
119
|
-
{
|
|
120
|
-
type: 'paragraph',
|
|
121
|
-
children: [
|
|
122
|
-
text('See '),
|
|
123
|
-
{
|
|
124
|
-
type: 'link',
|
|
125
|
-
url: 'https://example.com',
|
|
126
|
-
children: [text('Example')],
|
|
127
|
-
} as MarkdownLink,
|
|
128
|
-
],
|
|
129
|
-
} as MarkdownParagraph,
|
|
130
|
-
],
|
|
131
|
-
};
|
|
132
|
-
const pdf = await exportAndLoad(doc);
|
|
133
|
-
expect(pdf.getPageCount()).toBe(1);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('renders unordered and ordered lists', async () => {
|
|
137
|
-
const unordered: MarkdownList = {
|
|
138
|
-
type: 'list',
|
|
139
|
-
ordered: false,
|
|
140
|
-
children: [
|
|
141
|
-
{
|
|
142
|
-
type: 'listItem',
|
|
143
|
-
children: [{ type: 'paragraph', children: [text('Apple')] } as MarkdownParagraph],
|
|
144
|
-
} as MarkdownListItem,
|
|
145
|
-
{
|
|
146
|
-
type: 'listItem',
|
|
147
|
-
children: [{ type: 'paragraph', children: [text('Banana')] } as MarkdownParagraph],
|
|
148
|
-
} as MarkdownListItem,
|
|
149
|
-
],
|
|
150
|
-
};
|
|
151
|
-
const ordered: MarkdownList = {
|
|
152
|
-
type: 'list',
|
|
153
|
-
ordered: true,
|
|
154
|
-
children: [
|
|
155
|
-
{
|
|
156
|
-
type: 'listItem',
|
|
157
|
-
children: [{ type: 'paragraph', children: [text('First')] } as MarkdownParagraph],
|
|
158
|
-
} as MarkdownListItem,
|
|
159
|
-
{
|
|
160
|
-
type: 'listItem',
|
|
161
|
-
children: [{ type: 'paragraph', children: [text('Second')] } as MarkdownParagraph],
|
|
162
|
-
} as MarkdownListItem,
|
|
163
|
-
],
|
|
164
|
-
};
|
|
165
|
-
const doc: MarkdownDocument = { type: 'document', children: [unordered, ordered] };
|
|
166
|
-
const pdf = await exportAndLoad(doc);
|
|
167
|
-
expect(pdf.getPageCount()).toBeGreaterThanOrEqual(1);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('renders a code block', async () => {
|
|
171
|
-
const doc: MarkdownDocument = {
|
|
172
|
-
type: 'document',
|
|
173
|
-
children: [
|
|
174
|
-
{
|
|
175
|
-
type: 'code',
|
|
176
|
-
value: 'function hello() {\n return "world";\n}',
|
|
177
|
-
} as MarkdownCodeBlock,
|
|
178
|
-
],
|
|
179
|
-
};
|
|
180
|
-
const pdf = await exportAndLoad(doc);
|
|
181
|
-
expect(pdf.getPageCount()).toBe(1);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('renders a blockquote', async () => {
|
|
185
|
-
const doc: MarkdownDocument = {
|
|
186
|
-
type: 'document',
|
|
187
|
-
children: [
|
|
188
|
-
{
|
|
189
|
-
type: 'blockquote',
|
|
190
|
-
children: [{ type: 'paragraph', children: [text('Quoted text')] } as MarkdownParagraph],
|
|
191
|
-
} as MarkdownBlockquote,
|
|
192
|
-
],
|
|
193
|
-
};
|
|
194
|
-
const pdf = await exportAndLoad(doc);
|
|
195
|
-
expect(pdf.getPageCount()).toBe(1);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('renders a table', async () => {
|
|
199
|
-
const row = (cells: string[], isHeader: boolean): MarkdownTableRow => ({
|
|
200
|
-
type: 'tableRow',
|
|
201
|
-
children: cells.map(
|
|
202
|
-
(c) =>
|
|
203
|
-
({
|
|
204
|
-
type: 'tableCell',
|
|
205
|
-
isHeader,
|
|
206
|
-
children: [text(c)],
|
|
207
|
-
}) as MarkdownTableCell,
|
|
208
|
-
),
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
const doc: MarkdownDocument = {
|
|
212
|
-
type: 'document',
|
|
213
|
-
children: [
|
|
214
|
-
{
|
|
215
|
-
type: 'table',
|
|
216
|
-
children: [
|
|
217
|
-
row(['Name', 'Age', 'City'], true),
|
|
218
|
-
row(['Alice', '30', 'NYC'], false),
|
|
219
|
-
row(['Bob', '25', 'LA'], false),
|
|
220
|
-
],
|
|
221
|
-
} as MarkdownTable,
|
|
222
|
-
],
|
|
223
|
-
};
|
|
224
|
-
const pdf = await exportAndLoad(doc);
|
|
225
|
-
expect(pdf.getPageCount()).toBeGreaterThanOrEqual(1);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('renders a thematic break', async () => {
|
|
229
|
-
const doc: MarkdownDocument = {
|
|
230
|
-
type: 'document',
|
|
231
|
-
children: [
|
|
232
|
-
{ type: 'paragraph', children: [text('Before')] } as MarkdownParagraph,
|
|
233
|
-
{ type: 'thematicBreak' } as MarkdownThematicBreak,
|
|
234
|
-
{ type: 'paragraph', children: [text('After')] } as MarkdownParagraph,
|
|
235
|
-
],
|
|
236
|
-
};
|
|
237
|
-
const pdf = await exportAndLoad(doc);
|
|
238
|
-
expect(pdf.getPageCount()).toBe(1);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('uses A4 page size when specified', async () => {
|
|
242
|
-
const doc: MarkdownDocument = {
|
|
243
|
-
type: 'document',
|
|
244
|
-
children: [{ type: 'paragraph', children: [text('A4 page')] } as MarkdownParagraph],
|
|
245
|
-
};
|
|
246
|
-
const pdf = await exportAndLoad(doc, { pageSize: 'a4' });
|
|
247
|
-
const page = pdf.getPage(0);
|
|
248
|
-
const { width, height } = page.getSize();
|
|
249
|
-
// A4 is approximately 595 × 842
|
|
250
|
-
expect(width).toBeCloseTo(595.28, 0);
|
|
251
|
-
expect(height).toBeCloseTo(841.89, 0);
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('paginates long documents', async () => {
|
|
255
|
-
const children: MarkdownParagraph[] = [];
|
|
256
|
-
// Each paragraph is a few lines — 100 paragraphs should overflow a page
|
|
257
|
-
for (let i = 0; i < 100; i++) {
|
|
258
|
-
children.push({
|
|
259
|
-
type: 'paragraph',
|
|
260
|
-
children: [text(`Paragraph number ${i + 1}. This is some body text to fill the page.`)],
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
const doc: MarkdownDocument = { type: 'document', children };
|
|
264
|
-
const pdf = await exportAndLoad(doc);
|
|
265
|
-
expect(pdf.getPageCount()).toBeGreaterThan(1);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('handles nested lists', async () => {
|
|
269
|
-
const inner: MarkdownList = {
|
|
270
|
-
type: 'list',
|
|
271
|
-
ordered: false,
|
|
272
|
-
children: [
|
|
273
|
-
{
|
|
274
|
-
type: 'listItem',
|
|
275
|
-
children: [{ type: 'paragraph', children: [text('Nested item')] } as MarkdownParagraph],
|
|
276
|
-
} as MarkdownListItem,
|
|
277
|
-
],
|
|
278
|
-
};
|
|
279
|
-
const outer: MarkdownList = {
|
|
280
|
-
type: 'list',
|
|
281
|
-
ordered: false,
|
|
282
|
-
children: [
|
|
283
|
-
{
|
|
284
|
-
type: 'listItem',
|
|
285
|
-
children: [
|
|
286
|
-
{ type: 'paragraph', children: [text('Outer item')] } as MarkdownParagraph,
|
|
287
|
-
inner,
|
|
288
|
-
],
|
|
289
|
-
} as MarkdownListItem,
|
|
290
|
-
],
|
|
291
|
-
};
|
|
292
|
-
const doc: MarkdownDocument = { type: 'document', children: [outer] };
|
|
293
|
-
const pdf = await exportAndLoad(doc);
|
|
294
|
-
expect(pdf.getPageCount()).toBeGreaterThanOrEqual(1);
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// ============================================
|
|
299
|
-
// docToPdf convenience wrapper
|
|
300
|
-
// ============================================
|
|
301
|
-
|
|
302
|
-
describe('docToPdf', () => {
|
|
303
|
-
it('converts a Doc object to PDF', async () => {
|
|
304
|
-
const doc: Doc = {
|
|
305
|
-
articleId: 'test-article',
|
|
306
|
-
duration: 3,
|
|
307
|
-
audio: { segments: [] },
|
|
308
|
-
blocks: [
|
|
309
|
-
{
|
|
310
|
-
id: 'block-1',
|
|
311
|
-
startTime: 0,
|
|
312
|
-
duration: 3,
|
|
313
|
-
audioSegment: 0,
|
|
314
|
-
template: 'title',
|
|
315
|
-
},
|
|
316
|
-
],
|
|
317
|
-
};
|
|
318
|
-
const buffer = await docToPdf(doc);
|
|
319
|
-
expect(buffer).toBeInstanceOf(ArrayBuffer);
|
|
320
|
-
expect(buffer.byteLength).toBeGreaterThan(0);
|
|
321
|
-
});
|
|
322
|
-
});
|