@simon_he/vue-tui 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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-DWZrl3oh.js";
3
+ import { createTerminalGraphicPngSequence, getTerminalGraphicsOutput, getTerminalGraphicsOutputVersion, stableTerminalGraphicNumericId, subscribeTerminalGraphicsOutput } from "./terminal-graphics-D6KErRBY.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,223 @@ 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 isSvgVarNameChar(ch) {
253
+ const code = ch.charCodeAt(0);
254
+ return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || ch === "_" || ch === "-";
255
+ }
256
+ function isSvgWhitespace(ch) {
257
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f";
258
+ }
259
+ /**
260
+ * Find the next `<style ...>...</style>` block starting at or after `from`.
261
+ * Uses `indexOf` scans only (no regex backtracking), so untrusted SVG input
262
+ * cannot trigger ReDoS. Returns the outer range plus the inner content range,
263
+ * or null when no complete style block exists.
264
+ */
265
+ function findSvgStyleBlock(svg, from = 0) {
266
+ const lower = svg.toLowerCase();
267
+ const open = lower.indexOf("<style", from);
268
+ if (open < 0) return null;
269
+ const tagEnd = svg.indexOf(">", open);
270
+ if (tagEnd < 0) return null;
271
+ const close = lower.indexOf("</style", tagEnd + 1);
272
+ if (close < 0) return null;
273
+ const closeEnd = svg.indexOf(">", close);
274
+ if (closeEnd < 0) return null;
275
+ return {
276
+ start: open,
277
+ contentStart: tagEnd + 1,
278
+ contentEnd: close,
279
+ end: closeEnd + 1
280
+ };
281
+ }
282
+ function extractSvgCssVariables(svg) {
283
+ const map = new Map();
284
+ const block = findSvgStyleBlock(svg);
285
+ if (!block) return map;
286
+ const content = svg.slice(block.contentStart, block.contentEnd);
287
+ let cursor = 0;
288
+ while (cursor < content.length) {
289
+ const semi = content.indexOf(";", cursor);
290
+ if (semi < 0) break;
291
+ const statement = content.slice(cursor, semi);
292
+ let pos = 0;
293
+ while (pos < statement.length) {
294
+ const open = statement.indexOf("--", pos);
295
+ if (open < 0) break;
296
+ let nameEnd = open + 2;
297
+ while (nameEnd < statement.length && isSvgVarNameChar(statement[nameEnd])) nameEnd++;
298
+ if (nameEnd === open + 2) {
299
+ pos = open + 2;
300
+ continue;
301
+ }
302
+ let colonAt = nameEnd;
303
+ while (colonAt < statement.length && isSvgWhitespace(statement[colonAt])) colonAt++;
304
+ if (statement[colonAt] !== ":") {
305
+ pos = open + 2;
306
+ continue;
307
+ }
308
+ const value = statement.slice(colonAt + 1).trim();
309
+ if (value !== "" && !value.includes("{") && !value.includes("}")) map.set(statement.slice(open, nameEnd), value);
310
+ break;
311
+ }
312
+ cursor = semi + 1;
313
+ }
314
+ return map;
315
+ }
316
+ /**
317
+ * True when the `<style` at `open` looks like a real element opener: the tag
318
+ * name must be followed by whitespace, "/" or ">".
319
+ */
320
+ function isSvgStyleOpener(svg, open) {
321
+ const after = svg[open + 6];
322
+ return after === ">" || after === "/" || isSvgWhitespace(after);
323
+ }
324
+ function stripSvgStyleBlocks(svg) {
325
+ let result = svg;
326
+ let block = findSvgStyleBlock(result);
327
+ while (block) {
328
+ result = result.slice(0, block.start) + result.slice(block.end);
329
+ block = findSvgStyleBlock(result, block.start);
330
+ }
331
+ let open = result.toLowerCase().indexOf("<style");
332
+ while (open >= 0) {
333
+ if (!isSvgStyleOpener(result, open)) {
334
+ open = result.toLowerCase().indexOf("<style", open + 6);
335
+ continue;
336
+ }
337
+ const nextLt = result.indexOf("<", open + 6);
338
+ const tagEnd = result.indexOf(">", open);
339
+ if (tagEnd < 0 || nextLt >= 0 && nextLt < tagEnd) {
340
+ open = result.toLowerCase().indexOf("<style", open + 6);
341
+ continue;
342
+ }
343
+ result = result.slice(0, open) + result.slice(tagEnd + 1);
344
+ open = result.toLowerCase().indexOf("<style", open);
345
+ }
346
+ return result;
347
+ }
348
+ function findMatchingSvgParen(input, openIndex) {
349
+ let depth = 0;
350
+ for (let i = openIndex; i < input.length; i++) {
351
+ const ch = input[i];
352
+ if (ch === "(") depth++;
353
+ else if (ch === ")") {
354
+ depth--;
355
+ if (depth === 0) return i;
356
+ }
357
+ }
358
+ return -1;
359
+ }
360
+ function findTopLevelSvgComma(input) {
361
+ let depth = 0;
362
+ for (let i = 0; i < input.length; i++) {
363
+ const ch = input[i];
364
+ if (ch === "(") depth++;
365
+ else if (ch === ")") depth--;
366
+ else if (ch === "," && depth === 0) return i;
367
+ }
368
+ return -1;
369
+ }
370
+ function normalizeHexColor(value) {
371
+ const match = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.trim());
372
+ if (!match) return null;
373
+ let hex = match[1];
374
+ if (hex.length === 3) hex = hex.split("").map((ch) => ch + ch).join("");
375
+ return `#${hex.toLowerCase()}`;
376
+ }
377
+ function mixSrgbColors(aHex, bHex, percent) {
378
+ const a = parseHexRgb(aHex);
379
+ const b = parseHexRgb(bHex);
380
+ const p = Math.max(0, Math.min(100, percent)) / 100;
381
+ const mix = (x, y) => Math.round(x * p + y * (1 - p));
382
+ const toHex = (value) => value.toString(16).padStart(2, "0");
383
+ return `#${toHex(mix(a[0], b[0]))}${toHex(mix(a[1], b[1]))}${toHex(mix(a[2], b[2]))}`;
384
+ }
385
+ function parseHexRgb(hex) {
386
+ return [
387
+ parseInt(hex.slice(1, 3), 16),
388
+ parseInt(hex.slice(3, 5), 16),
389
+ parseInt(hex.slice(5, 7), 16)
390
+ ];
391
+ }
392
+ function resolveSvgColorExpr(expr, root, variables) {
393
+ const value = expr.trim();
394
+ if (value.startsWith("var(")) {
395
+ const close = findMatchingSvgParen(value, 3);
396
+ if (close < 0) return value;
397
+ const inner = value.slice(4, close).trim();
398
+ const comma = findTopLevelSvgComma(inner);
399
+ const name = (comma < 0 ? inner : inner.slice(0, comma)).trim();
400
+ const fallback = comma < 0 ? "" : inner.slice(comma + 1).trim();
401
+ const direct = name === "--fg" ? root.fg : name === "--bg" ? root.bg : variables.get(name);
402
+ if (direct != null) return resolveSvgColorExpr(direct, root, variables);
403
+ if (fallback) return resolveSvgColorExpr(fallback, root, variables);
404
+ return value;
405
+ }
406
+ if (value.startsWith("color-mix(")) {
407
+ const close = findMatchingSvgParen(value, 9);
408
+ if (close < 0) return value;
409
+ const inner = value.slice(10, close).trim();
410
+ const afterMethod = inner.replace(/^in\s+srgb\s*,\s*/i, "");
411
+ const comma = findTopLevelSvgComma(afterMethod);
412
+ if (comma < 0) return value;
413
+ const left = afterMethod.slice(0, comma).trim();
414
+ const right = afterMethod.slice(comma + 1).trim();
415
+ const leftMatch = /^([^%\s]+)\s+([\d.]+)%$/.exec(left) ?? /^([\d.]+)%\s+([^%\s]+)$/.exec(left);
416
+ if (!leftMatch) return value;
417
+ const a = normalizeHexColor(resolveSvgColorExpr(leftMatch[1], root, variables));
418
+ const b = normalizeHexColor(resolveSvgColorExpr(right, root, variables));
419
+ if (!a || !b) return value;
420
+ return mixSrgbColors(a, b, Number(leftMatch[2]));
421
+ }
422
+ return normalizeHexColor(value) ?? value;
423
+ }
326
424
  function loadBuiltinMermaidRasterizer() {
327
425
  if (!builtinRasterizerLoad) builtinRasterizerLoad = (async () => {
328
426
  try {
@@ -339,7 +437,8 @@ function loadBuiltinMermaidRasterizer() {
339
437
  });
340
438
  const svgText = String(svg ?? "");
341
439
  if (!svgText.includes("<svg")) return null;
342
- const rendered = new Resvg(svgText, { background: "rgba(0,0,0,0)" }).render();
440
+ const resolvedSvg = resolveMermaidSvgForResvg(svgText, options.bg, options.fg);
441
+ const rendered = new Resvg(resolvedSvg, { background: "rgba(0,0,0,0)" }).render();
343
442
  const naturalWidth = positiveInt(rendered?.width);
344
443
  const naturalHeight = positiveInt(rendered?.height);
345
444
  const png = typeof rendered?.asPng === "function" ? rendered.asPng() : void 0;
@@ -514,6 +613,18 @@ const tMermaidImageProps = {
514
613
  type: Number,
515
614
  default: void 0
516
615
  },
616
+ textRenderer: {
617
+ type: Function,
618
+ default: void 0
619
+ },
620
+ shouldRenderSource: {
621
+ type: Function,
622
+ default: void 0
623
+ },
624
+ textOptions: {
625
+ type: Object,
626
+ default: void 0
627
+ },
517
628
  box: {
518
629
  type: Boolean,
519
630
  default: true
@@ -557,6 +668,26 @@ const tMermaidImageProps = {
557
668
  ascii: {
558
669
  type: Boolean,
559
670
  default: false
671
+ },
672
+ zoomOnWheel: {
673
+ type: Boolean,
674
+ default: true
675
+ },
676
+ zoomModifier: {
677
+ type: String,
678
+ default: "metaCtrl"
679
+ },
680
+ minZoom: {
681
+ type: Number,
682
+ default: 1
683
+ },
684
+ maxZoom: {
685
+ type: Number,
686
+ default: 6
687
+ },
688
+ zoomSensitivity: {
689
+ type: Number,
690
+ default: .002
560
691
  }
561
692
  };
562
693
  const TMermaidImage = defineComponent({
@@ -571,8 +702,16 @@ const TMermaidImage = defineComponent({
571
702
  const { visible, rootProps } = useVisibility();
572
703
  const parentEventZ = inject(EventZIndexContextKey, computed(() => 0));
573
704
  const imageCells = shallowRef(null);
705
+ const textSnapshot = shallowRef(null);
706
+ const zoomScale = shallowRef(1);
707
+ const panX = shallowRef(0);
708
+ const panY = shallowRef(0);
574
709
  const documentVersion = shallowRef(0);
575
710
  const copied = shallowRef(false);
711
+ let dragState = null;
712
+ let dragMoved = false;
713
+ let lastDragAt = 0;
714
+ const DRAG_CLICK_SUPPRESS_MS = 250;
576
715
  let builtOnce = false;
577
716
  let renderVersion = 0;
578
717
  let alive = true;
@@ -581,6 +720,7 @@ const TMermaidImage = defineComponent({
581
720
  let copiedTimer = null;
582
721
  let copyRequestVersion = 0;
583
722
  let activeGraphicId = null;
723
+ let activeImageSignature = "";
584
724
  const source = computed(() => props.code ?? props.content ?? "");
585
725
  const waitingForStreamingSourceToFinish = computed(() => props.streaming && !props.final);
586
726
  const graphicsOutputVersion = shallowRef(getTerminalGraphicsOutputVersion(terminal));
@@ -635,6 +775,73 @@ const TMermaidImage = defineComponent({
635
775
  graphicsOutput.value?.clear?.(id);
636
776
  } catch {}
637
777
  }
778
+ function clampZoom(zoom) {
779
+ const min = Number.isFinite(Number(props.minZoom)) ? Math.max(.05, Number(props.minZoom)) : 1;
780
+ const max = Number.isFinite(Number(props.maxZoom)) ? Math.max(min, Number(props.maxZoom)) : Math.max(min, 6);
781
+ return Math.min(max, Math.max(min, zoom));
782
+ }
783
+ function zoomSensitivity() {
784
+ const value = Number(props.zoomSensitivity);
785
+ return Number.isFinite(value) && value > 0 ? value : .002;
786
+ }
787
+ /**
788
+ * Wheel delta -> multiplicative zoom factor. Discrete notches (CLI wheel
789
+ * reporting and browser line-mode deltas) map to one step each; trackpad
790
+ * pixel deltas use a continuous exponential so small movements stay smooth.
791
+ */
792
+ function wheelZoomFactor(deltaY, deltaMode) {
793
+ const sign = deltaY > 0 ? -1 : 1;
794
+ const sensitivity = zoomSensitivity();
795
+ const abs = Math.abs(deltaY);
796
+ const mode = deltaMode ?? 0;
797
+ const isNotch = mode === 1 || abs > 0 && abs <= 3 && Number.isInteger(deltaY);
798
+ const magnitude = isNotch ? 100 : abs;
799
+ return Math.exp(sign * magnitude * sensitivity);
800
+ }
801
+ function resetZoomState() {
802
+ if (zoomScale.value === 1 && panX.value === 0 && panY.value === 0) return;
803
+ zoomScale.value = 1;
804
+ panX.value = 0;
805
+ panY.value = 0;
806
+ dragState = null;
807
+ }
808
+ function hasZoomModifier(event) {
809
+ const modifier = props.zoomModifier ?? "metaCtrl";
810
+ if (modifier === "none") return true;
811
+ if (modifier === "meta") return event.metaKey === true;
812
+ if (modifier === "ctrl") return event.ctrlKey === true;
813
+ return event.ctrlKey === true || event.metaKey === true;
814
+ }
815
+ /**
816
+ * Clamp the pan so the zoomed image always covers the container area
817
+ * (no empty gaps, no over-scroll beyond the image edges).
818
+ */
819
+ function clampPan() {
820
+ const inner = innerRectFrom(fullRect.value);
821
+ const fit = displayFit.value;
822
+ if (!fit || inner.w <= 0 || inner.h <= 0) return;
823
+ const zoom = clampZoom(zoomScale.value);
824
+ const fullW = fit.displayW * zoom;
825
+ const fullH = fit.displayH * zoom;
826
+ const fitOriginX = inner.x + (inner.w > fit.displayW ? Math.floor((inner.w - fit.displayW) / 2) : 0);
827
+ let nextX = panX.value;
828
+ let nextY = panY.value;
829
+ if (fullW <= inner.w) nextX = 0;
830
+ else nextX = Math.min(inner.x - fitOriginX, Math.max(inner.x + inner.w - fullW - fitOriginX, nextX));
831
+ if (fullH <= inner.h) nextY = 0;
832
+ else nextY = Math.min(0, Math.max(inner.h - fullH, nextY));
833
+ if (nextX !== panX.value) panX.value = nextX;
834
+ if (nextY !== panY.value) panY.value = nextY;
835
+ }
836
+ /** Set the current image, resetting wheel zoom whenever the PNG changes. */
837
+ function setImageCells(cells) {
838
+ const signature = cells ? cells.base64 : "";
839
+ if (signature !== activeImageSignature) {
840
+ activeImageSignature = signature;
841
+ resetZoomState();
842
+ }
843
+ imageCells.value = cells;
844
+ }
638
845
  const hasBox = computed(() => props.box !== false);
639
846
  const normalizedWidth = computed(() => {
640
847
  const width = Math.floor(Number(props.w));
@@ -643,6 +850,11 @@ const TMermaidImage = defineComponent({
643
850
  const reservesBoxRows = computed(() => hasBox.value && normalizedWidth.value >= 2);
644
851
  const boxChars = computed(() => props.ascii ? ASCII_MERMAID_IMAGE_BOX_CHARS : UNICODE_MERMAID_IMAGE_BOX_CHARS);
645
852
  const sourceLines = computed(() => splitRenderedOutput(source.value));
853
+ const displayLines = computed(() => {
854
+ const snapshot = textSnapshot.value;
855
+ if (snapshot && snapshot.source === source.value && hasVisibleTextOutput(snapshot.lines)) return snapshot.lines;
856
+ return sourceLines.value;
857
+ });
646
858
  const currentStyle = computed(() => props.style ?? defaultStyle.value);
647
859
  const innerWidth = computed(() => {
648
860
  const w = normalizedWidth.value;
@@ -664,7 +876,7 @@ const TMermaidImage = defineComponent({
664
876
  });
665
877
  const contentHeight = computed(() => {
666
878
  if (!autoHeight.value) return fixedInnerHeight.value;
667
- return displayFit.value?.displayH ?? Math.max(1, sourceLines.value.length);
879
+ return displayFit.value?.displayH ?? Math.max(1, displayLines.value.length);
668
880
  });
669
881
  const fullHeight = computed(() => {
670
882
  if (props.h != null) {
@@ -717,6 +929,53 @@ const TMermaidImage = defineComponent({
717
929
  padding: props.padding
718
930
  });
719
931
  }
932
+ function hasVisibleTextOutput(lines) {
933
+ return lines.some((line) => line.trim().length > 0);
934
+ }
935
+ function resolveTextOptions() {
936
+ const base = props.textOptions ?? {};
937
+ return {
938
+ ...base,
939
+ useAscii: props.ascii,
940
+ colorMode: "none"
941
+ };
942
+ }
943
+ function textRenderSignature(code) {
944
+ return [
945
+ code,
946
+ props.ascii ? "a" : "",
947
+ JSON.stringify(props.textOptions ?? null),
948
+ props.textRenderer != null ? "r" : "",
949
+ props.shouldRenderSource != null ? "g" : ""
950
+ ].join("");
951
+ }
952
+ async function renderTextWithTimeout(renderer, code, options) {
953
+ const timeoutMs = normalizeNonNegativeInt(props.renderTimeoutMs, 0);
954
+ if (timeoutMs <= 0) return await renderer(code, options);
955
+ let timer = null;
956
+ try {
957
+ return await Promise.race([Promise.resolve().then(() => renderer(code, options)), new Promise((_resolve, reject) => {
958
+ timer = setTimeout(() => reject(new Error("Mermaid text render timeout")), timeoutMs);
959
+ })]);
960
+ } finally {
961
+ if (timer != null) clearTimeout(timer);
962
+ }
963
+ }
964
+ async function runCustomRasterizer(code, options) {
965
+ const renderer = props.renderer;
966
+ if (!renderer) return null;
967
+ const timeoutMs = normalizeNonNegativeInt(props.renderTimeoutMs, 0);
968
+ const run = () => renderer(code, options);
969
+ if (timeoutMs <= 0) return await run() ?? null;
970
+ let timer = null;
971
+ try {
972
+ return await Promise.race([run(), new Promise((resolve) => {
973
+ timer = setTimeout(() => resolve(null), timeoutMs);
974
+ })]) ?? null;
975
+ } finally {
976
+ if (timer != null) clearTimeout(timer);
977
+ }
978
+ }
720
979
  function shouldSkipRenderForSize(code) {
721
980
  const maxChars = normalizeNonNegativeInt(props.maxRenderSourceChars, DEFAULT_MERMAID_IMAGE_MAX_RENDER_SOURCE_CHARS);
722
981
  if (maxChars > 0 && code.length > maxChars) return true;
@@ -736,48 +995,107 @@ const TMermaidImage = defineComponent({
736
995
  if (timer != null) clearTimeout(timer);
737
996
  }
738
997
  }
739
- async function renderImage(version) {
998
+ async function renderTextFallback(version) {
999
+ const code = source.value;
1000
+ const renderer = props.textRenderer;
1001
+ if (!renderer) return;
1002
+ const guard = props.shouldRenderSource;
1003
+ if (guard) {
1004
+ let eligible = false;
1005
+ try {
1006
+ eligible = guard(code, {
1007
+ final: props.final,
1008
+ streaming: props.streaming
1009
+ });
1010
+ } catch {
1011
+ eligible = false;
1012
+ }
1013
+ if (!eligible) return;
1014
+ }
1015
+ try {
1016
+ const rendered = await renderTextWithTimeout(renderer, code, resolveTextOptions());
1017
+ if (!alive || version !== renderVersion) return;
1018
+ const lines = splitRenderedOutput(rendered);
1019
+ if (!hasVisibleTextOutput(lines)) return;
1020
+ textSnapshot.value = markRaw({
1021
+ source: code,
1022
+ signature: textRenderSignature(code),
1023
+ lines: markRaw(lines)
1024
+ });
1025
+ bump();
1026
+ } catch {}
1027
+ }
1028
+ /**
1029
+ * Adaptive presentation state machine:
1030
+ *
1031
+ * raw source
1032
+ * -> image (terminal supports Kitty/iTerm2 graphics and the PNG
1033
+ * rasterizer succeeds), else
1034
+ * -> ANSI text (a textRenderer is available and the source passes the
1035
+ * eligibility guard), else
1036
+ * -> raw source
1037
+ *
1038
+ * While any pipeline is pending the raw source stays visible (source-first).
1039
+ */
1040
+ async function renderPresentation(version) {
740
1041
  const code = source.value;
741
1042
  if (!code.trim()) {
742
1043
  if (!alive || version !== renderVersion) return;
743
- imageCells.value = null;
1044
+ setImageCells(null);
1045
+ textSnapshot.value = null;
744
1046
  bump();
745
1047
  return;
746
1048
  }
747
1049
  if (waitingForStreamingSourceToFinish.value || shouldSkipRenderForSize(code)) {
748
1050
  if (!alive || version !== renderVersion) return;
749
- imageCells.value = null;
1051
+ setImageCells(null);
1052
+ textSnapshot.value = null;
750
1053
  bump();
751
1054
  return;
752
1055
  }
753
- imageCells.value = null;
754
- bump();
755
1056
  const protocol = graphicsProtocol.value;
756
- if (!protocol) return;
757
1057
  const options = resolveImageOptions();
758
- const cached = getCachedMermaidImage(code, options);
1058
+ const cached = protocol && !props.renderer ? getCachedMermaidImage(code, options) : null;
759
1059
  if (cached) {
760
1060
  if (!alive || version !== renderVersion) return;
761
- imageCells.value = cached;
1061
+ setImageCells(cached);
1062
+ textSnapshot.value = null;
762
1063
  bump();
763
1064
  return;
764
1065
  }
765
- if (!isMermaidImageRendererReady()) {
766
- loadMermaidImageRenderer().then((ready) => {
767
- if (!alive || version !== renderVersion || !ready) return;
768
- renderImage(version);
769
- });
770
- return;
1066
+ const currentSignature = textRenderSignature(code);
1067
+ const keepText = textSnapshot.value != null && textSnapshot.value.source === code && textSnapshot.value.signature === currentSignature;
1068
+ if (!keepText) {
1069
+ setImageCells(null);
1070
+ textSnapshot.value = null;
1071
+ bump();
771
1072
  }
772
- const result = await rasterizeWithTimeout(code, options);
773
- if (!alive || version !== renderVersion) return;
774
- imageCells.value = result;
775
- bump();
1073
+ if (protocol) {
1074
+ let result = null;
1075
+ if (props.renderer) result = await runCustomRasterizer(code, options);
1076
+ else if (isMermaidImageRendererReady()) result = await rasterizeWithTimeout(code, options);
1077
+ else {
1078
+ loadMermaidImageRenderer().then((ready) => {
1079
+ if (!alive || version !== renderVersion || !ready) return;
1080
+ renderPresentation(version);
1081
+ });
1082
+ return;
1083
+ }
1084
+ if (!alive || version !== renderVersion) return;
1085
+ if (result) {
1086
+ setImageCells(result);
1087
+ textSnapshot.value = null;
1088
+ bump();
1089
+ return;
1090
+ }
1091
+ }
1092
+ if (keepText) return;
1093
+ await renderTextFallback(version);
776
1094
  }
777
1095
  function scheduleRender() {
778
1096
  const version = ++renderVersion;
779
1097
  resetCopyFeedback(false);
780
- imageCells.value = null;
1098
+ setImageCells(null);
781
1099
  if (waitingForStreamingSourceToFinish.value) {
782
1100
  builtOnce = true;
783
1101
  scheduler.cancelFrameTask?.(frameTaskId);
@@ -786,7 +1104,7 @@ const TMermaidImage = defineComponent({
786
1104
  }
787
1105
  if (!builtOnce || !props.streaming) {
788
1106
  builtOnce = true;
789
- renderImage(version);
1107
+ renderPresentation(version);
790
1108
  return;
791
1109
  }
792
1110
  const accepted = scheduler.queueFrameTask({
@@ -797,10 +1115,10 @@ const TMermaidImage = defineComponent({
797
1115
  run: () => {
798
1116
  if (!alive) return;
799
1117
  if (version !== renderVersion) return;
800
- renderImage(version);
1118
+ renderPresentation(version);
801
1119
  }
802
1120
  });
803
- if (accepted === false) renderImage(version);
1121
+ if (accepted === false) renderPresentation(version);
804
1122
  }
805
1123
  watch([
806
1124
  source,
@@ -817,7 +1135,11 @@ const TMermaidImage = defineComponent({
817
1135
  () => props.final,
818
1136
  () => props.maxRenderSourceChars,
819
1137
  () => props.maxRenderSourceLines,
820
- graphicsOutputVersion
1138
+ () => props.textRenderer,
1139
+ () => props.shouldRenderSource,
1140
+ () => props.textOptions,
1141
+ () => props.ascii,
1142
+ () => props.zoomModifier
821
1143
  ], () => {
822
1144
  scheduleRender();
823
1145
  }, {
@@ -826,7 +1148,13 @@ const TMermaidImage = defineComponent({
826
1148
  });
827
1149
  const unsubscribeMermaidImage = subscribeMermaidImage(() => {
828
1150
  if (!alive) return;
829
- scheduleRender();
1151
+ if (!graphicsProtocol.value || props.renderer) return;
1152
+ const cached = getCachedMermaidImage(source.value, resolveImageOptions());
1153
+ if (!cached) return;
1154
+ if (!alive) return;
1155
+ setImageCells(cached);
1156
+ textSnapshot.value = null;
1157
+ bump();
830
1158
  });
831
1159
  const unsubscribeGraphicsOutput = subscribeTerminalGraphicsOutput(terminal, () => {
832
1160
  if (!alive) return;
@@ -909,7 +1237,7 @@ const TMermaidImage = defineComponent({
909
1237
  }
910
1238
  function contentLine(rowIndex, width, pad) {
911
1239
  const showImage = imageCells.value != null && displayFit.value != null && graphicsProtocol.value != null;
912
- const src = showImage ? spaces(width) : sourceLines.value[rowIndex] ?? "";
1240
+ const src = showImage ? spaces(width) : displayLines.value[rowIndex] ?? "";
913
1241
  const clipped = sliceCells(src, width);
914
1242
  return pad ? padCells(clipped, width) : clipped;
915
1243
  }
@@ -937,12 +1265,28 @@ const TMermaidImage = defineComponent({
937
1265
  queueClearGraphic();
938
1266
  return;
939
1267
  }
940
- const offsetX = inner.w > fit.displayW ? Math.floor((inner.w - fit.displayW) / 2) : 0;
1268
+ const zoom = clampZoom(zoomScale.value);
1269
+ const fitW = fit.displayW;
1270
+ const fitH = fit.displayH;
1271
+ const fitOriginX = inner.x + (inner.w > fitW ? Math.floor((inner.w - fitW) / 2) : 0);
1272
+ const fullRect$1 = {
1273
+ x: fitOriginX + panX.value,
1274
+ y: inner.y + panY.value,
1275
+ w: fitW * zoom,
1276
+ h: fitH * zoom
1277
+ };
1278
+ const visible$1 = intersectRect(fullRect$1, inner);
1279
+ if (!visible$1 || visible$1.w < 1 || visible$1.h < 1) {
1280
+ queueClearGraphic();
1281
+ return;
1282
+ }
1283
+ const rectX = Math.max(inner.x, Math.round(visible$1.x));
1284
+ const rectY = Math.max(inner.y, Math.round(visible$1.y));
941
1285
  const rect = {
942
- x: inner.x + offsetX,
943
- y: inner.y,
944
- w: fit.displayW,
945
- h: fit.displayH
1286
+ x: rectX,
1287
+ y: rectY,
1288
+ w: Math.max(1, Math.min(inner.x + inner.w, rectX + Math.round(visible$1.w)) - rectX),
1289
+ h: Math.max(1, Math.min(inner.y + inner.h, rectY + Math.round(visible$1.h)) - rectY)
946
1290
  };
947
1291
  const imageId = stableTerminalGraphicNumericId(`mermaid-image:${uid}:${renderVersion}:img`);
948
1292
  const placementId = stableTerminalGraphicNumericId(`mermaid-image:${uid}:${renderVersion}:plc`);
@@ -955,10 +1299,13 @@ const TMermaidImage = defineComponent({
955
1299
  rows: cells.heightCells,
956
1300
  sourceWidth: cells.naturalWidth,
957
1301
  sourceHeight: cells.naturalHeight,
958
- placementColumns: fit.displayW,
959
- placementRows: fit.displayH,
1302
+ placementColumns: rect.w,
1303
+ placementRows: rect.h,
1304
+ transmissionColumns: fitW,
1305
+ transmissionRows: fitH,
1306
+ cropTransmission: false,
960
1307
  rect,
961
- fullRect: rect,
1308
+ fullRect: fullRect$1,
962
1309
  zIndex: -1,
963
1310
  fallback: source.value
964
1311
  });
@@ -982,13 +1329,14 @@ const TMermaidImage = defineComponent({
982
1329
  id,
983
1330
  x: rect.x,
984
1331
  y: rect.y,
985
- w: fit.displayW,
986
- h: fit.displayH,
1332
+ w: rect.w,
1333
+ h: rect.h,
987
1334
  protocol,
988
1335
  sequence: built.sequence,
989
1336
  resizeSequence: built.resizeSequence,
990
1337
  clearSequence: built.clearSequence,
991
- fallbackText: source.value
1338
+ fallbackText: source.value,
1339
+ placementMoveWithoutClear: protocol === "kitty"
992
1340
  });
993
1341
  if (accepted) activeGraphicId = id;
994
1342
  else queueClearGraphic();
@@ -1023,7 +1371,7 @@ const TMermaidImage = defineComponent({
1023
1371
  };
1024
1372
  });
1025
1373
  const contentHitRect = computed(() => {
1026
- if (!visible.value || !props.copyOnClick) return {
1374
+ if (!visible.value) return {
1027
1375
  x: 0,
1028
1376
  y: 0,
1029
1377
  w: 0,
@@ -1095,6 +1443,7 @@ const TMermaidImage = defineComponent({
1095
1443
  function onCopyClick(event) {
1096
1444
  event.preventDefault();
1097
1445
  event.stopPropagation();
1446
+ if (Date.now() - lastDragAt < DRAG_CLICK_SUPPRESS_MS) return;
1098
1447
  copySource();
1099
1448
  }
1100
1449
  function onCopyKeydown(event) {
@@ -1103,6 +1452,78 @@ const TMermaidImage = defineComponent({
1103
1452
  event.stopPropagation();
1104
1453
  copySource();
1105
1454
  }
1455
+ const zoomEnabled = computed(() => props.zoomOnWheel !== false && imageCells.value != null && displayFit.value != null && graphicsProtocol.value === "kitty" && contentHitRect.value.w > 0 && contentHitRect.value.h > 0);
1456
+ 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);
1457
+ function onWheel(event) {
1458
+ if (!zoomEnabled.value) return;
1459
+ if (!hasZoomModifier(event)) return;
1460
+ event.preventDefault();
1461
+ event.stopPropagation();
1462
+ const deltaY = event.deltaY ?? 0;
1463
+ if (!Number.isFinite(deltaY) || deltaY === 0) return;
1464
+ const inner = innerRectFrom(fullRect.value);
1465
+ const fit = displayFit.value;
1466
+ if (!fit || inner.w <= 0 || inner.h <= 0) return;
1467
+ const currentZoom = clampZoom(zoomScale.value);
1468
+ const fitW = fit.displayW;
1469
+ const fitH = fit.displayH;
1470
+ const fitOriginX = inner.x + (inner.w > fitW ? Math.floor((inner.w - fitW) / 2) : 0);
1471
+ const fullX = fitOriginX + panX.value;
1472
+ const fullY = inner.y + panY.value;
1473
+ const mouseX = event.cellX;
1474
+ const mouseY = event.cellY;
1475
+ const fx = currentZoom > 0 ? (mouseX - fullX) / (fitW * currentZoom) : .5;
1476
+ const fy = currentZoom > 0 ? (mouseY - fullY) / (fitH * currentZoom) : .5;
1477
+ const nextZoom = clampZoom(currentZoom * wheelZoomFactor(deltaY, event.deltaMode));
1478
+ if (nextZoom === currentZoom) return;
1479
+ const nextW = fitW * nextZoom;
1480
+ const nextH = fitH * nextZoom;
1481
+ let nextX = mouseX - fx * nextW;
1482
+ let nextY = mouseY - fy * nextH;
1483
+ if (nextW <= inner.w) nextX = inner.x + (inner.w - nextW) / 2;
1484
+ else nextX = Math.min(inner.x, Math.max(inner.x + inner.w - nextW, nextX));
1485
+ if (nextH <= inner.h) nextY = inner.y + (inner.h - nextH) / 2;
1486
+ else nextY = Math.min(inner.y, Math.max(inner.y + inner.h - nextH, nextY));
1487
+ zoomScale.value = nextZoom;
1488
+ panX.value = nextX - fitOriginX;
1489
+ panY.value = nextY - inner.y;
1490
+ clampPan();
1491
+ bump();
1492
+ }
1493
+ function onPointerDown(event) {
1494
+ if (!dragEnabled.value) return;
1495
+ event.preventDefault();
1496
+ event.stopPropagation();
1497
+ dragState = {
1498
+ startX: event.cellX,
1499
+ startY: event.cellY,
1500
+ panStartX: panX.value,
1501
+ panStartY: panY.value
1502
+ };
1503
+ dragMoved = false;
1504
+ }
1505
+ function onPointerMove(event) {
1506
+ const drag = dragState;
1507
+ if (!drag) return;
1508
+ event.preventDefault();
1509
+ event.stopPropagation();
1510
+ const dx = event.cellX - drag.startX;
1511
+ const dy = event.cellY - drag.startY;
1512
+ if (!dragMoved && Math.abs(dx) + Math.abs(dy) >= 1) dragMoved = true;
1513
+ if (!dragMoved) return;
1514
+ panX.value = drag.panStartX + dx;
1515
+ panY.value = drag.panStartY + dy;
1516
+ clampPan();
1517
+ bump();
1518
+ }
1519
+ function onPointerUp(event) {
1520
+ const drag = dragState;
1521
+ if (!drag) return;
1522
+ event.preventDefault();
1523
+ event.stopPropagation();
1524
+ dragState = null;
1525
+ if (dragMoved) lastDragAt = Date.now();
1526
+ }
1106
1527
  const copyNodeActive = computed(() => visible.value && props.copyButton && copyHitRect.value.w > 0 && copyHitRect.value.h > 0);
1107
1528
  useTerminalNode(() => ({
1108
1529
  rect: copyHitRect.value,
@@ -1115,14 +1536,20 @@ const TMermaidImage = defineComponent({
1115
1536
  keydown: onCopyKeydown
1116
1537
  } : {}
1117
1538
  }));
1118
- const contentNodeActive = computed(() => visible.value && props.copyOnClick && contentHitRect.value.w > 0 && contentHitRect.value.h > 0);
1539
+ const contentNodeActive = computed(() => visible.value && (props.copyOnClick || zoomEnabled.value || dragEnabled.value) && contentHitRect.value.w > 0 && contentHitRect.value.h > 0);
1119
1540
  useTerminalNode(() => ({
1120
1541
  rect: contentHitRect.value,
1121
1542
  zIndex: (parentEventZ.value ?? 0) + props.zIndex,
1122
1543
  visible: contentNodeActive.value,
1123
1544
  focusable: false,
1124
1545
  selectable: false,
1125
- handlers: contentNodeActive.value ? { click: onCopyClick } : {}
1546
+ handlers: contentNodeActive.value ? {
1547
+ click: props.copyOnClick ? onCopyClick : void 0,
1548
+ wheel: zoomEnabled.value ? onWheel : void 0,
1549
+ pointerdown: dragEnabled.value ? onPointerDown : void 0,
1550
+ pointermove: dragEnabled.value ? onPointerMove : void 0,
1551
+ pointerup: dragEnabled.value ? onPointerUp : void 0
1552
+ } : {}
1126
1553
  }));
1127
1554
  useRenderNode(() => ({
1128
1555
  zIndex: props.zIndex,
@@ -1138,6 +1565,11 @@ const TMermaidImage = defineComponent({
1138
1565
  fullRect.value,
1139
1566
  imageCells.value,
1140
1567
  displayFit.value,
1568
+ zoomScale.value,
1569
+ panX.value,
1570
+ panY.value,
1571
+ textSnapshot.value,
1572
+ displayLines.value,
1141
1573
  sourceLines.value,
1142
1574
  currentStyle.value,
1143
1575
  props.clear,
@@ -1202,4 +1634,148 @@ const TMermaidImage = defineComponent({
1202
1634
  });
1203
1635
 
1204
1636
  //#endregion
1205
- export { TBeautifulMermaid, TBeautifulMermaidText, TMermaid, TMermaidImage, TMermaidText$1 as TMermaidText, beautifulMermaidRenderer, clearMermaidImageCache, createBeautifulMermaidRenderer, getCachedMermaidImage, getMermaidImage, isMermaidImageRendererReady, loadMermaidImageRenderer, normalizeMermaidImageOptions, resolveMermaidImageColor, setMermaidImageRasterizer, subscribeMermaidImage };
1637
+ //#region src/vue/mermaid/beautiful-mermaid.ts
1638
+ 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.";
1639
+ const MISSING_BEAUTIFUL_MERMAID_ERROR_CODE = "VUE_TUI_MISSING_BEAUTIFUL_MERMAID";
1640
+ const INVALID_BEAUTIFUL_MERMAID_EXPORT_ERROR_CODE = "VUE_TUI_INVALID_BEAUTIFUL_MERMAID_EXPORT";
1641
+ let cachedBeautifulMermaid = null;
1642
+ function functionValue(value) {
1643
+ return typeof value === "function" ? value : null;
1644
+ }
1645
+ function functionProp(target, key) {
1646
+ if (!target || typeof target !== "object") return null;
1647
+ if (!(key in target)) return null;
1648
+ const value = target[key];
1649
+ return typeof value === "function" ? value : null;
1650
+ }
1651
+ function errorMessage(error) {
1652
+ return error instanceof Error ? error.message : String(error);
1653
+ }
1654
+ function codedError(message, code) {
1655
+ const error = new Error(message);
1656
+ error.code = code;
1657
+ return error;
1658
+ }
1659
+ function missingBeautifulMermaidError(error) {
1660
+ if (error && typeof error === "object" && errorCode(error) === MISSING_BEAUTIFUL_MERMAID_ERROR_CODE) return markMermaidRenderErrorFatal(error);
1661
+ const detail = errorMessage(error);
1662
+ return markMermaidRenderErrorFatal(codedError(`${BEAUTIFUL_MERMAID_INSTALL_HINT} (${detail})`, MISSING_BEAUTIFUL_MERMAID_ERROR_CODE));
1663
+ }
1664
+ function errorCode(error) {
1665
+ if (!error || typeof error !== "object") return "";
1666
+ const value = error.code;
1667
+ return typeof value === "string" ? value : "";
1668
+ }
1669
+ function errorCause(error) {
1670
+ if (!error || typeof error !== "object") return void 0;
1671
+ return error.cause;
1672
+ }
1673
+ function isMissingBeautifulMermaid(error) {
1674
+ return isMissingBeautifulMermaidWithSeen(error, new WeakSet());
1675
+ }
1676
+ function isMissingBeautifulMermaidWithSeen(error, seen) {
1677
+ if (error && typeof error === "object") {
1678
+ if (seen.has(error)) return false;
1679
+ seen.add(error);
1680
+ }
1681
+ const message = errorMessage(error);
1682
+ const code = errorCode(error);
1683
+ if (code === MISSING_BEAUTIFUL_MERMAID_ERROR_CODE) return true;
1684
+ 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);
1685
+ if (mentionsBeautifulMermaid && (code === "" || code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND" || code === "UNRESOLVED_IMPORT")) return true;
1686
+ const cause = errorCause(error);
1687
+ return cause !== void 0 && isMissingBeautifulMermaidWithSeen(cause, seen);
1688
+ }
1689
+ function resolveBeautifulMermaidRenderer(mod) {
1690
+ const renderer = functionProp(mod, "renderMermaidASCII") ?? functionProp(mod, "renderMermaidAscii") ?? functionValue(mod.default) ?? functionProp(mod.default, "renderMermaidASCII") ?? functionProp(mod.default, "renderMermaidAscii");
1691
+ if (!renderer) throw markMermaidRenderErrorFatal(codedError("beautiful-mermaid is installed but does not export renderMermaidASCII.", INVALID_BEAUTIFUL_MERMAID_EXPORT_ERROR_CODE));
1692
+ return renderer;
1693
+ }
1694
+ function hasErrorCode(error, code, seen = new WeakSet()) {
1695
+ if (!error || typeof error !== "object") return false;
1696
+ if (seen.has(error)) return false;
1697
+ seen.add(error);
1698
+ if (errorCode(error) === code) return true;
1699
+ const cause = errorCause(error);
1700
+ return cause !== void 0 && hasErrorCode(cause, code, seen);
1701
+ }
1702
+ function isBeautifulMermaidExportError(error) {
1703
+ return hasErrorCode(error, INVALID_BEAUTIFUL_MERMAID_EXPORT_ERROR_CODE);
1704
+ }
1705
+ const isTransientBeautifulMermaidRenderError = (error) => {
1706
+ if (isMissingBeautifulMermaid(error)) return false;
1707
+ if (isBeautifulMermaidExportError(error)) return false;
1708
+ return true;
1709
+ };
1710
+ async function loadBeautifulMermaid() {
1711
+ if (!cachedBeautifulMermaid) cachedBeautifulMermaid = import("beautiful-mermaid").then((mod) => mod).catch((error) => {
1712
+ cachedBeautifulMermaid = null;
1713
+ if (isMissingBeautifulMermaid(error)) throw missingBeautifulMermaidError(error);
1714
+ throw markMermaidRenderErrorFatal(error);
1715
+ });
1716
+ return cachedBeautifulMermaid;
1717
+ }
1718
+ const beautifulMermaidRenderer = async (code, options) => {
1719
+ try {
1720
+ const mod = await loadBeautifulMermaid();
1721
+ const renderer = resolveBeautifulMermaidRenderer(mod);
1722
+ return await renderer(code, options);
1723
+ } catch (error) {
1724
+ if (isMissingBeautifulMermaid(error)) throw missingBeautifulMermaidError(error);
1725
+ throw error;
1726
+ }
1727
+ };
1728
+ function createBeautifulMermaidRenderer() {
1729
+ return beautifulMermaidRenderer;
1730
+ }
1731
+ const TMermaidText$1 = defineComponent({
1732
+ name: "TMermaidText",
1733
+ inheritAttrs: false,
1734
+ props: tMermaidTextProps,
1735
+ emits: { copy: (_payload) => true },
1736
+ setup(props, { attrs, emit, slots }) {
1737
+ return () => {
1738
+ const renderer = props.renderer ?? beautifulMermaidRenderer;
1739
+ const shouldRenderSource = props.shouldRenderSource ?? isSimpleMermaidFlowchartSource;
1740
+ return h(TMermaidText, {
1741
+ ...attrs,
1742
+ ...props,
1743
+ renderer,
1744
+ shouldRenderSource,
1745
+ isTransientError: props.isTransientError ?? isTransientBeautifulMermaidRenderError,
1746
+ onCopy: (payload) => emit("copy", payload)
1747
+ }, slots);
1748
+ };
1749
+ }
1750
+ });
1751
+ /**
1752
+ * Adaptive default mermaid component: renders the diagram as a terminal image
1753
+ * (Kitty/iTerm2 graphics) when the terminal supports it, otherwise falls back
1754
+ * to the beautiful-mermaid ASCII renderer, otherwise shows the raw source.
1755
+ * While any pipeline is pending the raw source stays visible (source-first);
1756
+ * streaming sources are only rendered once `final` is true.
1757
+ */
1758
+ const TMermaid = defineComponent({
1759
+ name: "TMermaid",
1760
+ inheritAttrs: false,
1761
+ props: tMermaidImageProps,
1762
+ emits: { copy: (_payload) => true },
1763
+ setup(props, { attrs, emit, slots }) {
1764
+ return () => {
1765
+ const { renderer: _legacyTextRenderer,...rest } = props;
1766
+ const textRenderer = props.textRenderer ?? props.renderer ?? beautifulMermaidRenderer;
1767
+ return h(TMermaidImage, {
1768
+ ...attrs,
1769
+ ...rest,
1770
+ textRenderer,
1771
+ shouldRenderSource: props.shouldRenderSource ?? isSimpleMermaidFlowchartSource,
1772
+ onCopy: (payload) => emit("copy", payload)
1773
+ }, slots);
1774
+ };
1775
+ }
1776
+ });
1777
+ const TBeautifulMermaidText = TMermaidText$1;
1778
+ const TBeautifulMermaid = TMermaid;
1779
+
1780
+ //#endregion
1781
+ export { TBeautifulMermaid, TBeautifulMermaidText, TMermaid, TMermaidImage, TMermaidText$1 as TMermaidText, beautifulMermaidRenderer, clearMermaidImageCache, createBeautifulMermaidRenderer, getCachedMermaidImage, getMermaidImage, isMermaidImageRendererReady, loadMermaidImageRenderer, normalizeMermaidImageOptions, resolveMermaidImageColor, resolveMermaidSvgForResvg, setMermaidImageRasterizer, subscribeMermaidImage };