@bendyline/squisq-formats 2.7.1 → 2.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -87,7 +87,7 @@ 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). Pages stay white for printing. Theme text, heading, and link colors are retained when they meet 4.5:1 contrast against both white paper and the shaded table header; otherwise the exporter uses its default dark ink for that role. This also applies to inherited and custom themes. 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.
90
+ **Fidelity:** export uses pdf-lib's standard 14 fonts (`themeId` affects colors only). Text they cannot encode — Greek, Cyrillic, Vietnamese, arrows, math, box drawing, symbols — is drawn with an embedded Unicode font: the bundled DejaVu Sans faces, loaded and subset only for a document that needs them (`unicodeFonts` supplies your own faces, or `false` turns this off). CJK and emoji are still replaced, and reported once through `onWarning`. Pages stay white for printing. Theme text, heading, and link colors are retained when they meet 4.5:1 contrast against both white paper and the shaded table header; otherwise the exporter uses its default dark ink for that role. This also applies to inherited and custom themes. 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
91
 
92
92
  ### Diagrams as pictures
93
93
 
@@ -6,6 +6,7 @@ repository license, the pinned upstream copy is vendored and identified below.
6
6
 
7
7
  COMPONENTS
8
8
  - @fortawesome/fontawesome-free@7.3.1 ((CC-BY-4.0 AND OFL-1.1 AND MIT)); files: LICENSE.txt
9
+ - dejavu-fonts-ttf@2.37.3 (SEE LICENSE IN README.md AND LICENSE); files: LICENSE
9
10
 
10
11
  ==============================================================================
11
12
  @fortawesome/fontawesome-free@7.3.1 - LICENSE.txt
@@ -176,3 +177,194 @@ trademarks does not indicate endorsement of the trademark holder by Font
176
177
  Awesome, nor vice versa. **Please do not use brand logos for any purpose except
177
178
  to represent the company, product, or service to which they refer.**
178
179
 
180
+ ==============================================================================
181
+ dejavu-fonts-ttf@2.37.3 - LICENSE
182
+ ==============================================================================
183
+ Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
184
+ Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
185
+
186
+
187
+ Bitstream Vera Fonts Copyright
188
+ ------------------------------
189
+
190
+ Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
191
+ a trademark of Bitstream, Inc.
192
+
193
+ Permission is hereby granted, free of charge, to any person obtaining a copy
194
+ of the fonts accompanying this license ("Fonts") and associated
195
+ documentation files (the "Font Software"), to reproduce and distribute the
196
+ Font Software, including without limitation the rights to use, copy, merge,
197
+ publish, distribute, and/or sell copies of the Font Software, and to permit
198
+ persons to whom the Font Software is furnished to do so, subject to the
199
+ following conditions:
200
+
201
+ The above copyright and trademark notices and this permission notice shall
202
+ be included in all copies of one or more of the Font Software typefaces.
203
+
204
+ The Font Software may be modified, altered, or added to, and in particular
205
+ the designs of glyphs or characters in the Fonts may be modified and
206
+ additional glyphs or characters may be added to the Fonts, only if the fonts
207
+ are renamed to names not containing either the words "Bitstream" or the word
208
+ "Vera".
209
+
210
+ This License becomes null and void to the extent applicable to Fonts or Font
211
+ Software that has been modified and is distributed under the "Bitstream
212
+ Vera" names.
213
+
214
+ The Font Software may be sold as part of a larger software package but no
215
+ copy of one or more of the Font Software typefaces may be sold by itself.
216
+
217
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
218
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
219
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
220
+ TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
221
+ FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
222
+ ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
223
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
224
+ THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
225
+ FONT SOFTWARE.
226
+
227
+ Except as contained in this notice, the names of Gnome, the Gnome
228
+ Foundation, and Bitstream Inc., shall not be used in advertising or
229
+ otherwise to promote the sale, use or other dealings in this Font Software
230
+ without prior written authorization from the Gnome Foundation or Bitstream
231
+ Inc., respectively. For further information, contact: fonts at gnome dot
232
+ org.
233
+
234
+ Arev Fonts Copyright
235
+ ------------------------------
236
+
237
+ Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
238
+
239
+ Permission is hereby granted, free of charge, to any person obtaining
240
+ a copy of the fonts accompanying this license ("Fonts") and
241
+ associated documentation files (the "Font Software"), to reproduce
242
+ and distribute the modifications to the Bitstream Vera Font Software,
243
+ including without limitation the rights to use, copy, merge, publish,
244
+ distribute, and/or sell copies of the Font Software, and to permit
245
+ persons to whom the Font Software is furnished to do so, subject to
246
+ the following conditions:
247
+
248
+ The above copyright and trademark notices and this permission notice
249
+ shall be included in all copies of one or more of the Font Software
250
+ typefaces.
251
+
252
+ The Font Software may be modified, altered, or added to, and in
253
+ particular the designs of glyphs or characters in the Fonts may be
254
+ modified and additional glyphs or characters may be added to the
255
+ Fonts, only if the fonts are renamed to names not containing either
256
+ the words "Tavmjong Bah" or the word "Arev".
257
+
258
+ This License becomes null and void to the extent applicable to Fonts
259
+ or Font Software that has been modified and is distributed under the
260
+ "Tavmjong Bah Arev" names.
261
+
262
+ The Font Software may be sold as part of a larger software package but
263
+ no copy of one or more of the Font Software typefaces may be sold by
264
+ itself.
265
+
266
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
267
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
268
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
269
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
270
+ TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
271
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
272
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
273
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
274
+ OTHER DEALINGS IN THE FONT SOFTWARE.
275
+
276
+ Except as contained in this notice, the name of Tavmjong Bah shall not
277
+ be used in advertising or otherwise to promote the sale, use or other
278
+ dealings in this Font Software without prior written authorization
279
+ from Tavmjong Bah. For further information, contact: tavmjong @ free
280
+ . fr.
281
+
282
+ TeX Gyre DJV Math
283
+ -----------------
284
+ Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
285
+
286
+ Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski
287
+ (on behalf of TeX users groups) are in public domain.
288
+
289
+ Letters imported from Euler Fraktur from AMSfonts are (c) American
290
+ Mathematical Society (see below).
291
+ Bitstream Vera Fonts Copyright
292
+ Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera
293
+ is a trademark of Bitstream, Inc.
294
+
295
+ Permission is hereby granted, free of charge, to any person obtaining a copy
296
+ of the fonts accompanying this license (“Fonts”) and associated
297
+ documentation
298
+ files (the “Font Software”), to reproduce and distribute the Font Software,
299
+ including without limitation the rights to use, copy, merge, publish,
300
+ distribute,
301
+ and/or sell copies of the Font Software, and to permit persons to whom
302
+ the Font Software is furnished to do so, subject to the following
303
+ conditions:
304
+
305
+ The above copyright and trademark notices and this permission notice
306
+ shall be
307
+ included in all copies of one or more of the Font Software typefaces.
308
+
309
+ The Font Software may be modified, altered, or added to, and in particular
310
+ the designs of glyphs or characters in the Fonts may be modified and
311
+ additional
312
+ glyphs or characters may be added to the Fonts, only if the fonts are
313
+ renamed
314
+ to names not containing either the words “Bitstream” or the word “Vera”.
315
+
316
+ This License becomes null and void to the extent applicable to Fonts or
317
+ Font Software
318
+ that has been modified and is distributed under the “Bitstream Vera”
319
+ names.
320
+
321
+ The Font Software may be sold as part of a larger software package but
322
+ no copy
323
+ of one or more of the Font Software typefaces may be sold by itself.
324
+
325
+ THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
326
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
327
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
328
+ TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
329
+ FOUNDATION
330
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL,
331
+ SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN
332
+ ACTION
333
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
334
+ INABILITY TO USE
335
+ THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
336
+ Except as contained in this notice, the names of GNOME, the GNOME
337
+ Foundation,
338
+ and Bitstream Inc., shall not be used in advertising or otherwise to promote
339
+ the sale, use or other dealings in this Font Software without prior written
340
+ authorization from the GNOME Foundation or Bitstream Inc., respectively.
341
+ For further information, contact: fonts at gnome dot org.
342
+
343
+ AMSFonts (v. 2.2) copyright
344
+
345
+ The PostScript Type 1 implementation of the AMSFonts produced by and
346
+ previously distributed by Blue Sky Research and Y&Y, Inc. are now freely
347
+ available for general use. This has been accomplished through the
348
+ cooperation
349
+ of a consortium of scientific publishers with Blue Sky Research and Y&Y.
350
+ Members of this consortium include:
351
+
352
+ Elsevier Science IBM Corporation Society for Industrial and Applied
353
+ Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS)
354
+
355
+ In order to assure the authenticity of these fonts, copyright will be
356
+ held by
357
+ the American Mathematical Society. This is not meant to restrict in any way
358
+ the legitimate use of the fonts, such as (but not limited to) electronic
359
+ distribution of documents containing these fonts, inclusion of these fonts
360
+ into other public domain or commercial font collections or computer
361
+ applications, use of the outline data to create derivative fonts and/or
362
+ faces, etc. However, the AMS does require that the AMS copyright notice be
363
+ removed from any derivative versions of the fonts which have been altered in
364
+ any way. In addition, to ensure the fidelity of TeX documents using Computer
365
+ Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces,
366
+ has requested that any alterations which yield different font metrics be
367
+ given a different name.
368
+
369
+ $Id$
370
+
@@ -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
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  convert,
3
3
  defaultRegistry
4
- } from "./chunk-HNGS7A5Z.js";
4
+ } from "./chunk-CUQ7YITW.js";
5
5
  import {
6
6
  ConversionError
7
7
  } from "./chunk-KXOZMWBS.js";