@bendyline/squisq-formats 2.1.0 → 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-NNHKUXKA.js → chunk-26ISNJ7Y.js} +85 -65
- package/dist/{chunk-NKAJPJ4G.js → chunk-2JJ5RFDZ.js} +0 -1
- package/dist/{chunk-WQSHGBLN.js → chunk-3NKXBZSR.js} +193 -42
- package/dist/{chunk-KURGXM4I.js → chunk-4V3KCHAP.js} +3 -4
- package/dist/{chunk-MLX2BOJC.js → chunk-6RQOV3B3.js} +1 -2
- package/dist/{chunk-EW54IRRS.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-SC67HYQJ.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-RLU7UFYU.js → chunk-IPN56VLW.js} +83 -58
- package/dist/{chunk-DTDF6QDP.js → chunk-JE6LSIHE.js} +81 -20
- 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-74GO3FVS.js → chunk-PU7REGWV.js} +5 -8
- package/dist/{chunk-PN52A5AA.js → chunk-SBUW7NHR.js} +0 -1
- package/dist/{chunk-QFLDYKCR.js → chunk-TAAENIRB.js} +5 -8
- package/dist/{chunk-7ARKUCQT.js → chunk-X2DEAXNK.js} +54 -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 -11
- 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-4VUWTSGM.js.map +0 -1
- package/dist/chunk-6M7Z25LA.js +0 -46
- package/dist/chunk-6M7Z25LA.js.map +0 -1
- package/dist/chunk-74GO3FVS.js.map +0 -1
- package/dist/chunk-7ARKUCQT.js.map +0 -1
- package/dist/chunk-DTDF6QDP.js.map +0 -1
- package/dist/chunk-EW54IRRS.js.map +0 -1
- package/dist/chunk-FE6OJV6O.js.map +0 -1
- package/dist/chunk-GVS2XXV6.js.map +0 -1
- package/dist/chunk-KURGXM4I.js.map +0 -1
- package/dist/chunk-MLX2BOJC.js.map +0 -1
- package/dist/chunk-NKAJPJ4G.js.map +0 -1
- package/dist/chunk-NNHKUXKA.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-QFLDYKCR.js.map +0 -1
- package/dist/chunk-RFAPOKHJ.js.map +0 -1
- package/dist/chunk-RLU7UFYU.js.map +0 -1
- package/dist/chunk-SC67HYQJ.js.map +0 -1
- package/dist/chunk-SSUPBUF5.js.map +0 -1
- package/dist/chunk-U4MRIFKL.js.map +0 -1
- package/dist/chunk-UGYF5AZE.js +0 -275
- package/dist/chunk-UGYF5AZE.js.map +0 -1
- package/dist/chunk-WQSHGBLN.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-OJ7ZQV6P.js +0 -15
- package/dist/extract-OJ7ZQV6P.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-5VDIRPIJ.js +0 -12
- package/dist/layouts-5VDIRPIJ.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 -491
- package/src/__tests__/docxImport.test.ts +0 -531
- 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 -439
- 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 -1375
- package/src/docx/import.ts +0 -1250
- 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 -217
- 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,395 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for PPTX layout inference: placeholder extraction (EMU→%, master
|
|
3
|
-
* inheritance, chrome exclusion), the built-in-vs-custom classifier, custom
|
|
4
|
-
* template generation, importer annotations, and the dialog inspection API.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, expect, it } from 'vitest';
|
|
8
|
-
import type { MarkdownHeading, MarkdownList } from '@bendyline/squisq/markdown';
|
|
9
|
-
import { parseMarkdown, stringifyMarkdown } from '@bendyline/squisq/markdown';
|
|
10
|
-
import {
|
|
11
|
-
expandDocBlocks,
|
|
12
|
-
markdownToDoc,
|
|
13
|
-
readCustomTemplatesFromFrontmatter,
|
|
14
|
-
} from '@bendyline/squisq/doc';
|
|
15
|
-
import type { CustomTemplateLayer, TextLayer } from '@bendyline/squisq/schemas';
|
|
16
|
-
import { openPackage } from '../ooxml/reader';
|
|
17
|
-
import { analyzePptxLayouts, inspectPptxLayouts } from '../pptx/layouts';
|
|
18
|
-
import { pptxToContainer, pptxToMarkdownDoc } from '../pptx/import';
|
|
19
|
-
import { NS_DRAWINGML, NS_PML, NS_R, REL_IMAGE } from '../ooxml/namespaces';
|
|
20
|
-
import { xmlDeclaration } from '../ooxml/xmlUtils';
|
|
21
|
-
import { buildThemedPptx, layoutXml, phSp, slideXml } from './pptxInferFixtures';
|
|
22
|
-
|
|
23
|
-
// Shared EMU rects (12192000×6858000 slide → easy percentages).
|
|
24
|
-
const TITLE_RECT = { x: 914400, y: 685800, cx: 10363200, cy: 1371600 }; // 7.5,10,85,20
|
|
25
|
-
const LEFT_COL = { x: 609600, y: 1714500, cx: 5181600, cy: 4114800 }; // 5,25,42.5,60
|
|
26
|
-
const RIGHT_COL = { x: 6400800, y: 1714500, cx: 5181600, cy: 4114800 }; // 52.5,25,42.5,60
|
|
27
|
-
const PIC_LEFT = { x: 609600, y: 1714500, cx: 4876800, cy: 3429000 }; // 5,25,40,50
|
|
28
|
-
const TEXT_RIGHT = { x: 6096000, y: 1714500, cx: 5486400, cy: 3429000 }; // 50,25,45,50
|
|
29
|
-
|
|
30
|
-
const twoContentLayout = (name = 'Two Content') =>
|
|
31
|
-
layoutXml({
|
|
32
|
-
name,
|
|
33
|
-
shapes: [
|
|
34
|
-
phSp({ type: 'title', rect: TITLE_RECT }, 2),
|
|
35
|
-
phSp({ idx: 1, rect: LEFT_COL }, 3),
|
|
36
|
-
phSp({ idx: 2, rect: RIGHT_COL }, 4),
|
|
37
|
-
],
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('analyzePptxLayouts — extraction', () => {
|
|
41
|
-
it('converts explicit EMU geometry to percent rects', async () => {
|
|
42
|
-
const data = await buildThemedPptx({
|
|
43
|
-
layouts: [layoutXml({ name: 'Weird', shapes: [phSp({ type: 'title', rect: TITLE_RECT })] })],
|
|
44
|
-
slides: [{ xml: slideXml('T'), layoutIndex: 0 }],
|
|
45
|
-
});
|
|
46
|
-
const analysis = await analyzePptxLayouts(await openPackage(data));
|
|
47
|
-
const layout = analysis.layouts[0]!;
|
|
48
|
-
expect(layout.extracted.placeholders).toHaveLength(1);
|
|
49
|
-
expect(layout.extracted.placeholders[0]!.rect).toEqual({ x: 7.5, y: 10, w: 85, h: 20 });
|
|
50
|
-
expect(layout.extracted.placeholders[0]!.inheritedFromMaster).toBe(false);
|
|
51
|
-
expect(layout.extracted.slideCount).toBe(1);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('inherits geometry from the master by placeholder type', async () => {
|
|
55
|
-
const data = await buildThemedPptx({
|
|
56
|
-
masterShapes: [phSp({ type: 'title', rect: TITLE_RECT }, 9)],
|
|
57
|
-
layouts: [layoutXml({ name: 'Inherits', shapes: [phSp({ type: 'title' })] })],
|
|
58
|
-
slides: [{ xml: slideXml('T'), layoutIndex: 0 }],
|
|
59
|
-
});
|
|
60
|
-
const analysis = await analyzePptxLayouts(await openPackage(data));
|
|
61
|
-
const ph = analysis.layouts[0]!.extracted.placeholders[0]!;
|
|
62
|
-
expect(ph.inheritedFromMaster).toBe(true);
|
|
63
|
-
expect(ph.rect).toEqual({ x: 7.5, y: 10, w: 85, h: 20 });
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('excludes date/footer/slide-number chrome placeholders', async () => {
|
|
67
|
-
const data = await buildThemedPptx({
|
|
68
|
-
layouts: [
|
|
69
|
-
layoutXml({
|
|
70
|
-
name: 'Chromed',
|
|
71
|
-
shapes: [
|
|
72
|
-
phSp({ type: 'title', rect: TITLE_RECT }, 2),
|
|
73
|
-
phSp({ type: 'dt', idx: 10, rect: LEFT_COL }, 5),
|
|
74
|
-
phSp({ type: 'ftr', idx: 11, rect: RIGHT_COL }, 6),
|
|
75
|
-
phSp({ type: 'sldNum', idx: 12, rect: PIC_LEFT }, 7),
|
|
76
|
-
],
|
|
77
|
-
}),
|
|
78
|
-
],
|
|
79
|
-
slides: [{ xml: slideXml('T'), layoutIndex: 0 }],
|
|
80
|
-
});
|
|
81
|
-
const analysis = await analyzePptxLayouts(await openPackage(data));
|
|
82
|
-
const layout = analysis.layouts[0]!;
|
|
83
|
-
expect(layout.extracted.placeholders).toHaveLength(1);
|
|
84
|
-
expect(layout.extracted.placeholders[0]!.kind).toBe('title');
|
|
85
|
-
// Title-only layout → plain (no template adds value).
|
|
86
|
-
expect(layout.verdict.kind).toBe('plain');
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
describe('layout classification → importer annotations', () => {
|
|
91
|
-
it('annotates a title-layout slide with {[title subtitle="…"]} and omits the subtitle bullet', async () => {
|
|
92
|
-
const data = await buildThemedPptx({
|
|
93
|
-
layouts: [
|
|
94
|
-
layoutXml({
|
|
95
|
-
type: 'title',
|
|
96
|
-
name: 'Title Slide',
|
|
97
|
-
shapes: [
|
|
98
|
-
phSp({ type: 'ctrTitle', rect: TITLE_RECT }, 2),
|
|
99
|
-
phSp({ type: 'subTitle', idx: 1, rect: LEFT_COL }, 3),
|
|
100
|
-
],
|
|
101
|
-
}),
|
|
102
|
-
],
|
|
103
|
-
slides: [
|
|
104
|
-
{
|
|
105
|
-
xml: slideXml('Deck Title', [{ phType: 'subTitle', phIdx: 1, texts: ['The subtitle'] }]),
|
|
106
|
-
layoutIndex: 0,
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
});
|
|
110
|
-
const doc = await pptxToMarkdownDoc(data);
|
|
111
|
-
const heading = doc.children[0] as MarkdownHeading;
|
|
112
|
-
expect(heading.templateAnnotation).toEqual({
|
|
113
|
-
template: 'title',
|
|
114
|
-
params: { subtitle: 'The subtitle' },
|
|
115
|
-
});
|
|
116
|
-
// The subtitle moved into the card — no bullet list remains.
|
|
117
|
-
expect(doc.children.some((b) => b.type === 'list')).toBe(false);
|
|
118
|
-
|
|
119
|
-
// The annotation survives a stringify → parse round-trip.
|
|
120
|
-
const reparsed = parseMarkdown(stringifyMarkdown(doc));
|
|
121
|
-
const rtHeading = reparsed.children.find((b) => b.type === 'heading') as MarkdownHeading;
|
|
122
|
-
expect(rtHeading.templateAnnotation?.template).toBe('title');
|
|
123
|
-
expect(rtHeading.templateAnnotation?.params?.subtitle).toBe('The subtitle');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('generates a custom two-column template for "Two Content" and annotates its slides', async () => {
|
|
127
|
-
const data = await buildThemedPptx({
|
|
128
|
-
layouts: [twoContentLayout()],
|
|
129
|
-
slides: [
|
|
130
|
-
{
|
|
131
|
-
xml: slideXml('Side by side', [
|
|
132
|
-
{ phIdx: 1, texts: ['Left one', 'Left two'] },
|
|
133
|
-
{ phIdx: 2, texts: ['Right one'] },
|
|
134
|
-
]),
|
|
135
|
-
layoutIndex: 0,
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
});
|
|
139
|
-
const doc = await pptxToMarkdownDoc(data);
|
|
140
|
-
|
|
141
|
-
const heading = doc.children[0] as MarkdownHeading;
|
|
142
|
-
expect(heading.templateAnnotation?.template).toBe('pptx-two-content');
|
|
143
|
-
// Body text still flows as bullets (nothing is lost from the document).
|
|
144
|
-
const list = doc.children.find((b) => b.type === 'list') as MarkdownList;
|
|
145
|
-
expect(list.children).toHaveLength(3);
|
|
146
|
-
|
|
147
|
-
// The definition rides in frontmatter and round-trips through markdown.
|
|
148
|
-
const templates = readCustomTemplatesFromFrontmatter(doc.frontmatter);
|
|
149
|
-
expect(templates).toHaveLength(1);
|
|
150
|
-
const def = templates![0]!;
|
|
151
|
-
expect(def.name).toBe('pptx-two-content');
|
|
152
|
-
expect(def.label).toBe('Two Content');
|
|
153
|
-
|
|
154
|
-
const repeatLayer = def.layers.find((l): l is CustomTemplateLayer & TextLayer =>
|
|
155
|
-
Boolean(l.repeat),
|
|
156
|
-
);
|
|
157
|
-
expect(repeatLayer).toBeDefined();
|
|
158
|
-
expect(repeatLayer!.repeat).toEqual({ source: 'listItems', direction: 'row', gap: 5, max: 2 });
|
|
159
|
-
expect(repeatLayer!.type).toBe('text');
|
|
160
|
-
expect((repeatLayer as TextLayer).content.text).toBe('{item}');
|
|
161
|
-
expect((repeatLayer as TextLayer).position.x).toBe('5%');
|
|
162
|
-
expect((repeatLayer as TextLayer).position.width).toBe('42.5%');
|
|
163
|
-
|
|
164
|
-
// End-to-end: the custom template resolves tokens against the slide content.
|
|
165
|
-
const reparsed = parseMarkdown(stringifyMarkdown(doc));
|
|
166
|
-
const squisqDoc = markdownToDoc(reparsed);
|
|
167
|
-
expect(squisqDoc.customTemplates).toHaveLength(1);
|
|
168
|
-
const blocks = expandDocBlocks(squisqDoc.blocks, {
|
|
169
|
-
customTemplates: squisqDoc.customTemplates,
|
|
170
|
-
});
|
|
171
|
-
const block = blocks.find((b) => b.template === 'pptx-two-content');
|
|
172
|
-
expect(block).toBeDefined();
|
|
173
|
-
const textLayers = (block!.layers ?? []).filter((l): l is TextLayer => l.type === 'text');
|
|
174
|
-
expect(textLayers.some((l) => l.content.text === 'Side by side')).toBe(true);
|
|
175
|
-
expect(textLayers.some((l) => l.content.text === 'Left one')).toBe(true);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('maps a picture-beside-text layout to leftFeature with image params (container path)', async () => {
|
|
179
|
-
const pngBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 9, 9]);
|
|
180
|
-
const slideWithPic =
|
|
181
|
-
`${xmlDeclaration()}<p:sld xmlns:p="${NS_PML}" xmlns:a="${NS_DRAWINGML}" xmlns:r="${NS_R}"><p:cSld><p:spTree>` +
|
|
182
|
-
`<p:sp><p:nvSpPr><p:nvPr><p:ph type="title"/></p:nvPr></p:nvSpPr>` +
|
|
183
|
-
`<p:txBody><a:p><a:r><a:t>Photo slide</a:t></a:r></a:p></p:txBody></p:sp>` +
|
|
184
|
-
`<p:sp><p:nvSpPr><p:nvPr><p:ph idx="2"/></p:nvPr></p:nvSpPr>` +
|
|
185
|
-
`<p:txBody><a:p><a:r><a:t>Beside text</a:t></a:r></a:p></p:txBody></p:sp>` +
|
|
186
|
-
`<p:pic><p:nvPicPr><p:cNvPr id="9" name="Photo" descr="A nice photo"/></p:nvPicPr>` +
|
|
187
|
-
`<p:blipFill><a:blip r:embed="rIdImg"/></p:blipFill></p:pic>` +
|
|
188
|
-
`</p:spTree></p:cSld></p:sld>`;
|
|
189
|
-
const build = () =>
|
|
190
|
-
buildThemedPptx({
|
|
191
|
-
layouts: [
|
|
192
|
-
layoutXml({
|
|
193
|
-
type: 'cust',
|
|
194
|
-
name: 'Photo Left',
|
|
195
|
-
shapes: [
|
|
196
|
-
phSp({ type: 'title', rect: TITLE_RECT }, 2),
|
|
197
|
-
phSp({ type: 'pic', idx: 1, rect: PIC_LEFT }, 3),
|
|
198
|
-
phSp({ idx: 2, rect: TEXT_RIGHT }, 4),
|
|
199
|
-
],
|
|
200
|
-
}),
|
|
201
|
-
],
|
|
202
|
-
slides: [
|
|
203
|
-
{
|
|
204
|
-
xml: slideWithPic,
|
|
205
|
-
layoutIndex: 0,
|
|
206
|
-
rels: [{ id: 'rIdImg', type: REL_IMAGE, target: '../media/photo.png' }],
|
|
207
|
-
media: [{ path: 'ppt/media/photo.png', data: pngBytes, contentType: 'image/png' }],
|
|
208
|
-
},
|
|
209
|
-
],
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
// With extracted images (container path) → annotated with the image param.
|
|
213
|
-
const container = await pptxToContainer(await build());
|
|
214
|
-
const markdown = (await container.readDocument())!;
|
|
215
|
-
const parsed = parseMarkdown(markdown);
|
|
216
|
-
const heading = parsed.children.find((b) => b.type === 'heading') as MarkdownHeading;
|
|
217
|
-
expect(heading.templateAnnotation?.template).toBe('leftFeature');
|
|
218
|
-
expect(heading.templateAnnotation?.params?.imageSrc).toBe('images/image1.png');
|
|
219
|
-
expect(heading.templateAnnotation?.params?.imageAlt).toBe('A nice photo');
|
|
220
|
-
|
|
221
|
-
// Without extractImages → the feature verdict downgrades to plain.
|
|
222
|
-
const plainDoc = await pptxToMarkdownDoc(await build());
|
|
223
|
-
const plainHeading = plainDoc.children[0] as MarkdownHeading;
|
|
224
|
-
expect(plainHeading.templateAnnotation).toBeUndefined();
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('skips blank layouts and leaves title-and-content plain', async () => {
|
|
228
|
-
const data = await buildThemedPptx({
|
|
229
|
-
layouts: [
|
|
230
|
-
layoutXml({ type: 'blank', name: 'Blank', shapes: [] }),
|
|
231
|
-
layoutXml({
|
|
232
|
-
type: 'obj',
|
|
233
|
-
name: 'Title and Content',
|
|
234
|
-
shapes: [
|
|
235
|
-
phSp({ type: 'title', rect: TITLE_RECT }, 2),
|
|
236
|
-
phSp({ idx: 1, rect: LEFT_COL }, 3),
|
|
237
|
-
],
|
|
238
|
-
}),
|
|
239
|
-
],
|
|
240
|
-
slides: [
|
|
241
|
-
{ xml: slideXml('On blank', [{ texts: ['stray text'] }]), layoutIndex: 0 },
|
|
242
|
-
{ xml: slideXml('Normal', [{ phIdx: 1, texts: ['a bullet'] }]), layoutIndex: 1 },
|
|
243
|
-
],
|
|
244
|
-
});
|
|
245
|
-
const doc = await pptxToMarkdownDoc(data);
|
|
246
|
-
const headings = doc.children.filter((b) => b.type === 'heading') as MarkdownHeading[];
|
|
247
|
-
expect(headings[0]!.templateAnnotation).toBeUndefined();
|
|
248
|
-
expect(headings[1]!.templateAnnotation).toBeUndefined();
|
|
249
|
-
// Content still converts on both slides.
|
|
250
|
-
expect(doc.children.filter((b) => b.type === 'list')).toHaveLength(2);
|
|
251
|
-
// No custom templates were generated → no frontmatter payload.
|
|
252
|
-
expect(doc.frontmatter?.['squisq-custom-templates']).toBeUndefined();
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('deduplicates: two slides on one layout share a single definition', async () => {
|
|
256
|
-
const data = await buildThemedPptx({
|
|
257
|
-
layouts: [twoContentLayout()],
|
|
258
|
-
slides: [
|
|
259
|
-
{ xml: slideXml('One', [{ phIdx: 1, texts: ['a'] }]), layoutIndex: 0 },
|
|
260
|
-
{ xml: slideXml('Two', [{ phIdx: 1, texts: ['b'] }]), layoutIndex: 0 },
|
|
261
|
-
],
|
|
262
|
-
});
|
|
263
|
-
const doc = await pptxToMarkdownDoc(data);
|
|
264
|
-
const headings = doc.children.filter((b) => b.type === 'heading') as MarkdownHeading[];
|
|
265
|
-
expect(headings[0]!.templateAnnotation?.template).toBe('pptx-two-content');
|
|
266
|
-
expect(headings[1]!.templateAnnotation?.template).toBe('pptx-two-content');
|
|
267
|
-
expect(readCustomTemplatesFromFrontmatter(doc.frontmatter)).toHaveLength(1);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('builds twoColumn comparison params from idx-matched texts, downgrading when headers are missing', async () => {
|
|
271
|
-
const HEADER_L = { x: 609600, y: 1371600, cx: 5181600, cy: 685800 }; // 5,20,42.5,10
|
|
272
|
-
const HEADER_R = { x: 6400800, y: 1371600, cx: 5181600, cy: 685800 };
|
|
273
|
-
const BODY_L = { x: 609600, y: 2286000, cx: 5181600, cy: 3429000 }; // 5,33.33,42.5,50
|
|
274
|
-
const BODY_R = { x: 6400800, y: 2286000, cx: 5181600, cy: 3429000 };
|
|
275
|
-
const comparisonLayout = layoutXml({
|
|
276
|
-
type: 'twoTxTwoObj',
|
|
277
|
-
name: 'Comparison',
|
|
278
|
-
shapes: [
|
|
279
|
-
phSp({ type: 'title', rect: TITLE_RECT }, 2),
|
|
280
|
-
phSp({ type: 'body', idx: 1, rect: HEADER_L }, 3),
|
|
281
|
-
phSp({ type: 'body', idx: 2, rect: HEADER_R }, 4),
|
|
282
|
-
phSp({ idx: 3, rect: BODY_L }, 5),
|
|
283
|
-
phSp({ idx: 4, rect: BODY_R }, 6),
|
|
284
|
-
],
|
|
285
|
-
});
|
|
286
|
-
const data = await buildThemedPptx({
|
|
287
|
-
layouts: [comparisonLayout],
|
|
288
|
-
slides: [
|
|
289
|
-
{
|
|
290
|
-
xml: slideXml('Compare', [
|
|
291
|
-
{ phType: 'body', phIdx: 1, texts: ['Alpha'] },
|
|
292
|
-
{ phType: 'body', phIdx: 2, texts: ['Beta'] },
|
|
293
|
-
{ phIdx: 3, texts: ['fast'] },
|
|
294
|
-
{ phIdx: 4, texts: ['steady'] },
|
|
295
|
-
]),
|
|
296
|
-
layoutIndex: 0,
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
// Missing the right header text → no annotation.
|
|
300
|
-
xml: slideXml('Half compare', [{ phType: 'body', phIdx: 1, texts: ['Alpha'] }]),
|
|
301
|
-
layoutIndex: 0,
|
|
302
|
-
},
|
|
303
|
-
],
|
|
304
|
-
});
|
|
305
|
-
const doc = await pptxToMarkdownDoc(data);
|
|
306
|
-
const headings = doc.children.filter((b) => b.type === 'heading') as MarkdownHeading[];
|
|
307
|
-
expect(headings[0]!.templateAnnotation).toEqual({
|
|
308
|
-
template: 'twoColumn',
|
|
309
|
-
params: { left: 'Alpha|fast', right: 'Beta|steady' },
|
|
310
|
-
});
|
|
311
|
-
expect(headings[1]!.templateAnnotation).toBeUndefined();
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it('caps generated templates at maxTemplates, keeping the most-used layouts', async () => {
|
|
315
|
-
const data = await buildThemedPptx({
|
|
316
|
-
layouts: [twoContentLayout('Grid A'), twoContentLayout('Grid B'), twoContentLayout('Grid C')],
|
|
317
|
-
slides: [
|
|
318
|
-
{ xml: slideXml('a1', [{ phIdx: 1, texts: ['x'] }]), layoutIndex: 0 },
|
|
319
|
-
{ xml: slideXml('a2', [{ phIdx: 1, texts: ['x'] }]), layoutIndex: 0 },
|
|
320
|
-
{ xml: slideXml('b1', [{ phIdx: 1, texts: ['x'] }]), layoutIndex: 1 },
|
|
321
|
-
{ xml: slideXml('b2', [{ phIdx: 1, texts: ['x'] }]), layoutIndex: 1 },
|
|
322
|
-
{ xml: slideXml('c1', [{ phIdx: 1, texts: ['x'] }]), layoutIndex: 2 },
|
|
323
|
-
],
|
|
324
|
-
});
|
|
325
|
-
const analysis = await analyzePptxLayouts(await openPackage(data), { maxTemplates: 2 });
|
|
326
|
-
const verdicts = analysis.layouts.map((l) => l.verdict.kind);
|
|
327
|
-
expect(verdicts.filter((k) => k === 'custom')).toHaveLength(2);
|
|
328
|
-
// The least-used layout (Grid C, 1 slide) was downgraded.
|
|
329
|
-
const gridC = analysis.layouts.find((l) => l.extracted.name === 'Grid C')!;
|
|
330
|
-
expect(gridC.verdict.kind).toBe('plain');
|
|
331
|
-
expect(analysis.warnings.some((w) => w.includes('kept the 2 most used'))).toBe(true);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('uses a portrait viewport for portrait decks', async () => {
|
|
335
|
-
const data = await buildThemedPptx({
|
|
336
|
-
sldSz: { cx: 6858000, cy: 12192000 },
|
|
337
|
-
layouts: [
|
|
338
|
-
layoutXml({
|
|
339
|
-
name: 'Tall Two',
|
|
340
|
-
shapes: [
|
|
341
|
-
phSp({ type: 'title', rect: { x: 342900, y: 609600, cx: 6172200, cy: 1219200 } }, 2),
|
|
342
|
-
phSp({ idx: 1, rect: { x: 342900, y: 2438400, cx: 2743200, cy: 7315200 } }, 3),
|
|
343
|
-
phSp({ idx: 2, rect: { x: 3771900, y: 2438400, cx: 2743200, cy: 7315200 } }, 4),
|
|
344
|
-
],
|
|
345
|
-
}),
|
|
346
|
-
],
|
|
347
|
-
slides: [{ xml: slideXml('Tall', [{ phIdx: 1, texts: ['x'] }]), layoutIndex: 0 }],
|
|
348
|
-
});
|
|
349
|
-
const analysis = await analyzePptxLayouts(await openPackage(data));
|
|
350
|
-
const verdict = analysis.layouts[0]!.verdict;
|
|
351
|
-
expect(verdict.kind).toBe('custom');
|
|
352
|
-
if (verdict.kind === 'custom') {
|
|
353
|
-
expect(verdict.def.viewport).toEqual({ width: 1080, height: 1920 });
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
describe('inspectPptxLayouts', () => {
|
|
359
|
-
it('summarizes verdicts, includes built-in matches, and can list unused layouts', async () => {
|
|
360
|
-
const data = await buildThemedPptx({
|
|
361
|
-
layouts: [
|
|
362
|
-
layoutXml({
|
|
363
|
-
type: 'title',
|
|
364
|
-
name: 'Title Slide',
|
|
365
|
-
shapes: [phSp({ type: 'ctrTitle', rect: TITLE_RECT }, 2)],
|
|
366
|
-
}),
|
|
367
|
-
twoContentLayout(),
|
|
368
|
-
layoutXml({ type: 'blank', name: 'Blank', shapes: [] }),
|
|
369
|
-
],
|
|
370
|
-
slides: [
|
|
371
|
-
{ xml: slideXml('T'), layoutIndex: 0 },
|
|
372
|
-
{ xml: slideXml('S', [{ phIdx: 1, texts: ['x'] }]), layoutIndex: 1 },
|
|
373
|
-
],
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const usedOnly = await inspectPptxLayouts(data);
|
|
377
|
-
expect(usedOnly.layouts).toHaveLength(2);
|
|
378
|
-
expect(usedOnly.slideSize).toEqual({ cx: 12192000, cy: 6858000 });
|
|
379
|
-
|
|
380
|
-
const titleRow = usedOnly.layouts.find((l) => l.name === 'Title Slide')!;
|
|
381
|
-
expect(titleRow.verdict).toBe('builtin');
|
|
382
|
-
expect(titleRow.builtinTemplate).toBe('title');
|
|
383
|
-
expect(titleRow.slideCount).toBe(1);
|
|
384
|
-
|
|
385
|
-
const twoRow = usedOnly.layouts.find((l) => l.name === 'Two Content')!;
|
|
386
|
-
expect(twoRow.verdict).toBe('custom');
|
|
387
|
-
expect(twoRow.customTemplate?.name).toBe('pptx-two-content');
|
|
388
|
-
|
|
389
|
-
const all = await inspectPptxLayouts(data, { includeUnused: true });
|
|
390
|
-
expect(all.layouts).toHaveLength(3);
|
|
391
|
-
const blankRow = all.layouts.find((l) => l.name === 'Blank')!;
|
|
392
|
-
expect(blankRow.verdict).toBe('skip');
|
|
393
|
-
expect(blankRow.slideCount).toBe(0);
|
|
394
|
-
});
|
|
395
|
-
});
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Round-trip tests: MarkdownDocument → DOCX → MarkdownDocument.
|
|
3
|
-
*
|
|
4
|
-
* Verifies that core document structures survive the export → import cycle.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect } from 'vitest';
|
|
8
|
-
import type {
|
|
9
|
-
MarkdownDocument,
|
|
10
|
-
MarkdownHeading,
|
|
11
|
-
MarkdownParagraph,
|
|
12
|
-
MarkdownStrong,
|
|
13
|
-
MarkdownEmphasis,
|
|
14
|
-
MarkdownInlineNode,
|
|
15
|
-
MarkdownBlockNode,
|
|
16
|
-
} from '@bendyline/squisq/markdown';
|
|
17
|
-
|
|
18
|
-
import { markdownDocToDocx } from '../docx/export';
|
|
19
|
-
import { docxToMarkdownDoc } from '../docx/import';
|
|
20
|
-
|
|
21
|
-
// ============================================
|
|
22
|
-
// Helpers
|
|
23
|
-
// ============================================
|
|
24
|
-
|
|
25
|
-
async function roundTrip(doc: MarkdownDocument): Promise<MarkdownDocument> {
|
|
26
|
-
const buffer = await markdownDocToDocx(doc);
|
|
27
|
-
return docxToMarkdownDoc(buffer);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Extract plain text from an inline node tree.
|
|
32
|
-
*/
|
|
33
|
-
function extractText(node: MarkdownInlineNode): string {
|
|
34
|
-
if (node.type === 'text') return node.value;
|
|
35
|
-
if ('children' in node) return (node.children as MarkdownInlineNode[]).map(extractText).join('');
|
|
36
|
-
if ('value' in node) return node.value as string;
|
|
37
|
-
return '';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function extractBlockText(block: MarkdownBlockNode): string {
|
|
41
|
-
if ('children' in block)
|
|
42
|
-
return (block.children as MarkdownInlineNode[]).map(extractText).join('');
|
|
43
|
-
if ('value' in block) return block.value as string;
|
|
44
|
-
return '';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ============================================
|
|
48
|
-
// Round-trip Tests
|
|
49
|
-
// ============================================
|
|
50
|
-
|
|
51
|
-
describe('DOCX round-trip', () => {
|
|
52
|
-
it('round-trips headings', async () => {
|
|
53
|
-
const original: MarkdownDocument = {
|
|
54
|
-
type: 'document',
|
|
55
|
-
children: [
|
|
56
|
-
{
|
|
57
|
-
type: 'heading',
|
|
58
|
-
depth: 1,
|
|
59
|
-
children: [{ type: 'text', value: 'Main Title' }],
|
|
60
|
-
} satisfies MarkdownHeading,
|
|
61
|
-
{
|
|
62
|
-
type: 'heading',
|
|
63
|
-
depth: 2,
|
|
64
|
-
children: [{ type: 'text', value: 'Section' }],
|
|
65
|
-
} satisfies MarkdownHeading,
|
|
66
|
-
],
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const result = await roundTrip(original);
|
|
70
|
-
|
|
71
|
-
expect(result.children.length).toBeGreaterThanOrEqual(2);
|
|
72
|
-
expect(result.children[0].type).toBe('heading');
|
|
73
|
-
expect((result.children[0] as MarkdownHeading).depth).toBe(1);
|
|
74
|
-
expect(extractBlockText(result.children[0])).toBe('Main Title');
|
|
75
|
-
|
|
76
|
-
expect(result.children[1].type).toBe('heading');
|
|
77
|
-
expect((result.children[1] as MarkdownHeading).depth).toBe(2);
|
|
78
|
-
expect(extractBlockText(result.children[1])).toBe('Section');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('round-trips paragraphs', async () => {
|
|
82
|
-
const original: MarkdownDocument = {
|
|
83
|
-
type: 'document',
|
|
84
|
-
children: [
|
|
85
|
-
{
|
|
86
|
-
type: 'paragraph',
|
|
87
|
-
children: [{ type: 'text', value: 'Hello world' }],
|
|
88
|
-
} satisfies MarkdownParagraph,
|
|
89
|
-
{
|
|
90
|
-
type: 'paragraph',
|
|
91
|
-
children: [{ type: 'text', value: 'Second paragraph' }],
|
|
92
|
-
} satisfies MarkdownParagraph,
|
|
93
|
-
],
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const result = await roundTrip(original);
|
|
97
|
-
|
|
98
|
-
expect(result.children.length).toBe(2);
|
|
99
|
-
expect(extractBlockText(result.children[0])).toBe('Hello world');
|
|
100
|
-
expect(extractBlockText(result.children[1])).toBe('Second paragraph');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('round-trips bold text', async () => {
|
|
104
|
-
const original: MarkdownDocument = {
|
|
105
|
-
type: 'document',
|
|
106
|
-
children: [
|
|
107
|
-
{
|
|
108
|
-
type: 'paragraph',
|
|
109
|
-
children: [
|
|
110
|
-
{ type: 'text', value: 'Normal ' },
|
|
111
|
-
{
|
|
112
|
-
type: 'strong',
|
|
113
|
-
children: [{ type: 'text', value: 'bold' }],
|
|
114
|
-
} satisfies MarkdownStrong,
|
|
115
|
-
{ type: 'text', value: ' text' },
|
|
116
|
-
],
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
const result = await roundTrip(original);
|
|
122
|
-
const para = result.children[0] as MarkdownParagraph;
|
|
123
|
-
expect(para.type).toBe('paragraph');
|
|
124
|
-
|
|
125
|
-
// Find the bold node
|
|
126
|
-
const boldNode = para.children.find((c: MarkdownInlineNode) => c.type === 'strong');
|
|
127
|
-
expect(boldNode).toBeDefined();
|
|
128
|
-
expect(extractText(boldNode!)).toBe('bold');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('round-trips italic text', async () => {
|
|
132
|
-
const original: MarkdownDocument = {
|
|
133
|
-
type: 'document',
|
|
134
|
-
children: [
|
|
135
|
-
{
|
|
136
|
-
type: 'paragraph',
|
|
137
|
-
children: [
|
|
138
|
-
{
|
|
139
|
-
type: 'emphasis',
|
|
140
|
-
children: [{ type: 'text', value: 'italic' }],
|
|
141
|
-
} satisfies MarkdownEmphasis,
|
|
142
|
-
],
|
|
143
|
-
},
|
|
144
|
-
],
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const result = await roundTrip(original);
|
|
148
|
-
const para = result.children[0] as MarkdownParagraph;
|
|
149
|
-
const italicNode = para.children.find((c: MarkdownInlineNode) => c.type === 'emphasis');
|
|
150
|
-
expect(italicNode).toBeDefined();
|
|
151
|
-
expect(extractText(italicNode!)).toBe('italic');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('round-trips mixed content document', async () => {
|
|
155
|
-
const original: MarkdownDocument = {
|
|
156
|
-
type: 'document',
|
|
157
|
-
children: [
|
|
158
|
-
{
|
|
159
|
-
type: 'heading',
|
|
160
|
-
depth: 1,
|
|
161
|
-
children: [{ type: 'text', value: 'Document Title' }],
|
|
162
|
-
} satisfies MarkdownHeading,
|
|
163
|
-
{
|
|
164
|
-
type: 'paragraph',
|
|
165
|
-
children: [
|
|
166
|
-
{ type: 'text', value: 'This is ' },
|
|
167
|
-
{
|
|
168
|
-
type: 'strong',
|
|
169
|
-
children: [{ type: 'text', value: 'important' }],
|
|
170
|
-
},
|
|
171
|
-
{ type: 'text', value: ' content.' },
|
|
172
|
-
],
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
type: 'heading',
|
|
176
|
-
depth: 2,
|
|
177
|
-
children: [{ type: 'text', value: 'Details' }],
|
|
178
|
-
} satisfies MarkdownHeading,
|
|
179
|
-
{
|
|
180
|
-
type: 'paragraph',
|
|
181
|
-
children: [{ type: 'text', value: 'More info here.' }],
|
|
182
|
-
},
|
|
183
|
-
],
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const result = await roundTrip(original);
|
|
187
|
-
|
|
188
|
-
// Should have at least 4 blocks
|
|
189
|
-
expect(result.children.length).toBeGreaterThanOrEqual(4);
|
|
190
|
-
|
|
191
|
-
// First should be heading
|
|
192
|
-
expect(result.children[0].type).toBe('heading');
|
|
193
|
-
expect(extractBlockText(result.children[0])).toBe('Document Title');
|
|
194
|
-
|
|
195
|
-
// Then paragraph with mixed content
|
|
196
|
-
expect(result.children[1].type).toBe('paragraph');
|
|
197
|
-
const fullText = (result.children[1] as MarkdownParagraph).children.map(extractText).join('');
|
|
198
|
-
expect(fullText).toContain('important');
|
|
199
|
-
expect(fullText).toContain('content');
|
|
200
|
-
});
|
|
201
|
-
});
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Asset-fidelity guard: an embedded image must survive a full export → import
|
|
3
|
-
* round-trip for every container-producing format. We author a markdown doc
|
|
4
|
-
* with an image, export it (supplying the image bytes), re-import via the
|
|
5
|
-
* format's `*ToContainer`, and assert the bytes + reference come back intact.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, expect, it } from 'vitest';
|
|
9
|
-
import type { MarkdownDocument } from '@bendyline/squisq/markdown';
|
|
10
|
-
import { markdownDocToPptx } from '../pptx/export';
|
|
11
|
-
import { pptxToContainer } from '../pptx/import';
|
|
12
|
-
import { markdownDocToDocx } from '../docx/export';
|
|
13
|
-
import { docxToContainer } from '../docx/import';
|
|
14
|
-
|
|
15
|
-
/** PNG-signature-prefixed payload with a distinctive tail for byte comparison. */
|
|
16
|
-
const IMAGE_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 9, 8, 7, 6]);
|
|
17
|
-
|
|
18
|
-
const doc: MarkdownDocument = {
|
|
19
|
-
type: 'document',
|
|
20
|
-
children: [
|
|
21
|
-
{ type: 'heading', depth: 1, children: [{ type: 'text', value: 'Assets' }] },
|
|
22
|
-
{ type: 'paragraph', children: [{ type: 'image', url: 'hero.png', alt: 'Hero' }] },
|
|
23
|
-
],
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
describe('embedded image round-trips through container formats', () => {
|
|
27
|
-
it('survives markdown → pptx → pptxToContainer', async () => {
|
|
28
|
-
const pptx = await markdownDocToPptx(doc, {
|
|
29
|
-
images: new Map([['hero.png', IMAGE_BYTES.buffer]]),
|
|
30
|
-
});
|
|
31
|
-
const container = await pptxToContainer(pptx);
|
|
32
|
-
|
|
33
|
-
expect(await container.exists('images/image1.png')).toBe(true);
|
|
34
|
-
const bytes = await container.readFile('images/image1.png');
|
|
35
|
-
expect(new Uint8Array(bytes!)).toEqual(IMAGE_BYTES);
|
|
36
|
-
expect(await container.readDocument()).toContain('images/image1.png');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('survives markdown → docx → docxToContainer', async () => {
|
|
40
|
-
const docx = await markdownDocToDocx(doc, {
|
|
41
|
-
images: new Map([['hero.png', { data: IMAGE_BYTES.buffer, contentType: 'image/png' }]]),
|
|
42
|
-
});
|
|
43
|
-
const container = await docxToContainer(docx);
|
|
44
|
-
|
|
45
|
-
expect(await container.exists('images/image1.png')).toBe(true);
|
|
46
|
-
const bytes = await container.readFile('images/image1.png');
|
|
47
|
-
expect(new Uint8Array(bytes!)).toEqual(IMAGE_BYTES);
|
|
48
|
-
expect(await container.readDocument()).toContain('images/image1.png');
|
|
49
|
-
});
|
|
50
|
-
});
|