@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.
@@ -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);
@@ -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-GHHIYBUT.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-CUQ7YITW.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,
@@ -93,7 +94,7 @@ import {
93
94
  markdownDocToPdf,
94
95
  pdfToDoc,
95
96
  pdfToMarkdownDoc
96
- } from "./chunk-YERFQ2SB.js";
97
+ } from "./chunk-GRIARAMQ.js";
97
98
  import "./chunk-34KECNZO.js";
98
99
  import "./chunk-BXWNU4T5.js";
99
100
  import "./chunk-IIQYS2YH.js";
@@ -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-GHHIYBUT.js";
10
+ import "../chunk-CUQ7YITW.js";
11
11
  import "../chunk-KXOZMWBS.js";
12
12
  import {
13
13
  OUTSIDE_IN_FORMAT_IDS,
@@ -2,6 +2,36 @@ import { ThemeRegistry, Doc } from '@bendyline/squisq/schemas';
2
2
  import { MarkdownDocument } from '@bendyline/squisq/markdown';
3
3
  import { ContentContainer } from '@bendyline/squisq/storage';
4
4
 
5
+ /**
6
+ * Unicode text for PDF export.
7
+ *
8
+ * The 14 standard PDF fonts encode only WinAnsi (see `winAnsi.ts`), so Greek,
9
+ * Cyrillic, Vietnamese, arrows, math, box drawing and most symbols used to
10
+ * reach the page as `?` or an ASCII stand-in. When a document holds such text,
11
+ * the writer embeds a Unicode font beside the standard ones — by default the
12
+ * bundled DejaVu Sans faces — and draws those characters with it.
13
+ *
14
+ * Text is split into runs, each with the font that draws it:
15
+ * - characters WinAnsi encodes keep the standard font, so ordinary Latin text
16
+ * looks exactly as it did before;
17
+ * - a word that needs the Unicode font is drawn in it whole, so its letters and
18
+ * punctuation match rather than alternating faces mid-word;
19
+ * - a character neither font can draw (CJK and emoji, with the bundled faces)
20
+ * still goes through the WinAnsi sanitizer, which replaces it and counts it
21
+ * toward the export's one warning.
22
+ */
23
+
24
+ /**
25
+ * TrueType or OpenType bytes for the faces that draw what the standard fonts
26
+ * cannot. Bold falls back to `regular`, and `mono` (used for code) to
27
+ * `regular` as well.
28
+ */
29
+ interface PdfUnicodeFonts {
30
+ regular: Uint8Array | ArrayBuffer;
31
+ bold?: Uint8Array | ArrayBuffer;
32
+ mono?: Uint8Array | ArrayBuffer;
33
+ }
34
+
5
35
  /**
6
36
  * PDF Export
7
37
  *
@@ -74,6 +104,14 @@ interface PdfExportOptions {
74
104
  * `ConversionResult.warnings` instead.
75
105
  */
76
106
  onWarning?: (message: string) => void;
107
+ /**
108
+ * Fonts that draw what the standard PDF fonts cannot encode — Greek,
109
+ * Cyrillic, Vietnamese, arrows, math, box drawing, symbols. Default: the
110
+ * bundled DejaVu Sans faces, embedded (subset) only in a document that has
111
+ * such text. Pass your own faces to cover other scripts, or `false` to keep
112
+ * the standard fonts alone and replace those characters.
113
+ */
114
+ unicodeFonts?: PdfUnicodeFonts | false;
77
115
  }
78
116
  /**
79
117
  * Convert a MarkdownDocument to a PDF ArrayBuffer.
package/dist/pdf/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  pdfToContainer,
6
6
  pdfToDoc,
7
7
  pdfToMarkdownDoc
8
- } from "../chunk-YERFQ2SB.js";
8
+ } from "../chunk-GRIARAMQ.js";
9
9
  import "../chunk-34KECNZO.js";
10
10
  import "../chunk-BXWNU4T5.js";
11
11
  import "../chunk-IIQYS2YH.js";
@@ -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-CUQ7YITW.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
+ };