@simon_he/vue-tui 1.1.4 → 1.1.5
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 +18 -18
- package/dist/agent/mermaid.d.cts +1 -1
- package/dist/agent/mermaid.d.ts +1 -1
- package/dist/agent.cjs +5 -1
- package/dist/agent.js +3 -3
- package/dist/{block-source-D3YGTErb.js → block-source-DhzajB9H.js} +1 -1
- package/dist/cli.cjs +5 -1
- package/dist/cli.js +1 -1
- package/dist/experimental.cjs +5 -1
- package/dist/experimental.js +2 -2
- package/dist/markdown.js +2 -2
- package/dist/{mermaid-uEfRItDe.js → mermaid-N3kGTHFy.js} +655 -163
- package/dist/mermaid.cjs +544 -176
- package/dist/mermaid.d.cts +1 -1
- package/dist/mermaid.d.ts +1 -1
- package/dist/mermaid.js +3 -3
- package/dist/renderer/terminal-graphics.d.ts +17 -0
- package/dist/{terminal-graphics-DWZrl3oh.js → terminal-graphics-2FTRg9x3.js} +6 -1
- package/dist/{tlog-theme-DqnF_zNe.js → tlog-theme-B91Wv7Zj.js} +1 -1
- package/dist/vue/components/TMermaidImage.d.ts +187 -0
- package/dist/vue/mermaid/beautiful-mermaid.d.ts +282 -238
- package/dist/vue/mermaid/mermaid-image.d.ts +8 -0
- package/package.json +11 -10
|
@@ -1,128 +1,9 @@
|
|
|
1
1
|
import { EventZIndexContextKey, intersectRect, padEndByCells$1 as padEndByCells, repeatChar, sanitizeInlineText, sanitizeTextBlock, sliceByCells, sliceByCellsRange, spaces, textCellWidth$1 as textCellWidth, translateRect, useLayout, useRenderNode, useTerminal, useTerminalNode, useVisibility, withTextWidthProvider } from "./rect-JzWMJxxc.js";
|
|
2
2
|
import { TMermaidText$1 as TMermaidText, isSimpleMermaidFlowchartSource, markMermaidRenderErrorFatal, tMermaidTextProps } from "./TMermaidText-wLnMMvVS.js";
|
|
3
|
-
import { createTerminalGraphicPngSequence, getTerminalGraphicsOutput, getTerminalGraphicsOutputVersion, stableTerminalGraphicNumericId, subscribeTerminalGraphicsOutput } from "./terminal-graphics-
|
|
3
|
+
import { createTerminalGraphicPngSequence, getTerminalGraphicsOutput, getTerminalGraphicsOutputVersion, stableTerminalGraphicNumericId, subscribeTerminalGraphicsOutput } from "./terminal-graphics-2FTRg9x3.js";
|
|
4
4
|
import { resolveMarkdownMathColor } from "./math-image-BZhmfUeu.js";
|
|
5
|
-
import { computed, defineComponent, getCurrentInstance, h, inject, onBeforeUnmount, shallowRef, watch } from "vue";
|
|
5
|
+
import { computed, defineComponent, getCurrentInstance, h, inject, markRaw, onBeforeUnmount, shallowRef, watch } from "vue";
|
|
6
6
|
|
|
7
|
-
//#region src/vue/mermaid/beautiful-mermaid.ts
|
|
8
|
-
const BEAUTIFUL_MERMAID_INSTALL_HINT = "Install beautiful-mermaid and use TMermaidText from @simon_he/vue-tui/mermaid or @simon_he/vue-tui/agent/mermaid, or pass a custom renderer prop.";
|
|
9
|
-
const MISSING_BEAUTIFUL_MERMAID_ERROR_CODE = "VUE_TUI_MISSING_BEAUTIFUL_MERMAID";
|
|
10
|
-
const INVALID_BEAUTIFUL_MERMAID_EXPORT_ERROR_CODE = "VUE_TUI_INVALID_BEAUTIFUL_MERMAID_EXPORT";
|
|
11
|
-
let cachedBeautifulMermaid = null;
|
|
12
|
-
function functionValue(value) {
|
|
13
|
-
return typeof value === "function" ? value : null;
|
|
14
|
-
}
|
|
15
|
-
function functionProp(target, key) {
|
|
16
|
-
if (!target || typeof target !== "object") return null;
|
|
17
|
-
if (!(key in target)) return null;
|
|
18
|
-
const value = target[key];
|
|
19
|
-
return typeof value === "function" ? value : null;
|
|
20
|
-
}
|
|
21
|
-
function errorMessage(error) {
|
|
22
|
-
return error instanceof Error ? error.message : String(error);
|
|
23
|
-
}
|
|
24
|
-
function codedError(message, code) {
|
|
25
|
-
const error = new Error(message);
|
|
26
|
-
error.code = code;
|
|
27
|
-
return error;
|
|
28
|
-
}
|
|
29
|
-
function missingBeautifulMermaidError(error) {
|
|
30
|
-
if (error && typeof error === "object" && errorCode(error) === MISSING_BEAUTIFUL_MERMAID_ERROR_CODE) return markMermaidRenderErrorFatal(error);
|
|
31
|
-
const detail = errorMessage(error);
|
|
32
|
-
return markMermaidRenderErrorFatal(codedError(`${BEAUTIFUL_MERMAID_INSTALL_HINT} (${detail})`, MISSING_BEAUTIFUL_MERMAID_ERROR_CODE));
|
|
33
|
-
}
|
|
34
|
-
function errorCode(error) {
|
|
35
|
-
if (!error || typeof error !== "object") return "";
|
|
36
|
-
const value = error.code;
|
|
37
|
-
return typeof value === "string" ? value : "";
|
|
38
|
-
}
|
|
39
|
-
function errorCause(error) {
|
|
40
|
-
if (!error || typeof error !== "object") return void 0;
|
|
41
|
-
return error.cause;
|
|
42
|
-
}
|
|
43
|
-
function isMissingBeautifulMermaid(error) {
|
|
44
|
-
return isMissingBeautifulMermaidWithSeen(error, new WeakSet());
|
|
45
|
-
}
|
|
46
|
-
function isMissingBeautifulMermaidWithSeen(error, seen) {
|
|
47
|
-
if (error && typeof error === "object") {
|
|
48
|
-
if (seen.has(error)) return false;
|
|
49
|
-
seen.add(error);
|
|
50
|
-
}
|
|
51
|
-
const message = errorMessage(error);
|
|
52
|
-
const code = errorCode(error);
|
|
53
|
-
if (code === MISSING_BEAUTIFUL_MERMAID_ERROR_CODE) return true;
|
|
54
|
-
const mentionsBeautifulMermaid = /Cannot find package ['"]beautiful-mermaid['"]/.test(message) || /Cannot find module ['"]beautiful-mermaid['"]/.test(message) || /Cannot resolve module ['"]beautiful-mermaid['"]/.test(message) || /Can't resolve ['"]beautiful-mermaid['"]/.test(message) || /Failed to resolve module specifier ['"]beautiful-mermaid['"]/.test(message) || /Failed to resolve import ['"]beautiful-mermaid['"]/.test(message) || /Could not resolve ['"]beautiful-mermaid['"]/.test(message);
|
|
55
|
-
if (mentionsBeautifulMermaid && (code === "" || code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || code === "UNRESOLVED_IMPORT")) return true;
|
|
56
|
-
const cause = errorCause(error);
|
|
57
|
-
return cause !== void 0 && isMissingBeautifulMermaidWithSeen(cause, seen);
|
|
58
|
-
}
|
|
59
|
-
function resolveBeautifulMermaidRenderer(mod) {
|
|
60
|
-
const renderer = functionProp(mod, "renderMermaidASCII") ?? functionProp(mod, "renderMermaidAscii") ?? functionValue(mod.default) ?? functionProp(mod.default, "renderMermaidASCII") ?? functionProp(mod.default, "renderMermaidAscii");
|
|
61
|
-
if (!renderer) throw markMermaidRenderErrorFatal(codedError("beautiful-mermaid is installed but does not export renderMermaidASCII.", INVALID_BEAUTIFUL_MERMAID_EXPORT_ERROR_CODE));
|
|
62
|
-
return renderer;
|
|
63
|
-
}
|
|
64
|
-
function hasErrorCode(error, code, seen = new WeakSet()) {
|
|
65
|
-
if (!error || typeof error !== "object") return false;
|
|
66
|
-
if (seen.has(error)) return false;
|
|
67
|
-
seen.add(error);
|
|
68
|
-
if (errorCode(error) === code) return true;
|
|
69
|
-
const cause = errorCause(error);
|
|
70
|
-
return cause !== void 0 && hasErrorCode(cause, code, seen);
|
|
71
|
-
}
|
|
72
|
-
function isBeautifulMermaidExportError(error) {
|
|
73
|
-
return hasErrorCode(error, INVALID_BEAUTIFUL_MERMAID_EXPORT_ERROR_CODE);
|
|
74
|
-
}
|
|
75
|
-
const isTransientBeautifulMermaidRenderError = (error) => {
|
|
76
|
-
if (isMissingBeautifulMermaid(error)) return false;
|
|
77
|
-
if (isBeautifulMermaidExportError(error)) return false;
|
|
78
|
-
return true;
|
|
79
|
-
};
|
|
80
|
-
async function loadBeautifulMermaid() {
|
|
81
|
-
if (!cachedBeautifulMermaid) cachedBeautifulMermaid = import("beautiful-mermaid").then((mod) => mod).catch((error) => {
|
|
82
|
-
cachedBeautifulMermaid = null;
|
|
83
|
-
if (isMissingBeautifulMermaid(error)) throw missingBeautifulMermaidError(error);
|
|
84
|
-
throw markMermaidRenderErrorFatal(error);
|
|
85
|
-
});
|
|
86
|
-
return cachedBeautifulMermaid;
|
|
87
|
-
}
|
|
88
|
-
const beautifulMermaidRenderer = async (code, options) => {
|
|
89
|
-
try {
|
|
90
|
-
const mod = await loadBeautifulMermaid();
|
|
91
|
-
const renderer = resolveBeautifulMermaidRenderer(mod);
|
|
92
|
-
return await renderer(code, options);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
if (isMissingBeautifulMermaid(error)) throw missingBeautifulMermaidError(error);
|
|
95
|
-
throw error;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
function createBeautifulMermaidRenderer() {
|
|
99
|
-
return beautifulMermaidRenderer;
|
|
100
|
-
}
|
|
101
|
-
const TMermaidText$1 = defineComponent({
|
|
102
|
-
name: "TMermaidText",
|
|
103
|
-
inheritAttrs: false,
|
|
104
|
-
props: tMermaidTextProps,
|
|
105
|
-
emits: { copy: (_payload) => true },
|
|
106
|
-
setup(props, { attrs, emit, slots }) {
|
|
107
|
-
return () => {
|
|
108
|
-
const renderer = props.renderer ?? beautifulMermaidRenderer;
|
|
109
|
-
const shouldRenderSource = props.shouldRenderSource ?? isSimpleMermaidFlowchartSource;
|
|
110
|
-
return h(TMermaidText, {
|
|
111
|
-
...attrs,
|
|
112
|
-
...props,
|
|
113
|
-
renderer,
|
|
114
|
-
shouldRenderSource,
|
|
115
|
-
isTransientError: props.isTransientError ?? isTransientBeautifulMermaidRenderError,
|
|
116
|
-
onCopy: (payload) => emit("copy", payload)
|
|
117
|
-
}, slots);
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
const TMermaid = TMermaidText$1;
|
|
122
|
-
const TBeautifulMermaidText = TMermaidText$1;
|
|
123
|
-
const TBeautifulMermaid = TMermaidText$1;
|
|
124
|
-
|
|
125
|
-
//#endregion
|
|
126
7
|
//#region src/vue/mermaid/mermaid-image.ts
|
|
127
8
|
const DEFAULT_MERMAID_IMAGE_OPTIONS = Object.freeze({
|
|
128
9
|
cellWidthPx: 8,
|
|
@@ -323,6 +204,139 @@ function svgRenderFunction(mod) {
|
|
|
323
204
|
for (const candidate of candidates) if (typeof candidate === "function") return candidate;
|
|
324
205
|
return null;
|
|
325
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* @resvg/resvg-js (usvg) does not support CSS custom properties (`var()`) or
|
|
209
|
+
* `color-mix()`. beautiful-mermaid 1.x emits both, so feeding the raw SVG to
|
|
210
|
+
* resvg renders every shape black. This resolver inlines all `var(--x)` /
|
|
211
|
+
* `color-mix(in srgb, ...)` color expressions into concrete hex values before
|
|
212
|
+
* rasterization. Exported for tests.
|
|
213
|
+
*/
|
|
214
|
+
function resolveMermaidSvgForResvg(svg, fallbackBg, fallbackFg) {
|
|
215
|
+
const { bg, fg } = extractSvgRootStyleColors(svg, fallbackBg, fallbackFg);
|
|
216
|
+
const variables = extractSvgCssVariables(svg);
|
|
217
|
+
const root = {
|
|
218
|
+
bg,
|
|
219
|
+
fg
|
|
220
|
+
};
|
|
221
|
+
let resolved = svg;
|
|
222
|
+
let guard = 0;
|
|
223
|
+
while ((resolved.includes("var(") || resolved.includes("color-mix(")) && guard++ < 500) {
|
|
224
|
+
const varIndex = resolved.lastIndexOf("var(");
|
|
225
|
+
const mixIndex = resolved.lastIndexOf("color-mix(");
|
|
226
|
+
const at = Math.max(varIndex, mixIndex);
|
|
227
|
+
if (at < 0) break;
|
|
228
|
+
const open = resolved.indexOf("(", at);
|
|
229
|
+
const close = findMatchingSvgParen(resolved, open);
|
|
230
|
+
if (open < 0 || close < 0) break;
|
|
231
|
+
const expr = resolved.slice(at, close + 1);
|
|
232
|
+
const color = resolveSvgColorExpr(expr, root, variables);
|
|
233
|
+
resolved = resolved.slice(0, at) + color + resolved.slice(close + 1);
|
|
234
|
+
}
|
|
235
|
+
return stripSvgStyleBlocks(resolved);
|
|
236
|
+
}
|
|
237
|
+
function extractSvgRootStyleColors(svg, fallbackBg, fallbackFg) {
|
|
238
|
+
let bg = fallbackBg;
|
|
239
|
+
let fg = fallbackFg;
|
|
240
|
+
const styleMatch = /<svg\b[^>]*\bstyle="([^"]*)"/i.exec(svg);
|
|
241
|
+
if (styleMatch?.[1]) {
|
|
242
|
+
const bgMatch = /(?:^|;)\s*--bg\s*:\s*([^;]+)/i.exec(styleMatch[1]);
|
|
243
|
+
const fgMatch = /(?:^|;)\s*--fg\s*:\s*([^;]+)/i.exec(styleMatch[1]);
|
|
244
|
+
if (bgMatch?.[1]) bg = bgMatch[1].trim();
|
|
245
|
+
if (fgMatch?.[1]) fg = fgMatch[1].trim();
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
bg,
|
|
249
|
+
fg
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function extractSvgCssVariables(svg) {
|
|
253
|
+
const map = new Map();
|
|
254
|
+
const styleMatch = /<style[^>]*>([\s\S]*?)<\/style>/i.exec(svg);
|
|
255
|
+
if (!styleMatch?.[1]) return map;
|
|
256
|
+
const declaration = /(--[A-Za-z0-9_-]+)\s*:\s*([^;{}]+);/g;
|
|
257
|
+
let match;
|
|
258
|
+
while ((match = declaration.exec(styleMatch[1])) !== null) map.set(match[1], match[2].trim());
|
|
259
|
+
return map;
|
|
260
|
+
}
|
|
261
|
+
function stripSvgStyleBlocks(svg) {
|
|
262
|
+
return svg.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
263
|
+
}
|
|
264
|
+
function findMatchingSvgParen(input, openIndex) {
|
|
265
|
+
let depth = 0;
|
|
266
|
+
for (let i = openIndex; i < input.length; i++) {
|
|
267
|
+
const ch = input[i];
|
|
268
|
+
if (ch === "(") depth++;
|
|
269
|
+
else if (ch === ")") {
|
|
270
|
+
depth--;
|
|
271
|
+
if (depth === 0) return i;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return -1;
|
|
275
|
+
}
|
|
276
|
+
function findTopLevelSvgComma(input) {
|
|
277
|
+
let depth = 0;
|
|
278
|
+
for (let i = 0; i < input.length; i++) {
|
|
279
|
+
const ch = input[i];
|
|
280
|
+
if (ch === "(") depth++;
|
|
281
|
+
else if (ch === ")") depth--;
|
|
282
|
+
else if (ch === "," && depth === 0) return i;
|
|
283
|
+
}
|
|
284
|
+
return -1;
|
|
285
|
+
}
|
|
286
|
+
function normalizeHexColor(value) {
|
|
287
|
+
const match = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.trim());
|
|
288
|
+
if (!match) return null;
|
|
289
|
+
let hex = match[1];
|
|
290
|
+
if (hex.length === 3) hex = hex.split("").map((ch) => ch + ch).join("");
|
|
291
|
+
return `#${hex.toLowerCase()}`;
|
|
292
|
+
}
|
|
293
|
+
function mixSrgbColors(aHex, bHex, percent) {
|
|
294
|
+
const a = parseHexRgb(aHex);
|
|
295
|
+
const b = parseHexRgb(bHex);
|
|
296
|
+
const p = Math.max(0, Math.min(100, percent)) / 100;
|
|
297
|
+
const mix = (x, y) => Math.round(x * p + y * (1 - p));
|
|
298
|
+
const toHex = (value) => value.toString(16).padStart(2, "0");
|
|
299
|
+
return `#${toHex(mix(a[0], b[0]))}${toHex(mix(a[1], b[1]))}${toHex(mix(a[2], b[2]))}`;
|
|
300
|
+
}
|
|
301
|
+
function parseHexRgb(hex) {
|
|
302
|
+
return [
|
|
303
|
+
parseInt(hex.slice(1, 3), 16),
|
|
304
|
+
parseInt(hex.slice(3, 5), 16),
|
|
305
|
+
parseInt(hex.slice(5, 7), 16)
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
function resolveSvgColorExpr(expr, root, variables) {
|
|
309
|
+
const value = expr.trim();
|
|
310
|
+
if (value.startsWith("var(")) {
|
|
311
|
+
const close = findMatchingSvgParen(value, 3);
|
|
312
|
+
if (close < 0) return value;
|
|
313
|
+
const inner = value.slice(4, close).trim();
|
|
314
|
+
const comma = findTopLevelSvgComma(inner);
|
|
315
|
+
const name = (comma < 0 ? inner : inner.slice(0, comma)).trim();
|
|
316
|
+
const fallback = comma < 0 ? "" : inner.slice(comma + 1).trim();
|
|
317
|
+
const direct = name === "--fg" ? root.fg : name === "--bg" ? root.bg : variables.get(name);
|
|
318
|
+
if (direct != null) return resolveSvgColorExpr(direct, root, variables);
|
|
319
|
+
if (fallback) return resolveSvgColorExpr(fallback, root, variables);
|
|
320
|
+
return value;
|
|
321
|
+
}
|
|
322
|
+
if (value.startsWith("color-mix(")) {
|
|
323
|
+
const close = findMatchingSvgParen(value, 9);
|
|
324
|
+
if (close < 0) return value;
|
|
325
|
+
const inner = value.slice(10, close).trim();
|
|
326
|
+
const afterMethod = inner.replace(/^in\s+srgb\s*,\s*/i, "");
|
|
327
|
+
const comma = findTopLevelSvgComma(afterMethod);
|
|
328
|
+
if (comma < 0) return value;
|
|
329
|
+
const left = afterMethod.slice(0, comma).trim();
|
|
330
|
+
const right = afterMethod.slice(comma + 1).trim();
|
|
331
|
+
const leftMatch = /^([^%\s]+)\s+([\d.]+)%$/.exec(left) ?? /^([\d.]+)%\s+([^%\s]+)$/.exec(left);
|
|
332
|
+
if (!leftMatch) return value;
|
|
333
|
+
const a = normalizeHexColor(resolveSvgColorExpr(leftMatch[1], root, variables));
|
|
334
|
+
const b = normalizeHexColor(resolveSvgColorExpr(right, root, variables));
|
|
335
|
+
if (!a || !b) return value;
|
|
336
|
+
return mixSrgbColors(a, b, Number(leftMatch[2]));
|
|
337
|
+
}
|
|
338
|
+
return normalizeHexColor(value) ?? value;
|
|
339
|
+
}
|
|
326
340
|
function loadBuiltinMermaidRasterizer() {
|
|
327
341
|
if (!builtinRasterizerLoad) builtinRasterizerLoad = (async () => {
|
|
328
342
|
try {
|
|
@@ -339,7 +353,8 @@ function loadBuiltinMermaidRasterizer() {
|
|
|
339
353
|
});
|
|
340
354
|
const svgText = String(svg ?? "");
|
|
341
355
|
if (!svgText.includes("<svg")) return null;
|
|
342
|
-
const
|
|
356
|
+
const resolvedSvg = resolveMermaidSvgForResvg(svgText, options.bg, options.fg);
|
|
357
|
+
const rendered = new Resvg(resolvedSvg, { background: "rgba(0,0,0,0)" }).render();
|
|
343
358
|
const naturalWidth = positiveInt(rendered?.width);
|
|
344
359
|
const naturalHeight = positiveInt(rendered?.height);
|
|
345
360
|
const png = typeof rendered?.asPng === "function" ? rendered.asPng() : void 0;
|
|
@@ -514,6 +529,18 @@ const tMermaidImageProps = {
|
|
|
514
529
|
type: Number,
|
|
515
530
|
default: void 0
|
|
516
531
|
},
|
|
532
|
+
textRenderer: {
|
|
533
|
+
type: Function,
|
|
534
|
+
default: void 0
|
|
535
|
+
},
|
|
536
|
+
shouldRenderSource: {
|
|
537
|
+
type: Function,
|
|
538
|
+
default: void 0
|
|
539
|
+
},
|
|
540
|
+
textOptions: {
|
|
541
|
+
type: Object,
|
|
542
|
+
default: void 0
|
|
543
|
+
},
|
|
517
544
|
box: {
|
|
518
545
|
type: Boolean,
|
|
519
546
|
default: true
|
|
@@ -557,6 +584,26 @@ const tMermaidImageProps = {
|
|
|
557
584
|
ascii: {
|
|
558
585
|
type: Boolean,
|
|
559
586
|
default: false
|
|
587
|
+
},
|
|
588
|
+
zoomOnWheel: {
|
|
589
|
+
type: Boolean,
|
|
590
|
+
default: true
|
|
591
|
+
},
|
|
592
|
+
zoomModifier: {
|
|
593
|
+
type: String,
|
|
594
|
+
default: "metaCtrl"
|
|
595
|
+
},
|
|
596
|
+
minZoom: {
|
|
597
|
+
type: Number,
|
|
598
|
+
default: 1
|
|
599
|
+
},
|
|
600
|
+
maxZoom: {
|
|
601
|
+
type: Number,
|
|
602
|
+
default: 6
|
|
603
|
+
},
|
|
604
|
+
zoomSensitivity: {
|
|
605
|
+
type: Number,
|
|
606
|
+
default: .002
|
|
560
607
|
}
|
|
561
608
|
};
|
|
562
609
|
const TMermaidImage = defineComponent({
|
|
@@ -571,8 +618,16 @@ const TMermaidImage = defineComponent({
|
|
|
571
618
|
const { visible, rootProps } = useVisibility();
|
|
572
619
|
const parentEventZ = inject(EventZIndexContextKey, computed(() => 0));
|
|
573
620
|
const imageCells = shallowRef(null);
|
|
621
|
+
const textSnapshot = shallowRef(null);
|
|
622
|
+
const zoomScale = shallowRef(1);
|
|
623
|
+
const panX = shallowRef(0);
|
|
624
|
+
const panY = shallowRef(0);
|
|
574
625
|
const documentVersion = shallowRef(0);
|
|
575
626
|
const copied = shallowRef(false);
|
|
627
|
+
let dragState = null;
|
|
628
|
+
let dragMoved = false;
|
|
629
|
+
let lastDragAt = 0;
|
|
630
|
+
const DRAG_CLICK_SUPPRESS_MS = 250;
|
|
576
631
|
let builtOnce = false;
|
|
577
632
|
let renderVersion = 0;
|
|
578
633
|
let alive = true;
|
|
@@ -581,6 +636,7 @@ const TMermaidImage = defineComponent({
|
|
|
581
636
|
let copiedTimer = null;
|
|
582
637
|
let copyRequestVersion = 0;
|
|
583
638
|
let activeGraphicId = null;
|
|
639
|
+
let activeImageSignature = "";
|
|
584
640
|
const source = computed(() => props.code ?? props.content ?? "");
|
|
585
641
|
const waitingForStreamingSourceToFinish = computed(() => props.streaming && !props.final);
|
|
586
642
|
const graphicsOutputVersion = shallowRef(getTerminalGraphicsOutputVersion(terminal));
|
|
@@ -635,6 +691,73 @@ const TMermaidImage = defineComponent({
|
|
|
635
691
|
graphicsOutput.value?.clear?.(id);
|
|
636
692
|
} catch {}
|
|
637
693
|
}
|
|
694
|
+
function clampZoom(zoom) {
|
|
695
|
+
const min = Number.isFinite(Number(props.minZoom)) ? Math.max(.05, Number(props.minZoom)) : 1;
|
|
696
|
+
const max = Number.isFinite(Number(props.maxZoom)) ? Math.max(min, Number(props.maxZoom)) : Math.max(min, 6);
|
|
697
|
+
return Math.min(max, Math.max(min, zoom));
|
|
698
|
+
}
|
|
699
|
+
function zoomSensitivity() {
|
|
700
|
+
const value = Number(props.zoomSensitivity);
|
|
701
|
+
return Number.isFinite(value) && value > 0 ? value : .002;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Wheel delta -> multiplicative zoom factor. Discrete notches (CLI wheel
|
|
705
|
+
* reporting and browser line-mode deltas) map to one step each; trackpad
|
|
706
|
+
* pixel deltas use a continuous exponential so small movements stay smooth.
|
|
707
|
+
*/
|
|
708
|
+
function wheelZoomFactor(deltaY, deltaMode) {
|
|
709
|
+
const sign = deltaY > 0 ? -1 : 1;
|
|
710
|
+
const sensitivity = zoomSensitivity();
|
|
711
|
+
const abs = Math.abs(deltaY);
|
|
712
|
+
const mode = deltaMode ?? 0;
|
|
713
|
+
const isNotch = mode === 1 || abs > 0 && abs <= 3 && Number.isInteger(deltaY);
|
|
714
|
+
const magnitude = isNotch ? 100 : abs;
|
|
715
|
+
return Math.exp(sign * magnitude * sensitivity);
|
|
716
|
+
}
|
|
717
|
+
function resetZoomState() {
|
|
718
|
+
if (zoomScale.value === 1 && panX.value === 0 && panY.value === 0) return;
|
|
719
|
+
zoomScale.value = 1;
|
|
720
|
+
panX.value = 0;
|
|
721
|
+
panY.value = 0;
|
|
722
|
+
dragState = null;
|
|
723
|
+
}
|
|
724
|
+
function hasZoomModifier(event) {
|
|
725
|
+
const modifier = props.zoomModifier ?? "metaCtrl";
|
|
726
|
+
if (modifier === "none") return true;
|
|
727
|
+
if (modifier === "meta") return event.metaKey === true;
|
|
728
|
+
if (modifier === "ctrl") return event.ctrlKey === true;
|
|
729
|
+
return event.ctrlKey === true || event.metaKey === true;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Clamp the pan so the zoomed image always covers the container area
|
|
733
|
+
* (no empty gaps, no over-scroll beyond the image edges).
|
|
734
|
+
*/
|
|
735
|
+
function clampPan() {
|
|
736
|
+
const inner = innerRectFrom(fullRect.value);
|
|
737
|
+
const fit = displayFit.value;
|
|
738
|
+
if (!fit || inner.w <= 0 || inner.h <= 0) return;
|
|
739
|
+
const zoom = clampZoom(zoomScale.value);
|
|
740
|
+
const fullW = fit.displayW * zoom;
|
|
741
|
+
const fullH = fit.displayH * zoom;
|
|
742
|
+
const fitOriginX = inner.x + (inner.w > fit.displayW ? Math.floor((inner.w - fit.displayW) / 2) : 0);
|
|
743
|
+
let nextX = panX.value;
|
|
744
|
+
let nextY = panY.value;
|
|
745
|
+
if (fullW <= inner.w) nextX = 0;
|
|
746
|
+
else nextX = Math.min(inner.x - fitOriginX, Math.max(inner.x + inner.w - fullW - fitOriginX, nextX));
|
|
747
|
+
if (fullH <= inner.h) nextY = 0;
|
|
748
|
+
else nextY = Math.min(0, Math.max(inner.h - fullH, nextY));
|
|
749
|
+
if (nextX !== panX.value) panX.value = nextX;
|
|
750
|
+
if (nextY !== panY.value) panY.value = nextY;
|
|
751
|
+
}
|
|
752
|
+
/** Set the current image, resetting wheel zoom whenever the PNG changes. */
|
|
753
|
+
function setImageCells(cells) {
|
|
754
|
+
const signature = cells ? cells.base64 : "";
|
|
755
|
+
if (signature !== activeImageSignature) {
|
|
756
|
+
activeImageSignature = signature;
|
|
757
|
+
resetZoomState();
|
|
758
|
+
}
|
|
759
|
+
imageCells.value = cells;
|
|
760
|
+
}
|
|
638
761
|
const hasBox = computed(() => props.box !== false);
|
|
639
762
|
const normalizedWidth = computed(() => {
|
|
640
763
|
const width = Math.floor(Number(props.w));
|
|
@@ -643,6 +766,11 @@ const TMermaidImage = defineComponent({
|
|
|
643
766
|
const reservesBoxRows = computed(() => hasBox.value && normalizedWidth.value >= 2);
|
|
644
767
|
const boxChars = computed(() => props.ascii ? ASCII_MERMAID_IMAGE_BOX_CHARS : UNICODE_MERMAID_IMAGE_BOX_CHARS);
|
|
645
768
|
const sourceLines = computed(() => splitRenderedOutput(source.value));
|
|
769
|
+
const displayLines = computed(() => {
|
|
770
|
+
const snapshot = textSnapshot.value;
|
|
771
|
+
if (snapshot && snapshot.source === source.value && hasVisibleTextOutput(snapshot.lines)) return snapshot.lines;
|
|
772
|
+
return sourceLines.value;
|
|
773
|
+
});
|
|
646
774
|
const currentStyle = computed(() => props.style ?? defaultStyle.value);
|
|
647
775
|
const innerWidth = computed(() => {
|
|
648
776
|
const w = normalizedWidth.value;
|
|
@@ -664,7 +792,7 @@ const TMermaidImage = defineComponent({
|
|
|
664
792
|
});
|
|
665
793
|
const contentHeight = computed(() => {
|
|
666
794
|
if (!autoHeight.value) return fixedInnerHeight.value;
|
|
667
|
-
return displayFit.value?.displayH ?? Math.max(1,
|
|
795
|
+
return displayFit.value?.displayH ?? Math.max(1, displayLines.value.length);
|
|
668
796
|
});
|
|
669
797
|
const fullHeight = computed(() => {
|
|
670
798
|
if (props.h != null) {
|
|
@@ -717,6 +845,53 @@ const TMermaidImage = defineComponent({
|
|
|
717
845
|
padding: props.padding
|
|
718
846
|
});
|
|
719
847
|
}
|
|
848
|
+
function hasVisibleTextOutput(lines) {
|
|
849
|
+
return lines.some((line) => line.trim().length > 0);
|
|
850
|
+
}
|
|
851
|
+
function resolveTextOptions() {
|
|
852
|
+
const base = props.textOptions ?? {};
|
|
853
|
+
return {
|
|
854
|
+
...base,
|
|
855
|
+
useAscii: props.ascii,
|
|
856
|
+
colorMode: "none"
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function textRenderSignature(code) {
|
|
860
|
+
return [
|
|
861
|
+
code,
|
|
862
|
+
props.ascii ? "a" : "",
|
|
863
|
+
JSON.stringify(props.textOptions ?? null),
|
|
864
|
+
props.textRenderer != null ? "r" : "",
|
|
865
|
+
props.shouldRenderSource != null ? "g" : ""
|
|
866
|
+
].join("");
|
|
867
|
+
}
|
|
868
|
+
async function renderTextWithTimeout(renderer, code, options) {
|
|
869
|
+
const timeoutMs = normalizeNonNegativeInt(props.renderTimeoutMs, 0);
|
|
870
|
+
if (timeoutMs <= 0) return await renderer(code, options);
|
|
871
|
+
let timer = null;
|
|
872
|
+
try {
|
|
873
|
+
return await Promise.race([Promise.resolve().then(() => renderer(code, options)), new Promise((_resolve, reject) => {
|
|
874
|
+
timer = setTimeout(() => reject(new Error("Mermaid text render timeout")), timeoutMs);
|
|
875
|
+
})]);
|
|
876
|
+
} finally {
|
|
877
|
+
if (timer != null) clearTimeout(timer);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
async function runCustomRasterizer(code, options) {
|
|
881
|
+
const renderer = props.renderer;
|
|
882
|
+
if (!renderer) return null;
|
|
883
|
+
const timeoutMs = normalizeNonNegativeInt(props.renderTimeoutMs, 0);
|
|
884
|
+
const run = () => renderer(code, options);
|
|
885
|
+
if (timeoutMs <= 0) return await run() ?? null;
|
|
886
|
+
let timer = null;
|
|
887
|
+
try {
|
|
888
|
+
return await Promise.race([run(), new Promise((resolve) => {
|
|
889
|
+
timer = setTimeout(() => resolve(null), timeoutMs);
|
|
890
|
+
})]) ?? null;
|
|
891
|
+
} finally {
|
|
892
|
+
if (timer != null) clearTimeout(timer);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
720
895
|
function shouldSkipRenderForSize(code) {
|
|
721
896
|
const maxChars = normalizeNonNegativeInt(props.maxRenderSourceChars, DEFAULT_MERMAID_IMAGE_MAX_RENDER_SOURCE_CHARS);
|
|
722
897
|
if (maxChars > 0 && code.length > maxChars) return true;
|
|
@@ -736,48 +911,107 @@ const TMermaidImage = defineComponent({
|
|
|
736
911
|
if (timer != null) clearTimeout(timer);
|
|
737
912
|
}
|
|
738
913
|
}
|
|
739
|
-
async function
|
|
914
|
+
async function renderTextFallback(version) {
|
|
915
|
+
const code = source.value;
|
|
916
|
+
const renderer = props.textRenderer;
|
|
917
|
+
if (!renderer) return;
|
|
918
|
+
const guard = props.shouldRenderSource;
|
|
919
|
+
if (guard) {
|
|
920
|
+
let eligible = false;
|
|
921
|
+
try {
|
|
922
|
+
eligible = guard(code, {
|
|
923
|
+
final: props.final,
|
|
924
|
+
streaming: props.streaming
|
|
925
|
+
});
|
|
926
|
+
} catch {
|
|
927
|
+
eligible = false;
|
|
928
|
+
}
|
|
929
|
+
if (!eligible) return;
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
const rendered = await renderTextWithTimeout(renderer, code, resolveTextOptions());
|
|
933
|
+
if (!alive || version !== renderVersion) return;
|
|
934
|
+
const lines = splitRenderedOutput(rendered);
|
|
935
|
+
if (!hasVisibleTextOutput(lines)) return;
|
|
936
|
+
textSnapshot.value = markRaw({
|
|
937
|
+
source: code,
|
|
938
|
+
signature: textRenderSignature(code),
|
|
939
|
+
lines: markRaw(lines)
|
|
940
|
+
});
|
|
941
|
+
bump();
|
|
942
|
+
} catch {}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Adaptive presentation state machine:
|
|
946
|
+
*
|
|
947
|
+
* raw source
|
|
948
|
+
* -> image (terminal supports Kitty/iTerm2 graphics and the PNG
|
|
949
|
+
* rasterizer succeeds), else
|
|
950
|
+
* -> ANSI text (a textRenderer is available and the source passes the
|
|
951
|
+
* eligibility guard), else
|
|
952
|
+
* -> raw source
|
|
953
|
+
*
|
|
954
|
+
* While any pipeline is pending the raw source stays visible (source-first).
|
|
955
|
+
*/
|
|
956
|
+
async function renderPresentation(version) {
|
|
740
957
|
const code = source.value;
|
|
741
958
|
if (!code.trim()) {
|
|
742
959
|
if (!alive || version !== renderVersion) return;
|
|
743
|
-
|
|
960
|
+
setImageCells(null);
|
|
961
|
+
textSnapshot.value = null;
|
|
744
962
|
bump();
|
|
745
963
|
return;
|
|
746
964
|
}
|
|
747
965
|
if (waitingForStreamingSourceToFinish.value || shouldSkipRenderForSize(code)) {
|
|
748
966
|
if (!alive || version !== renderVersion) return;
|
|
749
|
-
|
|
967
|
+
setImageCells(null);
|
|
968
|
+
textSnapshot.value = null;
|
|
750
969
|
bump();
|
|
751
970
|
return;
|
|
752
971
|
}
|
|
753
|
-
imageCells.value = null;
|
|
754
|
-
bump();
|
|
755
972
|
const protocol = graphicsProtocol.value;
|
|
756
|
-
if (!protocol) return;
|
|
757
973
|
const options = resolveImageOptions();
|
|
758
|
-
const cached = getCachedMermaidImage(code, options);
|
|
974
|
+
const cached = protocol && !props.renderer ? getCachedMermaidImage(code, options) : null;
|
|
759
975
|
if (cached) {
|
|
760
976
|
if (!alive || version !== renderVersion) return;
|
|
761
|
-
|
|
977
|
+
setImageCells(cached);
|
|
978
|
+
textSnapshot.value = null;
|
|
762
979
|
bump();
|
|
763
980
|
return;
|
|
764
981
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
982
|
+
const currentSignature = textRenderSignature(code);
|
|
983
|
+
const keepText = textSnapshot.value != null && textSnapshot.value.source === code && textSnapshot.value.signature === currentSignature;
|
|
984
|
+
if (!keepText) {
|
|
985
|
+
setImageCells(null);
|
|
986
|
+
textSnapshot.value = null;
|
|
987
|
+
bump();
|
|
771
988
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
989
|
+
if (protocol) {
|
|
990
|
+
let result = null;
|
|
991
|
+
if (props.renderer) result = await runCustomRasterizer(code, options);
|
|
992
|
+
else if (isMermaidImageRendererReady()) result = await rasterizeWithTimeout(code, options);
|
|
993
|
+
else {
|
|
994
|
+
loadMermaidImageRenderer().then((ready) => {
|
|
995
|
+
if (!alive || version !== renderVersion || !ready) return;
|
|
996
|
+
renderPresentation(version);
|
|
997
|
+
});
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (!alive || version !== renderVersion) return;
|
|
1001
|
+
if (result) {
|
|
1002
|
+
setImageCells(result);
|
|
1003
|
+
textSnapshot.value = null;
|
|
1004
|
+
bump();
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
if (keepText) return;
|
|
1009
|
+
await renderTextFallback(version);
|
|
776
1010
|
}
|
|
777
1011
|
function scheduleRender() {
|
|
778
1012
|
const version = ++renderVersion;
|
|
779
1013
|
resetCopyFeedback(false);
|
|
780
|
-
|
|
1014
|
+
setImageCells(null);
|
|
781
1015
|
if (waitingForStreamingSourceToFinish.value) {
|
|
782
1016
|
builtOnce = true;
|
|
783
1017
|
scheduler.cancelFrameTask?.(frameTaskId);
|
|
@@ -786,7 +1020,7 @@ const TMermaidImage = defineComponent({
|
|
|
786
1020
|
}
|
|
787
1021
|
if (!builtOnce || !props.streaming) {
|
|
788
1022
|
builtOnce = true;
|
|
789
|
-
|
|
1023
|
+
renderPresentation(version);
|
|
790
1024
|
return;
|
|
791
1025
|
}
|
|
792
1026
|
const accepted = scheduler.queueFrameTask({
|
|
@@ -797,10 +1031,10 @@ const TMermaidImage = defineComponent({
|
|
|
797
1031
|
run: () => {
|
|
798
1032
|
if (!alive) return;
|
|
799
1033
|
if (version !== renderVersion) return;
|
|
800
|
-
|
|
1034
|
+
renderPresentation(version);
|
|
801
1035
|
}
|
|
802
1036
|
});
|
|
803
|
-
if (accepted === false)
|
|
1037
|
+
if (accepted === false) renderPresentation(version);
|
|
804
1038
|
}
|
|
805
1039
|
watch([
|
|
806
1040
|
source,
|
|
@@ -817,7 +1051,11 @@ const TMermaidImage = defineComponent({
|
|
|
817
1051
|
() => props.final,
|
|
818
1052
|
() => props.maxRenderSourceChars,
|
|
819
1053
|
() => props.maxRenderSourceLines,
|
|
820
|
-
|
|
1054
|
+
() => props.textRenderer,
|
|
1055
|
+
() => props.shouldRenderSource,
|
|
1056
|
+
() => props.textOptions,
|
|
1057
|
+
() => props.ascii,
|
|
1058
|
+
() => props.zoomModifier
|
|
821
1059
|
], () => {
|
|
822
1060
|
scheduleRender();
|
|
823
1061
|
}, {
|
|
@@ -826,7 +1064,13 @@ const TMermaidImage = defineComponent({
|
|
|
826
1064
|
});
|
|
827
1065
|
const unsubscribeMermaidImage = subscribeMermaidImage(() => {
|
|
828
1066
|
if (!alive) return;
|
|
829
|
-
|
|
1067
|
+
if (!graphicsProtocol.value || props.renderer) return;
|
|
1068
|
+
const cached = getCachedMermaidImage(source.value, resolveImageOptions());
|
|
1069
|
+
if (!cached) return;
|
|
1070
|
+
if (!alive) return;
|
|
1071
|
+
setImageCells(cached);
|
|
1072
|
+
textSnapshot.value = null;
|
|
1073
|
+
bump();
|
|
830
1074
|
});
|
|
831
1075
|
const unsubscribeGraphicsOutput = subscribeTerminalGraphicsOutput(terminal, () => {
|
|
832
1076
|
if (!alive) return;
|
|
@@ -909,7 +1153,7 @@ const TMermaidImage = defineComponent({
|
|
|
909
1153
|
}
|
|
910
1154
|
function contentLine(rowIndex, width, pad) {
|
|
911
1155
|
const showImage = imageCells.value != null && displayFit.value != null && graphicsProtocol.value != null;
|
|
912
|
-
const src = showImage ? spaces(width) :
|
|
1156
|
+
const src = showImage ? spaces(width) : displayLines.value[rowIndex] ?? "";
|
|
913
1157
|
const clipped = sliceCells(src, width);
|
|
914
1158
|
return pad ? padCells(clipped, width) : clipped;
|
|
915
1159
|
}
|
|
@@ -937,12 +1181,28 @@ const TMermaidImage = defineComponent({
|
|
|
937
1181
|
queueClearGraphic();
|
|
938
1182
|
return;
|
|
939
1183
|
}
|
|
940
|
-
const
|
|
1184
|
+
const zoom = clampZoom(zoomScale.value);
|
|
1185
|
+
const fitW = fit.displayW;
|
|
1186
|
+
const fitH = fit.displayH;
|
|
1187
|
+
const fitOriginX = inner.x + (inner.w > fitW ? Math.floor((inner.w - fitW) / 2) : 0);
|
|
1188
|
+
const fullRect$1 = {
|
|
1189
|
+
x: fitOriginX + panX.value,
|
|
1190
|
+
y: inner.y + panY.value,
|
|
1191
|
+
w: fitW * zoom,
|
|
1192
|
+
h: fitH * zoom
|
|
1193
|
+
};
|
|
1194
|
+
const visible$1 = intersectRect(fullRect$1, inner);
|
|
1195
|
+
if (!visible$1 || visible$1.w < 1 || visible$1.h < 1) {
|
|
1196
|
+
queueClearGraphic();
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
const rectX = Math.max(inner.x, Math.round(visible$1.x));
|
|
1200
|
+
const rectY = Math.max(inner.y, Math.round(visible$1.y));
|
|
941
1201
|
const rect = {
|
|
942
|
-
x:
|
|
943
|
-
y:
|
|
944
|
-
w:
|
|
945
|
-
h:
|
|
1202
|
+
x: rectX,
|
|
1203
|
+
y: rectY,
|
|
1204
|
+
w: Math.max(1, Math.min(inner.x + inner.w, rectX + Math.round(visible$1.w)) - rectX),
|
|
1205
|
+
h: Math.max(1, Math.min(inner.y + inner.h, rectY + Math.round(visible$1.h)) - rectY)
|
|
946
1206
|
};
|
|
947
1207
|
const imageId = stableTerminalGraphicNumericId(`mermaid-image:${uid}:${renderVersion}:img`);
|
|
948
1208
|
const placementId = stableTerminalGraphicNumericId(`mermaid-image:${uid}:${renderVersion}:plc`);
|
|
@@ -955,10 +1215,13 @@ const TMermaidImage = defineComponent({
|
|
|
955
1215
|
rows: cells.heightCells,
|
|
956
1216
|
sourceWidth: cells.naturalWidth,
|
|
957
1217
|
sourceHeight: cells.naturalHeight,
|
|
958
|
-
placementColumns:
|
|
959
|
-
placementRows:
|
|
1218
|
+
placementColumns: rect.w,
|
|
1219
|
+
placementRows: rect.h,
|
|
1220
|
+
transmissionColumns: fitW,
|
|
1221
|
+
transmissionRows: fitH,
|
|
1222
|
+
cropTransmission: false,
|
|
960
1223
|
rect,
|
|
961
|
-
fullRect:
|
|
1224
|
+
fullRect: fullRect$1,
|
|
962
1225
|
zIndex: -1,
|
|
963
1226
|
fallback: source.value
|
|
964
1227
|
});
|
|
@@ -982,13 +1245,14 @@ const TMermaidImage = defineComponent({
|
|
|
982
1245
|
id,
|
|
983
1246
|
x: rect.x,
|
|
984
1247
|
y: rect.y,
|
|
985
|
-
w:
|
|
986
|
-
h:
|
|
1248
|
+
w: rect.w,
|
|
1249
|
+
h: rect.h,
|
|
987
1250
|
protocol,
|
|
988
1251
|
sequence: built.sequence,
|
|
989
1252
|
resizeSequence: built.resizeSequence,
|
|
990
1253
|
clearSequence: built.clearSequence,
|
|
991
|
-
fallbackText: source.value
|
|
1254
|
+
fallbackText: source.value,
|
|
1255
|
+
placementMoveWithoutClear: protocol === "kitty"
|
|
992
1256
|
});
|
|
993
1257
|
if (accepted) activeGraphicId = id;
|
|
994
1258
|
else queueClearGraphic();
|
|
@@ -1023,7 +1287,7 @@ const TMermaidImage = defineComponent({
|
|
|
1023
1287
|
};
|
|
1024
1288
|
});
|
|
1025
1289
|
const contentHitRect = computed(() => {
|
|
1026
|
-
if (!visible.value
|
|
1290
|
+
if (!visible.value) return {
|
|
1027
1291
|
x: 0,
|
|
1028
1292
|
y: 0,
|
|
1029
1293
|
w: 0,
|
|
@@ -1095,6 +1359,7 @@ const TMermaidImage = defineComponent({
|
|
|
1095
1359
|
function onCopyClick(event) {
|
|
1096
1360
|
event.preventDefault();
|
|
1097
1361
|
event.stopPropagation();
|
|
1362
|
+
if (Date.now() - lastDragAt < DRAG_CLICK_SUPPRESS_MS) return;
|
|
1098
1363
|
copySource();
|
|
1099
1364
|
}
|
|
1100
1365
|
function onCopyKeydown(event) {
|
|
@@ -1103,6 +1368,78 @@ const TMermaidImage = defineComponent({
|
|
|
1103
1368
|
event.stopPropagation();
|
|
1104
1369
|
copySource();
|
|
1105
1370
|
}
|
|
1371
|
+
const zoomEnabled = computed(() => props.zoomOnWheel !== false && imageCells.value != null && displayFit.value != null && graphicsProtocol.value === "kitty" && contentHitRect.value.w > 0 && contentHitRect.value.h > 0);
|
|
1372
|
+
const dragEnabled = computed(() => props.zoomOnWheel !== false && imageCells.value != null && displayFit.value != null && graphicsProtocol.value === "kitty" && zoomScale.value > 1 && contentHitRect.value.w > 0 && contentHitRect.value.h > 0);
|
|
1373
|
+
function onWheel(event) {
|
|
1374
|
+
if (!zoomEnabled.value) return;
|
|
1375
|
+
if (!hasZoomModifier(event)) return;
|
|
1376
|
+
event.preventDefault();
|
|
1377
|
+
event.stopPropagation();
|
|
1378
|
+
const deltaY = event.deltaY ?? 0;
|
|
1379
|
+
if (!Number.isFinite(deltaY) || deltaY === 0) return;
|
|
1380
|
+
const inner = innerRectFrom(fullRect.value);
|
|
1381
|
+
const fit = displayFit.value;
|
|
1382
|
+
if (!fit || inner.w <= 0 || inner.h <= 0) return;
|
|
1383
|
+
const currentZoom = clampZoom(zoomScale.value);
|
|
1384
|
+
const fitW = fit.displayW;
|
|
1385
|
+
const fitH = fit.displayH;
|
|
1386
|
+
const fitOriginX = inner.x + (inner.w > fitW ? Math.floor((inner.w - fitW) / 2) : 0);
|
|
1387
|
+
const fullX = fitOriginX + panX.value;
|
|
1388
|
+
const fullY = inner.y + panY.value;
|
|
1389
|
+
const mouseX = event.cellX;
|
|
1390
|
+
const mouseY = event.cellY;
|
|
1391
|
+
const fx = currentZoom > 0 ? (mouseX - fullX) / (fitW * currentZoom) : .5;
|
|
1392
|
+
const fy = currentZoom > 0 ? (mouseY - fullY) / (fitH * currentZoom) : .5;
|
|
1393
|
+
const nextZoom = clampZoom(currentZoom * wheelZoomFactor(deltaY, event.deltaMode));
|
|
1394
|
+
if (nextZoom === currentZoom) return;
|
|
1395
|
+
const nextW = fitW * nextZoom;
|
|
1396
|
+
const nextH = fitH * nextZoom;
|
|
1397
|
+
let nextX = mouseX - fx * nextW;
|
|
1398
|
+
let nextY = mouseY - fy * nextH;
|
|
1399
|
+
if (nextW <= inner.w) nextX = inner.x + (inner.w - nextW) / 2;
|
|
1400
|
+
else nextX = Math.min(inner.x, Math.max(inner.x + inner.w - nextW, nextX));
|
|
1401
|
+
if (nextH <= inner.h) nextY = inner.y + (inner.h - nextH) / 2;
|
|
1402
|
+
else nextY = Math.min(inner.y, Math.max(inner.y + inner.h - nextH, nextY));
|
|
1403
|
+
zoomScale.value = nextZoom;
|
|
1404
|
+
panX.value = nextX - fitOriginX;
|
|
1405
|
+
panY.value = nextY - inner.y;
|
|
1406
|
+
clampPan();
|
|
1407
|
+
bump();
|
|
1408
|
+
}
|
|
1409
|
+
function onPointerDown(event) {
|
|
1410
|
+
if (!dragEnabled.value) return;
|
|
1411
|
+
event.preventDefault();
|
|
1412
|
+
event.stopPropagation();
|
|
1413
|
+
dragState = {
|
|
1414
|
+
startX: event.cellX,
|
|
1415
|
+
startY: event.cellY,
|
|
1416
|
+
panStartX: panX.value,
|
|
1417
|
+
panStartY: panY.value
|
|
1418
|
+
};
|
|
1419
|
+
dragMoved = false;
|
|
1420
|
+
}
|
|
1421
|
+
function onPointerMove(event) {
|
|
1422
|
+
const drag = dragState;
|
|
1423
|
+
if (!drag) return;
|
|
1424
|
+
event.preventDefault();
|
|
1425
|
+
event.stopPropagation();
|
|
1426
|
+
const dx = event.cellX - drag.startX;
|
|
1427
|
+
const dy = event.cellY - drag.startY;
|
|
1428
|
+
if (!dragMoved && Math.abs(dx) + Math.abs(dy) >= 1) dragMoved = true;
|
|
1429
|
+
if (!dragMoved) return;
|
|
1430
|
+
panX.value = drag.panStartX + dx;
|
|
1431
|
+
panY.value = drag.panStartY + dy;
|
|
1432
|
+
clampPan();
|
|
1433
|
+
bump();
|
|
1434
|
+
}
|
|
1435
|
+
function onPointerUp(event) {
|
|
1436
|
+
const drag = dragState;
|
|
1437
|
+
if (!drag) return;
|
|
1438
|
+
event.preventDefault();
|
|
1439
|
+
event.stopPropagation();
|
|
1440
|
+
dragState = null;
|
|
1441
|
+
if (dragMoved) lastDragAt = Date.now();
|
|
1442
|
+
}
|
|
1106
1443
|
const copyNodeActive = computed(() => visible.value && props.copyButton && copyHitRect.value.w > 0 && copyHitRect.value.h > 0);
|
|
1107
1444
|
useTerminalNode(() => ({
|
|
1108
1445
|
rect: copyHitRect.value,
|
|
@@ -1115,14 +1452,20 @@ const TMermaidImage = defineComponent({
|
|
|
1115
1452
|
keydown: onCopyKeydown
|
|
1116
1453
|
} : {}
|
|
1117
1454
|
}));
|
|
1118
|
-
const contentNodeActive = computed(() => visible.value && props.copyOnClick && contentHitRect.value.w > 0 && contentHitRect.value.h > 0);
|
|
1455
|
+
const contentNodeActive = computed(() => visible.value && (props.copyOnClick || zoomEnabled.value || dragEnabled.value) && contentHitRect.value.w > 0 && contentHitRect.value.h > 0);
|
|
1119
1456
|
useTerminalNode(() => ({
|
|
1120
1457
|
rect: contentHitRect.value,
|
|
1121
1458
|
zIndex: (parentEventZ.value ?? 0) + props.zIndex,
|
|
1122
1459
|
visible: contentNodeActive.value,
|
|
1123
1460
|
focusable: false,
|
|
1124
1461
|
selectable: false,
|
|
1125
|
-
handlers: contentNodeActive.value ? {
|
|
1462
|
+
handlers: contentNodeActive.value ? {
|
|
1463
|
+
click: props.copyOnClick ? onCopyClick : void 0,
|
|
1464
|
+
wheel: zoomEnabled.value ? onWheel : void 0,
|
|
1465
|
+
pointerdown: dragEnabled.value ? onPointerDown : void 0,
|
|
1466
|
+
pointermove: dragEnabled.value ? onPointerMove : void 0,
|
|
1467
|
+
pointerup: dragEnabled.value ? onPointerUp : void 0
|
|
1468
|
+
} : {}
|
|
1126
1469
|
}));
|
|
1127
1470
|
useRenderNode(() => ({
|
|
1128
1471
|
zIndex: props.zIndex,
|
|
@@ -1138,6 +1481,11 @@ const TMermaidImage = defineComponent({
|
|
|
1138
1481
|
fullRect.value,
|
|
1139
1482
|
imageCells.value,
|
|
1140
1483
|
displayFit.value,
|
|
1484
|
+
zoomScale.value,
|
|
1485
|
+
panX.value,
|
|
1486
|
+
panY.value,
|
|
1487
|
+
textSnapshot.value,
|
|
1488
|
+
displayLines.value,
|
|
1141
1489
|
sourceLines.value,
|
|
1142
1490
|
currentStyle.value,
|
|
1143
1491
|
props.clear,
|
|
@@ -1202,4 +1550,148 @@ const TMermaidImage = defineComponent({
|
|
|
1202
1550
|
});
|
|
1203
1551
|
|
|
1204
1552
|
//#endregion
|
|
1205
|
-
|
|
1553
|
+
//#region src/vue/mermaid/beautiful-mermaid.ts
|
|
1554
|
+
const BEAUTIFUL_MERMAID_INSTALL_HINT = "Install beautiful-mermaid and use TMermaidText from @simon_he/vue-tui/mermaid or @simon_he/vue-tui/agent/mermaid, or pass a custom renderer prop.";
|
|
1555
|
+
const MISSING_BEAUTIFUL_MERMAID_ERROR_CODE = "VUE_TUI_MISSING_BEAUTIFUL_MERMAID";
|
|
1556
|
+
const INVALID_BEAUTIFUL_MERMAID_EXPORT_ERROR_CODE = "VUE_TUI_INVALID_BEAUTIFUL_MERMAID_EXPORT";
|
|
1557
|
+
let cachedBeautifulMermaid = null;
|
|
1558
|
+
function functionValue(value) {
|
|
1559
|
+
return typeof value === "function" ? value : null;
|
|
1560
|
+
}
|
|
1561
|
+
function functionProp(target, key) {
|
|
1562
|
+
if (!target || typeof target !== "object") return null;
|
|
1563
|
+
if (!(key in target)) return null;
|
|
1564
|
+
const value = target[key];
|
|
1565
|
+
return typeof value === "function" ? value : null;
|
|
1566
|
+
}
|
|
1567
|
+
function errorMessage(error) {
|
|
1568
|
+
return error instanceof Error ? error.message : String(error);
|
|
1569
|
+
}
|
|
1570
|
+
function codedError(message, code) {
|
|
1571
|
+
const error = new Error(message);
|
|
1572
|
+
error.code = code;
|
|
1573
|
+
return error;
|
|
1574
|
+
}
|
|
1575
|
+
function missingBeautifulMermaidError(error) {
|
|
1576
|
+
if (error && typeof error === "object" && errorCode(error) === MISSING_BEAUTIFUL_MERMAID_ERROR_CODE) return markMermaidRenderErrorFatal(error);
|
|
1577
|
+
const detail = errorMessage(error);
|
|
1578
|
+
return markMermaidRenderErrorFatal(codedError(`${BEAUTIFUL_MERMAID_INSTALL_HINT} (${detail})`, MISSING_BEAUTIFUL_MERMAID_ERROR_CODE));
|
|
1579
|
+
}
|
|
1580
|
+
function errorCode(error) {
|
|
1581
|
+
if (!error || typeof error !== "object") return "";
|
|
1582
|
+
const value = error.code;
|
|
1583
|
+
return typeof value === "string" ? value : "";
|
|
1584
|
+
}
|
|
1585
|
+
function errorCause(error) {
|
|
1586
|
+
if (!error || typeof error !== "object") return void 0;
|
|
1587
|
+
return error.cause;
|
|
1588
|
+
}
|
|
1589
|
+
function isMissingBeautifulMermaid(error) {
|
|
1590
|
+
return isMissingBeautifulMermaidWithSeen(error, new WeakSet());
|
|
1591
|
+
}
|
|
1592
|
+
function isMissingBeautifulMermaidWithSeen(error, seen) {
|
|
1593
|
+
if (error && typeof error === "object") {
|
|
1594
|
+
if (seen.has(error)) return false;
|
|
1595
|
+
seen.add(error);
|
|
1596
|
+
}
|
|
1597
|
+
const message = errorMessage(error);
|
|
1598
|
+
const code = errorCode(error);
|
|
1599
|
+
if (code === MISSING_BEAUTIFUL_MERMAID_ERROR_CODE) return true;
|
|
1600
|
+
const mentionsBeautifulMermaid = /Cannot find package ['"]beautiful-mermaid['"]/.test(message) || /Cannot find module ['"]beautiful-mermaid['"]/.test(message) || /Cannot resolve module ['"]beautiful-mermaid['"]/.test(message) || /Can't resolve ['"]beautiful-mermaid['"]/.test(message) || /Failed to resolve module specifier ['"]beautiful-mermaid['"]/.test(message) || /Failed to resolve import ['"]beautiful-mermaid['"]/.test(message) || /Could not resolve ['"]beautiful-mermaid['"]/.test(message);
|
|
1601
|
+
if (mentionsBeautifulMermaid && (code === "" || code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || code === "UNRESOLVED_IMPORT")) return true;
|
|
1602
|
+
const cause = errorCause(error);
|
|
1603
|
+
return cause !== void 0 && isMissingBeautifulMermaidWithSeen(cause, seen);
|
|
1604
|
+
}
|
|
1605
|
+
function resolveBeautifulMermaidRenderer(mod) {
|
|
1606
|
+
const renderer = functionProp(mod, "renderMermaidASCII") ?? functionProp(mod, "renderMermaidAscii") ?? functionValue(mod.default) ?? functionProp(mod.default, "renderMermaidASCII") ?? functionProp(mod.default, "renderMermaidAscii");
|
|
1607
|
+
if (!renderer) throw markMermaidRenderErrorFatal(codedError("beautiful-mermaid is installed but does not export renderMermaidASCII.", INVALID_BEAUTIFUL_MERMAID_EXPORT_ERROR_CODE));
|
|
1608
|
+
return renderer;
|
|
1609
|
+
}
|
|
1610
|
+
function hasErrorCode(error, code, seen = new WeakSet()) {
|
|
1611
|
+
if (!error || typeof error !== "object") return false;
|
|
1612
|
+
if (seen.has(error)) return false;
|
|
1613
|
+
seen.add(error);
|
|
1614
|
+
if (errorCode(error) === code) return true;
|
|
1615
|
+
const cause = errorCause(error);
|
|
1616
|
+
return cause !== void 0 && hasErrorCode(cause, code, seen);
|
|
1617
|
+
}
|
|
1618
|
+
function isBeautifulMermaidExportError(error) {
|
|
1619
|
+
return hasErrorCode(error, INVALID_BEAUTIFUL_MERMAID_EXPORT_ERROR_CODE);
|
|
1620
|
+
}
|
|
1621
|
+
const isTransientBeautifulMermaidRenderError = (error) => {
|
|
1622
|
+
if (isMissingBeautifulMermaid(error)) return false;
|
|
1623
|
+
if (isBeautifulMermaidExportError(error)) return false;
|
|
1624
|
+
return true;
|
|
1625
|
+
};
|
|
1626
|
+
async function loadBeautifulMermaid() {
|
|
1627
|
+
if (!cachedBeautifulMermaid) cachedBeautifulMermaid = import("beautiful-mermaid").then((mod) => mod).catch((error) => {
|
|
1628
|
+
cachedBeautifulMermaid = null;
|
|
1629
|
+
if (isMissingBeautifulMermaid(error)) throw missingBeautifulMermaidError(error);
|
|
1630
|
+
throw markMermaidRenderErrorFatal(error);
|
|
1631
|
+
});
|
|
1632
|
+
return cachedBeautifulMermaid;
|
|
1633
|
+
}
|
|
1634
|
+
const beautifulMermaidRenderer = async (code, options) => {
|
|
1635
|
+
try {
|
|
1636
|
+
const mod = await loadBeautifulMermaid();
|
|
1637
|
+
const renderer = resolveBeautifulMermaidRenderer(mod);
|
|
1638
|
+
return await renderer(code, options);
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
if (isMissingBeautifulMermaid(error)) throw missingBeautifulMermaidError(error);
|
|
1641
|
+
throw error;
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
function createBeautifulMermaidRenderer() {
|
|
1645
|
+
return beautifulMermaidRenderer;
|
|
1646
|
+
}
|
|
1647
|
+
const TMermaidText$1 = defineComponent({
|
|
1648
|
+
name: "TMermaidText",
|
|
1649
|
+
inheritAttrs: false,
|
|
1650
|
+
props: tMermaidTextProps,
|
|
1651
|
+
emits: { copy: (_payload) => true },
|
|
1652
|
+
setup(props, { attrs, emit, slots }) {
|
|
1653
|
+
return () => {
|
|
1654
|
+
const renderer = props.renderer ?? beautifulMermaidRenderer;
|
|
1655
|
+
const shouldRenderSource = props.shouldRenderSource ?? isSimpleMermaidFlowchartSource;
|
|
1656
|
+
return h(TMermaidText, {
|
|
1657
|
+
...attrs,
|
|
1658
|
+
...props,
|
|
1659
|
+
renderer,
|
|
1660
|
+
shouldRenderSource,
|
|
1661
|
+
isTransientError: props.isTransientError ?? isTransientBeautifulMermaidRenderError,
|
|
1662
|
+
onCopy: (payload) => emit("copy", payload)
|
|
1663
|
+
}, slots);
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
/**
|
|
1668
|
+
* Adaptive default mermaid component: renders the diagram as a terminal image
|
|
1669
|
+
* (Kitty/iTerm2 graphics) when the terminal supports it, otherwise falls back
|
|
1670
|
+
* to the beautiful-mermaid ASCII renderer, otherwise shows the raw source.
|
|
1671
|
+
* While any pipeline is pending the raw source stays visible (source-first);
|
|
1672
|
+
* streaming sources are only rendered once `final` is true.
|
|
1673
|
+
*/
|
|
1674
|
+
const TMermaid = defineComponent({
|
|
1675
|
+
name: "TMermaid",
|
|
1676
|
+
inheritAttrs: false,
|
|
1677
|
+
props: tMermaidImageProps,
|
|
1678
|
+
emits: { copy: (_payload) => true },
|
|
1679
|
+
setup(props, { attrs, emit, slots }) {
|
|
1680
|
+
return () => {
|
|
1681
|
+
const { renderer: _legacyTextRenderer,...rest } = props;
|
|
1682
|
+
const textRenderer = props.textRenderer ?? props.renderer ?? beautifulMermaidRenderer;
|
|
1683
|
+
return h(TMermaidImage, {
|
|
1684
|
+
...attrs,
|
|
1685
|
+
...rest,
|
|
1686
|
+
textRenderer,
|
|
1687
|
+
shouldRenderSource: props.shouldRenderSource ?? isSimpleMermaidFlowchartSource,
|
|
1688
|
+
onCopy: (payload) => emit("copy", payload)
|
|
1689
|
+
}, slots);
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
const TBeautifulMermaidText = TMermaidText$1;
|
|
1694
|
+
const TBeautifulMermaid = TMermaid;
|
|
1695
|
+
|
|
1696
|
+
//#endregion
|
|
1697
|
+
export { TBeautifulMermaid, TBeautifulMermaidText, TMermaid, TMermaidImage, TMermaidText$1 as TMermaidText, beautifulMermaidRenderer, clearMermaidImageCache, createBeautifulMermaidRenderer, getCachedMermaidImage, getMermaidImage, isMermaidImageRendererReady, loadMermaidImageRenderer, normalizeMermaidImageOptions, resolveMermaidImageColor, resolveMermaidSvgForResvg, setMermaidImageRasterizer, subscribeMermaidImage };
|