@bendyline/squisq-formats 2.6.14 → 2.6.15

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 (34) hide show
  1. package/README.md +41 -13
  2. package/dist/{chunk-MIK4NMOP.js → chunk-EMAPNC32.js} +45 -6
  3. package/dist/{chunk-XJNOZTAY.js → chunk-EQAHFGGX.js} +17 -44
  4. package/dist/{chunk-3ITGQRL5.js → chunk-NVMYRBOJ.js} +1 -1
  5. package/dist/chunk-RSU3V5IQ.js +73 -0
  6. package/dist/{chunk-XG6KFCPM.js → chunk-SAMQZRJU.js} +1 -1
  7. package/dist/{chunk-C7SPLITE.js → chunk-TLDBJH7D.js} +1 -1
  8. package/dist/{chunk-2SUIARE4.js → chunk-VE2ADQ5T.js} +4 -1
  9. package/dist/{chunk-VPWPEMZJ.js → chunk-YVCUNZB5.js} +1 -1
  10. package/dist/{chunk-ROF7SSQP.js → chunk-ZDRSDYXN.js} +73 -1
  11. package/dist/diagrams/index.d.ts +86 -0
  12. package/dist/diagrams/index.js +222 -0
  13. package/dist/docx/index.d.ts +7 -0
  14. package/dist/docx/index.js +2 -2
  15. package/dist/epub/index.js +3 -3
  16. package/dist/html/index.js +3 -3
  17. package/dist/images-CAWESWRF.js +10 -0
  18. package/dist/{import-C16E8Y4X.d.ts → import-Cn_V1_7e.d.ts} +12 -0
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.js +11 -11
  21. package/dist/infer/index.js +6 -6
  22. package/dist/outside-in/index.d.ts +2 -2
  23. package/dist/outside-in/index.js +2 -2
  24. package/dist/pdf/index.d.ts +16 -0
  25. package/dist/pdf/index.js +2 -1
  26. package/dist/pptx/index.d.ts +2 -2
  27. package/dist/pptx/index.js +3 -3
  28. package/dist/registry/index.d.ts +3 -3
  29. package/dist/registry/index.js +1 -1
  30. package/dist/{types-Ba_aUHZJ.d.ts → types-Cubj9A8c.d.ts} +1 -1
  31. package/package.json +7 -2
  32. package/dist/chunk-B7GEAZBI.js +0 -37
  33. package/dist/images-JLBFCDD4.js +0 -6
  34. package/dist/{chunk-7DQP2I57.js → chunk-3KXEE7KH.js} +5 -5
package/README.md CHANGED
@@ -87,7 +87,34 @@ const imported = await pdfToMarkdownDoc(pdfBuffer);
87
87
  const container = await pdfToContainer(pdfBuffer);
88
88
  ```
89
89
 
90
- **Fidelity:** export uses pdf-lib's standard 14 fonts (`themeId` affects colors only). PDF has no semantic structure, so import is heuristic and best-effort: headings are detected by font size, and tables / code blocks / blockquotes / links via the `detectTables` / `detectCodeBlocks` / `detectBlockquotes` / `detectLinks` options (all default true). `pdfToMarkdownDoc` is text-only; `pdfToContainer` also extracts embedded XObject and inline raster images in browsers and Node, placed **by page** — each image is inserted after the last content block from its page (image-only pages fall back to the nearest preceding page with content, else the document end). Placement is page-level only; masks, vector artwork, and images that PDF.js cannot expose as decoded pixels are not emitted.
90
+ **Fidelity:** export uses pdf-lib's standard 14 fonts (`themeId` affects colors only). Pass `images` (keyed by the markdown URL, as for DOCX) to draw PNG and JPEG images that stand alone in a paragraph; other images print as an `[Image: alt]` note. PDF has no semantic structure, so import is heuristic and best-effort: headings are detected by font size, and tables / code blocks / blockquotes / links via the `detectTables` / `detectCodeBlocks` / `detectBlockquotes` / `detectLinks` options (all default true). `pdfToMarkdownDoc` is text-only; `pdfToContainer` also extracts embedded XObject and inline raster images in browsers and Node, placed **by page** — each image is inserted after the last content block from its page (image-only pages fall back to the nearest preceding page with content, else the document end). Placement is page-level only; masks, vector artwork, and images that PDF.js cannot expose as decoded pixels are not emitted.
91
+
92
+ ### Diagrams as pictures
93
+
94
+ DOCX, EPUB, PDF and PPTX cannot draw Mermaid or Squisq diagrams, so on their
95
+ own they export diagram source as text. `rasterizeDiagrams` finds every
96
+ diagram — Mermaid fences, ASCII `diagram`/`timeline`/`tree` fences, and
97
+ `{[drawing]}`/`{[layout]}`/`{[diagram]}` heading blocks — asks a renderer for
98
+ a PNG of each, and returns the document with each diagram replaced by an
99
+ image plus the bytes for the exporters' `images` option. Drawing needs a
100
+ browser; `@bendyline/squisq-react/diagram-pictures` provides the renderer.
101
+
102
+ ```ts
103
+ import { rasterizeDiagrams } from '@bendyline/squisq-formats/diagrams';
104
+ import { createDiagramPictureRenderer } from '@bendyline/squisq-react/diagram-pictures';
105
+
106
+ const pictured = await rasterizeDiagrams(markdownDoc, createDiagramPictureRenderer());
107
+ const docx = await markdownDocToDocx(pictured.markdownDoc, { images: pictured.images });
108
+ const pdf = await markdownDocToPdf(pictured.markdownDoc, { images: pictured.images });
109
+
110
+ // PPTX keeps drawings, timelines and ASCII diagrams as native shapes;
111
+ // only Mermaid needs a picture.
112
+ const pptx = await docToPptx(doc, { diagramImages: pictured.mermaid });
113
+ ```
114
+
115
+ A diagram the renderer cannot draw stays as source; the export never fails on
116
+ it. Image entries carry their display size (`width`/`height` in CSS pixels),
117
+ so a picture rendered at 2× prints at 1×.
91
118
 
92
119
  ### HTML
93
120
 
@@ -289,18 +316,19 @@ Also exported: `createRegistry` / `defaultRegistry` / `defaultFormats`, `BUILTIN
289
316
 
290
317
  ## Subpath Exports
291
318
 
292
- | Subpath | Contents |
293
- | ------------------------------------- | ------------------------------------------------------------------------------ |
294
- | `@bendyline/squisq-formats/docx` | DOCX import/export (+ `docxToContainer`) |
295
- | `@bendyline/squisq-formats/pdf` | PDF import/export (+ `pdfToContainer`, `configurePdfWorker`) |
296
- | `@bendyline/squisq-formats/html` | Player HTML export, static plain-HTML export + bundles, HTML import |
297
- | `@bendyline/squisq-formats/epub` | EPUB 3 e-book export (with optional Media Overlays) |
298
- | `@bendyline/squisq-formats/pptx` | PPTX export + import (text/lists/tables + slide images) |
299
- | `@bendyline/squisq-formats/xlsx` | XLSX import + tables-only export |
300
- | `@bendyline/squisq-formats/csv` | CSV import/export (RFC 4180) |
301
- | `@bendyline/squisq-formats/ooxml` | Shared OOXML infrastructure (package reader/writer, XML utilities, namespaces) |
302
- | `@bendyline/squisq-formats/container` | `ContentContainer` ↔ ZIP serialization |
303
- | `@bendyline/squisq-formats/registry` | Format registry + programmatic `convert()` (also re-exported from the root) |
319
+ | Subpath | Contents |
320
+ | ------------------------------------- | ------------------------------------------------------------------------------- |
321
+ | `@bendyline/squisq-formats/docx` | DOCX import/export (+ `docxToContainer`) |
322
+ | `@bendyline/squisq-formats/pdf` | PDF import/export (+ `pdfToContainer`, `configurePdfWorker`) |
323
+ | `@bendyline/squisq-formats/html` | Player HTML export, static plain-HTML export + bundles, HTML import |
324
+ | `@bendyline/squisq-formats/epub` | EPUB 3 e-book export (with optional Media Overlays) |
325
+ | `@bendyline/squisq-formats/pptx` | PPTX export + import (text/lists/tables + slide images) |
326
+ | `@bendyline/squisq-formats/xlsx` | XLSX import + tables-only export |
327
+ | `@bendyline/squisq-formats/csv` | CSV import/export (RFC 4180) |
328
+ | `@bendyline/squisq-formats/ooxml` | Shared OOXML infrastructure (package reader/writer, XML utilities, namespaces) |
329
+ | `@bendyline/squisq-formats/diagrams` | `rasterizeDiagrams` / `findDiagrams`: diagrams as pictures for document exports |
330
+ | `@bendyline/squisq-formats/container` | `ContentContainer` ↔ ZIP serialization |
331
+ | `@bendyline/squisq-formats/registry` | Format registry + programmatic `convert()` (also re-exported from the root) |
304
332
 
305
333
  The package root re-exports the common converters; `./container`, the plain-HTML/bundle functions, `docxToContainer`, `pdfToContainer`, `PdfPageSize`, and the image utilities are subpath-only.
306
334
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  inferMimeType
3
- } from "./chunk-VPWPEMZJ.js";
3
+ } from "./chunk-YVCUNZB5.js";
4
4
  import {
5
5
  RelIdAllocator,
6
6
  inlineNodeToRuns,
@@ -8,9 +8,6 @@ import {
8
8
  sanitizeOfficeHyperlink,
9
9
  toOoxmlHex
10
10
  } from "./chunk-A6N6IN3I.js";
11
- import {
12
- extToMime
13
- } from "./chunk-B7GEAZBI.js";
14
11
  import {
15
12
  extractPlainText,
16
13
  stripHtmlTags
@@ -59,6 +56,9 @@ import {
59
56
  fontAwesomeFaces,
60
57
  fontAwesomeGlyph
61
58
  } from "./chunk-USU6HTKB.js";
59
+ import {
60
+ extToMime
61
+ } from "./chunk-RSU3V5IQ.js";
62
62
 
63
63
  // src/pptx/export.ts
64
64
  import { resolveFontFamily, VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
@@ -162,12 +162,19 @@ async function docToPptx(doc, options = {}) {
162
162
  { persistentLayers: doc.persistentLayers },
163
163
  theme
164
164
  );
165
+ const mediaAspectRatios = {};
166
+ for (const [source, picture] of effectiveOptions.diagramImages ?? []) {
167
+ if (picture.width > 0 && picture.height > 0) {
168
+ mediaAspectRatios[source] = picture.width / picture.height;
169
+ }
170
+ }
165
171
  const expanded = expandDocBlocks(flatBlocks, {
166
172
  audioSegments,
167
173
  viewport: VIEWPORT_PRESETS.landscape,
168
174
  persistentLayers,
169
175
  theme,
170
176
  customTemplates: doc.customTemplates,
177
+ mediaAspectRatios,
171
178
  // A .pptx is a discrete deck: the >20s pacing split would emit N identical
172
179
  // copies of one authored slide (a long single block became 18 duplicates),
173
180
  // and the <5s pacing merge would delete authored slides outright. Keep one
@@ -206,6 +213,7 @@ async function docToPptx(doc, options = {}) {
206
213
  effectiveOptions.allowRelativeHyperlinks ?? false,
207
214
  effectiveOptions.signal
208
215
  );
216
+ ctx.diagramImages = effectiveOptions.diagramImages;
209
217
  slideXmls.push(buildLayerSlideXml(slideBlocks[index], ctx));
210
218
  slideContexts.push(ctx);
211
219
  }
@@ -545,15 +553,46 @@ function convertLayerToShapes(layer, ctx, textBottom) {
545
553
  )
546
554
  ];
547
555
  }
548
- case "mermaid":
556
+ case "mermaid": {
557
+ const picture = ctx.diagramImages?.get(layer.content.source.trim());
558
+ if (picture && picture.width > 0 && picture.height > 0) {
559
+ const data = picture.data instanceof Uint8Array ? picture.data.buffer.slice(
560
+ picture.data.byteOffset,
561
+ picture.data.byteOffset + picture.data.byteLength
562
+ ) : picture.data;
563
+ const relId = ctx.addImage(
564
+ `diagram-${ctx.embeddedImages.length + 1}.png`,
565
+ data,
566
+ mermaidTitle(layer.content.source)
567
+ );
568
+ const embedded = ctx.embeddedImages.find((candidate) => candidate.relId === relId);
569
+ const area = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
570
+ const scale = Math.min(area.width / picture.width, area.height / picture.height);
571
+ const width = Math.round(picture.width * scale);
572
+ const height = Math.round(picture.height * scale);
573
+ return [
574
+ buildImageShape(
575
+ embedded,
576
+ ctx.allocShapeId(),
577
+ area.x + Math.round((area.width - width) / 2),
578
+ area.y + Math.round((area.height - height) / 2),
579
+ width,
580
+ height
581
+ )
582
+ ];
583
+ }
549
584
  return [
550
585
  buildLayerTextShape(
551
586
  textLayerForPlaceholder(layer.position, layer.content.source, ctx),
552
587
  ctx.allocShapeId()
553
588
  )
554
589
  ];
590
+ }
555
591
  }
556
592
  }
593
+ function mermaidTitle(source) {
594
+ return /^\s*accTitle\s*:\s*(.+?)\s*$/mu.exec(source)?.[1] ?? "Diagram";
595
+ }
557
596
  function resolvePositionValue(value, dimension) {
558
597
  if (typeof value === "number") return value;
559
598
  if (typeof value === "string") {
@@ -1075,7 +1114,7 @@ function buildBodyShapeCustom(bodyContent, top, height, style) {
1075
1114
  return `<p:sp><p:nvSpPr><p:cNvPr id="3" name="Content"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph idx="1"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="${BODY_LEFT}" y="${top}"/><a:ext cx="${BODY_WIDTH}" cy="${height}"/></a:xfrm></p:spPr><p:txBody><a:bodyPr/><a:lstStyle><a:lvl1pPr>${defRPr}</a:lvl1pPr></a:lstStyle>` + bodyContent + `</p:txBody></p:sp>`;
1076
1115
  }
1077
1116
  function buildImageShape(img, shapeId, left, top, maxWidth, maxHeight) {
1078
- return `<p:pic><p:nvPicPr><p:cNvPr id="${shapeId}" name="${escapeXml(img.alt || "Picture")}"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="${img.relId}"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="${left}" y="${top}"/><a:ext cx="${maxWidth}" cy="${maxHeight}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>`;
1117
+ return `<p:pic><p:nvPicPr><p:cNvPr id="${shapeId}" name="${escapeXml(img.alt || "Picture")}"${img.alt ? ` descr="${escapeXml(img.alt)}"` : ""}/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill><a:blip r:embed="${img.relId}"/><a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="${left}" y="${top}"/><a:ext cx="${maxWidth}" cy="${maxHeight}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>`;
1079
1118
  }
1080
1119
  function convertBodyBlocks(nodes, ctx) {
1081
1120
  const parts = [];
@@ -5,9 +5,6 @@ import {
5
5
  normalizeOoxmlHex,
6
6
  sanitizeOfficeHyperlink
7
7
  } from "./chunk-A6N6IN3I.js";
8
- import {
9
- extToMime
10
- } from "./chunk-B7GEAZBI.js";
11
8
  import {
12
9
  stripHtmlTags
13
10
  } from "./chunk-AVOZAKGP.js";
@@ -62,6 +59,11 @@ import {
62
59
  fontAwesomeGlyph,
63
60
  obfuscateDocxFont
64
61
  } from "./chunk-USU6HTKB.js";
62
+ import {
63
+ extToMime,
64
+ fitWithin,
65
+ readImageDimensions
66
+ } from "./chunk-RSU3V5IQ.js";
65
67
 
66
68
  // src/docx/export.ts
67
69
  import { resolveFontFamily } from "@bendyline/squisq/schemas";
@@ -526,59 +528,30 @@ function convertImage(node, ctx) {
526
528
  const ext = contentType.split("/")[1]?.replace("jpeg", "jpg") || "png";
527
529
  const filename = `image${ctx.images.length + 1}.${ext}`;
528
530
  const { relId, docPrId } = ctx.addImage(data, contentType, filename);
529
- const dims = readImageDimensions(data);
531
+ const dims = imageEntry.width && imageEntry.height ? { width: imageEntry.width, height: imageEntry.height } : readImageDimensions(data);
530
532
  const EMU_PER_INCH = 914400;
531
533
  const MAX_WIDTH_EMU = 6 * EMU_PER_INCH;
534
+ const MAX_HEIGHT_EMU = 8 * EMU_PER_INCH;
532
535
  let cx;
533
536
  let cy;
534
537
  if (dims) {
535
- const widthEmu = dims.width / 96 * EMU_PER_INCH;
536
- const heightEmu = dims.height / 96 * EMU_PER_INCH;
537
- if (widthEmu > MAX_WIDTH_EMU) {
538
- const scale = MAX_WIDTH_EMU / widthEmu;
539
- cx = MAX_WIDTH_EMU;
540
- cy = Math.round(heightEmu * scale);
541
- } else {
542
- cx = Math.round(widthEmu);
543
- cy = Math.round(heightEmu);
544
- }
538
+ const fitted = fitWithin(
539
+ dims.width / 96 * EMU_PER_INCH,
540
+ dims.height / 96 * EMU_PER_INCH,
541
+ MAX_WIDTH_EMU,
542
+ MAX_HEIGHT_EMU
543
+ );
544
+ cx = Math.round(fitted.width);
545
+ cy = Math.round(fitted.height);
545
546
  } else {
546
547
  cx = 5 * EMU_PER_INCH;
547
548
  cy = 3 * EMU_PER_INCH;
548
549
  }
549
550
  const name = escapeXml(node.alt || filename);
551
+ const description = node.alt ? ` descr="${escapeXml(node.alt)}"` : "";
550
552
  const NS_A = "http://schemas.openxmlformats.org/drawingml/2006/main";
551
553
  const NS_PIC = "http://schemas.openxmlformats.org/drawingml/2006/picture";
552
- return `<w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${docPrId}" name="${name}"/><wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="${NS_A}" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="${NS_A}"><a:graphicData uri="${NS_PIC}"><pic:pic xmlns:pic="${NS_PIC}"><pic:nvPicPr><pic:cNvPr id="0" name="${name}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${relId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
553
- }
554
- function readImageDimensions(data) {
555
- const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
556
- if (bytes.length < 24) return null;
557
- if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
558
- const width = bytes[16] << 24 | bytes[17] << 16 | bytes[18] << 8 | bytes[19];
559
- const height = bytes[20] << 24 | bytes[21] << 16 | bytes[22] << 8 | bytes[23];
560
- return { width, height };
561
- }
562
- if (bytes[0] === 255 && bytes[1] === 216) {
563
- let offset = 2;
564
- while (offset < bytes.length - 9) {
565
- if (bytes[offset] !== 255) break;
566
- const marker = bytes[offset + 1];
567
- if (marker === 192 || marker === 194) {
568
- const height = bytes[offset + 5] << 8 | bytes[offset + 6];
569
- const width = bytes[offset + 7] << 8 | bytes[offset + 8];
570
- return { width, height };
571
- }
572
- const segLen = bytes[offset + 2] << 8 | bytes[offset + 3];
573
- offset += 2 + segLen;
574
- }
575
- }
576
- if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) {
577
- const width = bytes[6] | bytes[7] << 8;
578
- const height = bytes[8] | bytes[9] << 8;
579
- return { width, height };
580
- }
581
- return null;
554
+ return `<w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${docPrId}" name="${name}"${description}/><wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="${NS_A}" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="${NS_A}"><a:graphicData uri="${NS_PIC}"><pic:pic xmlns:pic="${NS_PIC}"><pic:nvPicPr><pic:cNvPr id="0" name="${name}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${relId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
582
555
  }
583
556
  function convertFootnoteRef(node, ctx) {
584
557
  const fnId = ctx.getFootnoteId(node.identifier);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  extractFilename,
3
3
  inferMimeType
4
- } from "./chunk-VPWPEMZJ.js";
4
+ } from "./chunk-YVCUNZB5.js";
5
5
  import {
6
6
  extractPlainText
7
7
  } from "./chunk-AVOZAKGP.js";
@@ -0,0 +1,73 @@
1
+ // src/shared/images.ts
2
+ var EXT_TO_MIME = {
3
+ // Raster / vector images
4
+ png: "image/png",
5
+ jpg: "image/jpeg",
6
+ jpeg: "image/jpeg",
7
+ gif: "image/gif",
8
+ webp: "image/webp",
9
+ bmp: "image/bmp",
10
+ svg: "image/svg+xml",
11
+ tiff: "image/tiff",
12
+ tif: "image/tiff",
13
+ ico: "image/x-icon",
14
+ avif: "image/avif",
15
+ // Office vector formats (docx/pptx embedded metafiles)
16
+ emf: "image/emf",
17
+ wmf: "image/wmf",
18
+ // Audio / video (html export inlines these as data URIs)
19
+ mp3: "audio/mpeg",
20
+ wav: "audio/wav",
21
+ ogg: "audio/ogg",
22
+ mp4: "video/mp4",
23
+ webm: "video/webm",
24
+ // Sidecar data files (`{[dataTable src=…]}` references)
25
+ csv: "text/csv",
26
+ tsv: "text/tab-separated-values",
27
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
28
+ parquet: "application/vnd.apache.parquet"
29
+ };
30
+ function extToMime(ext) {
31
+ const key = ext.replace(/^\./, "").toLowerCase();
32
+ return EXT_TO_MIME[key] ?? "application/octet-stream";
33
+ }
34
+ function readImageDimensions(data) {
35
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
36
+ if (bytes.length < 24) return null;
37
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
38
+ const width = bytes[16] << 24 | bytes[17] << 16 | bytes[18] << 8 | bytes[19];
39
+ const height = bytes[20] << 24 | bytes[21] << 16 | bytes[22] << 8 | bytes[23];
40
+ return { width, height };
41
+ }
42
+ if (bytes[0] === 255 && bytes[1] === 216) {
43
+ let offset = 2;
44
+ while (offset < bytes.length - 9) {
45
+ if (bytes[offset] !== 255) break;
46
+ const marker = bytes[offset + 1];
47
+ if (marker === 192 || marker === 194) {
48
+ const height = bytes[offset + 5] << 8 | bytes[offset + 6];
49
+ const width = bytes[offset + 7] << 8 | bytes[offset + 8];
50
+ return { width, height };
51
+ }
52
+ const segLen = bytes[offset + 2] << 8 | bytes[offset + 3];
53
+ offset += 2 + segLen;
54
+ }
55
+ }
56
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) {
57
+ const width = bytes[6] | bytes[7] << 8;
58
+ const height = bytes[8] | bytes[9] << 8;
59
+ return { width, height };
60
+ }
61
+ return null;
62
+ }
63
+ function fitWithin(width, height, maxWidth, maxHeight) {
64
+ if (width <= 0 || height <= 0) return { width: 0, height: 0 };
65
+ const scale = Math.min(1, maxWidth / width, maxHeight / height);
66
+ return { width: width * scale, height: height * scale };
67
+ }
68
+
69
+ export {
70
+ extToMime,
71
+ readImageDimensions,
72
+ fitWithin
73
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  convert,
3
3
  defaultRegistry
4
- } from "./chunk-2SUIARE4.js";
4
+ } from "./chunk-VE2ADQ5T.js";
5
5
  import {
6
6
  ConversionError
7
7
  } from "./chunk-KXOZMWBS.js";
@@ -2,7 +2,7 @@ import {
2
2
  arrayBufferToBase64DataUrl,
3
3
  extractFilename,
4
4
  inferMimeType
5
- } from "./chunk-VPWPEMZJ.js";
5
+ } from "./chunk-YVCUNZB5.js";
6
6
  import {
7
7
  FootnoteIndex,
8
8
  footnoteIds
@@ -262,7 +262,7 @@ function dataReferenceWarnings(doc, formatLabel) {
262
262
  }
263
263
  async function collectContainerDocxImages(container, signal) {
264
264
  const images = await collectContainerImages(container, signal);
265
- const { extToMime } = await import("./images-JLBFCDD4.js");
265
+ const { extToMime } = await import("./images-CAWESWRF.js");
266
266
  return new Map(
267
267
  [...images].map(([path, data]) => [
268
268
  path,
@@ -382,11 +382,14 @@ function defaultFormats() {
382
382
  const raw = optionsFor(options, "pdf");
383
383
  const markdownDoc = await markdownOf(input);
384
384
  const pdfWarnings = [];
385
+ const containerImages = await collectContainerDocxImages(input.container, options.signal);
386
+ const images = new Map([...containerImages, ...raw.images ?? /* @__PURE__ */ new Map()]);
385
387
  const buf = await markdownDocToPdf(markdownDoc, {
386
388
  ...raw,
387
389
  ...options.title !== void 0 ? { title: options.title } : {},
388
390
  themeId: resolveThemeId(input, options),
389
391
  themeRegistry: options.themeRegistry ?? raw.themeRegistry,
392
+ images,
390
393
  onWarning: (message) => pdfWarnings.push(message)
391
394
  });
392
395
  return ok(await toBytes(buf), MIME.pdf, [
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  extToMime
3
- } from "./chunk-B7GEAZBI.js";
3
+ } from "./chunk-RSU3V5IQ.js";
4
4
 
5
5
  // src/html/imageUtils.ts
6
6
  function inferMimeType(filename) {
@@ -9,6 +9,10 @@ import {
9
9
  fontAwesomeFaces,
10
10
  fontAwesomeGlyph
11
11
  } from "./chunk-USU6HTKB.js";
12
+ import {
13
+ fitWithin,
14
+ readImageDimensions
15
+ } from "./chunk-RSU3V5IQ.js";
12
16
 
13
17
  // src/pdf/export.ts
14
18
  import { PDFDocument, StandardFonts, rgb, PDFString } from "pdf-lib";
@@ -391,6 +395,7 @@ async function markdownDocToPdf(doc, options = {}) {
391
395
  pdfDoc.setCreationDate(/* @__PURE__ */ new Date());
392
396
  pdfDoc.setModificationDate(/* @__PURE__ */ new Date());
393
397
  const ctx = await createExportContext(pdfDoc, options, doc);
398
+ ctx.images = await embedDocumentImages(pdfDoc, doc, options.images);
394
399
  renderBlocks(doc.children, ctx, 0);
395
400
  const warning = ctx.text.warning();
396
401
  if (warning) {
@@ -404,6 +409,39 @@ async function docToPdf(doc, options = {}) {
404
409
  const markdownDoc = docToMarkdown(doc);
405
410
  return markdownDocToPdf(markdownDoc, options);
406
411
  }
412
+ var POINTS_PER_CSS_PIXEL = 0.75;
413
+ function collectImageUrls(nodes, into) {
414
+ for (const node of nodes) {
415
+ if (node.type === "image") into.add(node.url);
416
+ const children = node.children;
417
+ if (Array.isArray(children)) collectImageUrls(children, into);
418
+ }
419
+ return into;
420
+ }
421
+ async function embedDocumentImages(pdfDoc, doc, images) {
422
+ const embedded = /* @__PURE__ */ new Map();
423
+ if (!images || images.size === 0) return embedded;
424
+ for (const url of collectImageUrls(doc.children, /* @__PURE__ */ new Set())) {
425
+ const entry = images.get(url);
426
+ if (!entry) continue;
427
+ const bytes = entry.data instanceof Uint8Array ? entry.data : new Uint8Array(entry.data);
428
+ const isPng = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
429
+ const isJpeg = bytes[0] === 255 && bytes[1] === 216;
430
+ if (!isPng && !isJpeg) continue;
431
+ try {
432
+ const image = isPng ? await pdfDoc.embedPng(bytes) : await pdfDoc.embedJpg(bytes);
433
+ const pixels = readImageDimensions(bytes) ?? { width: image.width, height: image.height };
434
+ const sized = Boolean(entry.width && entry.height);
435
+ embedded.set(url, {
436
+ image,
437
+ width: sized ? entry.width : pixels.width,
438
+ height: sized ? entry.height : pixels.height
439
+ });
440
+ } catch {
441
+ }
442
+ }
443
+ return embedded;
444
+ }
407
445
  async function createExportContext(pdfDoc, options, doc) {
408
446
  const [regular, bold, italic, boldItalic, mono, monoBold] = await Promise.all([
409
447
  pdfDoc.embedFont(StandardFonts.Helvetica),
@@ -455,7 +493,8 @@ async function createExportContext(pdfDoc, options, doc) {
455
493
  colors: { text: colorText, heading: colorHeading, link: colorLink },
456
494
  footnotes: new FootnoteIndex(doc),
457
495
  text: new WinAnsiTracker(),
458
- signal: options.signal
496
+ signal: options.signal,
497
+ images: /* @__PURE__ */ new Map()
459
498
  };
460
499
  }
461
500
  function ensureSpace(ctx, needed) {
@@ -812,6 +851,7 @@ function renderHeading(node, ctx, extraIndent) {
812
851
  function renderParagraph(node, ctx, extraIndent, colorOverride) {
813
852
  const x0 = ctx.margin + extraIndent;
814
853
  const w = ctx.contentWidth - extraIndent;
854
+ if (renderImageParagraph(node, ctx, x0, w)) return;
815
855
  const spans = flattenInlines(node.children, ctx, {
816
856
  bold: false,
817
857
  italic: false,
@@ -821,6 +861,38 @@ function renderParagraph(node, ctx, extraIndent, colorOverride) {
821
861
  drawSpans(spans, ctx, w, x0);
822
862
  ctx.y -= PARAGRAPH_SPACING;
823
863
  }
864
+ function renderImageParagraph(node, ctx, x0, width) {
865
+ const images = node.children.filter((child) => child.type === "image");
866
+ const onlyImages = node.children.every(
867
+ (child) => child.type === "image" || child.type === "break" || child.type === "text" && !child.value.trim()
868
+ );
869
+ if (!onlyImages || images.length === 0) return false;
870
+ if (!images.some((image) => ctx.images.has(image.url))) return false;
871
+ const maxHeight = ctx.pageHeight - 2 * ctx.margin;
872
+ for (const imageNode of images) {
873
+ const embedded = ctx.images.get(imageNode.url);
874
+ if (!embedded) {
875
+ const note = flattenInlines([imageNode], ctx, { bold: false, italic: false, code: false });
876
+ drawSpans(note, ctx, width, x0);
877
+ continue;
878
+ }
879
+ const size = fitWithin(
880
+ embedded.width * POINTS_PER_CSS_PIXEL,
881
+ embedded.height * POINTS_PER_CSS_PIXEL,
882
+ width,
883
+ maxHeight
884
+ );
885
+ ensureSpace(ctx, size.height);
886
+ ctx.page.drawImage(embedded.image, {
887
+ x: x0 + (width - size.width) / 2,
888
+ y: ctx.y - size.height,
889
+ width: size.width,
890
+ height: size.height
891
+ });
892
+ ctx.y -= size.height + PARAGRAPH_SPACING;
893
+ }
894
+ return true;
895
+ }
824
896
  function renderBlockquote(node, ctx, extraIndent) {
825
897
  const barX = ctx.margin + extraIndent + 4;
826
898
  const indent = extraIndent + BLOCKQUOTE_INDENT;
@@ -0,0 +1,86 @@
1
+ import { MarkdownDocument, MarkdownCodeBlock } from '@bendyline/squisq/markdown';
2
+
3
+ /**
4
+ * Diagrams as pictures for document exports.
5
+ *
6
+ * DOCX, EPUB and PDF have no Mermaid or Squisq renderer, so a diagram would
7
+ * export as its source text. `rasterizeDiagrams` finds every diagram in a
8
+ * MarkdownDocument, asks a host-supplied renderer for a picture of it, and
9
+ * returns the document with each pictured diagram swapped for an image plus
10
+ * the picture bytes, ready for the exporters' `images` option. The renderer
11
+ * is supplied by the host because drawing needs a browser (Mermaid lays out
12
+ * text with the DOM); this module is environment-neutral.
13
+ *
14
+ * Diagrams are recognized the way the player recognizes them:
15
+ * - ```mermaid fences;
16
+ * - ASCII-art ```diagram, ```tree, and ```timeline fences (also unlabeled or
17
+ * `text` fences whose content the detectors accept);
18
+ * - heading blocks with a container template — `{[drawing]}`, `{[layout]}`,
19
+ * `{[diagram]}` — together with every child heading they own.
20
+ *
21
+ * `docToPptx` keeps drawings, timelines and ASCII diagrams as native shapes;
22
+ * for it, use the returned `mermaid` map as its `diagramImages` option.
23
+ */
24
+
25
+ /** What kind of diagram a picture is wanted for. */
26
+ type ExportDiagramKind = 'mermaid' | 'diagram' | 'timeline' | 'tree' | 'container';
27
+ interface ExportDiagram {
28
+ readonly kind: ExportDiagramKind;
29
+ /**
30
+ * The diagram's own markdown: its fence, or for a container its heading
31
+ * and every child heading and body it owns.
32
+ */
33
+ readonly markdown: string;
34
+ /** The fence content for fence kinds; '' for containers. */
35
+ readonly source: string;
36
+ /** The container's template (`drawing`, `layout`, `diagram`); '' for fences. */
37
+ readonly template: string;
38
+ /** A text alternative for the picture. */
39
+ readonly alt: string;
40
+ }
41
+ /** A PNG picture of a diagram, with the size it should display at. */
42
+ interface DiagramPicture {
43
+ readonly data: ArrayBuffer | Uint8Array;
44
+ /** Display size in CSS pixels (96 per inch); the PNG may be denser. */
45
+ readonly width: number;
46
+ readonly height: number;
47
+ }
48
+ /** Draws one diagram; resolves to null to leave that diagram as source. */
49
+ type DiagramRasterizer = (diagram: ExportDiagram) => Promise<DiagramPicture | null>;
50
+ interface RasterizeDiagramsOptions {
51
+ signal?: AbortSignal;
52
+ /** Limit the kinds pictured. Default: all of them. */
53
+ kinds?: readonly ExportDiagramKind[];
54
+ /** Prefix for the image URLs written into the document. Default: `diagram`. */
55
+ urlPrefix?: string;
56
+ /** Receives a note for each diagram the renderer could not draw. */
57
+ onWarning?: (message: string) => void;
58
+ }
59
+ interface RasterizedDiagrams {
60
+ /** The document with each pictured diagram replaced by an image of it. */
61
+ readonly markdownDoc: MarkdownDocument;
62
+ /**
63
+ * Picture bytes keyed by the image URL written into `markdownDoc`, in the
64
+ * shape the DOCX and PDF exporters take; EPUB takes `data` alone.
65
+ */
66
+ readonly images: Map<string, {
67
+ data: Uint8Array;
68
+ contentType: 'image/png';
69
+ width: number;
70
+ height: number;
71
+ }>;
72
+ /** Pictures of Mermaid diagrams keyed by trimmed source, for `docToPptx`'s `diagramImages`. */
73
+ readonly mermaid: Map<string, DiagramPicture>;
74
+ }
75
+ /** The diagram kind of a fence, or null for ordinary code. */
76
+ declare function fenceDiagramKind(node: MarkdownCodeBlock): ExportDiagramKind | null;
77
+ /** Every diagram in `doc`, in document order. */
78
+ declare function findDiagrams(doc: MarkdownDocument): ExportDiagram[];
79
+ /**
80
+ * Picture every diagram in `doc` with `render`, one at a time, and return
81
+ * the document with each pictured diagram replaced by an image of it. A
82
+ * diagram the renderer declines or fails on stays as it was.
83
+ */
84
+ declare function rasterizeDiagrams(doc: MarkdownDocument, render: DiagramRasterizer, options?: RasterizeDiagramsOptions): Promise<RasterizedDiagrams>;
85
+
86
+ export { type DiagramPicture, type DiagramRasterizer, type ExportDiagram, type ExportDiagramKind, type RasterizeDiagramsOptions, type RasterizedDiagrams, fenceDiagramKind, findDiagrams, rasterizeDiagrams };