@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.
- package/README.md +12 -0
- package/dist/{chunk-BSDTS72D.js → chunk-2FZUFQO3.js} +2 -2
- package/dist/{chunk-62UYYVCB.js → chunk-2VSD55NT.js} +123 -415
- package/dist/chunk-5FJZDX7H.js +298 -0
- package/dist/{chunk-5JGUJDPD.js → chunk-622IA3KB.js} +11 -9
- package/dist/{chunk-4L5MZ5OL.js → chunk-63G5OJWY.js} +16 -0
- package/dist/{chunk-VZ43ACHC.js → chunk-L4LIIOU2.js} +1 -1
- package/dist/{chunk-UD3VI5UB.js → chunk-PSAO2CTF.js} +1 -1
- package/dist/{chunk-GFUEFU6X.js → chunk-PZCWGBRQ.js} +2 -2
- package/dist/{chunk-RJ3SNKB7.js → chunk-QXZXQPL3.js} +1 -1
- package/dist/{chunk-CYXFNHWK.js → chunk-SVVPYDV3.js} +1 -1
- package/dist/diagram-pictures/index.d.ts +61 -0
- package/dist/diagram-pictures/index.js +213 -0
- package/dist/hooks/index.js +2 -2
- package/dist/index.js +24 -22
- package/dist/json-view/index.js +4 -4
- package/dist/layers/index.js +3 -3
- package/dist/markdown/index.js +3 -3
- package/dist/page/index.js +7 -6
- package/dist/player/index.js +10 -9
- package/dist/squisq-player.full.global.js +202 -202
- package/dist/squisq-player.global.js +16 -16
- package/dist/standalone-player-full.d.ts +1 -0
- package/dist/standalone-player-full.js +4 -0
- package/dist/standalone-source.js +1 -1
- package/package.json +10 -2
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
};
|
|
@@ -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
|
+
};
|