@bendyline/squisq-formats 1.4.2 → 2.0.1
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/README.md +48 -2
- package/dist/chunk-2KPARF2P.js +99 -0
- package/dist/chunk-2KPARF2P.js.map +1 -0
- package/dist/{chunk-5LSSWZPU.js → chunk-4VUWTSGM.js} +72 -25
- package/dist/chunk-4VUWTSGM.js.map +1 -0
- package/dist/{chunk-YESS7VY3.js → chunk-ABVI556T.js} +154 -39
- package/dist/chunk-ABVI556T.js.map +1 -0
- package/dist/{chunk-WWBIKRNP.js → chunk-CRAVSMPZ.js} +13 -11
- package/dist/chunk-CRAVSMPZ.js.map +1 -0
- package/dist/chunk-FE6OJV6O.js +560 -0
- package/dist/chunk-FE6OJV6O.js.map +1 -0
- package/dist/{chunk-D7IFPWNZ.js → chunk-GVS2XXV6.js} +27 -26
- package/dist/chunk-GVS2XXV6.js.map +1 -0
- package/dist/{chunk-RTS5XBZ7.js → chunk-HTW2M27H.js} +341 -100
- package/dist/chunk-HTW2M27H.js.map +1 -0
- package/dist/{chunk-46FMDOWZ.js → chunk-LXYLOOST.js} +49 -17
- package/dist/chunk-LXYLOOST.js.map +1 -0
- package/dist/{chunk-MJGRI6XR.js → chunk-MLX2BOJC.js} +12 -7
- package/dist/chunk-MLX2BOJC.js.map +1 -0
- package/dist/chunk-NKAJPJ4G.js +106 -0
- package/dist/chunk-NKAJPJ4G.js.map +1 -0
- package/dist/{chunk-DQAZR57U.js → chunk-O3GVVND4.js} +1 -1
- package/dist/chunk-ODL3SSPT.js +18 -0
- package/dist/chunk-ODL3SSPT.js.map +1 -0
- package/dist/chunk-QRVN6A6E.js +127 -0
- package/dist/chunk-QRVN6A6E.js.map +1 -0
- package/dist/chunk-U32AG3G3.js +195 -0
- package/dist/chunk-U32AG3G3.js.map +1 -0
- package/dist/{chunk-EHLGMSTE.js → chunk-VJJM2SSH.js} +10 -8
- package/dist/chunk-VJJM2SSH.js.map +1 -0
- package/dist/chunk-VSYHZECT.js +778 -0
- package/dist/chunk-VSYHZECT.js.map +1 -0
- package/dist/{chunk-SHLPSADL.js → chunk-WC7WULGV.js} +81 -189
- package/dist/chunk-WC7WULGV.js.map +1 -0
- package/dist/chunk-XKUMNGBW.js +131 -0
- package/dist/chunk-XKUMNGBW.js.map +1 -0
- package/dist/chunk-YRT7GQ5Y.js +28 -0
- package/dist/chunk-YRT7GQ5Y.js.map +1 -0
- package/dist/container/index.d.ts +13 -3
- package/dist/container/index.js +23 -20
- package/dist/container/index.js.map +1 -1
- package/dist/docx/index.d.ts +6 -2
- package/dist/docx/index.js +5 -3
- package/dist/epub/index.d.ts +3 -1
- package/dist/epub/index.js +3 -3
- package/dist/export-D2NkylDT.d.ts +69 -0
- package/dist/extract-H6RXJMHP.js +15 -0
- package/dist/extract-H6RXJMHP.js.map +1 -0
- package/dist/html/index.d.ts +15 -85
- package/dist/html/index.js +3 -3
- package/dist/images-7FBWPKE3.js +7 -0
- package/dist/images-7FBWPKE3.js.map +1 -0
- package/dist/import-DTkDxHmZ.d.ts +84 -0
- package/dist/import-K8mfc0fz.d.ts +136 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.js +33 -12
- package/dist/infer/index.d.ts +138 -0
- package/dist/infer/index.js +28 -0
- package/dist/infer/index.js.map +1 -0
- package/dist/layouts-BHrgZ5FS.d.ts +150 -0
- package/dist/layouts-QVPK3ZCU.js +12 -0
- package/dist/layouts-QVPK3ZCU.js.map +1 -0
- package/dist/mapTheme-IR27S6IV.js +11 -0
- package/dist/mapTheme-IR27S6IV.js.map +1 -0
- package/dist/ooxml/index.d.ts +21 -127
- package/dist/ooxml/index.js +31 -9
- package/dist/pdf/index.d.ts +3 -1
- package/dist/pdf/index.js +1 -1
- package/dist/pptx/index.d.ts +8 -108
- package/dist/pptx/index.js +13 -4
- package/dist/reader-B9L8Ucbj.d.ts +140 -0
- package/dist/registry/index.d.ts +60 -8
- package/dist/registry/index.js +9 -4
- package/dist/themeReader-DJKErl_j.d.ts +56 -0
- package/dist/xlsx/index.d.ts +6 -66
- package/dist/xlsx/index.js +4 -2
- package/dist/zipLimits-BOKCB7qk.d.ts +31 -0
- package/package.json +8 -2
- package/src/__tests__/container.test.ts +41 -0
- package/src/__tests__/convert.test.ts +310 -1
- package/src/__tests__/epub.test.ts +102 -1
- package/src/__tests__/html.test.ts +62 -0
- package/src/__tests__/inferTheme.test.ts +135 -0
- package/src/__tests__/lossyWarnings.test.ts +41 -0
- package/src/__tests__/ooxml.test.ts +76 -2
- package/src/__tests__/ooxmlCancellation.test.ts +113 -0
- package/src/__tests__/ooxmlThemeReader.test.ts +92 -0
- package/src/__tests__/plainHtml.test.ts +15 -1
- package/src/__tests__/plainHtmlBundle.test.ts +18 -0
- package/src/__tests__/pptxImport.test.ts +30 -0
- package/src/__tests__/pptxInferFixtures.ts +314 -0
- package/src/__tests__/pptxLayoutInfer.test.ts +395 -0
- package/src/__tests__/zipSafety.test.ts +317 -0
- package/src/container/index.ts +37 -32
- package/src/docx/export.ts +4 -2
- package/src/docx/import.ts +4 -3
- package/src/epub/export.ts +94 -25
- package/src/html/docsHtmlBundle.ts +6 -2
- package/src/html/htmlTemplate.ts +31 -6
- package/src/html/imageUtils.ts +14 -5
- package/src/html/index.ts +12 -1
- package/src/html/plainHtml.ts +7 -3
- package/src/html/plainHtmlBundle.ts +11 -9
- package/src/index.ts +23 -0
- package/src/infer/extract.ts +127 -0
- package/src/infer/index.ts +199 -0
- package/src/infer/mapTheme.ts +176 -0
- package/src/infer/types.ts +27 -0
- package/src/ooxml/index.ts +14 -0
- package/src/ooxml/readUtils.ts +44 -0
- package/src/ooxml/reader.ts +129 -29
- package/src/ooxml/themeReader.ts +197 -0
- package/src/ooxml/types.ts +8 -9
- package/src/ooxml/writer.ts +28 -10
- package/src/pdf/export.ts +4 -2
- package/src/pdf/import.ts +40 -34
- package/src/pptx/export.ts +4 -2
- package/src/pptx/import.ts +214 -32
- package/src/pptx/index.ts +14 -0
- package/src/pptx/layouts.ts +1222 -0
- package/src/registry/convert.ts +168 -53
- package/src/registry/defaultFormats.ts +135 -45
- package/src/registry/index.ts +6 -1
- package/src/registry/types.ts +69 -6
- package/src/shared/boundedZipArchive.ts +383 -0
- package/src/shared/fidelity.ts +130 -0
- package/src/shared/zipEntryCount.ts +151 -0
- package/src/shared/zipLimits.ts +296 -0
- package/src/shared/zipSafety.ts +19 -0
- package/src/xlsx/import.ts +3 -2
- package/dist/chunk-46FMDOWZ.js.map +0 -1
- package/dist/chunk-5LSSWZPU.js.map +0 -1
- package/dist/chunk-D7IFPWNZ.js.map +0 -1
- package/dist/chunk-EHLGMSTE.js.map +0 -1
- package/dist/chunk-MJGRI6XR.js.map +0 -1
- package/dist/chunk-RTS5XBZ7.js.map +0 -1
- package/dist/chunk-SHLPSADL.js.map +0 -1
- package/dist/chunk-WWBIKRNP.js.map +0 -1
- package/dist/chunk-YESS7VY3.js.map +0 -1
- /package/dist/{chunk-DQAZR57U.js.map → chunk-O3GVVND4.js.map} +0 -0
|
@@ -0,0 +1,395 @@
|
|
|
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
|
+
});
|