@bendyline/squisq-formats 2.7.1 → 2.7.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.
@@ -3,11 +3,16 @@ import {
3
3
  } from "./chunk-YVCUNZB5.js";
4
4
  import {
5
5
  RelIdAllocator,
6
+ blipXml,
6
7
  inlineNodeToRuns,
7
8
  inlineNodesToRuns,
8
9
  sanitizeOfficeHyperlink,
9
10
  toOoxmlHex
10
- } from "./chunk-A6N6IN3I.js";
11
+ } from "./chunk-XVBQB2SL.js";
12
+ import {
13
+ svgImageFromBytes,
14
+ svgIslandImage
15
+ } from "./chunk-VCZUXQUL.js";
11
16
  import {
12
17
  extractPlainText,
13
18
  stripHtmlTags
@@ -74,7 +79,87 @@ import {
74
79
  resolveThemeForDoc
75
80
  } from "@bendyline/squisq/doc";
76
81
  import { splitIconMarkers } from "@bendyline/squisq/icon-marker";
77
- import { parseVideoEmbedUrl, readFrontmatterThemeId } from "@bendyline/squisq/markdown";
82
+ import {
83
+ isSvgImageSource,
84
+ parseVideoEmbedUrl,
85
+ readFrontmatterThemeId
86
+ } from "@bendyline/squisq/markdown";
87
+
88
+ // src/pptx/pictureLayers.ts
89
+ import { svgSourceFromBytes } from "@bendyline/squisq/markdown";
90
+ function buildImageShape(img, shapeId, left, top, maxWidth, maxHeight, hyperlinkRelId, svgRelId) {
91
+ const name = escapeXml(img.alt || "Picture");
92
+ const descr = img.alt ? ` descr="${escapeXml(img.alt)}"` : "";
93
+ const link = hyperlinkRelId ? `<a:hlinkClick r:id="${hyperlinkRelId}"/>` : "";
94
+ return `<p:pic><p:nvPicPr>` + (link ? `<p:cNvPr id="${shapeId}" name="${name}"${descr}>${link}</p:cNvPr>` : `<p:cNvPr id="${shapeId}" name="${name}"${descr}/>`) + `<p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill>` + blipXml(img.relId, svgRelId) + `<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>`;
95
+ }
96
+ function fitCentered(area, width, height) {
97
+ const scale = Math.min(area.width / width, area.height / height);
98
+ const fitted = { width: Math.round(width * scale), height: Math.round(height * scale) };
99
+ return {
100
+ x: area.x + Math.round((area.width - fitted.width) / 2),
101
+ y: area.y + Math.round((area.height - fitted.height) / 2),
102
+ ...fitted
103
+ };
104
+ }
105
+ function arrayBufferOf(data) {
106
+ return data instanceof Uint8Array ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : data;
107
+ }
108
+ function usablePicture(picture) {
109
+ return !!picture && picture.width > 0 && picture.height > 0;
110
+ }
111
+ function mermaidTitle(source) {
112
+ return /^\s*accTitle\s*:\s*(.+?)\s*$/mu.exec(source)?.[1] ?? "Diagram";
113
+ }
114
+ function mermaidPictureShapes(layer, area, ctx) {
115
+ const picture = ctx.diagramImages?.get(layer.content.source.trim());
116
+ if (!usablePicture(picture)) return null;
117
+ const alt = mermaidTitle(layer.content.source);
118
+ const relId = ctx.addImage("diagram.png", arrayBufferOf(picture.data), alt);
119
+ const rect = fitCentered(area, picture.width, picture.height);
120
+ return [
121
+ buildImageShape({ relId, alt }, ctx.allocShapeId(), rect.x, rect.y, rect.width, rect.height)
122
+ ];
123
+ }
124
+ function svgIslandPictureShapes(layer, area, ctx) {
125
+ const image = svgIslandImage(layer.content.source, { color: layer.content.color });
126
+ if (!image) return null;
127
+ return svgPictureShapes(image, layer.content.source, area, ctx, layer.content.alt ?? image.alt);
128
+ }
129
+ function svgImagePictureShapes(layer, area, ctx) {
130
+ const data = ctx.images?.get(layer.content.src);
131
+ const image = data ? svgImageFromBytes(data) : null;
132
+ if (!data || !image) return null;
133
+ const alt = layer.content.alt || image.alt;
134
+ return svgPictureShapes(
135
+ image,
136
+ svgSourceFromBytes(data),
137
+ area,
138
+ ctx,
139
+ alt,
140
+ layer.content.fit === "fill"
141
+ );
142
+ }
143
+ function svgPictureShapes(image, source, area, ctx, alt, stretch = false) {
144
+ const hostPicture = ctx.diagramImages?.get(source.trim());
145
+ const fallback = usablePicture(hostPicture) ? arrayBufferOf(hostPicture.data) : ctx.svgIslandPosters?.svgIslandPosterPng(image.width / image.height)?.data;
146
+ if (!fallback) return null;
147
+ const relId = ctx.addImage("svg-island.png", fallback, alt);
148
+ const svgRelId = ctx.addImage("svg-island.svg", image.data, alt);
149
+ const rect = stretch ? area : fitCentered(area, image.width, image.height);
150
+ return [
151
+ buildImageShape(
152
+ { relId, alt },
153
+ ctx.allocShapeId(),
154
+ rect.x,
155
+ rect.y,
156
+ rect.width,
157
+ rect.height,
158
+ null,
159
+ svgRelId
160
+ )
161
+ ];
162
+ }
78
163
 
79
164
  // src/pptx/styles.ts
80
165
  var SLIDE_WIDTH = 12192e3;
@@ -202,9 +287,12 @@ async function docToPptx(doc, options = {}) {
202
287
  if (slideBlocks.length === 0) {
203
288
  slideBlocks.push({ id: "empty-slide", startTime: 0, duration: 0, audioSegment: 0, layers: [] });
204
289
  }
205
- const videoPosters = slideBlocks.some(
206
- (block) => block.layers?.some((layer) => layer.type === "videoEmbed")
207
- ) ? await import("./videoPoster-D7DTZC2L.js") : null;
290
+ const hasLayer = (type) => slideBlocks.some((b) => b.layers?.some((l) => l.type === type));
291
+ const videoPosters = hasLayer("videoEmbed") ? await import("./videoPoster-D7DTZC2L.js") : null;
292
+ const hasSvgImage = slideBlocks.some(
293
+ (b) => b.layers?.some((l) => l.type === "image" && isSvgImageSource(l.content.src))
294
+ );
295
+ const svgIslandPosters = hasLayer("svg") || hasSvgImage ? await import("./svgIslandPoster-RYA6PDX7.js") : null;
208
296
  const slideXmls = [];
209
297
  const slideContexts = [];
210
298
  for (let index = 0; index < slideBlocks.length; index++) {
@@ -218,6 +306,7 @@ async function docToPptx(doc, options = {}) {
218
306
  );
219
307
  ctx.diagramImages = effectiveOptions.diagramImages;
220
308
  ctx.videoPosters = videoPosters;
309
+ ctx.svgIslandPosters = svgIslandPosters;
221
310
  slideXmls.push(buildLayerSlideXml(slideBlocks[index], ctx));
222
311
  slideContexts.push(ctx);
223
312
  }
@@ -321,6 +410,8 @@ var SlideContext = class {
321
410
  this.relationships = [];
322
411
  /** Poster helpers for hosted videos, loaded only when the deck has one. */
323
412
  this.videoPosters = null;
413
+ /** Stand-in PNGs for SVG islands, loaded only when the deck has one. */
414
+ this.svgIslandPosters = null;
324
415
  this.embeddedImages = [];
325
416
  this.nextShapeId = 4;
326
417
  /** Font Awesome families referenced by semantic or materialized text runs. */
@@ -472,7 +563,11 @@ function convertLayerToShapes(layer, ctx, textBottom) {
472
563
  case "shape":
473
564
  return [buildLayerShape(layer, ctx.allocShapeId())];
474
565
  case "image": {
475
- const data = ctx.images?.get(layer.content.src);
566
+ const svg = isSvgImageSource(layer.content.src);
567
+ const area = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
568
+ const picture = svg ? svgImagePictureShapes(layer, area, ctx) : null;
569
+ if (picture) return picture;
570
+ const data = svg ? void 0 : ctx.images?.get(layer.content.src);
476
571
  if (data) {
477
572
  const image = ctx.addImage(layer.content.src, data, layer.content.alt);
478
573
  const rect = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
@@ -539,7 +634,7 @@ function convertLayerToShapes(layer, ctx, textBottom) {
539
634
  }
540
635
  case "video": {
541
636
  const poster = layer.content.posterSrc;
542
- const data = poster ? ctx.images?.get(poster) : void 0;
637
+ const data = poster && !isSvgImageSource(poster) ? ctx.images?.get(poster) : void 0;
543
638
  if (poster && data) {
544
639
  const relId = ctx.addImage(poster, data, layer.content.alt);
545
640
  const embedded = ctx.embeddedImages.find((candidate) => candidate.relId === relId);
@@ -598,45 +693,28 @@ function convertLayerToShapes(layer, ctx, textBottom) {
598
693
  ];
599
694
  }
600
695
  case "mermaid": {
601
- const picture = ctx.diagramImages?.get(layer.content.source.trim());
602
- if (picture && picture.width > 0 && picture.height > 0) {
603
- const data = picture.data instanceof Uint8Array ? picture.data.buffer.slice(
604
- picture.data.byteOffset,
605
- picture.data.byteOffset + picture.data.byteLength
606
- ) : picture.data;
607
- const relId = ctx.addImage(
608
- `diagram-${ctx.embeddedImages.length + 1}.png`,
609
- data,
610
- mermaidTitle(layer.content.source)
611
- );
612
- const embedded = ctx.embeddedImages.find((candidate) => candidate.relId === relId);
613
- const area = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
614
- const scale = Math.min(area.width / picture.width, area.height / picture.height);
615
- const width = Math.round(picture.width * scale);
616
- const height = Math.round(picture.height * scale);
617
- return [
618
- buildImageShape(
619
- embedded,
620
- ctx.allocShapeId(),
621
- area.x + Math.round((area.width - width) / 2),
622
- area.y + Math.round((area.height - height) / 2),
623
- width,
624
- height
625
- )
626
- ];
627
- }
628
- return [
696
+ const area = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
697
+ const picture = mermaidPictureShapes(layer, area, ctx);
698
+ return picture ?? [
629
699
  buildLayerTextShape(
630
700
  textLayerForPlaceholder(layer.position, layer.content.source, ctx),
631
701
  ctx.allocShapeId()
632
702
  )
633
703
  ];
634
704
  }
705
+ case "svg": {
706
+ const area = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
707
+ const picture = svgIslandPictureShapes(layer, area, ctx);
708
+ const label = layer.content.alt ?? "SVG illustration";
709
+ return picture ?? [
710
+ buildLayerTextShape(
711
+ textLayerForPlaceholder(layer.position, `[${label}]`, ctx),
712
+ ctx.allocShapeId()
713
+ )
714
+ ];
715
+ }
635
716
  }
636
717
  }
637
- function mermaidTitle(source) {
638
- return /^\s*accTitle\s*:\s*(.+?)\s*$/mu.exec(source)?.[1] ?? "Diagram";
639
- }
640
718
  function resolvePositionValue(value, dimension) {
641
719
  if (typeof value === "number") return value;
642
720
  if (typeof value === "string") {
@@ -1157,12 +1235,6 @@ function buildBodyShapeCustom(bodyContent, top, height, style) {
1157
1235
  const defRPr = style.hasTheme ? `<a:defRPr><a:solidFill><a:srgbClr val="${style.text}"/></a:solidFill><a:latin typeface="${escapeXml(style.bodyFont)}"/></a:defRPr>` : "";
1158
1236
  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>`;
1159
1237
  }
1160
- function buildImageShape(img, shapeId, left, top, maxWidth, maxHeight, hyperlinkRelId) {
1161
- const name = escapeXml(img.alt || "Picture");
1162
- const descr = img.alt ? ` descr="${escapeXml(img.alt)}"` : "";
1163
- const link = hyperlinkRelId ? `<a:hlinkClick r:id="${hyperlinkRelId}"/>` : "";
1164
- return `<p:pic><p:nvPicPr>` + (link ? `<p:cNvPr id="${shapeId}" name="${name}"${descr}>${link}</p:cNvPr>` : `<p:cNvPr id="${shapeId}" name="${name}"${descr}/>`) + `<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>`;
1165
- }
1166
1238
  function convertBodyBlocks(nodes, ctx) {
1167
1239
  const parts = [];
1168
1240
  for (const node of nodes) {
@@ -1212,7 +1284,7 @@ function isImageOnlyParagraph(node) {
1212
1284
  function convertParagraph(node, ctx) {
1213
1285
  const soloImage = isImageOnlyParagraph(node);
1214
1286
  if (soloImage && ctx.images) {
1215
- const data = ctx.images.get(soloImage.url);
1287
+ const data = isSvgImageSource(soloImage.url) ? void 0 : ctx.images.get(soloImage.url);
1216
1288
  if (data) {
1217
1289
  ctx.addImage(soloImage.url, data, soloImage.alt || "");
1218
1290
  return "";
@@ -1364,7 +1436,7 @@ function makeHyperlinkRun(text, rId, format) {
1364
1436
  }
1365
1437
  function convertImage(node, format, ctx) {
1366
1438
  if (ctx.images) {
1367
- const data = ctx.images.get(node.url);
1439
+ const data = isSvgImageSource(node.url) ? void 0 : ctx.images.get(node.url);
1368
1440
  if (data) {
1369
1441
  ctx.addImage(node.url, data, node.alt || "");
1370
1442
  return "";
@@ -3,6 +3,9 @@ import {
3
3
  extractFilename,
4
4
  inferMimeType
5
5
  } from "./chunk-YVCUNZB5.js";
6
+ import {
7
+ exportImageBytes
8
+ } from "./chunk-VCZUXQUL.js";
6
9
  import {
7
10
  FootnoteIndex,
8
11
  footnoteIds
@@ -137,7 +140,8 @@ function buildInlineImageMaps(images) {
137
140
  continue;
138
141
  }
139
142
  canonicalKeyFor.set(buffer, path);
140
- imageMap[path] = arrayBufferToBase64DataUrl(buffer, inferMimeType(path));
143
+ const bytes = exportImageBytes(path, buffer);
144
+ if (bytes) imageMap[path] = arrayBufferToBase64DataUrl(bytes, inferMimeType(path));
141
145
  }
142
146
  return { imageMap, imageAliases };
143
147
  }
@@ -286,7 +290,11 @@ function escapeHtml(str) {
286
290
  // src/html/plainHtml.ts
287
291
  import {
288
292
  findBlockVideoEmbed,
293
+ isSvgIslandLang,
294
+ parseSvgIsland,
289
295
  sanitizeHtmlNodes,
296
+ svgIslandDataUrl,
297
+ svgSourceFromDataUrl,
290
298
  sanitizeUrl,
291
299
  videoEmbedFromIframe,
292
300
  videoEmbedIframeAttributes,
@@ -308,7 +316,14 @@ function markdownDocToPlainHtml(doc, options = {}) {
308
316
  const resolveId = themeId ?? readFrontmatterThemeId2(doc.frontmatter);
309
317
  const theme = options.theme ?? (resolveId ? resolveThemeForDoc(doc, resolveId, options.themeRegistry) : void 0);
310
318
  const footnotes = new FootnoteIndex(doc);
311
- const ctx = { images, links, htmlPolicy, footnotes, videoEmbeds };
319
+ const ctx = {
320
+ images,
321
+ links,
322
+ htmlPolicy,
323
+ footnotes,
324
+ videoEmbeds,
325
+ textColor: theme?.colors.text
326
+ };
312
327
  const body = renderTopLevel(doc.children, ctx) + renderFootnotesSection(footnotes, ctx);
313
328
  const fontsLink = theme && externalResources === "allow" ? renderFontsLink(theme) : "";
314
329
  const usesIcons = docUsesIcons(doc);
@@ -322,8 +337,10 @@ ${escapeStyleText(iconsCss)}
322
337
  const themedCss = theme ? renderThemedCss(theme) : DEFAULT_CSS;
323
338
  const videoCss = docUsesVideoEmbeds(doc, htmlPolicy) ? `
324
339
  ${VIDEO_EMBED_CSS}` : "";
340
+ const svgCss = docUsesSvgIslands(doc) ? `
341
+ ${SVG_ISLAND_CSS}` : "";
325
342
  const safeCss = escapeStyleText(`${themedCss}
326
- ${FEATURE_CSS}${videoCss}`);
343
+ ${FEATURE_CSS}${videoCss}${svgCss}`);
327
344
  return `<!DOCTYPE html>
328
345
  <html lang="en">
329
346
  <head>
@@ -372,6 +389,15 @@ function docUsesVideoEmbeds(doc, htmlPolicy) {
372
389
  (node) => node.type === "paragraph" && findBlockVideoEmbed(node) !== null || visit(node)
373
390
  );
374
391
  }
392
+ function docUsesSvgIslands(doc) {
393
+ const visit = (node) => {
394
+ if (!node || typeof node !== "object") return false;
395
+ const n = node;
396
+ if (n.type === "code") return isSvgIslandLang(n.lang);
397
+ return Array.isArray(n.children) && n.children.some(visit);
398
+ };
399
+ return visit(doc);
400
+ }
375
401
  function renderTopLevel(children, ctx) {
376
402
  const out = [];
377
403
  for (let i = 0; i < children.length; i++) {
@@ -726,6 +752,10 @@ function nodeToHtml(node, ctx) {
726
752
  case "inlineCode":
727
753
  return `<code>${escapeHtml2(node.value ?? "")}</code>`;
728
754
  case "code": {
755
+ if (isSvgIslandLang(node.lang)) {
756
+ const island = svgIslandToHtml(node.value ?? "", ctx);
757
+ if (island) return island;
758
+ }
729
759
  const lang = node.lang ? ` class="language-${escapeAttr(node.lang)}"` : "";
730
760
  return `<pre class="squisq-code-block"><code${lang}>${escapeHtml2(node.value ?? "")}</code></pre>`;
731
761
  }
@@ -748,7 +778,7 @@ function nodeToHtml(node, ctx) {
748
778
  case "image": {
749
779
  const original = node.url ?? "";
750
780
  const resolved = ctx?.images?.get(original) ?? original;
751
- const safeSrc = sanitizeUrl(resolved, "media");
781
+ const safeSrc = sanitizeUrl(resolved, "media") ?? svgDataImageToHtmlSrc(resolved);
752
782
  if (!safeSrc) return escapeHtml2(node.alt ?? "");
753
783
  return `<img src="${escapeAttr(safeSrc)}" alt="${escapeAttr(node.alt ?? "")}" />`;
754
784
  }
@@ -896,6 +926,20 @@ var VIDEO_EMBED_CSS = ` .squisq-video-embed { display: block; max-width: 720px;
896
926
  .squisq-video-embed-play::after { content: ''; position: absolute; left: 26px; top: 12px; border-style: solid; border-width: 10px 0 10px 16px; border-color: transparent transparent transparent #fff; }
897
927
  .squisq-video-embed-provider { position: absolute; left: 12px; bottom: 10px; color: #fff; font-size: 0.8em; font-weight: 600; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); }
898
928
  .squisq-video-embed-caption { margin-top: 0.4em; font-size: 0.875em; opacity: 0.8; }`;
929
+ function svgDataImageToHtmlSrc(url) {
930
+ const source = svgSourceFromDataUrl(url);
931
+ const result = source === null ? null : parseSvgIsland(source);
932
+ return result?.ok ? svgIslandDataUrl(result.island) : null;
933
+ }
934
+ function svgIslandToHtml(source, ctx) {
935
+ const result = parseSvgIsland(source);
936
+ if (!result.ok) return null;
937
+ const { island } = result;
938
+ const src = svgIslandDataUrl(island, { color: ctx?.textColor });
939
+ return `<figure class="squisq-svg-island"><img src="${escapeAttr(src)}" alt="${escapeAttr(island.title ?? "Illustration")}" width="${island.width}" height="${island.height}"></figure>`;
940
+ }
941
+ var SVG_ISLAND_CSS = ` .squisq-svg-island { margin: 1.25em 0; }
942
+ .squisq-svg-island img { display: block; max-width: 100%; height: auto; }`;
899
943
  function escapeHtml2(s) {
900
944
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
901
945
  }
@@ -1069,7 +1113,8 @@ async function readImagesForDoc(mdDoc, docDir, readBinary) {
1069
1113
  const cleanAuthored = authored.replace(/[#?].*$/, "");
1070
1114
  const resolved = resolveRelative(docDir, cleanAuthored);
1071
1115
  if (resolved === null) continue;
1072
- const data = await readBinary(resolved);
1116
+ const read = await readBinary(resolved);
1117
+ const data = read && exportImageBytes(resolved, read);
1073
1118
  if (!data) continue;
1074
1119
  out.set(authored, { data, zipPath: resolved });
1075
1120
  }
@@ -1194,7 +1239,8 @@ async function markdownDocsToHtmlBundle(options) {
1194
1239
  const clean = authored.replace(/[#?].*$/, "");
1195
1240
  const resolvedImg = resolveRelative(docDir, clean);
1196
1241
  if (resolvedImg === null) continue;
1197
- const data = await readBinary(resolvedImg);
1242
+ const read = await readBinary(resolvedImg);
1243
+ const data = read && exportImageBytes(resolvedImg, read);
1198
1244
  if (!data) continue;
1199
1245
  const safe = sanitizeZipPath2(resolvedImg);
1200
1246
  if (!safe) continue;
@@ -2020,7 +2066,9 @@ async function docToHtmlZip(doc, options) {
2020
2066
  );
2021
2067
  continue;
2022
2068
  }
2023
- zip.file(zipPath, buffer);
2069
+ const bytes = exportImageBytes(originalPath, buffer);
2070
+ if (!bytes) continue;
2071
+ zip.file(zipPath, bytes);
2024
2072
  imagePathMap[originalPath] = zipPath;
2025
2073
  }
2026
2074
  }
@@ -2,6 +2,10 @@ import {
2
2
  extractFilename,
3
3
  inferMimeType
4
4
  } from "./chunk-YVCUNZB5.js";
5
+ import {
6
+ exportImageBytes,
7
+ svgIslandsAsImages
8
+ } from "./chunk-VCZUXQUL.js";
5
9
  import {
6
10
  extractPlainText
7
11
  } from "./chunk-AVOZAKGP.js";
@@ -23,8 +27,11 @@ import JSZip from "jszip";
23
27
  import { resolveFontFamily } from "@bendyline/squisq/schemas";
24
28
  import { resolveThemeForDoc } from "@bendyline/squisq/doc";
25
29
  import { readFrontmatterThemeId, sanitizeUrl } from "@bendyline/squisq/markdown";
26
- async function markdownDocToEpub(doc, options = {}) {
30
+ async function markdownDocToEpub(sourceDoc, options = {}) {
27
31
  options.signal?.throwIfAborted();
32
+ const themeId = options.themeId ?? readFrontmatterThemeId(sourceDoc.frontmatter);
33
+ const islandColor = themeId ? resolveThemeForDoc(sourceDoc, themeId, options.themeRegistry).colors.text : void 0;
34
+ const { doc, images: islandImages } = svgIslandsAsImages(sourceDoc, { color: islandColor });
28
35
  const fmTitle = doc.frontmatter?.title;
29
36
  const fmAuthor = doc.frontmatter?.author;
30
37
  const title = options.title ?? (typeof fmTitle === "string" ? fmTitle : "Untitled");
@@ -42,11 +49,12 @@ async function markdownDocToEpub(doc, options = {}) {
42
49
  const imageEntries = collectDocImages(doc.children);
43
50
  const resolvedImages = /* @__PURE__ */ new Map();
44
51
  const usedImageNames = /* @__PURE__ */ new Set();
45
- if (options.images) {
52
+ if (options.images || islandImages.size > 0) {
46
53
  let imageIndex = 0;
47
54
  for (const src of imageEntries) {
48
55
  if ((imageIndex++ & 63) === 0) options.signal?.throwIfAborted();
49
- const data = options.images.get(src);
56
+ const attached = options.images?.get(src);
57
+ const data = islandImages.get(src)?.data ?? (attached && exportImageBytes(src, attached));
50
58
  if (data) {
51
59
  const filename = uniqueFilename(safeArchiveBasename(src, "image"), usedImageNames);
52
60
  usedImageNames.add(filename);
@@ -54,12 +62,7 @@ async function markdownDocToEpub(doc, options = {}) {
54
62
  }
55
63
  }
56
64
  }
57
- const css = generateStylesheet(
58
- options.themeId ?? readFrontmatterThemeId(doc.frontmatter),
59
- doc,
60
- options.themeRegistry,
61
- embeddedIconFonts
62
- );
65
+ const css = generateStylesheet(themeId, doc, options.themeRegistry, embeddedIconFonts);
63
66
  const zip = new JSZip();
64
67
  zip.file("mimetype", "application/epub+zip", { compression: "STORE" });
65
68
  zip.file("META-INF/container.xml", CONTAINER_XML);
@@ -0,0 +1,90 @@
1
+ // src/shared/svgIslands.ts
2
+ import {
3
+ isSvgImageSource,
4
+ isSvgIslandLang,
5
+ parseSvgIsland,
6
+ svgIslandMarkup,
7
+ svgSourceFromBytes
8
+ } from "@bendyline/squisq/markdown";
9
+ var SVG_ISLAND_FALLBACK_ALT = "Illustration";
10
+ function ownBuffer(encoded) {
11
+ const bytes = new Uint8Array(encoded.length);
12
+ bytes.set(encoded);
13
+ return bytes.buffer;
14
+ }
15
+ function svgIslandImage(source, options = {}) {
16
+ const result = parseSvgIsland(source);
17
+ if (!result.ok) return null;
18
+ const { island } = result;
19
+ const data = ownBuffer(
20
+ new TextEncoder().encode(
21
+ svgIslandMarkup(island, { color: options.color, replaceCurrentColor: true })
22
+ )
23
+ );
24
+ return {
25
+ data,
26
+ contentType: "image/svg+xml",
27
+ width: island.width,
28
+ height: island.height,
29
+ alt: island.title ?? SVG_ISLAND_FALLBACK_ALT
30
+ };
31
+ }
32
+ function docHasSvgIslands(doc) {
33
+ const visit = (node) => {
34
+ if (node.type === "code") return isSvgIslandLang(node.lang);
35
+ const nested = node.children;
36
+ return Array.isArray(nested) && nested.some(visit);
37
+ };
38
+ return visit(doc);
39
+ }
40
+ function svgIslandsAsImages(doc, options = {}) {
41
+ const prefix = options.prefix ?? "squisq-svg-island";
42
+ const images = /* @__PURE__ */ new Map();
43
+ const replace = (nodes) => {
44
+ let changed = false;
45
+ const next = nodes.map((node) => {
46
+ if (node.type === "code" && isSvgIslandLang(node.lang)) {
47
+ const image = svgIslandImage(node.value, options);
48
+ if (!image) return node;
49
+ const url = `${prefix}-${images.size + 1}.svg`;
50
+ images.set(url, image);
51
+ changed = true;
52
+ const picture = { type: "image", url, alt: image.alt, title: null };
53
+ const paragraph = { type: "paragraph", children: [picture] };
54
+ return paragraph;
55
+ }
56
+ const nested = node.children;
57
+ if (Array.isArray(nested)) {
58
+ const replaced = replace(nested);
59
+ if (replaced !== nested) {
60
+ changed = true;
61
+ return { ...node, children: replaced };
62
+ }
63
+ }
64
+ return node;
65
+ });
66
+ return changed ? next : nodes;
67
+ };
68
+ if (!docHasSvgIslands(doc)) return { doc, images };
69
+ const children = replace(doc.children);
70
+ return { doc: children === doc.children ? doc : { ...doc, children }, images };
71
+ }
72
+ function isSvgImageEntry(path, contentType) {
73
+ return isSvgImageSource(path) || /^image\/svg\+xml\b/i.test(contentType ?? "");
74
+ }
75
+ function svgImageFromBytes(data) {
76
+ return svgIslandImage(svgSourceFromBytes(data));
77
+ }
78
+ function exportImageBytes(path, data, contentType) {
79
+ if (!isSvgImageEntry(path, contentType)) return data;
80
+ return svgImageFromBytes(data)?.data ?? null;
81
+ }
82
+
83
+ export {
84
+ svgIslandImage,
85
+ docHasSvgIslands,
86
+ svgIslandsAsImages,
87
+ isSvgImageEntry,
88
+ svgImageFromBytes,
89
+ exportImageBytes
90
+ };
@@ -117,11 +117,20 @@ function inlineNodeToRuns(node, handlers, format) {
117
117
  }
118
118
  }
119
119
 
120
+ // src/shared/officeSvgBlip.ts
121
+ var SVG_BLIP_EXT_URI = "{96DAC541-7B7A-43D3-8B79-37D633B846F1}";
122
+ var NS_ASVG = "http://schemas.microsoft.com/office/drawing/2016/SVG/main";
123
+ function blipXml(rasterRelId, svgRelId) {
124
+ if (!svgRelId) return `<a:blip r:embed="${rasterRelId}"/>`;
125
+ return `<a:blip r:embed="${rasterRelId}"><a:extLst><a:ext uri="${SVG_BLIP_EXT_URI}"><asvg:svgBlip xmlns:asvg="${NS_ASVG}" r:embed="${svgRelId}"/></a:ext></a:extLst></a:blip>`;
126
+ }
127
+
120
128
  export {
121
129
  RelIdAllocator,
122
130
  toOoxmlHex,
123
131
  normalizeOoxmlHex,
124
132
  sanitizeOfficeHyperlink,
133
+ blipXml,
125
134
  inlineNodesToRuns,
126
135
  inlineNodeToRuns
127
136
  };
@@ -1,10 +1,17 @@
1
1
  import {
2
2
  RelIdAllocator,
3
+ blipXml,
3
4
  inlineNodeToRuns,
4
5
  inlineNodesToRuns,
5
6
  normalizeOoxmlHex,
6
7
  sanitizeOfficeHyperlink
7
- } from "./chunk-A6N6IN3I.js";
8
+ } from "./chunk-XVBQB2SL.js";
9
+ import {
10
+ docHasSvgIslands,
11
+ isSvgImageEntry,
12
+ svgImageFromBytes,
13
+ svgIslandImage
14
+ } from "./chunk-VCZUXQUL.js";
8
15
  import {
9
16
  stripHtmlTags
10
17
  } from "./chunk-AVOZAKGP.js";
@@ -70,7 +77,9 @@ import { resolveFontFamily } from "@bendyline/squisq/schemas";
70
77
  import { docToMarkdown, resolveThemeForDoc } from "@bendyline/squisq/doc";
71
78
  import {
72
79
  findBlockVideoEmbed,
73
- readFrontmatterThemeId
80
+ isSvgIslandLang,
81
+ readFrontmatterThemeId,
82
+ svgSourceFromBytes
74
83
  } from "@bendyline/squisq/markdown";
75
84
 
76
85
  // src/docx/videoEmbed.ts
@@ -133,6 +142,22 @@ function readWordVideo(drawing, resolveHyperlink) {
133
142
  return { embed, title };
134
143
  }
135
144
 
145
+ // src/docx/svgIsland.ts
146
+ var NS_A2 = "http://schemas.openxmlformats.org/drawingml/2006/main";
147
+ var NS_PIC2 = "http://schemas.openxmlformats.org/drawingml/2006/picture";
148
+ var EMU_PER_INCH2 = 914400;
149
+ var MAX_WIDTH_EMU = 6 * EMU_PER_INCH2;
150
+ var MAX_HEIGHT_EMU = 8 * EMU_PER_INCH2;
151
+ function buildWordSvgIslandRun(parts) {
152
+ let cx = parts.width / 96 * EMU_PER_INCH2;
153
+ let cy = parts.height / 96 * EMU_PER_INCH2;
154
+ const scale = Math.min(1, MAX_WIDTH_EMU / cx, MAX_HEIGHT_EMU / cy);
155
+ cx = Math.max(1, Math.round(cx * scale));
156
+ cy = Math.max(1, Math.round(cy * scale));
157
+ const alt = escapeXml(parts.alt);
158
+ return `<w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${cx}" cy="${cy}"/><wp:docPr id="${parts.docPrId}" name="${alt}" descr="${alt}"/><wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="${NS_A2}" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="${NS_A2}"><a:graphicData uri="${NS_PIC2}"><pic:pic xmlns:pic="${NS_PIC2}"><pic:nvPicPr><pic:cNvPr id="0" name="${alt}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill>${blipXml(parts.pngRelId, parts.svgRelId)}<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>`;
159
+ }
160
+
136
161
  // src/docx/styles.ts
137
162
  var HEADING_STYLE_MAP = {
138
163
  Heading1: 1,
@@ -196,6 +221,12 @@ async function markdownDocToDocx(doc, options = {}) {
196
221
  if ([...stories.body, ...stories.header, ...stories.footer].some(isVideoBlock)) {
197
222
  ctx.videoPosters = await import("./videoPoster-D7DTZC2L.js");
198
223
  }
224
+ const hasSvgImages = [...options.images ?? []].some(
225
+ ([url, image]) => isSvgImageEntry(url, image.contentType)
226
+ );
227
+ if (docHasSvgIslands(doc) || hasSvgImages) {
228
+ ctx.svgIslandPosters = await import("./svgIslandPoster-RYA6PDX7.js");
229
+ }
199
230
  ctx.setRelationshipSource("document");
200
231
  const bodyXml = convertBlocks(stories.body, ctx);
201
232
  ctx.setRelationshipSource("header");
@@ -254,6 +285,8 @@ var ExportContext = class {
254
285
  this.iconFamilies = /* @__PURE__ */ new Set();
255
286
  /** Poster + embed-code helpers, loaded only when the document has a video. */
256
287
  this.videoPosters = null;
288
+ /** Stand-in PNGs for SVG islands, loaded only when the document has one. */
289
+ this.svgIslandPosters = null;
257
290
  this.nextDocPrId = 1;
258
291
  let themeFont;
259
292
  let themeTitleFont;
@@ -261,6 +294,7 @@ var ExportContext = class {
261
294
  let themeBodyColor;
262
295
  let themeMutedColor;
263
296
  let themeBackgroundColor;
297
+ let themeTextColor;
264
298
  if (options.themeId) {
265
299
  const theme = resolveThemeForDoc(doc, options.themeId, options.themeRegistry);
266
300
  themeFont = firstFontFromStack(resolveFontFamily(theme.typography?.bodyFont, ""));
@@ -269,6 +303,7 @@ var ExportContext = class {
269
303
  themeBodyColor = normalizeOoxmlHex(theme.colors?.text);
270
304
  themeMutedColor = normalizeOoxmlHex(theme.colors?.textMuted);
271
305
  themeBackgroundColor = normalizeOoxmlHex(theme.colors?.background);
306
+ themeTextColor = theme.colors?.text;
272
307
  }
273
308
  this.font = options.defaultFont ?? themeFont ?? DEFAULT_FONT;
274
309
  this.headingFont = themeTitleFont ?? this.font;
@@ -278,6 +313,8 @@ var ExportContext = class {
278
313
  this.mutedColor = themeMutedColor;
279
314
  this.backgroundColor = themeBackgroundColor;
280
315
  this.resolvedImages = options.images ?? /* @__PURE__ */ new Map();
316
+ this.svgIslandFallbacks = options.svgIslandFallbacks;
317
+ this.islandColor = themeTextColor;
281
318
  this.allowRelativeHyperlinks = options.allowRelativeHyperlinks ?? false;
282
319
  this.signal = options.signal;
283
320
  }
@@ -429,7 +466,7 @@ function convertBlock(node, ctx, listDepth) {
429
466
  case "list":
430
467
  return convertList(node, ctx, listDepth);
431
468
  case "code":
432
- return convertCodeBlock(node);
469
+ return isSvgIslandLang(node.lang) && convertSvgIsland(node, ctx) || convertCodeBlock(node);
433
470
  case "table":
434
471
  return convertTable(node, ctx);
435
472
  case "thematicBreak":
@@ -497,6 +534,27 @@ function convertListItem(item, ctx, numId, depth) {
497
534
  }
498
535
  return parts.join("");
499
536
  }
537
+ function convertSvgIsland(node, ctx) {
538
+ const image = svgIslandImage(node.value, { color: ctx.islandColor });
539
+ const run = image ? svgPictureRun(image, node.value, ctx) : "";
540
+ return run ? `<w:p>${run}</w:p>` : "";
541
+ }
542
+ function svgPictureRun(image, source, ctx, display = {}) {
543
+ const host = ctx.svgIslandFallbacks?.get(source.trim());
544
+ const fallback = host && host.width > 0 && host.height > 0 ? host.data : ctx.svgIslandPosters?.svgIslandPosterPng(image.width / image.height)?.data;
545
+ if (!fallback) return "";
546
+ const png = ctx.addImage(fallback, "image/png", `image${ctx.images.length + 1}.png`);
547
+ const svg = ctx.addImage(image.data, "image/svg+xml", `image${ctx.images.length + 1}.svg`);
548
+ const sized = Boolean(display.width && display.height);
549
+ return buildWordSvgIslandRun({
550
+ pngRelId: png.relId,
551
+ svgRelId: svg.relId,
552
+ docPrId: png.docPrId,
553
+ width: sized ? display.width : image.width,
554
+ height: sized ? display.height : image.height,
555
+ alt: display.alt || image.alt
556
+ });
557
+ }
500
558
  function convertCodeBlock(node) {
501
559
  const lines = node.value.split("\n");
502
560
  const parts = [];
@@ -632,34 +690,43 @@ function convertImage(node, ctx) {
632
690
  const alt = node.alt || node.url;
633
691
  return makeRun(`[Image: ${alt}]`, { italic: true });
634
692
  }
693
+ if (isSvgImageEntry(node.url, imageEntry.contentType)) {
694
+ const image = svgImageFromBytes(imageEntry.data);
695
+ const run = image && svgPictureRun(image, svgSourceFromBytes(imageEntry.data), ctx, {
696
+ width: imageEntry.width,
697
+ height: imageEntry.height,
698
+ alt: node.alt
699
+ });
700
+ return run || makeRun(`[Image: ${node.alt || node.url}]`, { italic: true });
701
+ }
635
702
  const { data, contentType } = imageEntry;
636
703
  const ext = contentType.split("/")[1]?.replace("jpeg", "jpg") || "png";
637
704
  const filename = `image${ctx.images.length + 1}.${ext}`;
638
705
  const { relId, docPrId } = ctx.addImage(data, contentType, filename);
639
706
  const dims = imageEntry.width && imageEntry.height ? { width: imageEntry.width, height: imageEntry.height } : readImageDimensions(data);
640
- const EMU_PER_INCH2 = 914400;
641
- const MAX_WIDTH_EMU = 6 * EMU_PER_INCH2;
642
- const MAX_HEIGHT_EMU = 8 * EMU_PER_INCH2;
707
+ const EMU_PER_INCH3 = 914400;
708
+ const MAX_WIDTH_EMU2 = 6 * EMU_PER_INCH3;
709
+ const MAX_HEIGHT_EMU2 = 8 * EMU_PER_INCH3;
643
710
  let cx;
644
711
  let cy;
645
712
  if (dims) {
646
713
  const fitted = fitWithin(
647
- dims.width / 96 * EMU_PER_INCH2,
648
- dims.height / 96 * EMU_PER_INCH2,
649
- MAX_WIDTH_EMU,
650
- MAX_HEIGHT_EMU
714
+ dims.width / 96 * EMU_PER_INCH3,
715
+ dims.height / 96 * EMU_PER_INCH3,
716
+ MAX_WIDTH_EMU2,
717
+ MAX_HEIGHT_EMU2
651
718
  );
652
719
  cx = Math.round(fitted.width);
653
720
  cy = Math.round(fitted.height);
654
721
  } else {
655
- cx = 5 * EMU_PER_INCH2;
656
- cy = 3 * EMU_PER_INCH2;
722
+ cx = 5 * EMU_PER_INCH3;
723
+ cy = 3 * EMU_PER_INCH3;
657
724
  }
658
725
  const name = escapeXml(node.alt || filename);
659
726
  const description = node.alt ? ` descr="${escapeXml(node.alt)}"` : "";
660
- const NS_A2 = "http://schemas.openxmlformats.org/drawingml/2006/main";
661
- const NS_PIC2 = "http://schemas.openxmlformats.org/drawingml/2006/picture";
662
- 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_A2}" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="${NS_A2}"><a:graphicData uri="${NS_PIC2}"><pic:pic xmlns:pic="${NS_PIC2}"><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>`;
727
+ const NS_A3 = "http://schemas.openxmlformats.org/drawingml/2006/main";
728
+ const NS_PIC3 = "http://schemas.openxmlformats.org/drawingml/2006/picture";
729
+ 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_A3}" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="${NS_A3}"><a:graphicData uri="${NS_PIC3}"><pic:pic xmlns:pic="${NS_PIC3}"><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>`;
663
730
  }
664
731
  function convertFootnoteRef(node, ctx) {
665
732
  const fnId = ctx.getFootnoteId(node.identifier);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  convert,
3
3
  defaultRegistry
4
- } from "./chunk-HNGS7A5Z.js";
4
+ } from "./chunk-XCHIYF3A.js";
5
5
  import {
6
6
  ConversionError
7
7
  } from "./chunk-KXOZMWBS.js";
@@ -16,14 +16,20 @@ import { MarkdownDocument, MarkdownCodeBlock } from '@bendyline/squisq/markdown'
16
16
  * - ASCII-art ```diagram, ```tree, and ```timeline fences (also unlabeled or
17
17
  * `text` fences whose content the detectors accept);
18
18
  * - heading blocks with a container template — `{[drawing]}`, `{[layout]}`,
19
- * `{[diagram]}` — together with every child heading they own.
19
+ * `{[diagram]}` — together with every child heading they own;
20
+ * - ```svg fences (SVG islands) — but only when `kinds` asks for `svg`: DOCX
21
+ * and PPTX embed an island natively as vector art, so by default it is
22
+ * left in place. Ask for it to picture islands for PDF, or to get the
23
+ * `svg` map of PNG fallbacks the Office exporters place beside the vector.
20
24
  *
21
25
  * `docToPptx` keeps drawings, timelines and ASCII diagrams as native shapes;
22
26
  * for it, use the returned `mermaid` map as its `diagramImages` option.
23
27
  */
24
28
 
25
29
  /** What kind of diagram a picture is wanted for. */
26
- type ExportDiagramKind = 'mermaid' | 'diagram' | 'timeline' | 'tree' | 'container';
30
+ type ExportDiagramKind = 'mermaid' | 'diagram' | 'timeline' | 'tree' | 'container' | 'svg';
31
+ /** Every kind, `svg` included — for exporters with no vector picture of their own (PDF). */
32
+ declare const EXPORT_DIAGRAM_KINDS: readonly ExportDiagramKind[];
27
33
  interface ExportDiagram {
28
34
  readonly kind: ExportDiagramKind;
29
35
  /**
@@ -49,7 +55,7 @@ interface DiagramPicture {
49
55
  type DiagramRasterizer = (diagram: ExportDiagram) => Promise<DiagramPicture | null>;
50
56
  interface RasterizeDiagramsOptions {
51
57
  signal?: AbortSignal;
52
- /** Limit the kinds pictured. Default: all of them. */
58
+ /** The kinds to picture. Default: all of them except `svg` (see above). */
53
59
  kinds?: readonly ExportDiagramKind[];
54
60
  /** Prefix for the image URLs written into the document. Default: `diagram`. */
55
61
  urlPrefix?: string;
@@ -71,6 +77,11 @@ interface RasterizedDiagrams {
71
77
  }>;
72
78
  /** Pictures of Mermaid diagrams keyed by trimmed source, for `docToPptx`'s `diagramImages`. */
73
79
  readonly mermaid: Map<string, DiagramPicture>;
80
+ /**
81
+ * Pictures of SVG islands keyed by trimmed source: the PNG fallbacks for
82
+ * DOCX's `svgIslandFallbacks` and PPTX's `diagramImages`.
83
+ */
84
+ readonly svg: Map<string, DiagramPicture>;
74
85
  }
75
86
  /** The diagram kind of a fence, or null for ordinary code. */
76
87
  declare function fenceDiagramKind(node: MarkdownCodeBlock): ExportDiagramKind | null;
@@ -83,4 +94,4 @@ declare function findDiagrams(doc: MarkdownDocument): ExportDiagram[];
83
94
  */
84
95
  declare function rasterizeDiagrams(doc: MarkdownDocument, render: DiagramRasterizer, options?: RasterizeDiagramsOptions): Promise<RasterizedDiagrams>;
85
96
 
86
- export { type DiagramPicture, type DiagramRasterizer, type ExportDiagram, type ExportDiagramKind, type RasterizeDiagramsOptions, type RasterizedDiagrams, fenceDiagramKind, findDiagrams, rasterizeDiagrams };
97
+ export { type DiagramPicture, type DiagramRasterizer, EXPORT_DIAGRAM_KINDS, type ExportDiagram, type ExportDiagramKind, type RasterizeDiagramsOptions, type RasterizedDiagrams, fenceDiagramKind, findDiagrams, rasterizeDiagrams };
@@ -1,5 +1,10 @@
1
1
  // src/diagrams/index.ts
2
- import { extractPlainText, stringifyMarkdown } from "@bendyline/squisq/markdown";
2
+ import {
3
+ extractPlainText,
4
+ isSvgIslandLang,
5
+ parseSvgIsland,
6
+ stringifyMarkdown
7
+ } from "@bendyline/squisq/markdown";
3
8
  import {
4
9
  detectAsciiDiagram,
5
10
  detectAsciiTimeline,
@@ -13,16 +18,26 @@ import {
13
18
  isExplicitTreeLang,
14
19
  resolveTemplateName
15
20
  } from "@bendyline/squisq/doc";
21
+ var DEFAULT_KINDS = [
22
+ "mermaid",
23
+ "diagram",
24
+ "timeline",
25
+ "tree",
26
+ "container"
27
+ ];
28
+ var EXPORT_DIAGRAM_KINDS = [...DEFAULT_KINDS, "svg"];
16
29
  var KIND_NAMES = {
17
30
  mermaid: "Diagram",
18
31
  diagram: "Diagram",
19
32
  timeline: "Timeline",
20
33
  tree: "Tree",
21
- container: "Diagram"
34
+ container: "Diagram",
35
+ svg: "Illustration"
22
36
  };
23
37
  function fenceDiagramKind(node) {
24
38
  const lang = node.lang ?? null;
25
39
  if ((lang ?? "").trim().toLowerCase() === "mermaid") return "mermaid";
40
+ if (isSvgIslandLang(lang)) return parseSvgIsland(node.value).ok ? "svg" : null;
26
41
  if (isEligibleAsciiFenceLang(lang)) {
27
42
  const detection = detectAsciiDiagram(node.value, { explicit: isExplicitDiagramLang(lang) });
28
43
  if (detection.isDiagram) return "diagram";
@@ -47,6 +62,14 @@ function mermaidAlt(source) {
47
62
  const description = /^\s*accDescr\s*:\s*(.+?)\s*$/mu.exec(source)?.[1] ?? "";
48
63
  return [title, description].filter(Boolean).join(". ");
49
64
  }
65
+ function fenceAlt(kind, source) {
66
+ if (kind === "mermaid") return mermaidAlt(source);
67
+ if (kind === "svg") {
68
+ const result = parseSvgIsland(source);
69
+ return result.ok && result.island.title || "";
70
+ }
71
+ return "";
72
+ }
50
73
  function fenceMarkdown(node) {
51
74
  return stringifyMarkdown({ type: "document", children: [node] }).trim();
52
75
  }
@@ -66,7 +89,7 @@ function findInDocument(doc) {
66
89
  markdown: fenceMarkdown(fence),
67
90
  source: fence.value,
68
91
  template: "",
69
- alt: (kind === "mermaid" ? mermaidAlt(fence.value) : "") || KIND_NAMES[kind]
92
+ alt: fenceAlt(kind, fence.value) || KIND_NAMES[kind]
70
93
  }
71
94
  });
72
95
  }
@@ -147,7 +170,7 @@ function replaceFences(nodes, replacements) {
147
170
  return changed ? next : nodes;
148
171
  }
149
172
  async function rasterizeDiagrams(doc, render, options = {}) {
150
- const kinds = options.kinds ? new Set(options.kinds) : null;
173
+ const kinds = new Set(options.kinds ?? DEFAULT_KINDS);
151
174
  const prefix = options.urlPrefix ?? "diagram";
152
175
  const taken = imageUrls(doc.children, /* @__PURE__ */ new Set());
153
176
  let counter = 0;
@@ -161,11 +184,12 @@ async function rasterizeDiagrams(doc, render, options = {}) {
161
184
  };
162
185
  const images = /* @__PURE__ */ new Map();
163
186
  const mermaid = /* @__PURE__ */ new Map();
187
+ const svg = /* @__PURE__ */ new Map();
164
188
  const fenceReplacements = /* @__PURE__ */ new Map();
165
189
  const containerReplacements = [];
166
190
  for (const entry of findInDocument(doc)) {
167
191
  options.signal?.throwIfAborted();
168
- if (kinds && !kinds.has(entry.diagram.kind)) continue;
192
+ if (!kinds.has(entry.diagram.kind)) continue;
169
193
  let picture = null;
170
194
  try {
171
195
  picture = await render(entry.diagram);
@@ -184,6 +208,7 @@ async function rasterizeDiagrams(doc, render, options = {}) {
184
208
  height: picture.height
185
209
  });
186
210
  if (entry.diagram.kind === "mermaid") mermaid.set(entry.diagram.source.trim(), picture);
211
+ if (entry.diagram.kind === "svg") svg.set(entry.diagram.source.trim(), picture);
187
212
  const paragraph = imageParagraph(url, entry.diagram.alt);
188
213
  if (entry.fence) {
189
214
  fenceReplacements.set(entry.fence, paragraph);
@@ -212,10 +237,12 @@ async function rasterizeDiagrams(doc, render, options = {}) {
212
237
  return {
213
238
  markdownDoc: children === doc.children ? doc : { ...doc, children },
214
239
  images,
215
- mermaid
240
+ mermaid,
241
+ svg
216
242
  };
217
243
  }
218
244
  export {
245
+ EXPORT_DIAGRAM_KINDS,
219
246
  fenceDiagramKind,
220
247
  findDiagrams,
221
248
  rasterizeDiagrams
@@ -65,6 +65,17 @@ interface DocxExportOptions {
65
65
  }>;
66
66
  /** Permit carefully validated relative hyperlink targets. Default: false. */
67
67
  allowRelativeHyperlinks?: boolean;
68
+ /**
69
+ * PNG renderings of SVG islands (```svg fences), keyed by the fence source
70
+ * as written (trimmed) — the `svg` map from `rasterizeDiagrams` with
71
+ * `kinds: ['svg']`. Word draws an island's SVG; the PNG is what older
72
+ * readers show. Without one, a generated stand-in is used.
73
+ */
74
+ svgIslandFallbacks?: Map<string, {
75
+ data: ArrayBuffer | Uint8Array;
76
+ width: number;
77
+ height: number;
78
+ }>;
68
79
  }
69
80
  /**
70
81
  * Convert a MarkdownDocument to a .docx Blob.
@@ -4,8 +4,9 @@ import {
4
4
  docxToDoc,
5
5
  docxToMarkdownDoc,
6
6
  markdownDocToDocx
7
- } from "../chunk-5ACTORJI.js";
8
- import "../chunk-A6N6IN3I.js";
7
+ } from "../chunk-YITBSNPM.js";
8
+ import "../chunk-XVBQB2SL.js";
9
+ import "../chunk-VCZUXQUL.js";
9
10
  import "../chunk-AVOZAKGP.js";
10
11
  import "../chunk-ILCJ3WFD.js";
11
12
  import "../chunk-JU2RHXUB.js";
@@ -66,7 +66,7 @@ interface EpubExportOptions {
66
66
  * Chapters are split at H1/H2 heading boundaries. All referenced images
67
67
  * (provided via `options.images`) are embedded in the archive.
68
68
  */
69
- declare function markdownDocToEpub(doc: MarkdownDocument, options?: EpubExportOptions): Promise<ArrayBuffer>;
69
+ declare function markdownDocToEpub(sourceDoc: MarkdownDocument, options?: EpubExportOptions): Promise<ArrayBuffer>;
70
70
  /**
71
71
  * Convert a squisq Doc to an EPUB 3 file.
72
72
  *
@@ -1,8 +1,9 @@
1
1
  import {
2
2
  docToEpub,
3
3
  markdownDocToEpub
4
- } from "../chunk-TX2HE34S.js";
4
+ } from "../chunk-PKFWF4FX.js";
5
5
  import "../chunk-YVCUNZB5.js";
6
+ import "../chunk-VCZUXQUL.js";
6
7
  import "../chunk-AVOZAKGP.js";
7
8
  import "../chunk-JU2RHXUB.js";
8
9
  import "../chunk-BXWNU4T5.js";
@@ -10,12 +10,13 @@ import {
10
10
  markdownDocToPlainHtml,
11
11
  markdownDocsToHtmlBundle,
12
12
  markdownDocsToPlainHtmlBundle
13
- } from "../chunk-3NKXJF4C.js";
13
+ } from "../chunk-ETK2SKVQ.js";
14
14
  import {
15
15
  arrayBufferToBase64DataUrl,
16
16
  extractFilename,
17
17
  inferMimeType
18
18
  } from "../chunk-YVCUNZB5.js";
19
+ import "../chunk-VCZUXQUL.js";
19
20
  import "../chunk-BXWNU4T5.js";
20
21
  import "../chunk-RSU3V5IQ.js";
21
22
  export {
package/dist/index.js CHANGED
@@ -5,11 +5,11 @@ import {
5
5
  htmlToMarkdown,
6
6
  htmlToMarkdownDoc,
7
7
  htmlToMarkdownDocSync
8
- } from "./chunk-3NKXJF4C.js";
8
+ } from "./chunk-ETK2SKVQ.js";
9
9
  import {
10
10
  docToEpub,
11
11
  markdownDocToEpub
12
- } from "./chunk-TX2HE34S.js";
12
+ } from "./chunk-PKFWF4FX.js";
13
13
  import {
14
14
  BUILTIN_FORMAT_IDS
15
15
  } from "./chunk-2P5HJAQL.js";
@@ -28,7 +28,7 @@ import {
28
28
  renderOutsideInDocument,
29
29
  withOutsideInMarkdownEditing,
30
30
  withOutsideInMetadata
31
- } from "./chunk-ERT4EVR2.js";
31
+ } from "./chunk-Z3QORA5G.js";
32
32
  import {
33
33
  DEFAULT_CONVERSION_LIMITS,
34
34
  convert,
@@ -37,7 +37,7 @@ import {
37
37
  defaultRegistry,
38
38
  prepareConversion,
39
39
  resolveConversionLimits
40
- } from "./chunk-HNGS7A5Z.js";
40
+ } from "./chunk-XCHIYF3A.js";
41
41
  import {
42
42
  ConversionError
43
43
  } from "./chunk-KXOZMWBS.js";
@@ -52,16 +52,17 @@ import {
52
52
  docxToDoc,
53
53
  docxToMarkdownDoc,
54
54
  markdownDocToDocx
55
- } from "./chunk-5ACTORJI.js";
55
+ } from "./chunk-YITBSNPM.js";
56
56
  import {
57
57
  docToPptx,
58
58
  markdownDocToPptx,
59
59
  pptxToDoc,
60
60
  pptxToMarkdownDoc
61
- } from "./chunk-BHWIHYUJ.js";
61
+ } from "./chunk-52RXDH24.js";
62
62
  import "./chunk-6N2J7C2B.js";
63
63
  import "./chunk-YVCUNZB5.js";
64
- import "./chunk-A6N6IN3I.js";
64
+ import "./chunk-XVBQB2SL.js";
65
+ import "./chunk-VCZUXQUL.js";
65
66
  import {
66
67
  docToXlsx,
67
68
  markdownDocToXlsx,
@@ -6,8 +6,8 @@ import {
6
6
  renderOutsideInDocument,
7
7
  withOutsideInMarkdownEditing,
8
8
  withOutsideInMetadata
9
- } from "../chunk-ERT4EVR2.js";
10
- import "../chunk-HNGS7A5Z.js";
9
+ } from "../chunk-Z3QORA5G.js";
10
+ import "../chunk-XCHIYF3A.js";
11
11
  import "../chunk-KXOZMWBS.js";
12
12
  import {
13
13
  OUTSIDE_IN_FORMAT_IDS,
@@ -4,13 +4,14 @@ import {
4
4
  pptxToContainer,
5
5
  pptxToDoc,
6
6
  pptxToMarkdownDoc
7
- } from "../chunk-BHWIHYUJ.js";
7
+ } from "../chunk-52RXDH24.js";
8
8
  import {
9
9
  analyzePptxLayouts,
10
10
  inspectPptxLayouts
11
11
  } from "../chunk-6N2J7C2B.js";
12
12
  import "../chunk-YVCUNZB5.js";
13
- import "../chunk-A6N6IN3I.js";
13
+ import "../chunk-XVBQB2SL.js";
14
+ import "../chunk-VCZUXQUL.js";
14
15
  import "../chunk-AVOZAKGP.js";
15
16
  import "../chunk-ILCJ3WFD.js";
16
17
  import "../chunk-JU2RHXUB.js";
@@ -9,7 +9,7 @@ import {
9
9
  defaultRegistry,
10
10
  prepareConversion,
11
11
  resolveConversionLimits
12
- } from "../chunk-HNGS7A5Z.js";
12
+ } from "../chunk-XCHIYF3A.js";
13
13
  import {
14
14
  ConversionError
15
15
  } from "../chunk-KXOZMWBS.js";
@@ -0,0 +1,73 @@
1
+ import {
2
+ upngEncoder
3
+ } from "./chunk-34KECNZO.js";
4
+
5
+ // src/shared/svgIslandPoster.ts
6
+ var LONG_EDGE = 480;
7
+ var posterCache = /* @__PURE__ */ new Map();
8
+ function svgIslandPosterPng(aspectRatio) {
9
+ const ratio = Number.isFinite(aspectRatio) && aspectRatio > 0 ? aspectRatio : 4 / 3;
10
+ const width = Math.max(48, Math.round(ratio >= 1 ? LONG_EDGE : LONG_EDGE * ratio));
11
+ const height = Math.max(48, Math.round(ratio >= 1 ? LONG_EDGE / ratio : LONG_EDGE));
12
+ const key = `${width}x${height}`;
13
+ if (posterCache.has(key)) return posterCache.get(key) ?? null;
14
+ if (!upngEncoder) {
15
+ posterCache.set(key, null);
16
+ return null;
17
+ }
18
+ const panel = [243, 244, 246];
19
+ const ink = [156, 163, 175];
20
+ const rgba = new Uint8Array(width * height * 4);
21
+ for (let i = 0; i < width * height; i++) {
22
+ rgba[i * 4] = panel[0];
23
+ rgba[i * 4 + 1] = panel[1];
24
+ rgba[i * 4 + 2] = panel[2];
25
+ rgba[i * 4 + 3] = 255;
26
+ }
27
+ const size = Math.round(Math.min(width, height) * 0.28);
28
+ const left = (width - size) / 2;
29
+ const top = (height - size * 0.8) / 2;
30
+ const glyphH = size * 0.8;
31
+ const stroke = Math.max(2, size * 0.06);
32
+ const inFrame = (x, y) => {
33
+ const inside = x >= left && x <= left + size && y >= top && y <= top + glyphH;
34
+ const inner = x >= left + stroke && x <= left + size - stroke && y >= top + stroke && y <= top + glyphH - stroke;
35
+ return inside && !inner;
36
+ };
37
+ const sunX = left + size * 0.32;
38
+ const sunY = top + glyphH * 0.34;
39
+ const sunR = size * 0.09;
40
+ const inSun = (x, y) => (x - sunX) ** 2 + (y - sunY) ** 2 <= sunR ** 2;
41
+ const base = top + glyphH - stroke;
42
+ const inMountain = (x, y) => {
43
+ if (y > base || x < left + stroke || x > left + size - stroke) return false;
44
+ const peakX = left + size * 0.62;
45
+ const peakY = top + glyphH * 0.42;
46
+ const halfW = size * 0.38;
47
+ return y >= peakY + (base - peakY) * Math.abs(x - peakX) / halfW;
48
+ };
49
+ const SAMPLES = 4;
50
+ for (let y = Math.floor(top); y < Math.ceil(top + glyphH); y++) {
51
+ for (let x = Math.floor(left); x < Math.ceil(left + size); x++) {
52
+ let hits = 0;
53
+ for (let sy = 0; sy < SAMPLES; sy++) {
54
+ for (let sx = 0; sx < SAMPLES; sx++) {
55
+ const px = x + (sx + 0.5) / SAMPLES;
56
+ const py = y + (sy + 0.5) / SAMPLES;
57
+ if (inFrame(px, py) || inSun(px, py) || inMountain(px, py)) hits++;
58
+ }
59
+ }
60
+ if (!hits) continue;
61
+ const t = hits / SAMPLES ** 2;
62
+ const at = (y * width + x) * 4;
63
+ for (let c = 0; c < 3; c++) rgba[at + c] = Math.round(panel[c] + (ink[c] - panel[c]) * t);
64
+ }
65
+ }
66
+ const data = upngEncoder.encode([rgba.buffer], width, height, 0);
67
+ const poster = { data, contentType: "image/png", width, height };
68
+ posterCache.set(key, poster);
69
+ return poster;
70
+ }
71
+ export {
72
+ svgIslandPosterPng
73
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-formats",
3
- "version": "2.7.1",
3
+ "version": "2.7.2",
4
4
  "description": "Document format converters — DOCX, PDF, OOXML import/export",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -125,7 +125,7 @@
125
125
  "typecheck": "tsc --noEmit"
126
126
  },
127
127
  "dependencies": {
128
- "@bendyline/squisq": "2.12.0",
128
+ "@bendyline/squisq": "2.12.2",
129
129
  "@pdf-lib/fontkit": "1.1.1",
130
130
  "@pdf-lib/upng": "1.0.1",
131
131
  "@xmldom/xmldom": "0.9.12",
File without changes