@bendyline/squisq-formats 1.4.0 → 1.4.2

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.
Files changed (83) hide show
  1. package/README.md +180 -43
  2. package/dist/{chunk-CRTC6DEX.js → chunk-46FMDOWZ.js} +2 -2
  3. package/dist/{chunk-ON232HJR.js → chunk-5LSSWZPU.js} +2 -2
  4. package/dist/{chunk-APSIAEXL.js → chunk-D7IFPWNZ.js} +87 -43
  5. package/dist/chunk-D7IFPWNZ.js.map +1 -0
  6. package/dist/chunk-DQAZR57U.js +33 -0
  7. package/dist/chunk-DQAZR57U.js.map +1 -0
  8. package/dist/chunk-EHLGMSTE.js +273 -0
  9. package/dist/chunk-EHLGMSTE.js.map +1 -0
  10. package/dist/{chunk-A3FHLTY5.js → chunk-MJGRI6XR.js} +6 -19
  11. package/dist/chunk-MJGRI6XR.js.map +1 -0
  12. package/dist/{chunk-JBNVE2GV.js → chunk-RFAPOKHJ.js} +9 -2
  13. package/dist/chunk-RFAPOKHJ.js.map +1 -0
  14. package/dist/chunk-RTS5XBZ7.js +526 -0
  15. package/dist/chunk-RTS5XBZ7.js.map +1 -0
  16. package/dist/{chunk-FMOIGA5T.js → chunk-SHLPSADL.js} +178 -5
  17. package/dist/chunk-SHLPSADL.js.map +1 -0
  18. package/dist/chunk-SSUPBUF5.js +15 -0
  19. package/dist/chunk-SSUPBUF5.js.map +1 -0
  20. package/dist/{chunk-BJQIJ4S4.js → chunk-WWBIKRNP.js} +17 -41
  21. package/dist/chunk-WWBIKRNP.js.map +1 -0
  22. package/dist/{chunk-I2GFTEOV.js → chunk-YESS7VY3.js} +77 -15
  23. package/dist/{chunk-I2GFTEOV.js.map → chunk-YESS7VY3.js.map} +1 -1
  24. package/dist/csv/index.d.ts +14 -1
  25. package/dist/csv/index.js +1 -1
  26. package/dist/docx/index.js +4 -3
  27. package/dist/epub/index.js +3 -2
  28. package/dist/html/index.d.ts +3 -0
  29. package/dist/html/index.js +3 -2
  30. package/dist/index.d.ts +1 -0
  31. package/dist/index.js +26 -11
  32. package/dist/ooxml/index.d.ts +5 -1
  33. package/dist/ooxml/index.js +16 -12
  34. package/dist/pdf/index.js +2 -1
  35. package/dist/pptx/index.d.ts +18 -3
  36. package/dist/pptx/index.js +7 -4
  37. package/dist/registry/index.d.ts +167 -0
  38. package/dist/registry/index.js +17 -0
  39. package/dist/registry/index.js.map +1 -0
  40. package/dist/xlsx/index.d.ts +44 -14
  41. package/dist/xlsx/index.js +4 -2
  42. package/package.json +7 -2
  43. package/src/__tests__/convert.test.ts +186 -0
  44. package/src/__tests__/csvImport.test.ts +34 -1
  45. package/src/__tests__/formatRegistry.test.ts +174 -0
  46. package/src/__tests__/lossyWarnings.test.ts +105 -0
  47. package/src/__tests__/pdfImport.test.ts +95 -1
  48. package/src/__tests__/pptxImport.test.ts +46 -1
  49. package/src/__tests__/roundTripAssets.test.ts +50 -0
  50. package/src/__tests__/roundTripMatrix.test.ts +10 -4
  51. package/src/__tests__/sharedContainer.test.ts +41 -0
  52. package/src/__tests__/sharedImages.test.ts +61 -0
  53. package/src/__tests__/xlsxExport.test.ts +164 -0
  54. package/src/csv/index.ts +25 -2
  55. package/src/docx/import.ts +6 -37
  56. package/src/html/imageUtils.ts +5 -19
  57. package/src/index.ts +29 -5
  58. package/src/ooxml/index.ts +3 -0
  59. package/src/ooxml/namespaces.ts +10 -0
  60. package/src/pdf/import.ts +145 -50
  61. package/src/pptx/import.ts +127 -10
  62. package/src/pptx/index.ts +1 -1
  63. package/src/registry/convert.ts +318 -0
  64. package/src/registry/defaultFormats.ts +323 -0
  65. package/src/registry/errors.ts +46 -0
  66. package/src/registry/index.ts +38 -0
  67. package/src/registry/registry.ts +48 -0
  68. package/src/registry/types.ts +107 -0
  69. package/src/shared/container.ts +28 -0
  70. package/src/shared/images.ts +44 -0
  71. package/src/xlsx/export.ts +253 -0
  72. package/src/xlsx/index.ts +10 -36
  73. package/dist/chunk-A3FHLTY5.js.map +0 -1
  74. package/dist/chunk-APSIAEXL.js.map +0 -1
  75. package/dist/chunk-BJQIJ4S4.js.map +0 -1
  76. package/dist/chunk-FMOIGA5T.js.map +0 -1
  77. package/dist/chunk-JBNVE2GV.js.map +0 -1
  78. package/dist/chunk-L53YIGYS.js +0 -141
  79. package/dist/chunk-L53YIGYS.js.map +0 -1
  80. package/dist/chunk-QSE7EWE7.js +0 -181
  81. package/dist/chunk-QSE7EWE7.js.map +0 -1
  82. /package/dist/{chunk-CRTC6DEX.js.map → chunk-46FMDOWZ.js.map} +0 -0
  83. /package/dist/{chunk-ON232HJR.js.map → chunk-5LSSWZPU.js.map} +0 -0
package/src/pdf/import.ts CHANGED
@@ -44,8 +44,8 @@ import type {
44
44
  MarkdownImage,
45
45
  } from '@bendyline/squisq/markdown';
46
46
 
47
- import { MemoryContentContainer } from '@bendyline/squisq/storage';
48
47
  import type { ContentContainer } from '@bendyline/squisq/storage';
48
+ import { buildContainer } from '../shared/container.js';
49
49
 
50
50
  import {
51
51
  DEFAULT_FONT_SIZE,
@@ -160,30 +160,44 @@ export async function pdfToContainer(
160
160
  const textLines = await extractTextLines(bytes);
161
161
  const images = await extractImages(bytes);
162
162
 
163
+ // Under Node there is no DOM/canvas, so extractImages() returns [] via its
164
+ // environment guard and embedded images can't be decoded. The registry's
165
+ // `importContainer` wrapper surfaces this (typeof document === 'undefined')
166
+ // as a ConversionResult warning; direct callers of pdfToContainer get no
167
+ // signal, which is an accepted trade-off for the degraded Node path.
168
+
163
169
  const bodySize = options.bodyFontSize ?? detectBodyFontSize(textLines);
164
170
 
165
- // If we have images, insert image references into the block list
166
- let blocks = classifyLines(textLines, bodySize, options);
171
+ // Classify text into blocks, capturing the originating page of each block
172
+ // (parallel array) so images can be placed on their own page rather than
173
+ // dumped at the document end.
174
+ const blockPages: number[] = [];
175
+ let blocks = classifyLines(textLines, bodySize, options, blockPages);
167
176
  if (images.length > 0) {
168
- blocks = insertImageBlocks(blocks, images);
177
+ blocks = insertImageBlocks(blocks, blockPages, images);
169
178
  }
170
179
 
171
180
  const markdownDoc: MarkdownDocument = { type: 'document', children: blocks };
172
181
 
173
- const markdown = stringifyMarkdown(markdownDoc);
174
-
175
- const container = new MemoryContentContainer();
176
- await container.writeDocument(markdown);
177
-
178
- for (const img of images) {
179
- await container.writeFile(img.path, new Uint8Array(img.data), 'image/png');
180
- }
181
-
182
- return container;
182
+ // pdf image extraction only ever produces PNG (canvas re-encode), so every
183
+ // entry gets image/png.
184
+ return buildContainer(
185
+ stringifyMarkdown(markdownDoc),
186
+ images.map((img) => [img.path, { data: img.data, mimeType: 'image/png' }] as const),
187
+ );
183
188
  }
184
189
 
185
- /** Extracted image with position info for placement. */
186
- interface ExtractedImage {
190
+ /**
191
+ * Extracted image with position info for placement.
192
+ *
193
+ * `page` is captured reliably from the operator-list walk. `y` is currently
194
+ * always 0 — recovering a real y within the page would require tracking the
195
+ * current transformation matrix (CTM) during the paint operator, which is out
196
+ * of scope here (see the placement note in `insertImageBlocks`).
197
+ *
198
+ * Exported for direct unit testing of `insertImageBlocks`.
199
+ */
200
+ export interface ExtractedImage {
187
201
  path: string;
188
202
  data: ArrayBuffer;
189
203
  page: number;
@@ -328,29 +342,86 @@ function imageDataToPng(img: PdfjsImageData): ArrayBuffer | null {
328
342
  }
329
343
  }
330
344
 
345
+ /** Wrap an extracted image as a paragraph containing an image node. */
346
+ function imageParagraph(img: ExtractedImage): MarkdownParagraph {
347
+ const imgNode: MarkdownImage = {
348
+ type: 'image',
349
+ url: img.path,
350
+ alt: `Image ${img.path.replace('images/image', '').replace('.png', '')}`,
351
+ };
352
+ return { type: 'paragraph', children: [imgNode] };
353
+ }
354
+
331
355
  /**
332
- * Insert image reference blocks between text blocks.
333
- * Places each image after the last text block on the same page (or at the end).
356
+ * Insert image reference blocks among the text blocks, page by page.
357
+ *
358
+ * Each image is placed immediately after the LAST content block that
359
+ * originated from the same page (per the parallel `blockPages` array).
360
+ * Images on a page that produced no text blocks (e.g. an image-only page)
361
+ * fall back to the last block of the nearest preceding page that did; if no
362
+ * such page exists, they are appended at the document end.
363
+ *
364
+ * Placement is intentionally page-level only. Ordering *within* a page follows
365
+ * the extraction order of the images (roughly the paint-operator order) — real
366
+ * vertical (y) ordering within a page is future work, since `ExtractedImage.y`
367
+ * is not yet populated (it needs CTM tracking during the paint operator).
368
+ *
369
+ * @param blocks - The classified content blocks, in document order.
370
+ * @param blockPages - Parallel array: `blockPages[i]` is the 0-based page that
371
+ * `blocks[i]` came from. Must be the same length as `blocks`.
372
+ * @param images - Extracted images with a reliable `page` field.
334
373
  */
335
- function insertImageBlocks(
374
+ export function insertImageBlocks(
336
375
  blocks: MarkdownBlockNode[],
376
+ blockPages: number[],
337
377
  images: ExtractedImage[],
338
378
  ): MarkdownBlockNode[] {
339
379
  if (images.length === 0) return blocks;
340
380
 
341
- // Simple strategy: append all images at the end as paragraphs
342
- const result = [...blocks];
381
+ // No text blocks at all every image simply appends in order.
382
+ if (blocks.length === 0) {
383
+ return images.map(imageParagraph);
384
+ }
385
+
386
+ // Map each page → index of its LAST block, and remember which pages have
387
+ // blocks (sorted ascending) for the image-only-page fallback lookup.
388
+ const lastBlockIndexByPage = new Map<number, number>();
389
+ for (let i = 0; i < blocks.length; i++) {
390
+ lastBlockIndexByPage.set(blockPages[i], i);
391
+ }
392
+ const pagesWithBlocks = [...lastBlockIndexByPage.keys()].sort((a, b) => a - b);
393
+
394
+ const lastIndex = blocks.length - 1;
395
+
396
+ /** Resolve the block index after which an image on `page` should be inserted. */
397
+ const anchorFor = (page: number): number => {
398
+ const direct = lastBlockIndexByPage.get(page);
399
+ if (direct !== undefined) return direct;
400
+ // Image-only page: fall back to the nearest preceding page with blocks.
401
+ let anchor = -1;
402
+ for (const p of pagesWithBlocks) {
403
+ if (p < page) anchor = lastBlockIndexByPage.get(p)!;
404
+ else break;
405
+ }
406
+ // No preceding page with blocks → append at the document end.
407
+ return anchor === -1 ? lastIndex : anchor;
408
+ };
409
+
410
+ // Group image paragraphs by the block index they should follow, preserving
411
+ // image order within each group.
412
+ const insertAfter = new Map<number, MarkdownParagraph[]>();
343
413
  for (const img of images) {
344
- const imgNode: MarkdownImage = {
345
- type: 'image',
346
- url: img.path,
347
- alt: `Image ${img.path.replace('images/image', '').replace('.png', '')}`,
348
- };
349
- const para: MarkdownParagraph = {
350
- type: 'paragraph',
351
- children: [imgNode],
352
- };
353
- result.push(para);
414
+ const anchor = anchorFor(img.page);
415
+ const group = insertAfter.get(anchor);
416
+ if (group) group.push(imageParagraph(img));
417
+ else insertAfter.set(anchor, [imageParagraph(img)]);
418
+ }
419
+
420
+ const result: MarkdownBlockNode[] = [];
421
+ for (let i = 0; i < blocks.length; i++) {
422
+ result.push(blocks[i]);
423
+ const imgs = insertAfter.get(i);
424
+ if (imgs) result.push(...imgs);
354
425
  }
355
426
  return result;
356
427
  }
@@ -592,8 +663,20 @@ function classifyLines(
592
663
  lines: TextLine[],
593
664
  bodySize: number,
594
665
  options: PdfImportOptions,
666
+ /**
667
+ * Optional out-parameter: when provided, receives one entry per produced
668
+ * block giving the 0-based page that block originated from. Kept parallel
669
+ * to the returned block array so image placement can be page-aware without
670
+ * mutating the (strictly-typed) block nodes themselves.
671
+ */
672
+ blockPages?: number[],
595
673
  ): MarkdownBlockNode[] {
596
674
  const blocks: MarkdownBlockNode[] = [];
675
+ /** Push a block and record its originating page in the parallel array. */
676
+ const pushBlock = (block: MarkdownBlockNode, page: number): void => {
677
+ blocks.push(block);
678
+ if (blockPages) blockPages.push(page);
679
+ };
597
680
  const detectTables = options.detectTables !== false;
598
681
  const detectCodeBlocks = options.detectCodeBlocks !== false;
599
682
  const detectBlockquotes = options.detectBlockquotes !== false;
@@ -610,11 +693,14 @@ function classifyLines(
610
693
  // --- Heading detection ---
611
694
  if (line.fontSize >= IMPORT_HEADING_MIN_SIZE && line.fontSize > bodySize + 1) {
612
695
  const depth = sizeToHeadingDepth(line.fontSize);
613
- blocks.push({
614
- type: 'heading',
615
- depth,
616
- children: buildInlineNodes(line, options),
617
- } as MarkdownHeading);
696
+ pushBlock(
697
+ {
698
+ type: 'heading',
699
+ depth,
700
+ children: buildInlineNodes(line, options),
701
+ } as MarkdownHeading,
702
+ line.page,
703
+ );
618
704
  i++;
619
705
  continue;
620
706
  }
@@ -626,10 +712,13 @@ function classifyLines(
626
712
  codeLines.push(lines[i].text);
627
713
  i++;
628
714
  }
629
- blocks.push({
630
- type: 'code',
631
- value: codeLines.join('\n'),
632
- } as MarkdownCodeBlock);
715
+ pushBlock(
716
+ {
717
+ type: 'code',
718
+ value: codeLines.join('\n'),
719
+ } as MarkdownCodeBlock,
720
+ line.page,
721
+ );
633
722
  continue;
634
723
  }
635
724
 
@@ -639,7 +728,7 @@ function classifyLines(
639
728
  if (tableLines > 0) {
640
729
  const table = buildTable(lines.slice(i, i + tableLines), options);
641
730
  if (table) {
642
- blocks.push(table);
731
+ pushBlock(table, line.page);
643
732
  i += tableLines;
644
733
  continue;
645
734
  }
@@ -651,7 +740,7 @@ function classifyLines(
651
740
  const orderedMatch = line.text.match(IMPORT_ORDERED_PREFIX);
652
741
  if (bulletMatch || orderedMatch) {
653
742
  const listResult = consumeList(lines, i, typicalLeftMargin, bodySize, options);
654
- blocks.push(listResult.list);
743
+ pushBlock(listResult.list, line.page);
655
744
  i = listResult.nextIndex;
656
745
  continue;
657
746
  }
@@ -675,10 +764,13 @@ function classifyLines(
675
764
  children: buildInlineNodes(ql, options),
676
765
  }) as MarkdownParagraph,
677
766
  );
678
- blocks.push({
679
- type: 'blockquote',
680
- children: quoteBlocks,
681
- } as MarkdownBlockquote);
767
+ pushBlock(
768
+ {
769
+ type: 'blockquote',
770
+ children: quoteBlocks,
771
+ } as MarkdownBlockquote,
772
+ line.page,
773
+ );
682
774
  continue;
683
775
  }
684
776
 
@@ -721,10 +813,13 @@ function classifyLines(
721
813
  }
722
814
 
723
815
  if (allInlines.length > 0) {
724
- blocks.push({
725
- type: 'paragraph',
726
- children: mergeAdjacentText(allInlines),
727
- } as MarkdownParagraph);
816
+ pushBlock(
817
+ {
818
+ type: 'paragraph',
819
+ children: mergeAdjacentText(allInlines),
820
+ } as MarkdownParagraph,
821
+ line.page,
822
+ );
728
823
  }
729
824
  }
730
825
 
@@ -12,20 +12,42 @@
12
12
  import type {
13
13
  MarkdownBlockNode,
14
14
  MarkdownDocument,
15
+ MarkdownImage,
15
16
  MarkdownListItem,
16
17
  MarkdownTable,
17
18
  MarkdownTableCell,
18
19
  MarkdownTableRow,
19
20
  } from '@bendyline/squisq/markdown';
20
- import { getPartRelationships, getPartXml, openPackage } from '../ooxml/reader.js';
21
+ import { stringifyMarkdown } from '@bendyline/squisq/markdown';
22
+ import { getPartBinary, getPartRelationships, getPartXml, openPackage } from '../ooxml/reader.js';
21
23
  import type { OoxmlPackage } from '../ooxml/types.js';
22
24
  import { NS_DRAWINGML, NS_PML, NS_R } from '../ooxml/namespaces.js';
25
+ import type { ContentContainer } from '@bendyline/squisq/storage';
26
+ import { buildContainer } from '../shared/container.js';
27
+ import { extToMime } from '../shared/images.js';
23
28
 
24
29
  export interface PptxImportOptions {
25
- /** Whether to extract embedded images as data URIs (not yet used). */
30
+ /**
31
+ * Whether to extract embedded slide images into the document as image nodes
32
+ * (referencing `images/imageN.ext`). When false, pictures are ignored so the
33
+ * markdown never carries dangling image references with no backing container.
34
+ * `pptxToContainer` forces this on. Default: false.
35
+ */
26
36
  extractImages?: boolean;
27
37
  }
28
38
 
39
+ /**
40
+ * Per-import mutable state used to collect embedded images across slides.
41
+ * Mirrors the docx import context's `extractedImages` / `imageCounter`.
42
+ */
43
+ interface ImportContext {
44
+ pkg: OoxmlPackage;
45
+ extractImages: boolean;
46
+ /** Collected image files: `images/imageN.ext` → { data, mimeType } */
47
+ extractedImages: Map<string, { data: ArrayBuffer; mimeType: string }>;
48
+ imageCounter: number;
49
+ }
50
+
29
51
  function attrNS(el: Element, ns: string, local: string, fallback: string): string | null {
30
52
  return el.getAttributeNS(ns, local) ?? el.getAttribute(fallback);
31
53
  }
@@ -92,11 +114,66 @@ function tableToMarkdown(tbl: Element): MarkdownTable {
92
114
  return { type: 'table', children: rows };
93
115
  }
94
116
 
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
+ /**
124
+ * Extract every `<p:pic>` picture in a slide as an image node, reading the
125
+ * `<a:blip r:embed>` relationship, resolving it to the media part, and copying
126
+ * the bytes into `ctx.extractedImages` under `images/imageN.ext`.
127
+ */
128
+ async function extractSlideImages(
129
+ doc: Document,
130
+ slidePath: string,
131
+ ctx: ImportContext,
132
+ ): Promise<MarkdownImage[]> {
133
+ const rels = await getPartRelationships(ctx.pkg, slidePath);
134
+ const relById = new Map(rels.map((r) => [r.id, r.target]));
135
+ const baseDir = baseDirOf(slidePath);
136
+
137
+ const images: MarkdownImage[] = [];
138
+ const pics = doc.getElementsByTagNameNS(NS_PML, 'pic');
139
+ for (let i = 0; i < pics.length; i++) {
140
+ const pic = pics[i]!;
141
+ const blips = pic.getElementsByTagNameNS(NS_DRAWINGML, 'blip');
142
+ if (!blips.length) continue;
143
+ const embed = attrNS(blips[0]!, NS_R, 'embed', 'r:embed');
144
+ if (!embed) continue;
145
+ const target = relById.get(embed);
146
+ if (!target) continue;
147
+
148
+ const mediaPath = resolveTarget(baseDir, target);
149
+ const data = await getPartBinary(ctx.pkg, mediaPath);
150
+ if (!data) continue;
151
+
152
+ const dot = mediaPath.lastIndexOf('.');
153
+ const ext = dot !== -1 ? mediaPath.slice(dot).toLowerCase() : '.png';
154
+ const mimeType = extToMime(ext);
155
+
156
+ ctx.imageCounter++;
157
+ const imagePath = `images/image${ctx.imageCounter}${ext}`;
158
+ ctx.extractedImages.set(imagePath, { data, mimeType });
159
+
160
+ // Alt text from the picture's non-visual properties (descr, then name).
161
+ const cNvPrs = pic.getElementsByTagNameNS(NS_PML, 'cNvPr');
162
+ const alt =
163
+ (cNvPrs.length ? cNvPrs[0]!.getAttribute('descr') || cNvPrs[0]!.getAttribute('name') : '') ||
164
+ 'Image';
165
+
166
+ images.push({ type: 'image', url: imagePath, alt });
167
+ }
168
+ return images;
169
+ }
170
+
95
171
  async function convertSlide(
96
- pkg: OoxmlPackage,
97
172
  path: string,
98
173
  index: number,
174
+ ctx: ImportContext,
99
175
  ): Promise<MarkdownBlockNode[]> {
176
+ const pkg = ctx.pkg;
100
177
  const doc = await getPartXml(pkg, path);
101
178
  if (!doc) return [];
102
179
  const out: MarkdownBlockNode[] = [];
@@ -136,21 +213,61 @@ async function convertSlide(
136
213
  out.push({ type: 'list', ordered: false, children: items });
137
214
  }
138
215
 
216
+ // Embedded pictures land after the bullet list and before any tables.
217
+ if (ctx.extractImages) {
218
+ const images = await extractSlideImages(doc, path, ctx);
219
+ for (const image of images) {
220
+ out.push({ type: 'paragraph', children: [image] });
221
+ }
222
+ }
223
+
139
224
  const tbls = doc.getElementsByTagNameNS(NS_DRAWINGML, 'tbl');
140
225
  for (let t = 0; t < tbls.length; t++) out.push(tableToMarkdown(tbls[t]!));
141
226
 
142
227
  return out;
143
228
  }
144
229
 
145
- export async function pptxToMarkdownDoc(
146
- data: ArrayBuffer | Blob,
147
- _options: PptxImportOptions = {},
148
- ): Promise<MarkdownDocument> {
149
- const pkg = await openPackage(data);
230
+ async function importDocument(
231
+ pkg: OoxmlPackage,
232
+ options: PptxImportOptions,
233
+ ): Promise<{ doc: MarkdownDocument; ctx: ImportContext }> {
234
+ const ctx: ImportContext = {
235
+ pkg,
236
+ extractImages: options.extractImages ?? false,
237
+ extractedImages: new Map(),
238
+ imageCounter: 0,
239
+ };
150
240
  const paths = await orderedSlidePaths(pkg);
151
241
  const children: MarkdownBlockNode[] = [];
152
242
  for (let i = 0; i < paths.length; i++) {
153
- children.push(...(await convertSlide(pkg, paths[i]!, i)));
243
+ children.push(...(await convertSlide(paths[i]!, i, ctx)));
154
244
  }
155
- return { type: 'document', children };
245
+ return { doc: { type: 'document', children }, ctx };
246
+ }
247
+
248
+ export async function pptxToMarkdownDoc(
249
+ data: ArrayBuffer | Blob,
250
+ options: PptxImportOptions = {},
251
+ ): Promise<MarkdownDocument> {
252
+ const pkg = await openPackage(data);
253
+ const { doc } = await importDocument(pkg, options);
254
+ return doc;
255
+ }
256
+
257
+ /**
258
+ * Convert a .pptx file to a ContentContainer with markdown + extracted images.
259
+ *
260
+ * The container holds the primary markdown document plus every embedded slide
261
+ * image under `images/` (e.g. `images/image1.png`). Image extraction is always
262
+ * forced on here so the markdown's image references resolve inside the
263
+ * container. Mirrors `docxToContainer`.
264
+ */
265
+ export async function pptxToContainer(
266
+ data: ArrayBuffer | Blob,
267
+ options: PptxImportOptions = {},
268
+ ): Promise<ContentContainer> {
269
+ const pkg = await openPackage(data);
270
+ const { doc, ctx } = await importDocument(pkg, { ...options, extractImages: true });
271
+
272
+ return buildContainer(stringifyMarkdown(doc), ctx.extractedImages);
156
273
  }
package/src/pptx/index.ts CHANGED
@@ -25,7 +25,7 @@ import type { Doc } from '@bendyline/squisq/schemas';
25
25
  import { type PptxImportOptions, pptxToMarkdownDoc } from './import.js';
26
26
 
27
27
  export type { PptxImportOptions } from './import.js';
28
- export { pptxToMarkdownDoc } from './import.js';
28
+ export { pptxToMarkdownDoc, pptxToContainer } from './import.js';
29
29
 
30
30
  /**
31
31
  * Convert a .pptx file to a squisq Doc (via the markdown model).