@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,57 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { htmlToMarkdown, htmlToMarkdownDocSync } from '../html/import.js';
|
|
3
|
-
|
|
4
|
-
describe('htmlToMarkdown', () => {
|
|
5
|
-
it('converts headings and paragraphs', () => {
|
|
6
|
-
const md = htmlToMarkdown('<h1>Title</h1><p>Hello <strong>world</strong>.</p>');
|
|
7
|
-
expect(md).toContain('# Title');
|
|
8
|
-
expect(md).toContain('Hello **world**.');
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('converts links and inline code', () => {
|
|
12
|
-
const md = htmlToMarkdown(
|
|
13
|
-
'<p>See <a href="https://x.test">site</a> and <code>npm i</code></p>',
|
|
14
|
-
);
|
|
15
|
-
expect(md).toContain('[site](https://x.test)');
|
|
16
|
-
expect(md).toContain('`npm i`');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('converts unordered and ordered lists', () => {
|
|
20
|
-
const md = htmlToMarkdown('<ul><li>a</li><li>b</li></ul><ol><li>one</li><li>two</li></ol>');
|
|
21
|
-
expect(md).toMatch(/[-*]\s+a/);
|
|
22
|
-
expect(md).toMatch(/[-*]\s+b/);
|
|
23
|
-
expect(md).toMatch(/1\.\s+one/);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('converts blockquotes and tables', () => {
|
|
27
|
-
const md = htmlToMarkdown(
|
|
28
|
-
'<blockquote><p>quoted</p></blockquote>' +
|
|
29
|
-
'<table><thead><tr><th>H</th></tr></thead><tbody><tr><td>cell</td></tr></tbody></table>',
|
|
30
|
-
);
|
|
31
|
-
expect(md).toContain('> quoted');
|
|
32
|
-
expect(md).toContain('cell');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('drops scripts and styles (sanitized by default)', () => {
|
|
36
|
-
const md = htmlToMarkdown('<p>safe</p><script>alert(1)</script><style>.x{color:red}</style>');
|
|
37
|
-
expect(md).toContain('safe');
|
|
38
|
-
expect(md).not.toContain('alert(1)');
|
|
39
|
-
expect(md).not.toContain('color:red');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('strips javascript: links via sanitizer', () => {
|
|
43
|
-
const md = htmlToMarkdown('<a href="javascript:alert(1)">click</a>');
|
|
44
|
-
expect(md).not.toContain('javascript:alert(1)');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('builds a document node tree', () => {
|
|
48
|
-
const doc = htmlToMarkdownDocSync('<h2>Hi</h2>');
|
|
49
|
-
expect(doc.type).toBe('document');
|
|
50
|
-
expect(doc.children[0]?.type).toBe('heading');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('handles unwrapped div/span containers', () => {
|
|
54
|
-
const md = htmlToMarkdown('<div><span>plain </span><em>text</em></div>');
|
|
55
|
-
expect(md).toContain('plain *text*');
|
|
56
|
-
});
|
|
57
|
-
});
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `inferThemeFromFile`: format sniffing, per-format extraction
|
|
3
|
-
* (incl. dark-deck clrMap inversion), theme compilation, and error paths.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, expect, it } from 'vitest';
|
|
7
|
-
import { ConversionError } from '../registry/errors';
|
|
8
|
-
import { inferThemeFromFile } from '../infer/index';
|
|
9
|
-
import {
|
|
10
|
-
buildThemeXml,
|
|
11
|
-
buildThemedDocx,
|
|
12
|
-
buildThemedPptx,
|
|
13
|
-
buildThemedXlsx,
|
|
14
|
-
} from './pptxInferFixtures';
|
|
15
|
-
|
|
16
|
-
describe('inferThemeFromFile — PPTX', () => {
|
|
17
|
-
it('maps a light deck onto seeds, palette, schemes, and fonts', async () => {
|
|
18
|
-
const data = await buildThemedPptx();
|
|
19
|
-
const result = await inferThemeFromFile(data);
|
|
20
|
-
|
|
21
|
-
expect(result.extraction.sourceFormat).toBe('pptx');
|
|
22
|
-
expect(result.extraction.colorMap).toEqual({ bg1: 'lt1', tx1: 'dk1' });
|
|
23
|
-
|
|
24
|
-
const { theme } = result;
|
|
25
|
-
expect(theme.seedColors).toMatchObject({
|
|
26
|
-
primary: '#4472c4',
|
|
27
|
-
secondary: '#ed7d31',
|
|
28
|
-
accent: '#a5a5a5',
|
|
29
|
-
background: '#fdfdf8',
|
|
30
|
-
text: '#1a1a2e',
|
|
31
|
-
});
|
|
32
|
-
expect(theme.colors.background).toBe('#fdfdf8');
|
|
33
|
-
expect(theme.colors.text).toBe('#1a1a2e');
|
|
34
|
-
// Companion slot (lt2) becomes backgroundLight; hlink becomes highlight.
|
|
35
|
-
expect(theme.colors.backgroundLight).toBe('#efefe4');
|
|
36
|
-
expect(theme.colors.highlight).toBe('#0563c1');
|
|
37
|
-
// Six accents → six color schemes keyed by slot.
|
|
38
|
-
expect(Object.keys(theme.colorSchemes)).toEqual([
|
|
39
|
-
'accent1',
|
|
40
|
-
'accent2',
|
|
41
|
-
'accent3',
|
|
42
|
-
'accent4',
|
|
43
|
-
'accent5',
|
|
44
|
-
'accent6',
|
|
45
|
-
]);
|
|
46
|
-
// Fonts: curated stack match + custom preservation.
|
|
47
|
-
expect(theme.typography.titleFont).toEqual({ stackId: 'playfair' });
|
|
48
|
-
expect(theme.typography.bodyFont).toEqual({
|
|
49
|
-
custom: { name: 'Aptos', fallback: 'sans-serif' },
|
|
50
|
-
});
|
|
51
|
-
// Compiled + validated, named from the theme part, re-editable via seeds.
|
|
52
|
-
expect(theme.name).toBe('Fixture Theme');
|
|
53
|
-
expect(theme.id).toBe('custom-fixture-theme');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('inverts background/text for a dark deck (clrMap bg1="dk1")', async () => {
|
|
57
|
-
const data = await buildThemedPptx({
|
|
58
|
-
clrMapAttrs: 'bg1="dk1" tx1="lt1" bg2="dk2" tx2="lt2"',
|
|
59
|
-
});
|
|
60
|
-
const result = await inferThemeFromFile(data);
|
|
61
|
-
expect(result.theme.seedColors?.background).toBe('#1a1a2e');
|
|
62
|
-
expect(result.theme.seedColors?.text).toBe('#fdfdf8');
|
|
63
|
-
// Dark companion slot backs backgroundLight.
|
|
64
|
-
expect(result.theme.colors.backgroundLight).toBe('#30304a');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('respects a nameHint over the file theme name', async () => {
|
|
68
|
-
const result = await inferThemeFromFile(await buildThemedPptx(), { nameHint: 'Q3 Deck' });
|
|
69
|
-
expect(result.theme.name).toBe('Q3 Deck');
|
|
70
|
-
expect(result.theme.id).toBe('custom-q3-deck');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('throws invalid-input when the file has no theme part', async () => {
|
|
74
|
-
const data = await buildThemedPptx({ themeXml: null });
|
|
75
|
-
await expect(inferThemeFromFile(data)).rejects.toMatchObject({
|
|
76
|
-
name: 'ConversionError',
|
|
77
|
-
code: 'invalid-input',
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe('inferThemeFromFile — DOCX / XLSX', () => {
|
|
83
|
-
it('extracts from a DOCX theme part', async () => {
|
|
84
|
-
const result = await inferThemeFromFile(await buildThemedDocx());
|
|
85
|
-
expect(result.extraction.sourceFormat).toBe('docx');
|
|
86
|
-
expect(result.theme.seedColors?.primary).toBe('#4472c4');
|
|
87
|
-
expect(result.theme.colors.background).toBe('#fdfdf8');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('extracts from an XLSX theme part', async () => {
|
|
91
|
-
const result = await inferThemeFromFile(await buildThemedXlsx());
|
|
92
|
-
expect(result.extraction.sourceFormat).toBe('xlsx');
|
|
93
|
-
expect(result.theme.seedColors?.primary).toBe('#4472c4');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('ignores inferLayouts for non-PPTX sources with a warning', async () => {
|
|
97
|
-
const result = await inferThemeFromFile(await buildThemedDocx(), { inferLayouts: true });
|
|
98
|
-
expect(result.layouts).toBeUndefined();
|
|
99
|
-
expect(result.warnings.some((w) => w.includes('PowerPoint'))).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('inferThemeFromFile — rejection paths', () => {
|
|
104
|
-
it('rejects PDF bytes with unsupported-input', async () => {
|
|
105
|
-
const pdf = new TextEncoder().encode('%PDF-1.4 fake').buffer as ArrayBuffer;
|
|
106
|
-
await expect(inferThemeFromFile(pdf)).rejects.toMatchObject({
|
|
107
|
-
code: 'unsupported-input',
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('rejects non-zip bytes with invalid-input', async () => {
|
|
112
|
-
const junk = new TextEncoder().encode('definitely not a zip').buffer as ArrayBuffer;
|
|
113
|
-
const err = await inferThemeFromFile(junk).catch((e: unknown) => e);
|
|
114
|
-
expect(err).toBeInstanceOf(ConversionError);
|
|
115
|
-
expect((err as ConversionError).code).toBe('invalid-input');
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('inferThemeFromFile — degraded themes', () => {
|
|
120
|
-
it('drops near-background accents from the scheme list with a warning', async () => {
|
|
121
|
-
// accent2 equals the background color — should be dropped from schemes.
|
|
122
|
-
const themeXml = buildThemeXml({ accents: ['4472c4', 'fdfdf8', 'a5a5a5'] });
|
|
123
|
-
const result = await inferThemeFromFile(await buildThemedPptx({ themeXml }));
|
|
124
|
-
expect(Object.keys(result.theme.colorSchemes)).toEqual(['accent1', 'accent3']);
|
|
125
|
-
expect(result.warnings.some((w) => w.includes('accent2'))).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('keeps fonts inherited when the theme has no fontScheme', async () => {
|
|
129
|
-
const themeXml = buildThemeXml({ omitFontScheme: true });
|
|
130
|
-
const result = await inferThemeFromFile(await buildThemedPptx({ themeXml }));
|
|
131
|
-
// STARTER defaults survive.
|
|
132
|
-
expect(result.theme.typography.titleFont).toEqual({ stackId: 'system-serif' });
|
|
133
|
-
expect(result.warnings.some((w) => w.includes('fontScheme'))).toBe(true);
|
|
134
|
-
});
|
|
135
|
-
});
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the registry's consolidated lossy-path warnings, all surfaced
|
|
3
|
-
* through `ConversionResult.warnings`:
|
|
4
|
-
* - xlsx export drops non-table content (tables-only fidelity)
|
|
5
|
-
* - pdf import under Node skips embedded images (no DOM canvas)
|
|
6
|
-
* - transform → markdown produces blocks that don't round-trip
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
10
|
-
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
11
|
-
import type { MarkdownDocument } from '@bendyline/squisq/markdown';
|
|
12
|
-
import { convert, defaultRegistry } from '../registry/index';
|
|
13
|
-
|
|
14
|
-
// ── shared Markdown exporter capability profiles ───────────────────
|
|
15
|
-
|
|
16
|
-
describe('Markdown exporters warn about unsupported AST extensions', () => {
|
|
17
|
-
const extendedMarkdown: MarkdownDocument = {
|
|
18
|
-
type: 'document',
|
|
19
|
-
children: [
|
|
20
|
-
{
|
|
21
|
-
type: 'paragraph',
|
|
22
|
-
children: [
|
|
23
|
-
{ type: 'text', value: 'Assigned to ' },
|
|
24
|
-
{
|
|
25
|
-
type: 'mention',
|
|
26
|
-
targetKind: 'user',
|
|
27
|
-
targetId: '42',
|
|
28
|
-
displayName: 'Ada',
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
type: 'containerDirective',
|
|
34
|
-
name: 'note',
|
|
35
|
-
children: [{ type: 'paragraph', children: [{ type: 'text', value: 'Important' }] }],
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
it('surfaces the omitted node types through ConversionResult', async () => {
|
|
41
|
-
const result = await convert({ kind: 'markdown', markdown: extendedMarkdown }, 'docx');
|
|
42
|
-
const warning = result.warnings.find((item) => /unsupported Markdown node/i.test(item));
|
|
43
|
-
|
|
44
|
-
expect(warning).toMatch(/mention \(1\)/);
|
|
45
|
-
expect(warning).toMatch(/containerDirective \(1\)/);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('does not warn for the supported Markdown subset', async () => {
|
|
49
|
-
const result = await convert({ kind: 'markdown', markdown: '# Title\n\nBody.' }, 'docx');
|
|
50
|
-
expect(result.warnings.some((item) => /unsupported Markdown node/i.test(item))).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// ── xlsx: tables-only fidelity ──────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
describe('xlsx export warns about omitted non-table content', () => {
|
|
57
|
-
it('warns when prose / lists accompany the table(s)', async () => {
|
|
58
|
-
const md =
|
|
59
|
-
'# Report\n\nIntro prose paragraph.\n\n- a\n- b\n\n| A | B |\n| - | - |\n| 1 | 2 |\n';
|
|
60
|
-
const result = await convert({ kind: 'markdown', markdown: md }, 'xlsx');
|
|
61
|
-
expect(result.warnings.some((w) => /tables-only/i.test(w))).toBe(true);
|
|
62
|
-
// Two omitted blocks: the paragraph and the list (heading names the sheet).
|
|
63
|
-
expect(result.warnings.find((w) => /tables-only/i.test(w))).toMatch(/2 non-table block/);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('does not warn for a heading + table only document', async () => {
|
|
67
|
-
const md = '# Sales\n\n| A |\n| - |\n| x |\n';
|
|
68
|
-
const result = await convert({ kind: 'markdown', markdown: md }, 'xlsx');
|
|
69
|
-
expect(result.warnings.some((w) => /tables-only/i.test(w))).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// ── pdf: Node has no DOM canvas → images skipped ────────────────────
|
|
74
|
-
|
|
75
|
-
describe('pdf import warns when running without a DOM (Node)', () => {
|
|
76
|
-
afterEach(() => {
|
|
77
|
-
vi.unstubAllGlobals();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
/** A pdf definition whose importContainer is stubbed (avoids pdfjs/canvas). */
|
|
81
|
-
function registryWithStubPdf() {
|
|
82
|
-
const registry = defaultRegistry();
|
|
83
|
-
registry.register({
|
|
84
|
-
...registry.get('pdf')!,
|
|
85
|
-
async importContainer() {
|
|
86
|
-
const c = new MemoryContentContainer();
|
|
87
|
-
await c.writeDocument('# From PDF\n\nBody.');
|
|
88
|
-
return c;
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
return registry;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const pdfBytes = new TextEncoder().encode('%PDF-1.4\n%mock\n');
|
|
95
|
-
|
|
96
|
-
it('surfaces the skipped-images warning when document is undefined', async () => {
|
|
97
|
-
vi.stubGlobal('document', undefined);
|
|
98
|
-
const registry = registryWithStubPdf();
|
|
99
|
-
const result = await convert({ kind: 'bytes', data: pdfBytes, filename: 'x.pdf' }, 'md', {
|
|
100
|
-
from: 'pdf',
|
|
101
|
-
registry,
|
|
102
|
-
});
|
|
103
|
-
expect(result.warnings.some((w) => /embedded images were skipped/i.test(w))).toBe(true);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('does not warn when a DOM is present (jsdom default)', async () => {
|
|
107
|
-
const registry = registryWithStubPdf();
|
|
108
|
-
const result = await convert({ kind: 'bytes', data: pdfBytes, filename: 'x.pdf' }, 'md', {
|
|
109
|
-
from: 'pdf',
|
|
110
|
-
registry,
|
|
111
|
-
});
|
|
112
|
-
expect(result.warnings.some((w) => /embedded images were skipped/i.test(w))).toBe(false);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// ── transform → markdown round-trip loss ────────────────────────────
|
|
117
|
-
|
|
118
|
-
describe('transform-to-markdown export warns about non-round-tripping blocks', () => {
|
|
119
|
-
const CONTENT_MD = `# Big Title
|
|
120
|
-
|
|
121
|
-
An intro paragraph with plenty of words so the transform has real content.
|
|
122
|
-
|
|
123
|
-
## Numbers
|
|
124
|
-
|
|
125
|
-
Revenue grew by 45% last quarter across every region and product line worldwide.
|
|
126
|
-
|
|
127
|
-
- Point one with several words to analyze here
|
|
128
|
-
- Point two with several words to analyze here
|
|
129
|
-
|
|
130
|
-
| Metric | Value |
|
|
131
|
-
| - | - |
|
|
132
|
-
| Users | 1000 |
|
|
133
|
-
`;
|
|
134
|
-
|
|
135
|
-
it('warns when a transform reshapes into template blocks that markdown can’t hold', async () => {
|
|
136
|
-
const result = await convert({ kind: 'markdown', markdown: CONTENT_MD }, 'md', {
|
|
137
|
-
transformStyle: 'magazine',
|
|
138
|
-
});
|
|
139
|
-
expect(result.warnings.some((w) => /round-trip to markdown/i.test(w))).toBe(true);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('does not warn without a transform', async () => {
|
|
143
|
-
const result = await convert({ kind: 'markdown', markdown: CONTENT_MD }, 'md');
|
|
144
|
-
expect(result.warnings.some((w) => /round-trip to markdown/i.test(w))).toBe(false);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the shared OOXML layer: xmlUtils, writer, reader round-trips.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { afterEach, describe, it, expect, vi } from 'vitest';
|
|
6
|
-
import {
|
|
7
|
-
escapeXml,
|
|
8
|
-
xmlElement,
|
|
9
|
-
selfClosingElement,
|
|
10
|
-
textElement,
|
|
11
|
-
attrString,
|
|
12
|
-
xmlDeclaration,
|
|
13
|
-
} from '../ooxml/xmlUtils';
|
|
14
|
-
import { createPackage } from '../ooxml/writer';
|
|
15
|
-
import { openPackage, getPartXml, getCoreProperties, getPartRelationships } from '../ooxml/reader';
|
|
16
|
-
import { REL_OFFICE_DOCUMENT } from '../ooxml/namespaces';
|
|
17
|
-
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
vi.unstubAllGlobals();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// ============================================
|
|
23
|
-
// XML Utilities
|
|
24
|
-
// ============================================
|
|
25
|
-
|
|
26
|
-
describe('escapeXml', () => {
|
|
27
|
-
it('escapes all five XML entities', () => {
|
|
28
|
-
expect(escapeXml('a & b < c > d " e \' f')).toBe('a & b < c > d " e ' f');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('returns empty string unchanged', () => {
|
|
32
|
-
expect(escapeXml('')).toBe('');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('returns plain text unchanged', () => {
|
|
36
|
-
expect(escapeXml('hello world')).toBe('hello world');
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('attrString', () => {
|
|
41
|
-
it('builds attribute string from object', () => {
|
|
42
|
-
const result = attrString({ 'w:val': 'Heading1' });
|
|
43
|
-
expect(result).toBe(' w:val="Heading1"');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('omits undefined values', () => {
|
|
47
|
-
const result = attrString({ 'w:val': 'test', 'w:other': undefined });
|
|
48
|
-
expect(result).toBe(' w:val="test"');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('returns empty string for empty attrs', () => {
|
|
52
|
-
expect(attrString({})).toBe('');
|
|
53
|
-
expect(attrString(undefined)).toBe('');
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('selfClosingElement', () => {
|
|
58
|
-
it('builds self-closing tag', () => {
|
|
59
|
-
expect(selfClosingElement('w:b')).toBe('<w:b/>');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('includes attributes', () => {
|
|
63
|
-
expect(selfClosingElement('w:pStyle', { 'w:val': 'Heading1' })).toBe(
|
|
64
|
-
'<w:pStyle w:val="Heading1"/>',
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('xmlElement', () => {
|
|
70
|
-
it('builds element with children', () => {
|
|
71
|
-
expect(xmlElement('w:p', {}, 'content')).toBe('<w:p>content</w:p>');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('self-closes when no children', () => {
|
|
75
|
-
expect(xmlElement('w:p', {})).toBe('<w:p/>');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('concatenates multiple children', () => {
|
|
79
|
-
expect(xmlElement('w:p', {}, 'a', 'b')).toBe('<w:p>ab</w:p>');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('textElement', () => {
|
|
84
|
-
it('escapes text content', () => {
|
|
85
|
-
expect(textElement('w:t', {}, 'a & b')).toBe('<w:t>a & b</w:t>');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('self-closes when no text', () => {
|
|
89
|
-
expect(textElement('w:t', {})).toBe('<w:t/>');
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
describe('xmlDeclaration', () => {
|
|
94
|
-
it('returns standard XML declaration', () => {
|
|
95
|
-
expect(xmlDeclaration()).toBe('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>');
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// ============================================
|
|
100
|
-
// Package Writer + Reader Round-Trip
|
|
101
|
-
// ============================================
|
|
102
|
-
|
|
103
|
-
describe('createPackage / openPackage round-trip', () => {
|
|
104
|
-
it('rejects unsafe authored part paths', () => {
|
|
105
|
-
const pkg = createPackage();
|
|
106
|
-
expect(() => pkg.addPart('../outside.xml', '<doc/>', 'application/xml')).toThrow(
|
|
107
|
-
/path is unsafe/i,
|
|
108
|
-
);
|
|
109
|
-
expect(() =>
|
|
110
|
-
pkg.addRelationship('..\\outside.xml', {
|
|
111
|
-
id: 'rId1',
|
|
112
|
-
type: REL_OFFICE_DOCUMENT,
|
|
113
|
-
target: 'word/document.xml',
|
|
114
|
-
}),
|
|
115
|
-
).toThrow(/path is unsafe/i);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('parses packages when the browser DOMParser global is unavailable', async () => {
|
|
119
|
-
const pkg = createPackage();
|
|
120
|
-
pkg.addPart('word/document.xml', '<doc><value>node</value></doc>', 'application/xml');
|
|
121
|
-
const buffer = await pkg.toArrayBuffer();
|
|
122
|
-
|
|
123
|
-
vi.stubGlobal('DOMParser', undefined);
|
|
124
|
-
const opened = await openPackage(buffer);
|
|
125
|
-
const doc = await getPartXml(opened, 'word/document.xml');
|
|
126
|
-
expect(doc?.getElementsByTagName('value')[0]?.textContent).toBe('node');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('ignores an incomplete ambient DOMParser when running in Node', async () => {
|
|
130
|
-
const pkg = createPackage();
|
|
131
|
-
pkg.addPart('word/document.xml', '<doc><value>node</value></doc>', 'application/xml');
|
|
132
|
-
const buffer = await pkg.toArrayBuffer();
|
|
133
|
-
|
|
134
|
-
class IncompleteDomParser {
|
|
135
|
-
parseFromString(): never {
|
|
136
|
-
throw new Error('ambient parser must not be used in Node');
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
vi.stubGlobal('DOMParser', IncompleteDomParser);
|
|
141
|
-
const opened = await openPackage(buffer);
|
|
142
|
-
const doc = await getPartXml(opened, 'word/document.xml');
|
|
143
|
-
expect(doc?.getElementsByTagName('value')[0]?.textContent).toBe('node');
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('applies archive resource limits before parsing OOXML parts', async () => {
|
|
147
|
-
const pkg = createPackage();
|
|
148
|
-
pkg.addPart('word/document.xml', '<doc/>', 'application/xml');
|
|
149
|
-
const buffer = await pkg.toArrayBuffer();
|
|
150
|
-
|
|
151
|
-
await expect(openPackage(buffer, { maxEntries: 1 })).rejects.toMatchObject({
|
|
152
|
-
code: 'too-many-entries',
|
|
153
|
-
actual: 3,
|
|
154
|
-
});
|
|
155
|
-
await expect(openPackage(buffer, { maxUncompressedBytes: 1 })).rejects.toThrow(
|
|
156
|
-
/uncompressed content exceeds 1 byte limit/,
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('creates a valid ZIP with content types and parts', async () => {
|
|
161
|
-
const pkg = createPackage();
|
|
162
|
-
pkg.addPart(
|
|
163
|
-
'word/document.xml',
|
|
164
|
-
'<w:document><w:body/></w:document>',
|
|
165
|
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml',
|
|
166
|
-
);
|
|
167
|
-
pkg.addRelationship('', {
|
|
168
|
-
id: 'rId1',
|
|
169
|
-
type: REL_OFFICE_DOCUMENT,
|
|
170
|
-
target: 'word/document.xml',
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const arrayBuffer = await pkg.toArrayBuffer();
|
|
174
|
-
expect(arrayBuffer).toBeInstanceOf(ArrayBuffer);
|
|
175
|
-
expect(arrayBuffer.byteLength).toBeGreaterThan(0);
|
|
176
|
-
|
|
177
|
-
// Read it back
|
|
178
|
-
const opened = await openPackage(arrayBuffer);
|
|
179
|
-
expect(opened).not.toHaveProperty('zip');
|
|
180
|
-
expect(opened.contentTypes.overrides.has('word/document.xml')).toBe(true);
|
|
181
|
-
expect(opened.rootRelationships.length).toBeGreaterThanOrEqual(1);
|
|
182
|
-
expect(opened.rootRelationships[0].type).toBe(REL_OFFICE_DOCUMENT);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('round-trips XML part content', async () => {
|
|
186
|
-
const pkg = createPackage();
|
|
187
|
-
const testXml =
|
|
188
|
-
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><root><child attr="value">text</child></root>';
|
|
189
|
-
pkg.addPart('test/data.xml', testXml, 'application/xml');
|
|
190
|
-
|
|
191
|
-
const buffer = await pkg.toArrayBuffer();
|
|
192
|
-
const opened = await openPackage(buffer);
|
|
193
|
-
const doc = await getPartXml(opened, 'test/data.xml');
|
|
194
|
-
|
|
195
|
-
expect(doc).not.toBeNull();
|
|
196
|
-
const child = doc!.getElementsByTagName('child')[0];
|
|
197
|
-
expect(child.getAttribute('attr')).toBe('value');
|
|
198
|
-
expect(child.textContent).toBe('text');
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('round-trips core properties', async () => {
|
|
202
|
-
const pkg = createPackage();
|
|
203
|
-
pkg.addPart('word/document.xml', '<doc/>', 'application/xml');
|
|
204
|
-
pkg.setCoreProperties({
|
|
205
|
-
title: 'Test Title',
|
|
206
|
-
creator: 'Test Author',
|
|
207
|
-
description: 'Test description with <special> & chars',
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
const buffer = await pkg.toArrayBuffer();
|
|
211
|
-
const opened = await openPackage(buffer);
|
|
212
|
-
const props = await getCoreProperties(opened);
|
|
213
|
-
|
|
214
|
-
expect(props.title).toBe('Test Title');
|
|
215
|
-
expect(props.creator).toBe('Test Author');
|
|
216
|
-
expect(props.description).toBe('Test description with <special> & chars');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('serializes repeatedly without accumulating generated relationships', async () => {
|
|
220
|
-
const pkg = createPackage();
|
|
221
|
-
pkg.addPart('word/document.xml', '<doc/>', 'application/xml');
|
|
222
|
-
pkg.setCoreProperties({ title: 'Stable' });
|
|
223
|
-
|
|
224
|
-
await pkg.toArrayBuffer();
|
|
225
|
-
const second = await pkg.toArrayBuffer();
|
|
226
|
-
const opened = await openPackage(second);
|
|
227
|
-
const coreRelationships = opened.rootRelationships.filter((rel) =>
|
|
228
|
-
rel.type.endsWith('/metadata/core-properties'),
|
|
229
|
-
);
|
|
230
|
-
expect(coreRelationships).toHaveLength(1);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('round-trips part relationships', async () => {
|
|
234
|
-
const pkg = createPackage();
|
|
235
|
-
pkg.addPart('word/document.xml', '<doc/>', 'application/xml');
|
|
236
|
-
pkg.addRelationship('word/document.xml', {
|
|
237
|
-
id: 'rId1',
|
|
238
|
-
type: 'http://example.com/styles',
|
|
239
|
-
target: 'styles.xml',
|
|
240
|
-
});
|
|
241
|
-
pkg.addRelationship('word/document.xml', {
|
|
242
|
-
id: 'rId2',
|
|
243
|
-
type: 'http://example.com/hyperlink',
|
|
244
|
-
target: 'https://example.com',
|
|
245
|
-
targetMode: 'External',
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const buffer = await pkg.toArrayBuffer();
|
|
249
|
-
const opened = await openPackage(buffer);
|
|
250
|
-
const rels = await getPartRelationships(opened, 'word/document.xml');
|
|
251
|
-
|
|
252
|
-
expect(rels.length).toBe(2);
|
|
253
|
-
expect(rels[0].id).toBe('rId1');
|
|
254
|
-
expect(rels[0].target).toBe('styles.xml');
|
|
255
|
-
expect(rels[1].id).toBe('rId2');
|
|
256
|
-
expect(rels[1].target).toBe('https://example.com');
|
|
257
|
-
expect(rels[1].targetMode).toBe('External');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('handles binary parts', async () => {
|
|
261
|
-
const pkg = createPackage();
|
|
262
|
-
const testData = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); // PNG magic bytes
|
|
263
|
-
pkg.addBinaryPart('word/media/image1.png', testData, 'image/png');
|
|
264
|
-
|
|
265
|
-
const buffer = await pkg.toArrayBuffer();
|
|
266
|
-
const opened = await openPackage(buffer);
|
|
267
|
-
|
|
268
|
-
// Binary part should appear in content types via extension default
|
|
269
|
-
expect(opened.contentTypes.defaults.has('png')).toBe(true);
|
|
270
|
-
});
|
|
271
|
-
});
|