@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,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Export theme reconciliation.
|
|
3
|
-
*
|
|
4
|
-
* Every export format must honor the theme the document selects in its
|
|
5
|
-
* frontmatter (`squisq-theme`) — including an inline custom theme stored in
|
|
6
|
-
* `squisq-custom-themes` — without an explicit `themeId` option. This guards
|
|
7
|
-
* the fix that made PDF/EPUB read the frontmatter theme like PPTX/DOCX/HTML,
|
|
8
|
-
* and that custom themes resolve doc-scoped in exports (no global register).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it, expect } from 'vitest';
|
|
12
|
-
import JSZip from 'jszip';
|
|
13
|
-
import { compileTheme } from '@bendyline/squisq/schemas';
|
|
14
|
-
import { writeCustomThemesToFrontmatter } from '@bendyline/squisq/doc';
|
|
15
|
-
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
16
|
-
import { markdownDocToPptx } from '../pptx/export';
|
|
17
|
-
import { markdownDocToDocx } from '../docx/export';
|
|
18
|
-
import { markdownDocToPdf } from '../pdf/export';
|
|
19
|
-
import { markdownDocToEpub } from '../epub/export';
|
|
20
|
-
import { markdownDocToPlainHtml } from '../html/plainHtml';
|
|
21
|
-
|
|
22
|
-
const brand = compileTheme({
|
|
23
|
-
id: 'custom-brand',
|
|
24
|
-
name: 'Brand',
|
|
25
|
-
seedColors: { primary: '#ff0088', background: '#123456', text: '#abcdef' },
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const withTheme = parseMarkdown(`---
|
|
29
|
-
title: Audit
|
|
30
|
-
squisq-theme: custom-brand
|
|
31
|
-
squisq-custom-themes: ${writeCustomThemesToFrontmatter([brand])}
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
# Heading One
|
|
35
|
-
|
|
36
|
-
Body with a [link](https://example.com).
|
|
37
|
-
`);
|
|
38
|
-
|
|
39
|
-
const noTheme = parseMarkdown('# Heading One\n\nBody.\n');
|
|
40
|
-
|
|
41
|
-
/** Concatenate the text of every XML/CSS/HTML entry in a ZIP export. */
|
|
42
|
-
async function zipText(buf: ArrayBuffer): Promise<string> {
|
|
43
|
-
const zip = await JSZip.loadAsync(buf);
|
|
44
|
-
let text = '';
|
|
45
|
-
for (const name of Object.keys(zip.files)) {
|
|
46
|
-
if (/\.(xml|css|html|xhtml|rels)$/i.test(name)) {
|
|
47
|
-
text += await zip.files[name].async('string');
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return text.toLowerCase();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe('export theme reconciliation (frontmatter-only custom theme)', () => {
|
|
54
|
-
it('HTML applies the frontmatter custom theme colors', () => {
|
|
55
|
-
const html = markdownDocToPlainHtml(withTheme, {}).toLowerCase();
|
|
56
|
-
expect(html).toContain('123456'); // background
|
|
57
|
-
expect(html).toContain('abcdef'); // text
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('PPTX applies the frontmatter custom theme colors', async () => {
|
|
61
|
-
const text = await zipText(await markdownDocToPptx(withTheme, {}));
|
|
62
|
-
expect(text).toContain('123456');
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('DOCX applies the frontmatter custom theme colors', async () => {
|
|
66
|
-
const text = await zipText(await markdownDocToDocx(withTheme, {}));
|
|
67
|
-
expect(text).toContain('123456');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('EPUB applies the frontmatter custom theme colors (frontmatter fallback)', async () => {
|
|
71
|
-
const text = await zipText(await markdownDocToEpub(withTheme, {}));
|
|
72
|
-
expect(text).toContain('123456');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('PDF themes from frontmatter — themed output differs from un-themed', async () => {
|
|
76
|
-
const themed = new Uint8Array(await markdownDocToPdf(withTheme, {}));
|
|
77
|
-
const plain = new Uint8Array(await markdownDocToPdf(noTheme, {}));
|
|
78
|
-
const differs = themed.length !== plain.length || !themed.every((b, i) => b === plain[i]);
|
|
79
|
-
expect(differs).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('an explicit themeId option still overrides the frontmatter theme', async () => {
|
|
83
|
-
const bare = await zipText(await markdownDocToPptx(withTheme, { themeId: 'standard' }));
|
|
84
|
-
// The custom background must NOT appear when the caller forces 'standard'.
|
|
85
|
-
expect(bare).not.toContain('123456');
|
|
86
|
-
});
|
|
87
|
-
});
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the format registry: createRegistry / defaultRegistry, capability
|
|
3
|
-
* consistency across every built-in, and the structured error codes.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect } from 'vitest';
|
|
7
|
-
import { parseMarkdown } from '@bendyline/squisq/markdown';
|
|
8
|
-
import { markdownToDoc } from '@bendyline/squisq/doc';
|
|
9
|
-
import { MemoryContentContainer } from '@bendyline/squisq/storage';
|
|
10
|
-
import {
|
|
11
|
-
createRegistry,
|
|
12
|
-
defaultRegistry,
|
|
13
|
-
defaultFormats,
|
|
14
|
-
ConversionError,
|
|
15
|
-
BUILTIN_FORMAT_IDS,
|
|
16
|
-
} from '../registry/index';
|
|
17
|
-
import type { FormatDefinition, NormalizedInput } from '../registry/index';
|
|
18
|
-
|
|
19
|
-
const SAMPLE_MD = `# Title
|
|
20
|
-
|
|
21
|
-
Some intro text.
|
|
22
|
-
|
|
23
|
-
| Name | Value |
|
|
24
|
-
| --- | --- |
|
|
25
|
-
| A | 1 |
|
|
26
|
-
| B | 2 |
|
|
27
|
-
`;
|
|
28
|
-
|
|
29
|
-
async function makeInput(baseName = 'sample'): Promise<NormalizedInput> {
|
|
30
|
-
const markdownDoc = parseMarkdown(SAMPLE_MD);
|
|
31
|
-
const doc = markdownToDoc(markdownDoc);
|
|
32
|
-
const container = new MemoryContentContainer();
|
|
33
|
-
await container.writeDocument(SAMPLE_MD);
|
|
34
|
-
return { doc, markdownDoc, container, baseName };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const stubDef = (id: string, extensions: string[]): FormatDefinition => ({
|
|
38
|
-
id,
|
|
39
|
-
label: id.toUpperCase(),
|
|
40
|
-
mimeType: 'application/octet-stream',
|
|
41
|
-
extensions,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// ── Registry mechanics ──────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
describe('createRegistry', () => {
|
|
47
|
-
it('registers and gets definitions by id', () => {
|
|
48
|
-
const reg = createRegistry();
|
|
49
|
-
reg.register(stubDef('foo', ['.foo']));
|
|
50
|
-
expect(reg.get('foo')?.id).toBe('foo');
|
|
51
|
-
expect(reg.get('missing')).toBeUndefined();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('resolves byExtension, normalizing leading dot + case', () => {
|
|
55
|
-
const reg = createRegistry();
|
|
56
|
-
reg.register(stubDef('foo', ['.foo', '.f']));
|
|
57
|
-
expect(reg.byExtension('foo')?.id).toBe('foo');
|
|
58
|
-
expect(reg.byExtension('.FOO')?.id).toBe('foo');
|
|
59
|
-
expect(reg.byExtension('f')?.id).toBe('foo');
|
|
60
|
-
expect(reg.byExtension('nope')).toBeUndefined();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('is last-write-wins by id', () => {
|
|
64
|
-
const reg = createRegistry();
|
|
65
|
-
reg.register(stubDef('foo', ['.foo']));
|
|
66
|
-
reg.register({ ...stubDef('foo', ['.foo']), label: 'SECOND' });
|
|
67
|
-
expect(reg.get('foo')?.label).toBe('SECOND');
|
|
68
|
-
expect(reg.list().filter((d) => d.id === 'foo')).toHaveLength(1);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('lists all registered definitions', () => {
|
|
72
|
-
const reg = createRegistry();
|
|
73
|
-
reg.register(stubDef('a', ['.a']));
|
|
74
|
-
reg.register(stubDef('b', ['.b']));
|
|
75
|
-
expect(
|
|
76
|
-
reg
|
|
77
|
-
.list()
|
|
78
|
-
.map((d) => d.id)
|
|
79
|
-
.sort(),
|
|
80
|
-
).toEqual(['a', 'b']);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('defaultRegistry', () => {
|
|
85
|
-
it('registers all built-in formats', () => {
|
|
86
|
-
const reg = defaultRegistry();
|
|
87
|
-
for (const id of BUILTIN_FORMAT_IDS) {
|
|
88
|
-
expect(reg.get(id), `built-in "${id}" should be registered`).toBeDefined();
|
|
89
|
-
}
|
|
90
|
-
expect(reg.list()).toHaveLength(BUILTIN_FORMAT_IDS.length);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('defaultFormats matches BUILTIN_FORMAT_IDS exactly', () => {
|
|
94
|
-
const ids = defaultFormats()
|
|
95
|
-
.map((d) => d.id)
|
|
96
|
-
.sort();
|
|
97
|
-
expect(ids).toEqual([...BUILTIN_FORMAT_IDS].sort());
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// ── Capability consistency ──────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
describe('built-in capability consistency', () => {
|
|
104
|
-
const formats = defaultFormats();
|
|
105
|
-
|
|
106
|
-
for (const def of formats) {
|
|
107
|
-
if (!def.exportDoc) continue;
|
|
108
|
-
it(`${def.id}: exportDoc produces bytes (or a tolerated not-implemented error)`, async () => {
|
|
109
|
-
const input = await makeInput();
|
|
110
|
-
// HTML formats need a player script; provide a trivial stub.
|
|
111
|
-
const options = { resolvePlayerScript: async () => '/* stub */' };
|
|
112
|
-
try {
|
|
113
|
-
const result = await def.exportDoc!(input, options);
|
|
114
|
-
// Use ArrayBuffer.isView rather than `instanceof Uint8Array`: under the
|
|
115
|
-
// jsdom test env, bytes built from node's TextEncoder/Uint8Array live in
|
|
116
|
-
// a different realm than the test file's Uint8Array global.
|
|
117
|
-
expect(ArrayBuffer.isView(result.bytes)).toBe(true);
|
|
118
|
-
expect(result.bytes.byteLength).toBeGreaterThan(0);
|
|
119
|
-
expect(typeof result.mimeType).toBe('string');
|
|
120
|
-
expect(Array.isArray(result.warnings)).toBe(true);
|
|
121
|
-
} catch (err: unknown) {
|
|
122
|
-
// xlsx export is being implemented concurrently and may still throw
|
|
123
|
-
// 'not yet implemented'. Tolerate that specific case only.
|
|
124
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
125
|
-
expect(def.id, `unexpected export failure for "${def.id}": ${message}`).toBe('xlsx');
|
|
126
|
-
expect(message).toMatch(/not yet implemented/i);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
it('csv export warns when the document has multiple tables', async () => {
|
|
132
|
-
const multiTable = parseMarkdown(
|
|
133
|
-
`| A | B |\n| - | - |\n| 1 | 2 |\n\n| C | D |\n| - | - |\n| 3 | 4 |\n`,
|
|
134
|
-
);
|
|
135
|
-
const container = new MemoryContentContainer();
|
|
136
|
-
const input: NormalizedInput = {
|
|
137
|
-
doc: markdownToDoc(multiTable),
|
|
138
|
-
markdownDoc: multiTable,
|
|
139
|
-
container,
|
|
140
|
-
baseName: 'tables',
|
|
141
|
-
};
|
|
142
|
-
const csv = defaultFormats().find((d) => d.id === 'csv')!;
|
|
143
|
-
const result = await csv.exportDoc!(input, {});
|
|
144
|
-
expect(result.warnings.length).toBeGreaterThan(0);
|
|
145
|
-
expect(result.warnings[0]).toMatch(/table/i);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// ── Error codes ─────────────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
describe('error codes', () => {
|
|
152
|
-
it('ConversionError carries a stable code + optional fields', () => {
|
|
153
|
-
const err = new ConversionError('invalid-input', 'boom', {
|
|
154
|
-
format: 'docx',
|
|
155
|
-
hint: 'try again',
|
|
156
|
-
cause: new Error('root'),
|
|
157
|
-
});
|
|
158
|
-
expect(err).toBeInstanceOf(Error);
|
|
159
|
-
expect(err.name).toBe('ConversionError');
|
|
160
|
-
expect(err.code).toBe('invalid-input');
|
|
161
|
-
expect(err.format).toBe('docx');
|
|
162
|
-
expect(err.hint).toBe('try again');
|
|
163
|
-
expect((err as Error & { cause?: unknown }).cause).toBeInstanceOf(Error);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('html export without a player script → missing-dependency', async () => {
|
|
167
|
-
const input = await makeInput();
|
|
168
|
-
const html = defaultFormats().find((d) => d.id === 'html')!;
|
|
169
|
-
await expect(html.exportDoc!(input, {})).rejects.toMatchObject({
|
|
170
|
-
name: 'ConversionError',
|
|
171
|
-
code: 'missing-dependency',
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
});
|
|
@@ -1,435 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for HTML export: docToHtml, docToHtmlZip, image utilities.
|
|
3
|
-
*
|
|
4
|
-
* Verifies that generated HTML contains the expected structure,
|
|
5
|
-
* embedded script, and image handling. ZIP exports are also validated.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it, expect } from 'vitest';
|
|
9
|
-
import JSZip from 'jszip';
|
|
10
|
-
import type { Doc, Block, ImageLayer } from '@bendyline/squisq/schemas';
|
|
11
|
-
import { compileTheme, createThemeRegistry } from '@bendyline/squisq/schemas';
|
|
12
|
-
import { docToHtml, docToHtmlZip, collectImagePaths } from '../html/index';
|
|
13
|
-
import { inferMimeType, arrayBufferToBase64DataUrl, extractFilename } from '../html/imageUtils';
|
|
14
|
-
|
|
15
|
-
// ============================================
|
|
16
|
-
// Helpers
|
|
17
|
-
// ============================================
|
|
18
|
-
|
|
19
|
-
const MOCK_PLAYER_SCRIPT = 'var SquisqPlayer={mount:function(){}};';
|
|
20
|
-
|
|
21
|
-
/** Helper to read a Blob as Uint8Array (works in jsdom where blob.arrayBuffer() may not exist) */
|
|
22
|
-
async function blobToUint8Array(blob: Blob): Promise<Uint8Array> {
|
|
23
|
-
return new Promise((resolve, reject) => {
|
|
24
|
-
const reader = new FileReader();
|
|
25
|
-
reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));
|
|
26
|
-
reader.onerror = () => reject(reader.error);
|
|
27
|
-
reader.readAsArrayBuffer(blob);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function makeDoc(overrides: Partial<Doc> = {}): Doc {
|
|
32
|
-
return {
|
|
33
|
-
articleId: 'test-doc',
|
|
34
|
-
duration: 10,
|
|
35
|
-
blocks: [],
|
|
36
|
-
audio: {
|
|
37
|
-
segments: [{ src: 'intro.mp3', name: 'intro', duration: 10, startTime: 0 }],
|
|
38
|
-
},
|
|
39
|
-
...overrides,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function makeImageBlock(imageSrc: string): Block {
|
|
44
|
-
const layer: ImageLayer = {
|
|
45
|
-
id: 'img-1',
|
|
46
|
-
type: 'image',
|
|
47
|
-
position: { x: 0, y: 0, width: '100%', height: '100%' },
|
|
48
|
-
content: { src: imageSrc, alt: 'test image' },
|
|
49
|
-
};
|
|
50
|
-
return {
|
|
51
|
-
id: 'block-1',
|
|
52
|
-
startTime: 0,
|
|
53
|
-
duration: 5,
|
|
54
|
-
audioSegment: 0,
|
|
55
|
-
layers: [layer],
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function makeImageBuffer(): ArrayBuffer {
|
|
60
|
-
// Minimal 1x1 PNG
|
|
61
|
-
const bytes = new Uint8Array([
|
|
62
|
-
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
|
63
|
-
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
|
|
64
|
-
0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00,
|
|
65
|
-
0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e,
|
|
66
|
-
0x44, 0xae, 0x42, 0x60, 0x82,
|
|
67
|
-
]);
|
|
68
|
-
return bytes.buffer;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ============================================
|
|
72
|
-
// Image Utilities
|
|
73
|
-
// ============================================
|
|
74
|
-
|
|
75
|
-
describe('inferMimeType', () => {
|
|
76
|
-
it('returns correct MIME types for common extensions', () => {
|
|
77
|
-
expect(inferMimeType('photo.jpg')).toBe('image/jpeg');
|
|
78
|
-
expect(inferMimeType('photo.jpeg')).toBe('image/jpeg');
|
|
79
|
-
expect(inferMimeType('logo.png')).toBe('image/png');
|
|
80
|
-
expect(inferMimeType('anim.gif')).toBe('image/gif');
|
|
81
|
-
expect(inferMimeType('hero.webp')).toBe('image/webp');
|
|
82
|
-
expect(inferMimeType('icon.svg')).toBe('image/svg+xml');
|
|
83
|
-
expect(inferMimeType('track.mp3')).toBe('audio/mpeg');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('returns octet-stream for unknown extensions', () => {
|
|
87
|
-
expect(inferMimeType('data.xyz')).toBe('application/octet-stream');
|
|
88
|
-
expect(inferMimeType('noext')).toBe('application/octet-stream');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('arrayBufferToBase64DataUrl', () => {
|
|
93
|
-
it('produces a valid data URI', () => {
|
|
94
|
-
const buffer = new TextEncoder().encode('hello').buffer;
|
|
95
|
-
const result = arrayBufferToBase64DataUrl(buffer, 'text/plain');
|
|
96
|
-
expect(result).toMatch(/^data:text\/plain;base64,/);
|
|
97
|
-
expect(result).toBe('data:text/plain;base64,aGVsbG8=');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('encodes large buffers correctly across chunk boundaries', () => {
|
|
101
|
-
const bytes = Uint8Array.from({ length: 100_003 }, (_, index) => index % 251);
|
|
102
|
-
const result = arrayBufferToBase64DataUrl(bytes.buffer, 'application/octet-stream');
|
|
103
|
-
const decoded = Uint8Array.from(atob(result.split(',')[1]), (character) =>
|
|
104
|
-
character.charCodeAt(0),
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
expect(decoded).toEqual(bytes);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('extractFilename', () => {
|
|
112
|
-
it('extracts filename from path', () => {
|
|
113
|
-
expect(extractFilename('images/hero.jpg')).toBe('hero.jpg');
|
|
114
|
-
expect(extractFilename('hero.jpg')).toBe('hero.jpg');
|
|
115
|
-
expect(extractFilename('a/b/c/deep.png')).toBe('deep.png');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('strips query and hash', () => {
|
|
119
|
-
expect(extractFilename('photo.jpg?v=2')).toBe('photo.jpg');
|
|
120
|
-
expect(extractFilename('photo.jpg#anchor')).toBe('photo.jpg');
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// ============================================
|
|
125
|
-
// Image Path Collection
|
|
126
|
-
// ============================================
|
|
127
|
-
|
|
128
|
-
describe('collectImagePaths', () => {
|
|
129
|
-
it('collects image layer src from blocks', () => {
|
|
130
|
-
const doc = makeDoc({ blocks: [makeImageBlock('hero.jpg')] });
|
|
131
|
-
const paths = collectImagePaths(doc);
|
|
132
|
-
expect(paths.has('hero.jpg')).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('ignores absolute URLs', () => {
|
|
136
|
-
const doc = makeDoc({ blocks: [makeImageBlock('https://example.com/photo.jpg')] });
|
|
137
|
-
const paths = collectImagePaths(doc);
|
|
138
|
-
expect(paths.size).toBe(0);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('ignores data URIs', () => {
|
|
142
|
-
const doc = makeDoc({ blocks: [makeImageBlock('data:image/png;base64,abc')] });
|
|
143
|
-
const paths = collectImagePaths(doc);
|
|
144
|
-
expect(paths.size).toBe(0);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('collects startBlock heroSrc', () => {
|
|
148
|
-
const doc = makeDoc({ startBlock: { heroSrc: 'cover.jpg', title: 'Test' } });
|
|
149
|
-
const paths = collectImagePaths(doc);
|
|
150
|
-
expect(paths.has('cover.jpg')).toBe(true);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('collects markdown image refs from block contents', () => {
|
|
154
|
-
const doc = makeDoc({
|
|
155
|
-
blocks: [
|
|
156
|
-
{
|
|
157
|
-
id: 'b1',
|
|
158
|
-
startTime: 0,
|
|
159
|
-
duration: 5,
|
|
160
|
-
audioSegment: 0,
|
|
161
|
-
contents: [
|
|
162
|
-
{
|
|
163
|
-
type: 'paragraph',
|
|
164
|
-
children: [{ type: 'image', url: 'body.jpg', alt: '' }],
|
|
165
|
-
},
|
|
166
|
-
] as unknown as Block['contents'],
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
});
|
|
170
|
-
const paths = collectImagePaths(doc);
|
|
171
|
-
expect(paths.has('body.jpg')).toBe(true);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('collects raw HTML <img> refs from block contents (WYSIWYG resize case)', () => {
|
|
175
|
-
// When the WYSIWYG editor serializes a resized image, it emits a raw
|
|
176
|
-
// `<img src width>` tag because markdown shorthand has no width syntax.
|
|
177
|
-
// The parser produces an htmlBlock node with htmlChildren rather than
|
|
178
|
-
// an image node — earlier this was invisible to the export pipeline.
|
|
179
|
-
const doc = makeDoc({
|
|
180
|
-
blocks: [
|
|
181
|
-
{
|
|
182
|
-
id: 'b1',
|
|
183
|
-
startTime: 0,
|
|
184
|
-
duration: 5,
|
|
185
|
-
audioSegment: 0,
|
|
186
|
-
contents: [
|
|
187
|
-
{
|
|
188
|
-
type: 'htmlBlock',
|
|
189
|
-
rawHtml: '<img alt="resized" src="resized.jpg" width="194">',
|
|
190
|
-
htmlChildren: [
|
|
191
|
-
{
|
|
192
|
-
type: 'htmlElement',
|
|
193
|
-
tagName: 'img',
|
|
194
|
-
attributes: { src: 'resized.jpg', alt: 'resized', width: '194' },
|
|
195
|
-
children: [],
|
|
196
|
-
selfClosing: true,
|
|
197
|
-
},
|
|
198
|
-
],
|
|
199
|
-
},
|
|
200
|
-
] as unknown as Block['contents'],
|
|
201
|
-
},
|
|
202
|
-
],
|
|
203
|
-
});
|
|
204
|
-
const paths = collectImagePaths(doc);
|
|
205
|
-
expect(paths.has('resized.jpg')).toBe(true);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// ============================================
|
|
210
|
-
// Single HTML Export
|
|
211
|
-
// ============================================
|
|
212
|
-
|
|
213
|
-
describe('docToHtml', () => {
|
|
214
|
-
it('produces a complete HTML document', () => {
|
|
215
|
-
const doc = makeDoc();
|
|
216
|
-
const html = docToHtml(doc, { playerScript: MOCK_PLAYER_SCRIPT });
|
|
217
|
-
|
|
218
|
-
expect(html).toContain('<!DOCTYPE html>');
|
|
219
|
-
expect(html).toContain('<html');
|
|
220
|
-
expect(html).toContain('</html>');
|
|
221
|
-
expect(html).toContain('<meta charset="UTF-8">');
|
|
222
|
-
expect(html).toContain('<div id="squisq-root">');
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('embeds the player script inline', () => {
|
|
226
|
-
const doc = makeDoc();
|
|
227
|
-
const html = docToHtml(doc, { playerScript: MOCK_PLAYER_SCRIPT });
|
|
228
|
-
|
|
229
|
-
expect(html).toContain(MOCK_PLAYER_SCRIPT);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it('embeds the doc JSON', () => {
|
|
233
|
-
const doc = makeDoc();
|
|
234
|
-
const html = docToHtml(doc, { playerScript: MOCK_PLAYER_SCRIPT });
|
|
235
|
-
|
|
236
|
-
expect(html).toContain('test-doc');
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('embeds an explicitly registered theme selected by the document', () => {
|
|
240
|
-
const external = compileTheme({
|
|
241
|
-
id: 'tenant-brand',
|
|
242
|
-
name: 'External Tenant Brand',
|
|
243
|
-
seedColors: { primary: '#6633cc' },
|
|
244
|
-
});
|
|
245
|
-
const html = docToHtml(makeDoc({ themeId: external.id }), {
|
|
246
|
-
playerScript: MOCK_PLAYER_SCRIPT,
|
|
247
|
-
themeRegistry: createThemeRegistry([external]),
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
expect(html).toContain('External Tenant Brand');
|
|
251
|
-
expect(html).toContain('\\"themeId\\":\\"tenant-brand\\"');
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('keeps a document-scoped theme ahead of the explicit registry', () => {
|
|
255
|
-
const inline = compileTheme({
|
|
256
|
-
id: 'tenant-brand',
|
|
257
|
-
name: 'Inline Tenant Brand',
|
|
258
|
-
seedColors: { primary: '#112233' },
|
|
259
|
-
});
|
|
260
|
-
const external = compileTheme({
|
|
261
|
-
id: 'tenant-brand',
|
|
262
|
-
name: 'External Tenant Brand',
|
|
263
|
-
seedColors: { primary: '#6633cc' },
|
|
264
|
-
});
|
|
265
|
-
const html = docToHtml(makeDoc({ themeId: inline.id, customThemes: [inline] }), {
|
|
266
|
-
playerScript: MOCK_PLAYER_SCRIPT,
|
|
267
|
-
themeRegistry: createThemeRegistry([external]),
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
expect(html).toContain('Inline Tenant Brand');
|
|
271
|
-
expect(html).not.toContain('External Tenant Brand');
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('calls SquisqPlayer.mount', () => {
|
|
275
|
-
const doc = makeDoc();
|
|
276
|
-
const html = docToHtml(doc, { playerScript: MOCK_PLAYER_SCRIPT });
|
|
277
|
-
|
|
278
|
-
expect(html).toContain('SquisqPlayer.mount');
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('sets the page title', () => {
|
|
282
|
-
const doc = makeDoc();
|
|
283
|
-
const html = docToHtml(doc, {
|
|
284
|
-
playerScript: MOCK_PLAYER_SCRIPT,
|
|
285
|
-
title: 'My Amazing Doc',
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
expect(html).toContain('<title>My Amazing Doc</title>');
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it('uses slideshow mode by default', () => {
|
|
292
|
-
const doc = makeDoc();
|
|
293
|
-
const html = docToHtml(doc, { playerScript: MOCK_PLAYER_SCRIPT });
|
|
294
|
-
|
|
295
|
-
expect(html).toContain('"slideshow"');
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it('supports static mode', () => {
|
|
299
|
-
const doc = makeDoc();
|
|
300
|
-
const html = docToHtml(doc, { playerScript: MOCK_PLAYER_SCRIPT, mode: 'static' });
|
|
301
|
-
|
|
302
|
-
expect(html).toContain('"static"');
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('inlines images as base64 data URIs', () => {
|
|
306
|
-
const doc = makeDoc({ blocks: [makeImageBlock('hero.png')] });
|
|
307
|
-
const images = new Map([['hero.png', makeImageBuffer()]]);
|
|
308
|
-
const html = docToHtml(doc, { playerScript: MOCK_PLAYER_SCRIPT, images });
|
|
309
|
-
|
|
310
|
-
expect(html).toContain('data:image/png;base64,');
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it('escapes script-breaking content in title', () => {
|
|
314
|
-
const doc = makeDoc();
|
|
315
|
-
const html = docToHtml(doc, {
|
|
316
|
-
playerScript: MOCK_PLAYER_SCRIPT,
|
|
317
|
-
title: '<script>alert("xss")</script>',
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// Title should be escaped
|
|
321
|
-
expect(html).not.toContain('<script>alert');
|
|
322
|
-
expect(html).toContain('<script>');
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
// ============================================
|
|
327
|
-
// ZIP Archive Export
|
|
328
|
-
// ============================================
|
|
329
|
-
|
|
330
|
-
describe('docToHtmlZip', () => {
|
|
331
|
-
it('produces a valid ZIP blob', async () => {
|
|
332
|
-
const doc = makeDoc();
|
|
333
|
-
const blob = await docToHtmlZip(doc, { playerScript: MOCK_PLAYER_SCRIPT });
|
|
334
|
-
|
|
335
|
-
expect(blob).toBeInstanceOf(Blob);
|
|
336
|
-
expect(blob.size).toBeGreaterThan(0);
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('contains index.html and squisq-player.js', async () => {
|
|
340
|
-
const doc = makeDoc();
|
|
341
|
-
const blob = await docToHtmlZip(doc, { playerScript: MOCK_PLAYER_SCRIPT });
|
|
342
|
-
|
|
343
|
-
const zip = await JSZip.loadAsync(await blobToUint8Array(blob));
|
|
344
|
-
expect(zip.file('index.html')).not.toBeNull();
|
|
345
|
-
expect(zip.file('squisq-player.js')).not.toBeNull();
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it('references player script via src attribute', async () => {
|
|
349
|
-
const doc = makeDoc();
|
|
350
|
-
const blob = await docToHtmlZip(doc, { playerScript: MOCK_PLAYER_SCRIPT });
|
|
351
|
-
|
|
352
|
-
const zip = await JSZip.loadAsync(await blobToUint8Array(blob));
|
|
353
|
-
const html = await zip.file('index.html')!.async('text');
|
|
354
|
-
expect(html).toContain('src="squisq-player.js"');
|
|
355
|
-
// Should NOT contain the inline script
|
|
356
|
-
expect(html).not.toContain(MOCK_PLAYER_SCRIPT);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it('embeds a caller-owned theme selected by the document', async () => {
|
|
360
|
-
const external = compileTheme({
|
|
361
|
-
id: 'tenant-zip-brand',
|
|
362
|
-
name: 'Tenant ZIP Brand',
|
|
363
|
-
seedColors: { primary: '#224466' },
|
|
364
|
-
});
|
|
365
|
-
const blob = await docToHtmlZip(makeDoc({ themeId: external.id }), {
|
|
366
|
-
playerScript: MOCK_PLAYER_SCRIPT,
|
|
367
|
-
themeRegistry: createThemeRegistry([external]),
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
const zip = await JSZip.loadAsync(await blobToUint8Array(blob));
|
|
371
|
-
const html = await zip.file('index.html')!.async('text');
|
|
372
|
-
expect(html).toContain('Tenant ZIP Brand');
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it('preserves the original image path inside the zip', async () => {
|
|
376
|
-
// Direct `<img src="folder/file.png">` references in the rendered HTML
|
|
377
|
-
// resolve only if the zip mirrors the doc's path layout — flattening
|
|
378
|
-
// to `images/<basename>` would 404 in the static renderer (which does
|
|
379
|
-
// not rewrite paths through imagePathMap).
|
|
380
|
-
const doc = makeDoc({ blocks: [makeImageBlock('mikehome_files/hero.png')] });
|
|
381
|
-
const images = new Map([['mikehome_files/hero.png', makeImageBuffer()]]);
|
|
382
|
-
const blob = await docToHtmlZip(doc, { playerScript: MOCK_PLAYER_SCRIPT, images });
|
|
383
|
-
|
|
384
|
-
const zip = await JSZip.loadAsync(await blobToUint8Array(blob));
|
|
385
|
-
expect(zip.file('mikehome_files/hero.png')).not.toBeNull();
|
|
386
|
-
expect(zip.file('images/hero.png')).toBeNull();
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it('strips leading slashes and rejects parent-traversal paths', async () => {
|
|
390
|
-
const doc = makeDoc();
|
|
391
|
-
const buffer = makeImageBuffer();
|
|
392
|
-
const images = new Map([
|
|
393
|
-
['/leading/slash.png', buffer],
|
|
394
|
-
['../escape.png', buffer],
|
|
395
|
-
['nested/../sneaky.png', buffer],
|
|
396
|
-
['ok/path.png', buffer],
|
|
397
|
-
]);
|
|
398
|
-
const blob = await docToHtmlZip(doc, { playerScript: MOCK_PLAYER_SCRIPT, images });
|
|
399
|
-
|
|
400
|
-
const zip = await JSZip.loadAsync(await blobToUint8Array(blob));
|
|
401
|
-
expect(zip.file('leading/slash.png')).not.toBeNull();
|
|
402
|
-
expect(zip.file('ok/path.png')).not.toBeNull();
|
|
403
|
-
expect(zip.file('../escape.png')).toBeNull();
|
|
404
|
-
expect(zip.file('nested/../sneaky.png')).toBeNull();
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('includes audio in audio/ folder when provided', async () => {
|
|
408
|
-
const audioData = new Uint8Array([0x49, 0x44, 0x33, 0x00]).buffer;
|
|
409
|
-
const doc = makeDoc();
|
|
410
|
-
const audio = new Map([['intro.mp3', audioData]]);
|
|
411
|
-
const blob = await docToHtmlZip(doc, { playerScript: MOCK_PLAYER_SCRIPT, audio });
|
|
412
|
-
|
|
413
|
-
const zip = await JSZip.loadAsync(await blobToUint8Array(blob));
|
|
414
|
-
expect(zip.file('audio/intro.mp3')).not.toBeNull();
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it('HTML references doc JSON inline', async () => {
|
|
418
|
-
const doc = makeDoc();
|
|
419
|
-
const blob = await docToHtmlZip(doc, { playerScript: MOCK_PLAYER_SCRIPT });
|
|
420
|
-
|
|
421
|
-
const zip = await JSZip.loadAsync(await blobToUint8Array(blob));
|
|
422
|
-
const html = await zip.file('index.html')!.async('text');
|
|
423
|
-
expect(html).toContain('test-doc');
|
|
424
|
-
expect(html).toContain('SquisqPlayer.mount');
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
it('player.js contains the provided script', async () => {
|
|
428
|
-
const doc = makeDoc();
|
|
429
|
-
const blob = await docToHtmlZip(doc, { playerScript: MOCK_PLAYER_SCRIPT });
|
|
430
|
-
|
|
431
|
-
const zip = await JSZip.loadAsync(await blobToUint8Array(blob));
|
|
432
|
-
const js = await zip.file('squisq-player.js')!.async('text');
|
|
433
|
-
expect(js).toBe(MOCK_PLAYER_SCRIPT);
|
|
434
|
-
});
|
|
435
|
-
});
|