@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
package/src/pdf/import.ts
CHANGED
|
@@ -107,16 +107,21 @@ export async function pdfToMarkdownDoc(
|
|
|
107
107
|
? new Uint8Array(data)
|
|
108
108
|
: data;
|
|
109
109
|
|
|
110
|
-
const
|
|
110
|
+
const loaded = await loadPdfDocument(bytes);
|
|
111
|
+
try {
|
|
112
|
+
const textLines = await extractTextLines(loaded.pdf);
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
if (textLines.length === 0) {
|
|
115
|
+
return { type: 'document', children: [] };
|
|
116
|
+
}
|
|
115
117
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
const bodySize = options.bodyFontSize ?? detectBodyFontSize(textLines);
|
|
119
|
+
const blocks = classifyLines(textLines, bodySize, options);
|
|
118
120
|
|
|
119
|
-
|
|
121
|
+
return { type: 'document', children: blocks };
|
|
122
|
+
} finally {
|
|
123
|
+
await loaded.pdf.destroy?.();
|
|
124
|
+
}
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
/**
|
|
@@ -157,8 +162,14 @@ export async function pdfToContainer(
|
|
|
157
162
|
? new Uint8Array(data)
|
|
158
163
|
: data;
|
|
159
164
|
|
|
160
|
-
const
|
|
161
|
-
|
|
165
|
+
const loaded = await loadPdfDocument(bytes);
|
|
166
|
+
let textLines: TextLine[];
|
|
167
|
+
let images: ExtractedImage[];
|
|
168
|
+
try {
|
|
169
|
+
[textLines, images] = await Promise.all([extractTextLines(loaded.pdf), extractImages(loaded)]);
|
|
170
|
+
} finally {
|
|
171
|
+
await loaded.pdf.destroy?.();
|
|
172
|
+
}
|
|
162
173
|
|
|
163
174
|
// Under Node there is no DOM/canvas, so extractImages() returns [] via its
|
|
164
175
|
// environment guard and embedded images can't be decoded. The registry's
|
|
@@ -208,32 +219,14 @@ export interface ExtractedImage {
|
|
|
208
219
|
* Extract embedded images from a PDF using pdfjs-dist operator list API.
|
|
209
220
|
* Requires browser canvas for PNG encoding.
|
|
210
221
|
*/
|
|
211
|
-
async function extractImages(
|
|
222
|
+
async function extractImages(loaded: LoadedPdf): Promise<ExtractedImage[]> {
|
|
212
223
|
// Canvas is required for PNG encoding — skip in non-browser environments
|
|
213
224
|
if (typeof document === 'undefined') return [];
|
|
214
225
|
|
|
215
|
-
|
|
216
|
-
try {
|
|
217
|
-
pdfjsLib = (await import('pdfjs-dist/legacy/build/pdf.mjs')) as unknown as PdfjsLib & {
|
|
218
|
-
OPS?: Record<string, number>;
|
|
219
|
-
};
|
|
220
|
-
} catch {
|
|
221
|
-
pdfjsLib = (await import('pdfjs-dist')) as unknown as PdfjsLib & {
|
|
222
|
-
OPS?: Record<string, number>;
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
await applyWorkerConfig(pdfjsLib);
|
|
226
|
+
const { pdfjsLib, pdf } = loaded;
|
|
227
227
|
|
|
228
228
|
const OPS_paintImageXObject = pdfjsLib.OPS?.paintImageXObject ?? 85;
|
|
229
229
|
|
|
230
|
-
const loadingTask = pdfjsLib.getDocument({
|
|
231
|
-
data,
|
|
232
|
-
isEvalSupported: false,
|
|
233
|
-
useSystemFonts: true,
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const pdf = await loadingTask.promise;
|
|
237
230
|
const images: ExtractedImage[] = [];
|
|
238
231
|
let counter = 0;
|
|
239
232
|
|
|
@@ -501,6 +494,12 @@ interface PdfjsLib {
|
|
|
501
494
|
interface PdfjsDocument {
|
|
502
495
|
numPages: number;
|
|
503
496
|
getPage(pageNum: number): Promise<PdfjsPage>;
|
|
497
|
+
destroy?(): Promise<void>;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
interface LoadedPdf {
|
|
501
|
+
pdfjsLib: PdfjsLib & { OPS?: Record<string, number> };
|
|
502
|
+
pdf: PdfjsDocument;
|
|
504
503
|
}
|
|
505
504
|
|
|
506
505
|
interface PdfjsPage {
|
|
@@ -528,14 +527,18 @@ async function applyWorkerConfig(pdfjsLib: PdfjsLib): Promise<void> {
|
|
|
528
527
|
// Node.js the caller must have called configurePdfWorker() first.
|
|
529
528
|
}
|
|
530
529
|
|
|
531
|
-
async function
|
|
530
|
+
async function loadPdfDocument(data: Uint8Array): Promise<LoadedPdf> {
|
|
532
531
|
// Dynamic import — the legacy build bundles a fake-worker fallback
|
|
533
532
|
// that avoids a real Web Worker in environments that don't support it.
|
|
534
|
-
let pdfjsLib: PdfjsLib;
|
|
533
|
+
let pdfjsLib: PdfjsLib & { OPS?: Record<string, number> };
|
|
535
534
|
try {
|
|
536
|
-
pdfjsLib = (await import('pdfjs-dist/legacy/build/pdf.mjs')) as unknown as PdfjsLib
|
|
535
|
+
pdfjsLib = (await import('pdfjs-dist/legacy/build/pdf.mjs')) as unknown as PdfjsLib & {
|
|
536
|
+
OPS?: Record<string, number>;
|
|
537
|
+
};
|
|
537
538
|
} catch {
|
|
538
|
-
pdfjsLib = (await import('pdfjs-dist')) as unknown as PdfjsLib
|
|
539
|
+
pdfjsLib = (await import('pdfjs-dist')) as unknown as PdfjsLib & {
|
|
540
|
+
OPS?: Record<string, number>;
|
|
541
|
+
};
|
|
539
542
|
}
|
|
540
543
|
|
|
541
544
|
await applyWorkerConfig(pdfjsLib);
|
|
@@ -546,7 +549,10 @@ async function extractTextLines(data: Uint8Array): Promise<TextLine[]> {
|
|
|
546
549
|
useSystemFonts: true,
|
|
547
550
|
});
|
|
548
551
|
|
|
549
|
-
|
|
552
|
+
return { pdfjsLib, pdf: await loadingTask.promise };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
async function extractTextLines(pdf: PdfjsDocument): Promise<TextLine[]> {
|
|
550
556
|
const allLines: TextLine[] = [];
|
|
551
557
|
|
|
552
558
|
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
|
package/src/pptx/export.ts
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import type { Doc, Transition, TransitionDirection } from '@bendyline/squisq/schemas';
|
|
31
31
|
import { resolveFontFamily } from '@bendyline/squisq/schemas';
|
|
32
|
-
import type { Theme } from '@bendyline/squisq/schemas';
|
|
32
|
+
import type { Theme, ThemeRegistry } from '@bendyline/squisq/schemas';
|
|
33
33
|
import { docToMarkdown, resolveThemeForDoc } from '@bendyline/squisq/doc';
|
|
34
34
|
import type {
|
|
35
35
|
MarkdownDocument,
|
|
@@ -132,6 +132,8 @@ export interface PptxExportOptions {
|
|
|
132
132
|
* Falls back to the document's frontmatter `themeId` if not set here.
|
|
133
133
|
*/
|
|
134
134
|
themeId?: string;
|
|
135
|
+
/** Explicit caller-owned registry for non-document custom themes. */
|
|
136
|
+
themeRegistry?: ThemeRegistry;
|
|
135
137
|
/**
|
|
136
138
|
* Image data keyed by the path/URL used in markdown.
|
|
137
139
|
* When provided, images are embedded as picture shapes instead of
|
|
@@ -224,7 +226,7 @@ function resolveSlideStyle(
|
|
|
224
226
|
};
|
|
225
227
|
}
|
|
226
228
|
|
|
227
|
-
const theme: Theme = resolveThemeForDoc(doc, themeId);
|
|
229
|
+
const theme: Theme = resolveThemeForDoc(doc, themeId, options.themeRegistry);
|
|
228
230
|
const c = theme.colors;
|
|
229
231
|
|
|
230
232
|
return {
|
package/src/pptx/import.ts
CHANGED
|
@@ -7,9 +7,20 @@
|
|
|
7
7
|
* (or "Slide N"), the remaining text as a bullet list, and any slide tables
|
|
8
8
|
* (`<a:tbl>`) as markdown tables. Text lives in the DrawingML namespace
|
|
9
9
|
* (`a:p` / `a:r` / `a:t`) inside PresentationML shapes (`p:sp`).
|
|
10
|
+
*
|
|
11
|
+
* Theme + layout inference (default ON): the deck's theme part is compiled
|
|
12
|
+
* into a Squisq custom theme and carried in the returned document's
|
|
13
|
+
* frontmatter (`squisq-custom-themes` + `squisq-theme`); slide layouts are
|
|
14
|
+
* classified against the built-in template set, distinctive ones become
|
|
15
|
+
* `squisq-custom-templates` definitions, and each slide's heading is
|
|
16
|
+
* annotated (`{[templateName …]}`) per its layout's verdict. Pass
|
|
17
|
+
* `inferTheme: false` / `inferLayouts: false` for legacy plain imports.
|
|
18
|
+
* Inference never fails an import — extraction errors degrade to plain
|
|
19
|
+
* output with a console warning.
|
|
10
20
|
*/
|
|
11
21
|
|
|
12
22
|
import type {
|
|
23
|
+
HeadingTemplateAnnotation,
|
|
13
24
|
MarkdownBlockNode,
|
|
14
25
|
MarkdownDocument,
|
|
15
26
|
MarkdownImage,
|
|
@@ -19,14 +30,19 @@ import type {
|
|
|
19
30
|
MarkdownTableRow,
|
|
20
31
|
} from '@bendyline/squisq/markdown';
|
|
21
32
|
import { stringifyMarkdown } from '@bendyline/squisq/markdown';
|
|
33
|
+
import type { CustomTemplateDefinition, Theme } from '@bendyline/squisq/schemas';
|
|
22
34
|
import { getPartBinary, getPartRelationships, getPartXml, openPackage } from '../ooxml/reader.js';
|
|
35
|
+
import type { OoxmlOpenOptions } from '../ooxml/reader.js';
|
|
23
36
|
import type { OoxmlPackage } from '../ooxml/types.js';
|
|
24
37
|
import { NS_DRAWINGML, NS_PML, NS_R } from '../ooxml/namespaces.js';
|
|
38
|
+
import { attrNS, baseDirOf, resolveTarget } from '../ooxml/readUtils.js';
|
|
25
39
|
import type { ContentContainer } from '@bendyline/squisq/storage';
|
|
26
40
|
import { buildContainer } from '../shared/container.js';
|
|
27
41
|
import { extToMime } from '../shared/images.js';
|
|
42
|
+
import type { ExtractedFileTheme } from '../infer/types.js';
|
|
43
|
+
import type { AnalyzedLayout, PptxLayoutInference } from './layouts.js';
|
|
28
44
|
|
|
29
|
-
export interface PptxImportOptions {
|
|
45
|
+
export interface PptxImportOptions extends OoxmlOpenOptions {
|
|
30
46
|
/**
|
|
31
47
|
* Whether to extract embedded slide images into the document as image nodes
|
|
32
48
|
* (referencing `images/imageN.ext`). When false, pictures are ignored so the
|
|
@@ -34,6 +50,19 @@ export interface PptxImportOptions {
|
|
|
34
50
|
* `pptxToContainer` forces this on. Default: false.
|
|
35
51
|
*/
|
|
36
52
|
extractImages?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Infer a Squisq theme from the deck's theme part (colors + fonts) and
|
|
55
|
+
* carry it in the returned document's frontmatter (`squisq-custom-themes`
|
|
56
|
+
* + `squisq-theme`). Default: true — pass false for a legacy import with
|
|
57
|
+
* no frontmatter.
|
|
58
|
+
*/
|
|
59
|
+
inferTheme?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Derive custom layout templates from the deck's slide layouts/masters
|
|
62
|
+
* (`squisq-custom-templates` frontmatter) and annotate slide headings
|
|
63
|
+
* with matching built-in or generated templates. Default: true.
|
|
64
|
+
*/
|
|
65
|
+
inferLayouts?: boolean;
|
|
37
66
|
}
|
|
38
67
|
|
|
39
68
|
/**
|
|
@@ -46,20 +75,10 @@ interface ImportContext {
|
|
|
46
75
|
/** Collected image files: `images/imageN.ext` → { data, mimeType } */
|
|
47
76
|
extractedImages: Map<string, { data: ArrayBuffer; mimeType: string }>;
|
|
48
77
|
imageCounter: number;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function resolveTarget(baseDir: string, target: string): string {
|
|
56
|
-
if (target.startsWith('/')) return target.replace(/^\//, '');
|
|
57
|
-
const stack = baseDir ? baseDir.split('/') : [];
|
|
58
|
-
for (const seg of target.split('/')) {
|
|
59
|
-
if (seg === '..') stack.pop();
|
|
60
|
-
else if (seg !== '.') stack.push(seg);
|
|
61
|
-
}
|
|
62
|
-
return stack.join('/');
|
|
78
|
+
/** Layout analysis when `inferLayouts` is on. */
|
|
79
|
+
inference?: PptxLayoutInference;
|
|
80
|
+
/** Generated custom templates actually referenced by ≥1 slide annotation. */
|
|
81
|
+
usedCustomTemplates: Map<string, CustomTemplateDefinition>;
|
|
63
82
|
}
|
|
64
83
|
|
|
65
84
|
async function orderedSlidePaths(pkg: OoxmlPackage): Promise<string[]> {
|
|
@@ -114,12 +133,6 @@ function tableToMarkdown(tbl: Element): MarkdownTable {
|
|
|
114
133
|
return { type: 'table', children: rows };
|
|
115
134
|
}
|
|
116
135
|
|
|
117
|
-
/** Directory portion of a part path, e.g. `ppt/slides/slide1.xml` → `ppt/slides`. */
|
|
118
|
-
function baseDirOf(path: string): string {
|
|
119
|
-
const slash = path.lastIndexOf('/');
|
|
120
|
-
return slash === -1 ? '' : path.slice(0, slash);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
136
|
/**
|
|
124
137
|
* Extract every `<p:pic>` picture in a slide as an image node, reading the
|
|
125
138
|
* `<a:blip r:embed>` relationship, resolving it to the media part, and copying
|
|
@@ -168,6 +181,93 @@ async function extractSlideImages(
|
|
|
168
181
|
return images;
|
|
169
182
|
}
|
|
170
183
|
|
|
184
|
+
// ── Layout-verdict annotation building ───────────────────────────────
|
|
185
|
+
|
|
186
|
+
/** One non-title text shape on a slide, with its placeholder identity. */
|
|
187
|
+
interface SlideTextEntry {
|
|
188
|
+
rawType: string;
|
|
189
|
+
idx: number;
|
|
190
|
+
texts: string[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Annotation params live on a single heading line — flatten whitespace. */
|
|
194
|
+
function cleanParamText(text: string): string {
|
|
195
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Turn a slide's layout verdict into a heading annotation, downgrading to
|
|
200
|
+
* plain when the slide lacks the content the template needs (a bare
|
|
201
|
+
* annotation without its essential params renders an empty card in the
|
|
202
|
+
* player path, which never derives inputs for annotated blocks).
|
|
203
|
+
*/
|
|
204
|
+
function buildSlideAnnotation(
|
|
205
|
+
analyzed: AnalyzedLayout | undefined,
|
|
206
|
+
entries: SlideTextEntry[],
|
|
207
|
+
images: MarkdownImage[],
|
|
208
|
+
ctx: ImportContext,
|
|
209
|
+
): { annotation?: HeadingTemplateAnnotation; omitted: Set<SlideTextEntry> } {
|
|
210
|
+
const omitted = new Set<SlideTextEntry>();
|
|
211
|
+
if (!analyzed) return { omitted };
|
|
212
|
+
const verdict = analyzed.verdict;
|
|
213
|
+
|
|
214
|
+
if (verdict.kind === 'custom') {
|
|
215
|
+
ctx.usedCustomTemplates.set(verdict.def.name, verdict.def);
|
|
216
|
+
return { annotation: { template: verdict.def.name }, omitted };
|
|
217
|
+
}
|
|
218
|
+
if (verdict.kind !== 'builtin') return { omitted };
|
|
219
|
+
|
|
220
|
+
switch (verdict.paramSpec) {
|
|
221
|
+
case 'titleSubtitle': {
|
|
222
|
+
const sub = entries.find((e) => e.rawType === 'subTitle');
|
|
223
|
+
const subtitle = sub ? cleanParamText(sub.texts.join(' ')) : '';
|
|
224
|
+
// The subtitle moves into the card; keep it out of the bullets.
|
|
225
|
+
if (sub && subtitle) omitted.add(sub);
|
|
226
|
+
return {
|
|
227
|
+
annotation: {
|
|
228
|
+
template: verdict.template,
|
|
229
|
+
...(subtitle ? { params: { subtitle } } : {}),
|
|
230
|
+
},
|
|
231
|
+
omitted,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
case 'comparisonPairs': {
|
|
235
|
+
if (!verdict.columns) return { omitted };
|
|
236
|
+
const textAt = (idx: number | undefined): string[] =>
|
|
237
|
+
idx === undefined ? [] : (entries.find((e) => e.idx === idx)?.texts ?? []);
|
|
238
|
+
const side = (idxs: number[]): string => {
|
|
239
|
+
const header = cleanParamText(textAt(idxs[0])[0] ?? '');
|
|
240
|
+
const body = cleanParamText(textAt(idxs[1])[0] ?? '');
|
|
241
|
+
return header ? (body ? `${header}|${body}` : header) : '';
|
|
242
|
+
};
|
|
243
|
+
const left = side(verdict.columns.left);
|
|
244
|
+
const right = side(verdict.columns.right);
|
|
245
|
+
// twoColumn requires both labels — a bare annotation renders nothing.
|
|
246
|
+
if (!left || !right) return { omitted };
|
|
247
|
+
return { annotation: { template: 'twoColumn', params: { left, right } }, omitted };
|
|
248
|
+
}
|
|
249
|
+
case 'featureImage': {
|
|
250
|
+
const image = images[0];
|
|
251
|
+
if (!image) return { omitted };
|
|
252
|
+
const params: Record<string, string> = { imageSrc: image.url };
|
|
253
|
+
if (image.alt && image.alt !== 'Image') params.imageAlt = cleanParamText(image.alt);
|
|
254
|
+
return { annotation: { template: verdict.template, params }, omitted };
|
|
255
|
+
}
|
|
256
|
+
case 'photoGridGate': {
|
|
257
|
+
if (images.length < 2) return { omitted };
|
|
258
|
+
return {
|
|
259
|
+
annotation: {
|
|
260
|
+
template: 'photoGrid',
|
|
261
|
+
params: { images: images.map((i) => i.url).join(',') },
|
|
262
|
+
},
|
|
263
|
+
omitted,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
default:
|
|
267
|
+
return { annotation: { template: verdict.template }, omitted };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
171
271
|
async function convertSlide(
|
|
172
272
|
path: string,
|
|
173
273
|
index: number,
|
|
@@ -179,7 +279,7 @@ async function convertSlide(
|
|
|
179
279
|
const out: MarkdownBlockNode[] = [];
|
|
180
280
|
|
|
181
281
|
let title = '';
|
|
182
|
-
const
|
|
282
|
+
const entries: SlideTextEntry[] = [];
|
|
183
283
|
const shapes = doc.getElementsByTagNameNS(NS_PML, 'sp');
|
|
184
284
|
for (let s = 0; s < shapes.length; s++) {
|
|
185
285
|
const sp = shapes[s]!;
|
|
@@ -194,17 +294,32 @@ async function convertSlide(
|
|
|
194
294
|
if (texts.length === 0) continue;
|
|
195
295
|
if (isTitleShape(sp) && !title) {
|
|
196
296
|
title = texts.join(' ');
|
|
197
|
-
|
|
198
|
-
bullets.push(...texts);
|
|
297
|
+
continue;
|
|
199
298
|
}
|
|
299
|
+
const phs = sp.getElementsByTagNameNS(NS_PML, 'ph');
|
|
300
|
+
const rawType = phs.length ? (phs[0]!.getAttribute('type') ?? '') : '';
|
|
301
|
+
const idxRaw = phs.length ? phs[0]!.getAttribute('idx') : null;
|
|
302
|
+
const idx = idxRaw ? parseInt(idxRaw, 10) || 0 : 0;
|
|
303
|
+
entries.push({ rawType, idx, texts });
|
|
200
304
|
}
|
|
201
305
|
|
|
306
|
+
// Images are extracted before annotation building so feature/photoGrid
|
|
307
|
+
// params can reference their container paths; they still land after the
|
|
308
|
+
// bullet list in the output, unchanged.
|
|
309
|
+
const images = ctx.extractImages ? await extractSlideImages(doc, path, ctx) : [];
|
|
310
|
+
|
|
311
|
+
const layoutPath = ctx.inference?.layoutPathBySlide.get(path);
|
|
312
|
+
const analyzed = layoutPath ? ctx.inference?.byLayoutPath.get(layoutPath) : undefined;
|
|
313
|
+
const { annotation, omitted } = buildSlideAnnotation(analyzed, entries, images, ctx);
|
|
314
|
+
|
|
202
315
|
out.push({
|
|
203
316
|
type: 'heading',
|
|
204
317
|
depth: 2,
|
|
205
318
|
children: [{ type: 'text', value: title || `Slide ${index + 1}` }],
|
|
319
|
+
...(annotation ? { templateAnnotation: annotation } : {}),
|
|
206
320
|
});
|
|
207
321
|
|
|
322
|
+
const bullets = entries.filter((e) => !omitted.has(e)).flatMap((e) => e.texts);
|
|
208
323
|
if (bullets.length > 0) {
|
|
209
324
|
const items: MarkdownListItem[] = bullets.map((text) => ({
|
|
210
325
|
type: 'listItem',
|
|
@@ -214,11 +329,8 @@ async function convertSlide(
|
|
|
214
329
|
}
|
|
215
330
|
|
|
216
331
|
// Embedded pictures land after the bullet list and before any tables.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
for (const image of images) {
|
|
220
|
-
out.push({ type: 'paragraph', children: [image] });
|
|
221
|
-
}
|
|
332
|
+
for (const image of images) {
|
|
333
|
+
out.push({ type: 'paragraph', children: [image] });
|
|
222
334
|
}
|
|
223
335
|
|
|
224
336
|
const tbls = doc.getElementsByTagNameNS(NS_DRAWINGML, 'tbl');
|
|
@@ -227,6 +339,11 @@ async function convertSlide(
|
|
|
227
339
|
return out;
|
|
228
340
|
}
|
|
229
341
|
|
|
342
|
+
function warnInferenceFailure(step: string, err: unknown): void {
|
|
343
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
344
|
+
console.warn(`pptx import: ${step} failed; importing without it — ${message}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
230
347
|
async function importDocument(
|
|
231
348
|
pkg: OoxmlPackage,
|
|
232
349
|
options: PptxImportOptions,
|
|
@@ -236,20 +353,85 @@ async function importDocument(
|
|
|
236
353
|
extractImages: options.extractImages ?? false,
|
|
237
354
|
extractedImages: new Map(),
|
|
238
355
|
imageCounter: 0,
|
|
356
|
+
usedCustomTemplates: new Map(),
|
|
239
357
|
};
|
|
358
|
+
|
|
359
|
+
const inferTheme = options.inferTheme !== false;
|
|
360
|
+
const inferLayouts = options.inferLayouts !== false;
|
|
361
|
+
|
|
362
|
+
// All inference modules load lazily so a plain import stays light, and
|
|
363
|
+
// every inference step degrades to a warning rather than failing the import.
|
|
364
|
+
let extraction: ExtractedFileTheme | null = null;
|
|
365
|
+
if (inferTheme || inferLayouts) {
|
|
366
|
+
try {
|
|
367
|
+
const { extractPptxTheme } = await import('../infer/extract.js');
|
|
368
|
+
extraction = await extractPptxTheme(pkg);
|
|
369
|
+
} catch (err: unknown) {
|
|
370
|
+
warnInferenceFailure('theme extraction', err);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let theme: Theme | undefined;
|
|
375
|
+
if (inferTheme && extraction) {
|
|
376
|
+
try {
|
|
377
|
+
const { compileExtractedTheme } = await import('../infer/mapTheme.js');
|
|
378
|
+
theme = compileExtractedTheme(extraction).theme;
|
|
379
|
+
} catch (err: unknown) {
|
|
380
|
+
warnInferenceFailure('theme compilation', err);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (inferLayouts) {
|
|
385
|
+
try {
|
|
386
|
+
const { analyzePptxLayouts } = await import('./layouts.js');
|
|
387
|
+
const { colorHintsFromExtraction } = await import('../infer/mapTheme.js');
|
|
388
|
+
ctx.inference = await analyzePptxLayouts(pkg, {
|
|
389
|
+
colors: extraction ? colorHintsFromExtraction(extraction) : {},
|
|
390
|
+
});
|
|
391
|
+
} catch (err: unknown) {
|
|
392
|
+
warnInferenceFailure('layout inference', err);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
240
396
|
const paths = await orderedSlidePaths(pkg);
|
|
241
397
|
const children: MarkdownBlockNode[] = [];
|
|
242
398
|
for (let i = 0; i < paths.length; i++) {
|
|
243
399
|
children.push(...(await convertSlide(paths[i]!, i, ctx)));
|
|
244
400
|
}
|
|
245
|
-
|
|
401
|
+
const doc: MarkdownDocument = { type: 'document', children };
|
|
402
|
+
|
|
403
|
+
const frontmatter: Record<string, unknown> = {};
|
|
404
|
+
if (theme || ctx.usedCustomTemplates.size > 0) {
|
|
405
|
+
const {
|
|
406
|
+
writeCustomThemesToFrontmatter,
|
|
407
|
+
writeCustomTemplatesToFrontmatter,
|
|
408
|
+
FRONTMATTER_CUSTOM_THEMES_KEY,
|
|
409
|
+
FRONTMATTER_CUSTOM_TEMPLATES_KEY,
|
|
410
|
+
} = await import('@bendyline/squisq/doc');
|
|
411
|
+
if (theme) {
|
|
412
|
+
const payload = writeCustomThemesToFrontmatter([theme]);
|
|
413
|
+
if (payload) {
|
|
414
|
+
frontmatter[FRONTMATTER_CUSTOM_THEMES_KEY] = payload;
|
|
415
|
+
// The doc-level selector `resolveThemeForDoc` reads — activates the
|
|
416
|
+
// inferred theme without any global registration.
|
|
417
|
+
frontmatter['squisq-theme'] = theme.id;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (ctx.usedCustomTemplates.size > 0) {
|
|
421
|
+
const payload = writeCustomTemplatesToFrontmatter([...ctx.usedCustomTemplates.values()]);
|
|
422
|
+
if (payload) frontmatter[FRONTMATTER_CUSTOM_TEMPLATES_KEY] = payload;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (Object.keys(frontmatter).length > 0) doc.frontmatter = frontmatter;
|
|
426
|
+
|
|
427
|
+
return { doc, ctx };
|
|
246
428
|
}
|
|
247
429
|
|
|
248
430
|
export async function pptxToMarkdownDoc(
|
|
249
431
|
data: ArrayBuffer | Blob,
|
|
250
432
|
options: PptxImportOptions = {},
|
|
251
433
|
): Promise<MarkdownDocument> {
|
|
252
|
-
const pkg = await openPackage(data);
|
|
434
|
+
const pkg = await openPackage(data, options);
|
|
253
435
|
const { doc } = await importDocument(pkg, options);
|
|
254
436
|
return doc;
|
|
255
437
|
}
|
|
@@ -266,7 +448,7 @@ export async function pptxToContainer(
|
|
|
266
448
|
data: ArrayBuffer | Blob,
|
|
267
449
|
options: PptxImportOptions = {},
|
|
268
450
|
): Promise<ContentContainer> {
|
|
269
|
-
const pkg = await openPackage(data);
|
|
451
|
+
const pkg = await openPackage(data, options);
|
|
270
452
|
const { doc, ctx } = await importDocument(pkg, { ...options, extractImages: true });
|
|
271
453
|
|
|
272
454
|
return buildContainer(stringifyMarkdown(doc), ctx.extractedImages);
|
package/src/pptx/index.ts
CHANGED
|
@@ -27,6 +27,20 @@ import { type PptxImportOptions, pptxToMarkdownDoc } from './import.js';
|
|
|
27
27
|
export type { PptxImportOptions } from './import.js';
|
|
28
28
|
export { pptxToMarkdownDoc, pptxToContainer } from './import.js';
|
|
29
29
|
|
|
30
|
+
// Layout inference (used by the PPTX importer and the theme dialog)
|
|
31
|
+
export { analyzePptxLayouts, inspectPptxLayouts } from './layouts.js';
|
|
32
|
+
export type {
|
|
33
|
+
AnalyzedLayout,
|
|
34
|
+
AnalyzePptxLayoutsOptions,
|
|
35
|
+
ExtractedPlaceholder,
|
|
36
|
+
ExtractedSlideLayout,
|
|
37
|
+
InspectPptxLayoutsOptions,
|
|
38
|
+
LayoutVerdict,
|
|
39
|
+
PptxColorHints,
|
|
40
|
+
PptxLayoutInference,
|
|
41
|
+
PptxLayoutSummary,
|
|
42
|
+
} from './layouts.js';
|
|
43
|
+
|
|
30
44
|
/**
|
|
31
45
|
* Convert a .pptx file to a squisq Doc (via the markdown model).
|
|
32
46
|
*/
|