@bendyline/squisq-react 2.11.16 → 2.11.17

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.
@@ -0,0 +1,298 @@
1
+ import {
2
+ ImageLayer,
3
+ MapLayer,
4
+ MermaidLayer,
5
+ PathLayer,
6
+ ShapeLayer,
7
+ TableLayer,
8
+ TextLayer,
9
+ TreeLayer,
10
+ VideoLayer,
11
+ getTransitionClass
12
+ } from "./chunk-PZCWGBRQ.js";
13
+
14
+ // src/BlockRenderer.tsx
15
+ import { useId, useLayoutEffect, useRef, useState } from "react";
16
+ import { resolveTransitionDuration } from "@bendyline/squisq/schemas";
17
+ import { isGeometryBoundTemplate } from "@bendyline/squisq/doc";
18
+
19
+ // src/utils/textFit.ts
20
+ function largestFittingTextScale(fits, maximum = 2) {
21
+ if (maximum <= 1 || !fits(1)) return 1;
22
+ if (fits(maximum)) return maximum;
23
+ let low = 1;
24
+ let high = maximum;
25
+ for (let attempt = 0; attempt < 10; attempt += 1) {
26
+ const candidate = (low + high) / 2;
27
+ if (fits(candidate)) low = candidate;
28
+ else high = candidate;
29
+ }
30
+ return Math.max(1, Math.floor(low * 100) / 100);
31
+ }
32
+
33
+ // src/BlockRenderer.tsx
34
+ import { jsx, jsxs } from "react/jsx-runtime";
35
+ var DEFAULT_VIEWPORT = {
36
+ width: 1920,
37
+ height: 1080
38
+ };
39
+ var FIT_TOLERANCE_PX = 1;
40
+ function applyTextScale(targets, scale) {
41
+ for (const target of targets) {
42
+ if (target.dataset.squisqTextFit === "html") {
43
+ const baseFontSize = Number(target.dataset.squisqBaseFontSize);
44
+ if (Number.isFinite(baseFontSize)) target.style.fontSize = `${baseFontSize * scale}px`;
45
+ } else {
46
+ target.style.transform = `scale(${scale})`;
47
+ }
48
+ }
49
+ }
50
+ function cssPixels(value) {
51
+ const parsed = parseFloat(value);
52
+ return Number.isFinite(parsed) ? parsed : 0;
53
+ }
54
+ function htmlTargetFits(target) {
55
+ const content = target.querySelector('[data-squisq-text-content="true"]');
56
+ if (!content || content.scrollWidth <= 0 || content.scrollHeight <= 0) return false;
57
+ const view = target.ownerDocument.defaultView;
58
+ const computed = view?.getComputedStyle(target);
59
+ const horizontalInset = computed ? cssPixels(computed.paddingLeft) + cssPixels(computed.paddingRight) + cssPixels(computed.borderLeftWidth) + cssPixels(computed.borderRightWidth) : 0;
60
+ const verticalInset = computed ? cssPixels(computed.paddingTop) + cssPixels(computed.paddingBottom) + cssPixels(computed.borderTopWidth) + cssPixels(computed.borderBottomWidth) : 0;
61
+ const availableWidth = Math.max(0, target.clientWidth - horizontalInset);
62
+ const availableHeight = Math.max(0, target.clientHeight - verticalInset);
63
+ if (content.scrollWidth > availableWidth + FIT_TOLERANCE_PX || content.scrollHeight > availableHeight + FIT_TOLERANCE_PX) {
64
+ return false;
65
+ }
66
+ for (const scrollRegion of content.querySelectorAll("[data-squisq-table-scroll]")) {
67
+ if (scrollRegion.clientWidth > 0 && scrollRegion.scrollWidth > scrollRegion.clientWidth + FIT_TOLERANCE_PX || scrollRegion.clientHeight > 0 && scrollRegion.scrollHeight > scrollRegion.clientHeight + FIT_TOLERANCE_PX) {
68
+ return false;
69
+ }
70
+ }
71
+ return true;
72
+ }
73
+ function svgTargetFits(target, scale) {
74
+ const content = target.querySelector('[data-squisq-text-content="true"]');
75
+ if (!content || typeof content.getBBox !== "function") return false;
76
+ try {
77
+ const box = content.getBBox();
78
+ const availableWidth = Number(target.dataset.squisqFitWidth);
79
+ const availableHeight = Number(target.dataset.squisqFitHeight);
80
+ return box.width > 0 && box.height > 0 && box.width * scale <= availableWidth + FIT_TOLERANCE_PX && box.height * scale <= availableHeight + FIT_TOLERANCE_PX;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+ function renderedTextRects(targets) {
86
+ return targets.map(
87
+ (target) => target.querySelector('[data-squisq-text-content="true"]')?.getBoundingClientRect()
88
+ ).filter((rect) => !!rect && rect.width > 0 && rect.height > 0);
89
+ }
90
+ function overlapPairs(rects) {
91
+ const pairs = /* @__PURE__ */ new Set();
92
+ for (let left = 0; left < rects.length; left += 1) {
93
+ for (let right = left + 1; right < rects.length; right += 1) {
94
+ const a = rects[left];
95
+ const b = rects[right];
96
+ if (a.left < b.right - FIT_TOLERANCE_PX && a.right > b.left + FIT_TOLERANCE_PX && a.top < b.bottom - FIT_TOLERANCE_PX && a.bottom > b.top + FIT_TOLERANCE_PX) {
97
+ pairs.add(`${left}:${right}`);
98
+ }
99
+ }
100
+ }
101
+ return pairs;
102
+ }
103
+ function BlockRenderer({
104
+ block,
105
+ blockTime,
106
+ basePath,
107
+ isEntering = false,
108
+ isExiting = false,
109
+ transition,
110
+ viewport = DEFAULT_VIEWPORT,
111
+ isPlaying,
112
+ muted = false,
113
+ animationsEnabled = true,
114
+ theme,
115
+ tableContentRenderer,
116
+ growTextToFit = false
117
+ }) {
118
+ let transitionClass = "";
119
+ const transitionStyle = {};
120
+ const activeTransition = animationsEnabled ? transition ?? block.transition : void 0;
121
+ if (activeTransition && isEntering) {
122
+ transitionClass = getTransitionClass(activeTransition.type, true, activeTransition.direction);
123
+ transitionStyle["--transition-duration"] = `${resolveTransitionDuration(activeTransition)}s`;
124
+ } else if (activeTransition && isExiting) {
125
+ transitionClass = getTransitionClass(activeTransition.type, false, activeTransition.direction);
126
+ transitionStyle["--transition-duration"] = `${resolveTransitionDuration(activeTransition)}s`;
127
+ }
128
+ const instanceId = useId().replace(/:/g, "");
129
+ const clipId = `vb-clip-${instanceId}-${block.id}`;
130
+ const svgRef = useRef(null);
131
+ const [textScale, setTextScale] = useState(1);
132
+ const growText = growTextToFit && !isGeometryBoundTemplate(block.template);
133
+ useLayoutEffect(() => {
134
+ const svg = svgRef.current;
135
+ let cancelled = false;
136
+ if (!svg) return;
137
+ const measure = () => {
138
+ if (cancelled) return;
139
+ const targets = Array.from(svg.querySelectorAll("[data-squisq-text-fit]"));
140
+ if (!growText || targets.length === 0) {
141
+ applyTextScale(targets, 1);
142
+ setTextScale(1);
143
+ return;
144
+ }
145
+ applyTextScale(targets, 1);
146
+ const baselineOverlaps = overlapPairs(renderedTextRects(targets));
147
+ const fits = (scale) => {
148
+ applyTextScale(targets, scale);
149
+ if (targets.some(
150
+ (target) => target.dataset.squisqTextFit === "html" ? !htmlTargetFits(target) : !svgTargetFits(target, scale)
151
+ )) {
152
+ return false;
153
+ }
154
+ const candidateOverlaps = overlapPairs(renderedTextRects(targets));
155
+ for (const pair of candidateOverlaps) {
156
+ if (!baselineOverlaps.has(pair)) return false;
157
+ }
158
+ return true;
159
+ };
160
+ const nextScale = largestFittingTextScale(fits);
161
+ applyTextScale(targets, nextScale);
162
+ setTextScale((current) => current === nextScale ? current : nextScale);
163
+ };
164
+ measure();
165
+ const fonts = svg.ownerDocument?.fonts;
166
+ if (fonts && fonts.status !== "loaded") void fonts.ready.then(measure);
167
+ return () => {
168
+ cancelled = true;
169
+ };
170
+ }, [block.layers, growText, viewport.height, viewport.width]);
171
+ return /* @__PURE__ */ jsxs(
172
+ "svg",
173
+ {
174
+ ref: svgRef,
175
+ className: `block-svg ${transitionClass}`,
176
+ style: transitionStyle,
177
+ viewBox: `0 0 ${viewport.width} ${viewport.height}`,
178
+ preserveAspectRatio: "xMidYMid meet",
179
+ overflow: "hidden",
180
+ "data-block-id": block.id,
181
+ "data-squisq-text-scale": growText ? textScale : void 0,
182
+ children: [
183
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: viewport.width, height: viewport.height }) }) }),
184
+ /* @__PURE__ */ jsx("g", { clipPath: `url(#${clipId})`, children: (block.layers ?? []).map((layer) => /* @__PURE__ */ jsx(
185
+ LayerRenderer,
186
+ {
187
+ block,
188
+ layer,
189
+ basePath,
190
+ viewport,
191
+ blockTime,
192
+ isPlaying,
193
+ muted,
194
+ animationsEnabled,
195
+ theme,
196
+ tableContentRenderer,
197
+ textScale
198
+ },
199
+ layer.id
200
+ )) })
201
+ ]
202
+ }
203
+ );
204
+ }
205
+ function LayerRenderer({
206
+ block,
207
+ layer,
208
+ basePath,
209
+ viewport,
210
+ blockTime,
211
+ isPlaying,
212
+ muted,
213
+ animationsEnabled,
214
+ theme,
215
+ tableContentRenderer,
216
+ textScale
217
+ }) {
218
+ const renderedLayer = animationsEnabled || !layer.animation ? layer : { ...layer, animation: void 0 };
219
+ switch (renderedLayer.type) {
220
+ case "image":
221
+ return /* @__PURE__ */ jsx(
222
+ ImageLayer,
223
+ {
224
+ layer: renderedLayer,
225
+ basePath,
226
+ viewport,
227
+ blockTime,
228
+ animationsEnabled
229
+ }
230
+ );
231
+ case "text":
232
+ return /* @__PURE__ */ jsx(
233
+ TextLayer,
234
+ {
235
+ layer: renderedLayer,
236
+ viewport,
237
+ blockTime,
238
+ textScale
239
+ }
240
+ );
241
+ case "shape":
242
+ return /* @__PURE__ */ jsx(ShapeLayer, { layer: renderedLayer, viewport, blockTime });
243
+ case "path":
244
+ return /* @__PURE__ */ jsx(PathLayer, { layer: renderedLayer, viewport, blockTime });
245
+ case "map":
246
+ return /* @__PURE__ */ jsx(
247
+ MapLayer,
248
+ {
249
+ layer: renderedLayer,
250
+ basePath,
251
+ viewport,
252
+ blockTime
253
+ }
254
+ );
255
+ case "video":
256
+ return /* @__PURE__ */ jsx(
257
+ VideoLayer,
258
+ {
259
+ layer: renderedLayer,
260
+ basePath,
261
+ viewport,
262
+ blockTime,
263
+ isPlaying,
264
+ muted
265
+ }
266
+ );
267
+ case "table":
268
+ return /* @__PURE__ */ jsx(
269
+ TableLayer,
270
+ {
271
+ block,
272
+ layer: renderedLayer,
273
+ viewport,
274
+ blockTime,
275
+ contentRenderer: tableContentRenderer
276
+ }
277
+ );
278
+ case "tree":
279
+ return /* @__PURE__ */ jsx(TreeLayer, { layer: renderedLayer, viewport, blockTime });
280
+ case "mermaid":
281
+ return /* @__PURE__ */ jsx(
282
+ MermaidLayer,
283
+ {
284
+ layer: renderedLayer,
285
+ viewport,
286
+ blockTime,
287
+ theme
288
+ }
289
+ );
290
+ default:
291
+ console.warn(`Unknown layer type: ${renderedLayer.type}`);
292
+ return null;
293
+ }
294
+ }
295
+
296
+ export {
297
+ BlockRenderer
298
+ };
@@ -1,28 +1,30 @@
1
1
  import {
2
- BlockRenderer,
3
2
  LinearDocView
4
- } from "./chunk-62UYYVCB.js";
5
- import {
6
- isCaptureDrivenVideo,
7
- seekVideoToFrame
8
- } from "./chunk-GFUEFU6X.js";
3
+ } from "./chunk-2VSD55NT.js";
9
4
  import {
10
5
  useAudioSync,
11
6
  useDocPlayback,
12
7
  useMediaSchedule,
13
8
  useViewportOrientation
14
- } from "./chunk-CYXFNHWK.js";
9
+ } from "./chunk-SVVPYDV3.js";
15
10
  import {
16
11
  useAutoSurface
17
12
  } from "./chunk-TT6ENR6T.js";
18
13
  import {
19
14
  MarkdownRenderer
20
- } from "./chunk-BSDTS72D.js";
15
+ } from "./chunk-2FZUFQO3.js";
16
+ import {
17
+ BlockRenderer
18
+ } from "./chunk-5FJZDX7H.js";
19
+ import {
20
+ isCaptureDrivenVideo,
21
+ seekVideoToFrame
22
+ } from "./chunk-PZCWGBRQ.js";
21
23
  import {
22
24
  useMediaProvider,
23
25
  useMediaUrl,
24
26
  useResourcePolicy
25
- } from "./chunk-4L5MZ5OL.js";
27
+ } from "./chunk-63G5OJWY.js";
26
28
 
27
29
  // src/MediaClipLayer.tsx
28
30
  import { useEffect, useRef } from "react";
@@ -91,6 +91,21 @@ function renderMermaidSvg(id, source, container, theme = DEFAULT_THEME) {
91
91
  return { svg, diagramType };
92
92
  });
93
93
  }
94
+ function renderMermaidSvgForExport(id, source, theme = DEFAULT_THEME, fontFamily) {
95
+ return enqueue(async () => {
96
+ const mermaid = await loadMermaid();
97
+ const themeVariables = buildMermaidThemeVariables(theme);
98
+ mermaid.initialize({
99
+ ...MERMAID_BASE_CONFIG,
100
+ theme: "base",
101
+ themeVariables: fontFamily ? { ...themeVariables, fontFamily } : themeVariables,
102
+ htmlLabels: false,
103
+ journey: { textPlacement: "tspan" }
104
+ });
105
+ const { svg } = await mermaid.render(id, source);
106
+ return svg;
107
+ });
108
+ }
94
109
  function mermaidRenderErrorMessage(error) {
95
110
  const raw = error instanceof Error ? error.message : String(error);
96
111
  return raw.split(/\r?\n/).map((line) => line.trimEnd()).filter(Boolean).slice(0, 6).join("\n") || "Unknown Mermaid rendering error.";
@@ -103,5 +118,6 @@ export {
103
118
  useMediaProvider,
104
119
  useMediaUrl,
105
120
  renderMermaidSvg,
121
+ renderMermaidSvgForExport,
106
122
  mermaidRenderErrorMessage
107
123
  };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-TT6ENR6T.js";
4
4
  import {
5
5
  MarkdownRenderer
6
- } from "./chunk-BSDTS72D.js";
6
+ } from "./chunk-2FZUFQO3.js";
7
7
 
8
8
  // src/jsonView/useJsonViewTokens.ts
9
9
  import { useMemo } from "react";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  LinearDocView
3
- } from "./chunk-62UYYVCB.js";
3
+ } from "./chunk-2VSD55NT.js";
4
4
 
5
5
  // src/PaginatedDocView.tsx
6
6
  import {
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  MermaidDiagram
3
- } from "./chunk-RJ3SNKB7.js";
3
+ } from "./chunk-QXZXQPL3.js";
4
4
  import {
5
5
  useMediaUrl,
6
6
  useResourcePolicy
7
- } from "./chunk-4L5MZ5OL.js";
7
+ } from "./chunk-63G5OJWY.js";
8
8
 
9
9
  // src/utils/animationUtils.ts
10
10
  import {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  mermaidRenderErrorMessage,
3
3
  renderMermaidSvg
4
- } from "./chunk-4L5MZ5OL.js";
4
+ } from "./chunk-63G5OJWY.js";
5
5
 
6
6
  // src/mermaid/MermaidDiagram.tsx
7
7
  import { useEffect, useState } from "react";
@@ -2,7 +2,7 @@ import {
2
2
  renderMermaidSvg,
3
3
  useMediaProvider,
4
4
  useResourcePolicy
5
- } from "./chunk-4L5MZ5OL.js";
5
+ } from "./chunk-63G5OJWY.js";
6
6
 
7
7
  // src/hooks/useMediaSchedule.ts
8
8
  import { useMemo } from "react";
@@ -0,0 +1,61 @@
1
+ import { Theme } from '@bendyline/squisq/schemas';
2
+
3
+ /**
4
+ * PNG pictures of diagrams, for document exports (DOCX, EPUB, PDF, PPTX).
5
+ *
6
+ * Browser-only. A diagram is drawn as SVG, the SVG is loaded as an image,
7
+ * and the image is painted onto a canvas and read back as PNG. That last
8
+ * step fails in some browsers once the SVG holds `<foreignObject>`, so every
9
+ * path here produces plain SVG: Mermaid with SVG-text labels, Squisq
10
+ * diagrams through `BlockRenderer` without their slide chrome, and file
11
+ * trees as monospace text. A picture also cannot load the page's web fonts,
12
+ * so text is set in a system font stack.
13
+ *
14
+ * `createDiagramPictureRenderer` matches the `DiagramRasterizer` that
15
+ * `@bendyline/squisq-formats/diagrams` takes.
16
+ */
17
+
18
+ /** A PNG and the size it should display at, in CSS pixels. */
19
+ interface DiagramPicturePng {
20
+ data: Uint8Array;
21
+ width: number;
22
+ height: number;
23
+ }
24
+ interface DiagramPictureOptions {
25
+ /** Colors for the diagram. Default: Squisq's default (light) theme. */
26
+ theme?: Theme;
27
+ /** Pixel density of the PNG relative to its display size. Default: 2. */
28
+ scale?: number;
29
+ /** Widest display size in CSS pixels. Default: 680. */
30
+ maxWidth?: number;
31
+ /** Background painted behind the diagram. Default: white. */
32
+ background?: string;
33
+ }
34
+ /** The diagram description `@bendyline/squisq-formats/diagrams` passes. */
35
+ interface DiagramToPicture {
36
+ kind: 'mermaid' | 'diagram' | 'timeline' | 'tree' | 'container';
37
+ markdown: string;
38
+ source: string;
39
+ }
40
+ /** System fonts every platform has, since a picture can't load web fonts. */
41
+ declare const EXPORT_FONT_STACK = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
42
+ /** Paint `svg` (viewBox already set) at `width` × `height` CSS pixels and read back PNG bytes. */
43
+ declare function svgToPng(svg: SVGSVGElement, width: number, height: number, options?: DiagramPictureOptions): Promise<DiagramPicturePng>;
44
+ /** A Mermaid diagram as a PNG at its natural size (capped at `maxWidth`). */
45
+ declare function renderMermaidPicture(source: string, options?: DiagramPictureOptions): Promise<DiagramPicturePng>;
46
+ /** A file-tree fence as monospace text lines, exactly as written. */
47
+ declare function renderTreePicture(source: string, options?: DiagramPictureOptions): Promise<DiagramPicturePng | null>;
48
+ /**
49
+ * A Squisq-rendered diagram — an ASCII diagram or timeline fence, or a
50
+ * drawing, layout, or diagram heading block — as a PNG cropped to its
51
+ * content, or null when it has nothing to draw.
52
+ */
53
+ declare function renderSquisqDiagramPicture(markdown: string, options?: DiagramPictureOptions): Promise<DiagramPicturePng | null>;
54
+ /**
55
+ * A renderer for `rasterizeDiagrams` from `@bendyline/squisq-formats/diagrams`:
56
+ * Mermaid, ASCII diagrams and timelines, file trees, and drawing, layout,
57
+ * and diagram blocks.
58
+ */
59
+ declare function createDiagramPictureRenderer(options?: DiagramPictureOptions): (diagram: DiagramToPicture) => Promise<DiagramPicturePng | null>;
60
+
61
+ export { type DiagramPictureOptions, type DiagramPicturePng, type DiagramToPicture, EXPORT_FONT_STACK, createDiagramPictureRenderer, renderMermaidPicture, renderSquisqDiagramPicture, renderTreePicture, svgToPng };
@@ -0,0 +1,213 @@
1
+ import {
2
+ BlockRenderer
3
+ } from "../chunk-5FJZDX7H.js";
4
+ import "../chunk-PZCWGBRQ.js";
5
+ import "../chunk-QXZXQPL3.js";
6
+ import {
7
+ renderMermaidSvgForExport
8
+ } from "../chunk-63G5OJWY.js";
9
+
10
+ // src/export/diagramPictures.ts
11
+ import { createElement } from "react";
12
+ import { createRoot } from "react-dom/client";
13
+ import { VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
14
+ import {
15
+ DEFAULT_THEME,
16
+ flattenRenderableBlocks,
17
+ markdownToDoc,
18
+ materializeBlockLayers
19
+ } from "@bendyline/squisq/doc";
20
+ import { parseMarkdown } from "@bendyline/squisq/markdown";
21
+ var EXPORT_FONT_STACK = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif";
22
+ var EXPORT_MONO_STACK = "ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace";
23
+ var CANVAS_UNITS_PER_CSS_PIXEL = 2.5;
24
+ var PADDING = 16;
25
+ var SVG_NS = "http://www.w3.org/2000/svg";
26
+ var pictureCount = 0;
27
+ function settings(options) {
28
+ return {
29
+ theme: options.theme ?? DEFAULT_THEME,
30
+ scale: options.scale ?? 2,
31
+ maxWidth: options.maxWidth ?? 680,
32
+ background: options.background ?? "#ffffff"
33
+ };
34
+ }
35
+ async function svgToPng(svg, width, height, options = {}) {
36
+ const { scale, background } = settings(options);
37
+ const pixelWidth = Math.max(1, Math.round(width * scale));
38
+ const pixelHeight = Math.max(1, Math.round(height * scale));
39
+ const clone = svg.cloneNode(true);
40
+ clone.setAttribute("xmlns", SVG_NS);
41
+ clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
42
+ clone.setAttribute("width", String(pixelWidth));
43
+ clone.setAttribute("height", String(pixelHeight));
44
+ clone.removeAttribute("style");
45
+ const xml = new XMLSerializer().serializeToString(clone);
46
+ const image = new Image();
47
+ image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(xml)}`;
48
+ await image.decode();
49
+ const canvas = document.createElement("canvas");
50
+ canvas.width = pixelWidth;
51
+ canvas.height = pixelHeight;
52
+ const context = canvas.getContext("2d");
53
+ if (!context) throw new Error("No 2D canvas is available to draw the diagram.");
54
+ context.fillStyle = background;
55
+ context.fillRect(0, 0, pixelWidth, pixelHeight);
56
+ context.drawImage(image, 0, 0, pixelWidth, pixelHeight);
57
+ const blob = await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
58
+ if (!blob) throw new Error("The diagram could not be encoded as PNG.");
59
+ return {
60
+ data: new Uint8Array(await blob.arrayBuffer()),
61
+ width: Math.round(width),
62
+ height: Math.round(height)
63
+ };
64
+ }
65
+ function parseViewBox(svg) {
66
+ const values = (svg.getAttribute("viewBox") ?? "").trim().split(/[\s,]+/u).map(Number);
67
+ if (values.length !== 4 || values.some((value) => !Number.isFinite(value))) return null;
68
+ const [x = 0, y = 0, width = 0, height = 0] = values;
69
+ return width > 0 && height > 0 ? { x, y, width, height } : null;
70
+ }
71
+ async function renderMermaidPicture(source, options = {}) {
72
+ const { theme, maxWidth } = settings(options);
73
+ const markup = await renderMermaidSvgForExport(
74
+ `squisq-export-mermaid-${String(++pictureCount)}`,
75
+ source,
76
+ theme,
77
+ EXPORT_FONT_STACK
78
+ );
79
+ const template = document.createElement("template");
80
+ template.innerHTML = markup;
81
+ const svg = template.content.querySelector("svg");
82
+ if (!svg) throw new Error("Mermaid produced no SVG.");
83
+ const box = parseViewBox(svg);
84
+ if (!box) throw new Error("The Mermaid SVG has no size.");
85
+ const style = document.createElementNS(SVG_NS, "style");
86
+ style.textContent = `.edgeLabel text, .edgeLabel tspan { fill: ${theme.colors.text} !important; }`;
87
+ svg.append(style);
88
+ const width = Math.min(maxWidth, box.width);
89
+ return svgToPng(svg, width, box.height * width / box.width, options);
90
+ }
91
+ async function renderTreePicture(source, options = {}) {
92
+ const { theme, maxWidth } = settings(options);
93
+ const lines = source.replace(/\s+$/u, "").split("\n");
94
+ if (lines.length === 0) return null;
95
+ const fontSize = 14;
96
+ const lineHeight = 20;
97
+ const longest = Math.max(...lines.map((line) => [...line].length));
98
+ const width = Math.min(maxWidth, Math.ceil(longest * fontSize * 0.62) + 2 * PADDING);
99
+ const height = lines.length * lineHeight + 2 * PADDING;
100
+ const svg = document.createElementNS(SVG_NS, "svg");
101
+ svg.setAttribute("viewBox", `0 0 ${String(width)} ${String(height)}`);
102
+ const text = document.createElementNS(SVG_NS, "text");
103
+ text.setAttribute("font-family", EXPORT_MONO_STACK);
104
+ text.setAttribute("font-size", String(fontSize));
105
+ text.setAttribute("fill", theme.colors.text);
106
+ text.setAttribute("xml:space", "preserve");
107
+ lines.forEach((line, index) => {
108
+ const span = document.createElementNS(SVG_NS, "tspan");
109
+ span.setAttribute("x", String(PADDING));
110
+ span.setAttribute("y", String(PADDING + (index + 0.75) * lineHeight));
111
+ span.textContent = line;
112
+ text.append(span);
113
+ });
114
+ svg.append(text);
115
+ return svgToPng(svg, width, height, options);
116
+ }
117
+ function isChrome(layer) {
118
+ const id = layer.id;
119
+ if (id === "bg" || id === "title" || id.endsWith("-title") || id.endsWith("-description")) {
120
+ return true;
121
+ }
122
+ if (id.endsWith("-empty")) return true;
123
+ return layer.type === "shape" && typeof layer.content.fill === "string" && layer.content.fill.includes("gradient(");
124
+ }
125
+ function withExportFont(layer) {
126
+ if (layer.type !== "text") return layer;
127
+ return {
128
+ ...layer,
129
+ content: { ...layer.content, style: { ...layer.content.style, fontFamily: EXPORT_FONT_STACK } }
130
+ };
131
+ }
132
+ async function settle() {
133
+ await document.fonts?.ready;
134
+ for (let frame = 0; frame < 2; frame++) {
135
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
136
+ }
137
+ }
138
+ async function renderSquisqDiagramPicture(markdown, options = {}) {
139
+ const { theme, maxWidth } = settings(options);
140
+ const doc = markdownToDoc(parseMarkdown(markdown), { generateCoverBlock: false });
141
+ const block = flattenRenderableBlocks(doc.blocks)[0];
142
+ if (!block) return null;
143
+ const viewport = VIEWPORT_PRESETS.landscape;
144
+ const { layers } = materializeBlockLayers(block, { theme, viewport, persistentLayers: false });
145
+ const kept = layers.filter((layer) => !isChrome(layer)).map(withExportFont);
146
+ if (kept.length === 0) return null;
147
+ const host = document.createElement("div");
148
+ host.setAttribute("aria-hidden", "true");
149
+ host.style.cssText = `position:fixed;left:-${String(viewport.width * 2)}px;top:0;width:${String(
150
+ viewport.width
151
+ )}px;height:${String(viewport.height)}px;pointer-events:none;`;
152
+ document.body.append(host);
153
+ const root = createRoot(host);
154
+ try {
155
+ const pictureBlock = {
156
+ id: `squisq-export-${String(++pictureCount)}`,
157
+ startTime: 0,
158
+ duration: 0,
159
+ audioSegment: 0,
160
+ layers: kept
161
+ };
162
+ root.render(
163
+ createElement(BlockRenderer, {
164
+ block: pictureBlock,
165
+ blockTime: 0,
166
+ basePath: "",
167
+ viewport,
168
+ animationsEnabled: false,
169
+ theme
170
+ })
171
+ );
172
+ await settle();
173
+ const svg = host.querySelector("svg.block-svg");
174
+ if (!svg) return null;
175
+ const box = svg.getBBox();
176
+ if (box.width <= 0 || box.height <= 0) return null;
177
+ const x = box.x - PADDING;
178
+ const y = box.y - PADDING;
179
+ const contentWidth = box.width + 2 * PADDING;
180
+ const contentHeight = box.height + 2 * PADDING;
181
+ const cropped = svg.cloneNode(true);
182
+ cropped.setAttribute(
183
+ "viewBox",
184
+ `${String(x)} ${String(y)} ${String(contentWidth)} ${String(contentHeight)}`
185
+ );
186
+ cropped.removeAttribute("clip-path");
187
+ const width = Math.min(maxWidth, contentWidth / CANVAS_UNITS_PER_CSS_PIXEL);
188
+ return await svgToPng(cropped, width, contentHeight * width / contentWidth, options);
189
+ } finally {
190
+ root.unmount();
191
+ host.remove();
192
+ }
193
+ }
194
+ function createDiagramPictureRenderer(options = {}) {
195
+ return async (diagram) => {
196
+ switch (diagram.kind) {
197
+ case "mermaid":
198
+ return renderMermaidPicture(diagram.source, options);
199
+ case "tree":
200
+ return renderTreePicture(diagram.source, options);
201
+ default:
202
+ return renderSquisqDiagramPicture(diagram.markdown, options);
203
+ }
204
+ };
205
+ }
206
+ export {
207
+ EXPORT_FONT_STACK,
208
+ createDiagramPictureRenderer,
209
+ renderMermaidPicture,
210
+ renderSquisqDiagramPicture,
211
+ renderTreePicture,
212
+ svgToPng
213
+ };