@simon_he/vue-tui 1.0.1 → 1.1.1
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 +99 -46
- package/dist/TMermaidText-CXm_L969.js +885 -0
- package/dist/{TSelect-HzILuti9.js → TSelect-CiyMvwl-.js} +60 -81
- package/dist/{TTree-ChOot2Bh.js → TTree-DKGiuIcM.js} +140 -103
- package/dist/agent/mermaid.cjs +2 -0
- package/dist/agent/mermaid.d.cts +2 -0
- package/dist/agent/mermaid.d.ts +2 -0
- package/dist/agent/mermaid.js +1 -0
- package/dist/agent.cjs +31184 -24907
- package/dist/agent.d.cts +11 -1
- package/dist/agent.d.ts +11 -1
- package/dist/agent.js +1700 -13
- package/dist/block-source-5CQp4tF8.js +3230 -0
- package/dist/cli/stdin-buffer.d.ts +1 -1
- package/dist/cli.cjs +5363 -6178
- package/dist/cli.d.cts +5 -1
- package/dist/cli.d.ts +5 -1
- package/dist/cli.js +3569 -302
- package/dist/core/buffer/buffer.d.ts +2 -2
- package/dist/core/buffer/eaw-ranges-unicode-17.d.ts +14 -0
- package/dist/core/perf/instrumentation-noop.d.ts +48 -0
- package/dist/core/perf/instrumentation.d.ts +136 -0
- package/dist/core/terminal/create-terminal.d.ts +1 -0
- package/dist/core/types.d.ts +0 -4
- package/dist/core.cjs +851 -1462
- package/dist/core.js +6 -6
- package/dist/{create-terminal-C2oiWFJU.js → create-terminal-DXuZ2fci.js} +54 -19
- package/dist/debug-logger-ChY_PXVk.js +74 -0
- package/dist/{dom-renderer-BqGijQID.js → dom-renderer-DNzmg2As.js} +2 -2
- package/dist/experimental.cjs +4115 -5928
- package/dist/experimental.d.cts +2 -0
- package/dist/experimental.d.ts +2 -0
- package/dist/experimental.js +1490 -9
- package/dist/frame-perf-BBhE-iuS.js +111 -0
- package/dist/{sgr-BmzxEjAO.js → hyperlink-B35-O-cW.js} +100 -1
- package/dist/index.cjs +5025 -10332
- package/dist/index.js +16 -14
- package/dist/{linkify-D_gzn9Xq.js → linkify-CuuquMQT.js} +32 -3
- package/dist/markdown.cjs +21295 -12691
- package/dist/markdown.d.cts +1 -1
- package/dist/markdown.d.ts +1 -1
- package/dist/markdown.js +211 -51
- package/dist/mermaid-CWto52Wk.js +123 -0
- package/dist/mermaid.cjs +1707 -0
- package/dist/mermaid.d.cts +3 -0
- package/dist/mermaid.d.ts +3 -0
- package/dist/mermaid.js +6 -0
- package/dist/observability/frame-perf-store.d.ts +2 -1
- package/dist/observability/frame-perf.d.ts +33 -0
- package/dist/observability.cjs +106 -107
- package/dist/observability.d.cts +2 -2
- package/dist/observability.d.ts +2 -2
- package/dist/observability.js +3 -3
- package/dist/{path-suggest-BNqcuE3-.js → path-suggest-fPzRsgPv.js} +1 -1
- package/dist/{wheel-scroll-vBRaQ7p-.js → rect-Ng-3eKzF.js} +253 -117
- package/dist/renderer/cli/index.d.ts +1 -1
- package/dist/renderer/cli/stdout-metrics.d.ts +10 -0
- package/dist/renderer/cli/stdout-renderer.d.ts +53 -7
- package/dist/renderer/terminal-graphic-render-queue.d.ts +50 -0
- package/dist/renderer/terminal-graphics-trace.d.ts +53 -0
- package/dist/renderer/terminal-graphics.d.ts +146 -0
- package/dist/renderer-dom.cjs +364 -903
- package/dist/renderer-dom.js +4 -4
- package/dist/runtime/index.d.ts +2 -0
- package/dist/{runtime-rfcZCyJq.js → runtime-D77C03RM.js} +28 -15
- package/dist/runtime.cjs +427 -889
- package/dist/runtime.js +2 -2
- package/dist/{terminal-selection-DBinO6p5.js → terminal-selection-CQYr--26.js} +4 -1
- package/dist/{tlog-theme-CvLFNMwk.js → tlog-theme-CopRt9SC.js} +37 -6
- package/dist/{trace-CjzYeIxN.js → trace-CYHDKM1m.js} +5 -0
- package/dist/vue/agent/create-png-terminal-graphic-renderer.d.ts +19 -0
- package/dist/vue/components/TAgentTerminalGraphic.d.ts +481 -0
- package/dist/vue/components/TCharts.d.ts +1121 -0
- package/dist/vue/components/TFeedback.d.ts +1 -1
- package/dist/vue/components/TFlex.d.ts +389 -0
- package/dist/vue/components/TForm.d.ts +4 -4
- package/dist/vue/components/TLogSearchBar.d.ts +3 -3
- package/dist/vue/components/TLogSearchPager.d.ts +2 -2
- package/dist/vue/components/TMarkdownText.d.ts +162 -4
- package/dist/vue/components/TMermaidText.d.ts +900 -0
- package/dist/vue/components/TVirtualList.d.ts +9 -0
- package/dist/vue/components/TVirtualMarkdown.d.ts +170 -9
- package/dist/vue/components/TVirtualRows.d.ts +9 -0
- package/dist/vue/components/TerminalProvider.d.ts +2 -0
- package/dist/vue/context.d.ts +24 -0
- package/dist/vue/index.d.ts +5 -0
- package/dist/vue/layout/flex.d.ts +67 -0
- package/dist/vue/markdown/ast.d.ts +5 -2
- package/dist/vue/markdown/document.d.ts +5 -0
- package/dist/vue/markdown/image-actions.d.ts +14 -0
- package/dist/vue/markdown/image.d.ts +11 -0
- package/dist/vue/markdown/link-actions.d.ts +14 -0
- package/dist/vue/markdown/math-actions.d.ts +14 -0
- package/dist/vue/markdown/math.d.ts +6 -0
- package/dist/vue/markdown/render.d.ts +28 -0
- package/dist/vue/markdown/types.d.ts +78 -0
- package/dist/vue/mermaid/beautiful-mermaid.d.ts +1436 -0
- package/dist/vue/render/render-manager.d.ts +7 -0
- package/dist/vue.cjs +7215 -12430
- package/dist/vue.js +706 -39
- package/dist/wheel-scroll-CQ22Hcyj.js +106 -0
- package/dist/{hyperlink-Bn02UXJb.js → width-GqllnV8C.js} +166 -111
- package/package.json +107 -18
- package/dist/block-source-CKUOnkgT.js +0 -1663
- package/dist/frame-perf-C6Xfg85O.js +0 -24
- /package/dist/{env-Cl8qmNFN.js → env-DNCzvX8b.js} +0 -0
- /package/dist/{path-mnSgEpCm.js → path-BI9W15Zt.js} +0 -0
|
@@ -0,0 +1,3230 @@
|
|
|
1
|
+
import { getRootTerminal } from "./create-terminal-DXuZ2fci.js";
|
|
2
|
+
import { hasEncodedControl, sanitizeDomHref } from "./hyperlink-B35-O-cW.js";
|
|
3
|
+
import { EventZIndexContextKey, currentTextWidthProvider, forEachTextCellSegment, intersectRect, normalizeCellRect, repeatChar, sanitizeInlineText, sanitizeTextBlock, sliceByCellsRange, spaces, textCellWidth$1 as textCellWidth, translateRect, useLayout, useRenderNode, useTerminal, useTerminalNode, useVisibility, withTextWidthProvider } from "./rect-Ng-3eKzF.js";
|
|
4
|
+
import { terminalSelectionRowSpans, terminalSelectionVisibleRowSpans } from "./terminal-selection-CQYr--26.js";
|
|
5
|
+
import { createDebugLogger, isDebugEnabled } from "./debug-logger-ChY_PXVk.js";
|
|
6
|
+
import { applyWheelScroll, createWheelScrollState, resetWheelScrollState } from "./wheel-scroll-CQ22Hcyj.js";
|
|
7
|
+
import { computed, defineComponent, getCurrentInstance, h, inject, markRaw, onBeforeUnmount, ref, shallowRef, watch, watchEffect } from "vue";
|
|
8
|
+
import katex from "katex";
|
|
9
|
+
import { getMarkdown, parseMarkdownToStructure } from "stream-markdown-parser";
|
|
10
|
+
|
|
11
|
+
//#region src/renderer/terminal-graphics.ts
|
|
12
|
+
const TERMINAL_GRAPHICS_PROTOCOL_PRIORITY = [
|
|
13
|
+
"kitty",
|
|
14
|
+
"iterm2",
|
|
15
|
+
"sixel"
|
|
16
|
+
];
|
|
17
|
+
const ESC = "\x1B";
|
|
18
|
+
const BEL = "\x07";
|
|
19
|
+
const ST = `${ESC}\\`;
|
|
20
|
+
const ESC_RE = "\\u001B";
|
|
21
|
+
const BEL_RE = "\\u0007";
|
|
22
|
+
const CSI_RE = new RegExp(`${ESC_RE}\\[[0-?]*[ -/]*[@-~]`, "g");
|
|
23
|
+
const OSC_RE = new RegExp(`${ESC_RE}\\][\\s\\S]*?(?:${BEL_RE}|${ESC_RE}\\\\)`, "g");
|
|
24
|
+
const DCS_RE = new RegExp(`${ESC_RE}P[\\s\\S]*?${ESC_RE}\\\\`, "g");
|
|
25
|
+
const APC_RE = new RegExp(`${ESC_RE}_[\\s\\S]*?${ESC_RE}\\\\`, "g");
|
|
26
|
+
const C1_CSI_RE = new RegExp("\\u009B[0-?]*[ -/]*[@-~]", "g");
|
|
27
|
+
const C1_OSC_RE = new RegExp(`\\u009D[\\s\\S]*?(?:${BEL_RE}|\\u009C)`, "g");
|
|
28
|
+
const C1_DCS_RE = new RegExp("\\u0090[\\s\\S]*?\\u009C", "g");
|
|
29
|
+
const C1_APC_RE = new RegExp("\\u009F[\\s\\S]*?\\u009C", "g");
|
|
30
|
+
const MAX_TERMINAL_GRAPHICS_SEQUENCE_CHARS = 16 * 1024 * 1024;
|
|
31
|
+
const MAX_TERMINAL_GRAPHICS_FALLBACK_CHARS = 16384;
|
|
32
|
+
const MAX_TERMINAL_GRAPHIC_CELLS = 1e4;
|
|
33
|
+
const TERMINAL_GRAPHICS_REGISTRY = Symbol.for("@simon_he/vue-tui:v1:terminal-graphics");
|
|
34
|
+
const terminalGraphicsGlobal = globalThis;
|
|
35
|
+
const terminalGraphicsRegistry = terminalGraphicsGlobal[TERMINAL_GRAPHICS_REGISTRY] ?? {
|
|
36
|
+
outputs: new WeakMap(),
|
|
37
|
+
outputVersions: new WeakMap(),
|
|
38
|
+
outputListeners: new WeakMap()
|
|
39
|
+
};
|
|
40
|
+
terminalGraphicsGlobal[TERMINAL_GRAPHICS_REGISTRY] = terminalGraphicsRegistry;
|
|
41
|
+
const terminalGraphicsOutputs = terminalGraphicsRegistry.outputs;
|
|
42
|
+
const terminalGraphicsOutputVersions = terminalGraphicsRegistry.outputVersions;
|
|
43
|
+
const terminalGraphicsOutputListeners = terminalGraphicsRegistry.outputListeners;
|
|
44
|
+
function getTerminalGraphicsOutput(terminal) {
|
|
45
|
+
return terminalGraphicsOutputs.get(getRootTerminal(terminal)) ?? null;
|
|
46
|
+
}
|
|
47
|
+
function getTerminalGraphicsOutputVersion(terminal) {
|
|
48
|
+
return terminalGraphicsOutputVersions.get(getRootTerminal(terminal)) ?? 0;
|
|
49
|
+
}
|
|
50
|
+
function subscribeTerminalGraphicsOutput(terminal, listener) {
|
|
51
|
+
const rootTerminal = getRootTerminal(terminal);
|
|
52
|
+
let listeners = terminalGraphicsOutputListeners.get(rootTerminal);
|
|
53
|
+
if (!listeners) {
|
|
54
|
+
listeners = new Set();
|
|
55
|
+
terminalGraphicsOutputListeners.set(rootTerminal, listeners);
|
|
56
|
+
}
|
|
57
|
+
listeners.add(listener);
|
|
58
|
+
return () => {
|
|
59
|
+
listeners.delete(listener);
|
|
60
|
+
if (listeners.size === 0) terminalGraphicsOutputListeners.delete(rootTerminal);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function envString(env, key) {
|
|
64
|
+
const value = env[key];
|
|
65
|
+
return value == null ? void 0 : String(value);
|
|
66
|
+
}
|
|
67
|
+
function truthy(value) {
|
|
68
|
+
if (value == null) return false;
|
|
69
|
+
return /^(1|true|yes|on)$/i.test(String(value));
|
|
70
|
+
}
|
|
71
|
+
function readStdoutIsTTY() {
|
|
72
|
+
const maybeProcess = globalThis.process;
|
|
73
|
+
return Boolean(maybeProcess?.stdout?.isTTY);
|
|
74
|
+
}
|
|
75
|
+
function readProcessEnv() {
|
|
76
|
+
const maybeProcess = globalThis.process;
|
|
77
|
+
return maybeProcess?.env ?? {};
|
|
78
|
+
}
|
|
79
|
+
function normalizeMode(value) {
|
|
80
|
+
const raw = String(value ?? "auto").trim().toLowerCase();
|
|
81
|
+
if (raw === "auto" || raw === "off" || raw === "none" || raw === "kitty" || raw === "iterm2" || raw === "sixel" || raw === "unicode") return raw === "off" ? "none" : raw;
|
|
82
|
+
return "auto";
|
|
83
|
+
}
|
|
84
|
+
function isTerminalGraphicsProtocol(value) {
|
|
85
|
+
return value === "kitty" || value === "iterm2" || value === "sixel";
|
|
86
|
+
}
|
|
87
|
+
function uniqueProtocols(protocols) {
|
|
88
|
+
return [...new Set(protocols)];
|
|
89
|
+
}
|
|
90
|
+
function pickBestTerminalGraphicsProtocol(protocols) {
|
|
91
|
+
const available = new Set(protocols);
|
|
92
|
+
for (const protocol of TERMINAL_GRAPHICS_PROTOCOL_PRIORITY) if (available.has(protocol)) return protocol;
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function detectCandidates(env) {
|
|
96
|
+
const candidates = [];
|
|
97
|
+
const term = String(envString(env, "TERM") ?? "").toLowerCase();
|
|
98
|
+
const termProgram = String(envString(env, "TERM_PROGRAM") ?? "").toLowerCase();
|
|
99
|
+
const termProgramVersion = String(envString(env, "TERM_PROGRAM_VERSION") ?? "").toLowerCase();
|
|
100
|
+
if (envString(env, "KITTY_WINDOW_ID") || envString(env, "GHOSTTY_RESOURCES_DIR") || /(?:^|-)kitty(?:-|$)/i.test(term) || term.includes("ghostty") || termProgram.includes("ghostty")) candidates.push("kitty");
|
|
101
|
+
if (termProgram.includes("iterm") || termProgram.includes("wezterm") || envString(env, "WEZTERM_PANE") || envString(env, "WEZTERM_EXECUTABLE")) candidates.push("iterm2");
|
|
102
|
+
if (truthy(envString(env, "VUE_TUI_SIXEL")) || truthy(envString(env, "VUE_TUI_GRAPHICS_SIXEL")) || String(envString(env, "TERMINAL_GRAPHICS") ?? "").toLowerCase().includes("sixel") || /\bsixel\b/i.test(`${term} ${termProgram} ${termProgramVersion}`)) candidates.push("sixel");
|
|
103
|
+
return uniqueProtocols(candidates);
|
|
104
|
+
}
|
|
105
|
+
function capabilitiesFor(protocol, input) {
|
|
106
|
+
const supported = protocol === "kitty" || protocol === "iterm2" || protocol === "sixel";
|
|
107
|
+
const candidates = uniqueProtocols([...input.candidates ?? (supported ? [protocol] : [])]);
|
|
108
|
+
return {
|
|
109
|
+
supported,
|
|
110
|
+
kitty: protocol === "kitty" || candidates.includes("kitty"),
|
|
111
|
+
iterm2: protocol === "iterm2" || candidates.includes("iterm2"),
|
|
112
|
+
sixel: protocol === "sixel" || candidates.includes("sixel"),
|
|
113
|
+
preferredProtocol: supported ? protocol : null,
|
|
114
|
+
protocol,
|
|
115
|
+
candidates,
|
|
116
|
+
stdoutIsTTY: input.stdoutIsTTY,
|
|
117
|
+
insideTmux: input.insideTmux,
|
|
118
|
+
insideScreen: input.insideScreen,
|
|
119
|
+
insideZellij: input.insideZellij,
|
|
120
|
+
multiplexer: input.multiplexer,
|
|
121
|
+
passthrough: input.passthrough,
|
|
122
|
+
forced: input.forced,
|
|
123
|
+
reason: input.reason
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function detectTerminalGraphicsCapabilities(options = {}) {
|
|
127
|
+
const env = options.env ?? readProcessEnv();
|
|
128
|
+
const stdoutIsTTY = options.stdoutIsTTY ?? options.isTTY ?? readStdoutIsTTY();
|
|
129
|
+
const term = String(envString(env, "TERM") ?? "").toLowerCase();
|
|
130
|
+
const insideTmux = Boolean(envString(env, "TMUX")) || term.includes("tmux");
|
|
131
|
+
const insideScreen = Boolean(envString(env, "STY")) || term.startsWith("screen");
|
|
132
|
+
const insideZellij = Boolean(envString(env, "ZELLIJ") || envString(env, "ZELLIJ_SESSION_NAME")) || term.includes("zellij");
|
|
133
|
+
const multiplexer = insideTmux ? "tmux" : insideScreen ? "screen" : insideZellij ? "zellij" : null;
|
|
134
|
+
const passthroughRequested = options.passthrough ?? (truthy(envString(env, "VUE_TUI_TERMINAL_GRAPHICS_PASSTHROUGH")) || truthy(envString(env, "VUE_TUI_GRAPHICS_TMUX_PASSTHROUGH")));
|
|
135
|
+
const passthroughSupported = !multiplexer || multiplexer === "tmux";
|
|
136
|
+
const passthrough = passthroughRequested && passthroughSupported;
|
|
137
|
+
const protocolOptionProvided = options.protocol != null;
|
|
138
|
+
const mode = normalizeMode(options.protocol ?? envString(env, "VUE_TUI_TERMINAL_GRAPHICS") ?? envString(env, "VUE_TUI_GRAPHICS_PROTOCOL"));
|
|
139
|
+
const forceFromOption = options.force === true;
|
|
140
|
+
const force = options.force ?? truthy(envString(env, "VUE_TUI_GRAPHICS_FORCE"));
|
|
141
|
+
const base = {
|
|
142
|
+
stdoutIsTTY,
|
|
143
|
+
insideTmux,
|
|
144
|
+
insideScreen,
|
|
145
|
+
insideZellij,
|
|
146
|
+
multiplexer,
|
|
147
|
+
passthrough,
|
|
148
|
+
forced: force
|
|
149
|
+
};
|
|
150
|
+
if (mode === "none") return capabilitiesFor("none", {
|
|
151
|
+
...base,
|
|
152
|
+
reason: protocolOptionProvided ? "disabled-by-option" : "disabled-by-env"
|
|
153
|
+
});
|
|
154
|
+
if (!stdoutIsTTY && !force) return capabilitiesFor("unicode", {
|
|
155
|
+
...base,
|
|
156
|
+
reason: "stdout-is-not-tty"
|
|
157
|
+
});
|
|
158
|
+
if (truthy(envString(env, "CI")) && !force) return capabilitiesFor("unicode", {
|
|
159
|
+
...base,
|
|
160
|
+
reason: "ci"
|
|
161
|
+
});
|
|
162
|
+
if (multiplexer && passthroughRequested && !passthroughSupported && !force) return capabilitiesFor("unicode", {
|
|
163
|
+
...base,
|
|
164
|
+
reason: `${multiplexer}-passthrough-not-implemented`
|
|
165
|
+
});
|
|
166
|
+
if (multiplexer && !passthrough && !force) return capabilitiesFor("unicode", {
|
|
167
|
+
...base,
|
|
168
|
+
reason: `${multiplexer}-without-passthrough`
|
|
169
|
+
});
|
|
170
|
+
if (mode === "kitty" || mode === "iterm2" || mode === "sixel") return capabilitiesFor(mode, {
|
|
171
|
+
...base,
|
|
172
|
+
candidates: [mode],
|
|
173
|
+
reason: force ? forceFromOption ? "forced-by-option" : "forced-by-env" : protocolOptionProvided ? "selected-by-option" : "selected-by-env"
|
|
174
|
+
});
|
|
175
|
+
if (mode === "unicode") return capabilitiesFor("unicode", {
|
|
176
|
+
...base,
|
|
177
|
+
reason: protocolOptionProvided ? "unicode-forced-by-option" : "unicode-forced-by-env"
|
|
178
|
+
});
|
|
179
|
+
const candidates = detectCandidates(env);
|
|
180
|
+
const preferredProtocol = pickBestTerminalGraphicsProtocol(candidates);
|
|
181
|
+
const protocol = preferredProtocol ?? "unicode";
|
|
182
|
+
return capabilitiesFor(protocol, {
|
|
183
|
+
...base,
|
|
184
|
+
candidates,
|
|
185
|
+
reason: protocol === "unicode" ? "no-supported-graphics-protocol-detected" : "auto-detected"
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function hashTerminalGraphicsString(input) {
|
|
189
|
+
let hash = 2166136261;
|
|
190
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
191
|
+
hash ^= input.charCodeAt(index);
|
|
192
|
+
hash = Math.imul(hash, 16777619);
|
|
193
|
+
}
|
|
194
|
+
return (hash >>> 0).toString(36);
|
|
195
|
+
}
|
|
196
|
+
function stableTerminalGraphicNumericId(input, options = {}) {
|
|
197
|
+
const min = Math.floor(options.min ?? 1);
|
|
198
|
+
const max = Math.floor(options.max ?? 2147483647);
|
|
199
|
+
const span = Math.max(1, max - min + 1);
|
|
200
|
+
let hash = 2166136261;
|
|
201
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
202
|
+
hash ^= input.charCodeAt(index);
|
|
203
|
+
hash = Math.imul(hash, 16777619);
|
|
204
|
+
}
|
|
205
|
+
return min + (hash >>> 0) % span;
|
|
206
|
+
}
|
|
207
|
+
function positiveGraphicInt(value, max) {
|
|
208
|
+
const n = Math.trunc(Number(value));
|
|
209
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
210
|
+
return Math.min(max, n);
|
|
211
|
+
}
|
|
212
|
+
function normalizeTerminalGraphicSize(width, height) {
|
|
213
|
+
const w = positiveGraphicInt(width, MAX_TERMINAL_GRAPHIC_CELLS);
|
|
214
|
+
const h$1 = positiveGraphicInt(height, MAX_TERMINAL_GRAPHIC_CELLS);
|
|
215
|
+
if (w == null || h$1 == null) return null;
|
|
216
|
+
if (w * h$1 > MAX_TERMINAL_GRAPHIC_CELLS) return null;
|
|
217
|
+
return {
|
|
218
|
+
width: w,
|
|
219
|
+
height: h$1
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function canDrawTerminalGraphicRect(rect, size) {
|
|
223
|
+
const x = Math.floor(rect.x);
|
|
224
|
+
const y = Math.floor(rect.y);
|
|
225
|
+
const graphicSize = normalizeTerminalGraphicSize(rect.w, rect.h);
|
|
226
|
+
const cols = Math.max(0, Math.floor(size.cols));
|
|
227
|
+
const rows = Math.max(0, Math.floor(size.rows));
|
|
228
|
+
return graphicSize != null && cols > 0 && rows > 0 && Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0 && x < cols && y < rows;
|
|
229
|
+
}
|
|
230
|
+
function withoutNestedEscapes(body) {
|
|
231
|
+
return !body.includes(ESC) && !body.includes(BEL);
|
|
232
|
+
}
|
|
233
|
+
function isBase64ish(value) {
|
|
234
|
+
return /^[A-Za-z0-9+/=]*$/.test(value);
|
|
235
|
+
}
|
|
236
|
+
function parseKittyControls(raw) {
|
|
237
|
+
const out = new Map();
|
|
238
|
+
if (!/^[A-Za-z0-9_,=+.\-:]*$/.test(raw)) return null;
|
|
239
|
+
for (const part of raw.split(",")) {
|
|
240
|
+
if (!part) continue;
|
|
241
|
+
const eq = part.indexOf("=");
|
|
242
|
+
if (eq <= 0) return null;
|
|
243
|
+
const key = part.slice(0, eq);
|
|
244
|
+
const value = part.slice(eq + 1);
|
|
245
|
+
if (!/^[A-Za-z]$/.test(key)) return null;
|
|
246
|
+
if (!/^[A-Za-z0-9+.\-:]*$/.test(value)) return null;
|
|
247
|
+
out.set(key, value);
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
function parseKittyPackets(sequence) {
|
|
252
|
+
const packets = [];
|
|
253
|
+
let index = 0;
|
|
254
|
+
while (index < sequence.length) {
|
|
255
|
+
if (!sequence.startsWith(`${ESC}_G`, index)) return null;
|
|
256
|
+
const end = sequence.indexOf(ST, index + 3);
|
|
257
|
+
if (end < 0) return null;
|
|
258
|
+
const body = sequence.slice(index + 3, end);
|
|
259
|
+
if (!withoutNestedEscapes(body)) return null;
|
|
260
|
+
const semicolon = body.indexOf(";");
|
|
261
|
+
const rawControls = semicolon >= 0 ? body.slice(0, semicolon) : body;
|
|
262
|
+
const payload = semicolon >= 0 ? body.slice(semicolon + 1) : "";
|
|
263
|
+
const controls = parseKittyControls(rawControls);
|
|
264
|
+
if (!controls) return null;
|
|
265
|
+
if (payload && !isBase64ish(payload)) return null;
|
|
266
|
+
packets.push({
|
|
267
|
+
controls,
|
|
268
|
+
payload
|
|
269
|
+
});
|
|
270
|
+
index = end + ST.length;
|
|
271
|
+
}
|
|
272
|
+
return packets.length ? packets : null;
|
|
273
|
+
}
|
|
274
|
+
const KITTY_DRAW_FIRST_CONTROL_KEYS = new Set([
|
|
275
|
+
"a",
|
|
276
|
+
"f",
|
|
277
|
+
"q",
|
|
278
|
+
"C",
|
|
279
|
+
"i",
|
|
280
|
+
"I",
|
|
281
|
+
"p",
|
|
282
|
+
"c",
|
|
283
|
+
"r",
|
|
284
|
+
"x",
|
|
285
|
+
"y",
|
|
286
|
+
"w",
|
|
287
|
+
"h",
|
|
288
|
+
"z",
|
|
289
|
+
"m",
|
|
290
|
+
"t"
|
|
291
|
+
]);
|
|
292
|
+
const KITTY_DRAW_CONTINUATION_CONTROL_KEYS = new Set([
|
|
293
|
+
"a",
|
|
294
|
+
"q",
|
|
295
|
+
"m"
|
|
296
|
+
]);
|
|
297
|
+
const KITTY_CLEAR_CONTROL_KEYS = new Set([
|
|
298
|
+
"a",
|
|
299
|
+
"d",
|
|
300
|
+
"i",
|
|
301
|
+
"I",
|
|
302
|
+
"p",
|
|
303
|
+
"q"
|
|
304
|
+
]);
|
|
305
|
+
function kittyControlsUseOnly(controls, allowed) {
|
|
306
|
+
for (const key of controls.keys()) if (!allowed.has(key)) return false;
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
const KITTY_UINT32_MAX = 4294967295;
|
|
310
|
+
const KITTY_INT32_MIN = -2147483648;
|
|
311
|
+
const KITTY_INT32_MAX = 2147483647;
|
|
312
|
+
function kittyControlIsUint32(controls, key, min = 0) {
|
|
313
|
+
const value = controls.get(key);
|
|
314
|
+
if (value == null) return true;
|
|
315
|
+
if (!/^\d+$/.test(value)) return false;
|
|
316
|
+
const n = Number(value);
|
|
317
|
+
return Number.isSafeInteger(n) && n >= min && n <= KITTY_UINT32_MAX;
|
|
318
|
+
}
|
|
319
|
+
function kittyControlIsInt32(controls, key) {
|
|
320
|
+
const value = controls.get(key);
|
|
321
|
+
if (value == null) return true;
|
|
322
|
+
if (!/^-?\d+$/.test(value)) return false;
|
|
323
|
+
const n = Number(value);
|
|
324
|
+
return Number.isSafeInteger(n) && n >= KITTY_INT32_MIN && n <= KITTY_INT32_MAX;
|
|
325
|
+
}
|
|
326
|
+
function kittyControlPositiveInt(controls, key, fallback) {
|
|
327
|
+
const value = controls.get(key);
|
|
328
|
+
if (value == null) return fallback;
|
|
329
|
+
if (!/^\d+$/.test(value)) return null;
|
|
330
|
+
const n = Number(value);
|
|
331
|
+
return Number.isSafeInteger(n) && n >= 1 ? n : null;
|
|
332
|
+
}
|
|
333
|
+
function kittyControlIsTerminalGraphicCellsBounded(controls) {
|
|
334
|
+
const columns = kittyControlPositiveInt(controls, "c", 1);
|
|
335
|
+
const rows = kittyControlPositiveInt(controls, "r", 1);
|
|
336
|
+
return columns != null && rows != null && columns * rows <= MAX_TERMINAL_GRAPHIC_CELLS;
|
|
337
|
+
}
|
|
338
|
+
function validateKittySharedSafeControls(controls) {
|
|
339
|
+
const q = controls.get("q");
|
|
340
|
+
if (q != null && !/^[0-2]$/.test(q)) return false;
|
|
341
|
+
const cursorMove = controls.get("C");
|
|
342
|
+
if (cursorMove != null && !/^[01]$/.test(cursorMove)) return false;
|
|
343
|
+
for (const key of [
|
|
344
|
+
"i",
|
|
345
|
+
"I",
|
|
346
|
+
"p"
|
|
347
|
+
]) if (!kittyControlIsUint32(controls, key)) return false;
|
|
348
|
+
for (const key of ["c", "r"]) if (!kittyControlIsUint32(controls, key, 1)) return false;
|
|
349
|
+
if (!kittyControlIsTerminalGraphicCellsBounded(controls)) return false;
|
|
350
|
+
for (const key of ["x", "y"]) if (!kittyControlIsUint32(controls, key)) return false;
|
|
351
|
+
for (const key of ["w", "h"]) if (!kittyControlIsUint32(controls, key, 1)) return false;
|
|
352
|
+
if (!kittyControlIsInt32(controls, "z")) return false;
|
|
353
|
+
const transmissionMedium = controls.get("t");
|
|
354
|
+
if (transmissionMedium != null && transmissionMedium !== "d") return false;
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
function validateKittySequence(sequence, op = "draw") {
|
|
358
|
+
const packets = parseKittyPackets(sequence);
|
|
359
|
+
if (!packets) return false;
|
|
360
|
+
if (op === "clear") {
|
|
361
|
+
if (packets.length !== 1) return false;
|
|
362
|
+
const packet = packets[0];
|
|
363
|
+
if (!kittyControlsUseOnly(packet.controls, KITTY_CLEAR_CONTROL_KEYS)) return false;
|
|
364
|
+
if (!validateKittySharedSafeControls(packet.controls)) return false;
|
|
365
|
+
if (packet.payload) return false;
|
|
366
|
+
if (packet.controls.get("a") !== "d") return false;
|
|
367
|
+
const d = packet.controls.get("d");
|
|
368
|
+
return d == null || /^[aAcCiI]$/.test(d);
|
|
369
|
+
}
|
|
370
|
+
const first = packets[0];
|
|
371
|
+
if (!kittyControlsUseOnly(first.controls, KITTY_DRAW_FIRST_CONTROL_KEYS)) return false;
|
|
372
|
+
if (!validateKittySharedSafeControls(first.controls)) return false;
|
|
373
|
+
if (first.controls.get("a") === "p") {
|
|
374
|
+
if (packets.length !== 1) return false;
|
|
375
|
+
if (first.payload) return false;
|
|
376
|
+
if (!first.controls.has("i") && !first.controls.has("I")) return false;
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
if (first.controls.get("a") !== "T") return false;
|
|
380
|
+
if (!first.payload) return false;
|
|
381
|
+
const f = first.controls.get("f");
|
|
382
|
+
if (f != null && f !== "24" && f !== "32" && f !== "100") return false;
|
|
383
|
+
const firstMore = first.controls.get("m");
|
|
384
|
+
if (firstMore != null && firstMore !== "0" && firstMore !== "1") return false;
|
|
385
|
+
if (packets.length > 1 && firstMore !== "1") return false;
|
|
386
|
+
for (let i = 1; i < packets.length; i++) {
|
|
387
|
+
const packet = packets[i];
|
|
388
|
+
if (!kittyControlsUseOnly(packet.controls, KITTY_DRAW_CONTINUATION_CONTROL_KEYS)) return false;
|
|
389
|
+
if (!validateKittySharedSafeControls(packet.controls)) return false;
|
|
390
|
+
const action = packet.controls.get("a");
|
|
391
|
+
if (action != null && action !== "T") return false;
|
|
392
|
+
const m = packet.controls.get("m");
|
|
393
|
+
if (m !== "0" && m !== "1") return false;
|
|
394
|
+
if (!packet.payload && !(i === packets.length - 1 && m === "0")) return false;
|
|
395
|
+
}
|
|
396
|
+
if (packets.length > 1 && packets[packets.length - 1].controls.get("m") === "1") return false;
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
function validateIterm2Dimension(value) {
|
|
400
|
+
return /^auto$/i.test(value) || /^(?:[1-9]\d{0,4})(?:px|%)?$/.test(value);
|
|
401
|
+
}
|
|
402
|
+
function validateIterm2Params(params) {
|
|
403
|
+
const seen = new Set();
|
|
404
|
+
let inline = false;
|
|
405
|
+
for (const part of params.split(";").filter(Boolean)) {
|
|
406
|
+
const eq = part.indexOf("=");
|
|
407
|
+
if (eq <= 0) return false;
|
|
408
|
+
const key = part.slice(0, eq);
|
|
409
|
+
const value = part.slice(eq + 1);
|
|
410
|
+
if (seen.has(key)) return false;
|
|
411
|
+
seen.add(key);
|
|
412
|
+
switch (key) {
|
|
413
|
+
case "inline":
|
|
414
|
+
if (value !== "1") return false;
|
|
415
|
+
inline = true;
|
|
416
|
+
break;
|
|
417
|
+
case "width":
|
|
418
|
+
case "height":
|
|
419
|
+
if (!validateIterm2Dimension(value)) return false;
|
|
420
|
+
break;
|
|
421
|
+
case "preserveAspectRatio":
|
|
422
|
+
case "doNotMoveCursor":
|
|
423
|
+
if (!/^[01]$/.test(value)) return false;
|
|
424
|
+
break;
|
|
425
|
+
default: return false;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return inline;
|
|
429
|
+
}
|
|
430
|
+
function validateIterm2Sequence(sequence, op = "draw") {
|
|
431
|
+
if (op === "clear") return false;
|
|
432
|
+
let index = 0;
|
|
433
|
+
while (index < sequence.length) {
|
|
434
|
+
const prefix = `${ESC}]1337;File=`;
|
|
435
|
+
if (!sequence.startsWith(prefix, index)) return false;
|
|
436
|
+
const belEnd = sequence.indexOf(BEL, index + prefix.length);
|
|
437
|
+
const stEnd = sequence.indexOf(ST, index + prefix.length);
|
|
438
|
+
const end = belEnd >= 0 && stEnd >= 0 ? Math.min(belEnd, stEnd) : belEnd >= 0 ? belEnd : stEnd;
|
|
439
|
+
if (end < 0) return false;
|
|
440
|
+
const body = sequence.slice(index + `${ESC}]1337;`.length, end);
|
|
441
|
+
if (!body.startsWith("File=")) return false;
|
|
442
|
+
if (!withoutNestedEscapes(body)) return false;
|
|
443
|
+
const colon = body.indexOf(":");
|
|
444
|
+
if (colon < 0) return false;
|
|
445
|
+
const params = body.slice(5, colon);
|
|
446
|
+
const payload = body.slice(colon + 1);
|
|
447
|
+
if (!/^[A-Za-z0-9_=;,.%+\-:]*$/.test(params)) return false;
|
|
448
|
+
if (!payload || !isBase64ish(payload)) return false;
|
|
449
|
+
if (!validateIterm2Params(params)) return false;
|
|
450
|
+
index = end + (end === stEnd ? ST.length : BEL.length);
|
|
451
|
+
}
|
|
452
|
+
return sequence.length > 0;
|
|
453
|
+
}
|
|
454
|
+
function validateSixelSequence(sequence, op = "draw") {
|
|
455
|
+
if (op === "clear") return false;
|
|
456
|
+
let index = 0;
|
|
457
|
+
while (index < sequence.length) {
|
|
458
|
+
if (!sequence.startsWith(`${ESC}P`, index)) return false;
|
|
459
|
+
const end = sequence.indexOf(ST, index + 2);
|
|
460
|
+
if (end < 0) return false;
|
|
461
|
+
const body = sequence.slice(index + 2, end);
|
|
462
|
+
if (!withoutNestedEscapes(body)) return false;
|
|
463
|
+
for (let i = 0; i < body.length; i++) {
|
|
464
|
+
const code = body.charCodeAt(i);
|
|
465
|
+
if (code < 32 || code > 126) return false;
|
|
466
|
+
}
|
|
467
|
+
const q = body.indexOf("q");
|
|
468
|
+
if (q < 0 || q > 32) return false;
|
|
469
|
+
const introducer = body.slice(0, q);
|
|
470
|
+
if (!/^[0-9;?]*$/.test(introducer)) return false;
|
|
471
|
+
const data = body.slice(q + 1);
|
|
472
|
+
if (!data.length) return false;
|
|
473
|
+
index = end + ST.length;
|
|
474
|
+
}
|
|
475
|
+
return sequence.length > 0;
|
|
476
|
+
}
|
|
477
|
+
function stripTerminalControlChars(value) {
|
|
478
|
+
let out = "";
|
|
479
|
+
for (let index = 0; index < value.length; index++) {
|
|
480
|
+
const code = value.charCodeAt(index);
|
|
481
|
+
if (code <= 8 || code === 11 || code === 12 || code >= 14 && code <= 31 || code === 127 || code >= 128 && code <= 159) continue;
|
|
482
|
+
out += value[index];
|
|
483
|
+
}
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
function sanitizeTerminalFallbackText(text) {
|
|
487
|
+
return stripTerminalControlChars(String(text ?? "").replace(OSC_RE, "").replace(CSI_RE, "").replace(DCS_RE, "").replace(APC_RE, "").replace(C1_OSC_RE, "").replace(C1_CSI_RE, "").replace(C1_DCS_RE, "").replace(C1_APC_RE, "")).slice(0, MAX_TERMINAL_GRAPHICS_FALLBACK_CHARS);
|
|
488
|
+
}
|
|
489
|
+
function isSafeTerminalGraphicsSequence(sequence, protocol, op = "draw") {
|
|
490
|
+
if (!isTerminalGraphicsProtocol(protocol)) return false;
|
|
491
|
+
if (!sequence || sequence.length > MAX_TERMINAL_GRAPHICS_SEQUENCE_CHARS) return false;
|
|
492
|
+
return protocol === "kitty" ? validateKittySequence(sequence, op) : protocol === "iterm2" ? validateIterm2Sequence(sequence, op) : validateSixelSequence(sequence, op);
|
|
493
|
+
}
|
|
494
|
+
function uint32Control(key, value, min = 0) {
|
|
495
|
+
if (value == null) return "";
|
|
496
|
+
const n = Math.floor(value);
|
|
497
|
+
return Number.isSafeInteger(n) && n >= min && n <= KITTY_UINT32_MAX ? `${key}=${n}` : "";
|
|
498
|
+
}
|
|
499
|
+
function int32Control(key, value) {
|
|
500
|
+
if (value == null) return "";
|
|
501
|
+
const n = Math.floor(value);
|
|
502
|
+
return Number.isSafeInteger(n) && n >= KITTY_INT32_MIN && n <= KITTY_INT32_MAX ? `${key}=${n}` : "";
|
|
503
|
+
}
|
|
504
|
+
function sanitizeBase64(value) {
|
|
505
|
+
const data = String(value ?? "").replace(/\s+/g, "");
|
|
506
|
+
return isBase64ish(data) ? data : "";
|
|
507
|
+
}
|
|
508
|
+
function kittyGraphicCellControls(columns, rows) {
|
|
509
|
+
if (columns == null && rows == null) return [];
|
|
510
|
+
const size = normalizeTerminalGraphicSize(columns ?? 1, rows ?? 1);
|
|
511
|
+
if (!size) return [];
|
|
512
|
+
return [columns == null ? "" : `c=${size.width}`, rows == null ? "" : `r=${size.height}`];
|
|
513
|
+
}
|
|
514
|
+
function kittyGraphicSourceControls(sourceX, sourceY, sourceWidth, sourceHeight) {
|
|
515
|
+
return [
|
|
516
|
+
uint32Control("x", sourceX),
|
|
517
|
+
uint32Control("y", sourceY),
|
|
518
|
+
uint32Control("w", sourceWidth, 1),
|
|
519
|
+
uint32Control("h", sourceHeight, 1)
|
|
520
|
+
];
|
|
521
|
+
}
|
|
522
|
+
function chunkBase64(value, chunkSize) {
|
|
523
|
+
const normalizedSize = Math.max(4, Math.floor(chunkSize / 4) * 4);
|
|
524
|
+
const chunks = [];
|
|
525
|
+
for (let index = 0; index < value.length; index += normalizedSize) chunks.push(value.slice(index, index + normalizedSize));
|
|
526
|
+
return chunks.length ? chunks : [""];
|
|
527
|
+
}
|
|
528
|
+
function createKittyPlacementSequence(options) {
|
|
529
|
+
const imageId = uint32Control("i", options.imageId);
|
|
530
|
+
const imageNumber = uint32Control("I", options.imageNumber);
|
|
531
|
+
if (!imageId && !imageNumber) return "";
|
|
532
|
+
const controls = [
|
|
533
|
+
"a=p",
|
|
534
|
+
options.quiet === false ? "" : "q=2",
|
|
535
|
+
options.noCursorMove === false ? "" : "C=1",
|
|
536
|
+
imageId,
|
|
537
|
+
imageNumber,
|
|
538
|
+
uint32Control("p", options.placementId),
|
|
539
|
+
...kittyGraphicCellControls(options.columns, options.rows),
|
|
540
|
+
...kittyGraphicSourceControls(options.sourceX, options.sourceY, options.sourceWidth, options.sourceHeight),
|
|
541
|
+
int32Control("z", options.zIndex)
|
|
542
|
+
].filter(Boolean);
|
|
543
|
+
return `${ESC}_G${controls.join(",")}${ST}`;
|
|
544
|
+
}
|
|
545
|
+
function createKittyGraphicsSequence(base64Png, options = {}) {
|
|
546
|
+
const data = sanitizeBase64(base64Png);
|
|
547
|
+
if (!data) return "";
|
|
548
|
+
const chunks = chunkBase64(data, options.chunkSize ?? 4096);
|
|
549
|
+
const baseControls = [
|
|
550
|
+
"a=T",
|
|
551
|
+
"f=100",
|
|
552
|
+
options.quiet === false ? "" : "q=2",
|
|
553
|
+
options.noCursorMove === false ? "" : "C=1",
|
|
554
|
+
uint32Control("i", options.imageId),
|
|
555
|
+
uint32Control("I", options.imageNumber),
|
|
556
|
+
uint32Control("p", options.placementId),
|
|
557
|
+
...kittyGraphicCellControls(options.columns, options.rows),
|
|
558
|
+
...kittyGraphicSourceControls(options.sourceX, options.sourceY, options.sourceWidth, options.sourceHeight),
|
|
559
|
+
int32Control("z", options.zIndex)
|
|
560
|
+
].filter(Boolean);
|
|
561
|
+
return chunks.map((chunk, index) => {
|
|
562
|
+
const last = index === chunks.length - 1;
|
|
563
|
+
const controls = index === 0 ? [...baseControls, chunks.length > 1 ? `m=${last ? 0 : 1}` : ""] : [options.quiet === false ? "" : "q=2", `m=${last ? 0 : 1}`].filter(Boolean);
|
|
564
|
+
return `${ESC}_G${controls.filter(Boolean).join(",")};${chunk}${ST}`;
|
|
565
|
+
}).join("");
|
|
566
|
+
}
|
|
567
|
+
function createKittyDeleteGraphicsSequence(options = {}) {
|
|
568
|
+
const controls = ["a=d"];
|
|
569
|
+
if (options.allVisible) controls.push(`d=${options.freeImageData ? "A" : "a"}`);
|
|
570
|
+
else if (options.imageId != null) {
|
|
571
|
+
const imageId = uint32Control("i", options.imageId);
|
|
572
|
+
const placementId = uint32Control("p", options.placementId);
|
|
573
|
+
if (!imageId || options.placementId != null && !placementId) return "";
|
|
574
|
+
controls.push(`d=${options.freeImageData ? "I" : "i"}`);
|
|
575
|
+
controls.push(imageId);
|
|
576
|
+
controls.push(placementId);
|
|
577
|
+
} else if (options.currentCell) controls.push(`d=${options.freeImageData ? "C" : "c"}`);
|
|
578
|
+
else return "";
|
|
579
|
+
if (options.quiet !== false) controls.push("q=2");
|
|
580
|
+
return `${ESC}_G${controls.filter(Boolean).join(",")}${ST}`;
|
|
581
|
+
}
|
|
582
|
+
function createIterm2InlineImageSequence(base64Data, options = {}) {
|
|
583
|
+
const data = sanitizeBase64(base64Data);
|
|
584
|
+
if (!data) return "";
|
|
585
|
+
const params = [
|
|
586
|
+
"inline=1",
|
|
587
|
+
options.width != null ? `width=${formatItermDimension(options.width)}` : "",
|
|
588
|
+
options.height != null ? `height=${formatItermDimension(options.height)}` : "",
|
|
589
|
+
`preserveAspectRatio=${options.preserveAspectRatio === false ? 0 : 1}`,
|
|
590
|
+
options.doNotMoveCursor === false ? "" : "doNotMoveCursor=1"
|
|
591
|
+
].filter(Boolean);
|
|
592
|
+
return `${ESC}]1337;File=${params.join(";")}:${data}${BEL}`;
|
|
593
|
+
}
|
|
594
|
+
function formatItermDimension(value) {
|
|
595
|
+
if (typeof value === "number") {
|
|
596
|
+
const n = Math.floor(value);
|
|
597
|
+
if (!Number.isFinite(n)) return "auto";
|
|
598
|
+
return String(Math.min(99999, Math.max(1, n)));
|
|
599
|
+
}
|
|
600
|
+
const raw = String(value ?? "").trim();
|
|
601
|
+
return validateIterm2Dimension(raw) ? raw : "auto";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region src/vue/markdown/image.ts
|
|
606
|
+
const DATA_IMAGE_RE = /^data:(image\/[A-Za-z0-9.+-]+);base64,([A-Za-z0-9+/=\s]+)$/i;
|
|
607
|
+
const SAFE_IMAGE_MIME_RE = /^image\/(?:png|jpeg|jpg|gif|webp)$/i;
|
|
608
|
+
function sanitizeMarkdownImageSource(src) {
|
|
609
|
+
if (typeof src !== "string") return null;
|
|
610
|
+
const raw = src.trim();
|
|
611
|
+
if (!raw || raw !== src) return null;
|
|
612
|
+
const data = raw.match(DATA_IMAGE_RE);
|
|
613
|
+
if (data) {
|
|
614
|
+
const mime = data[1].toLowerCase();
|
|
615
|
+
const base64 = data[2].replace(/\s+/g, "");
|
|
616
|
+
if (!SAFE_IMAGE_MIME_RE.test(mime)) return null;
|
|
617
|
+
if (!/^[A-Za-z0-9+/=]+$/.test(base64)) return null;
|
|
618
|
+
return {
|
|
619
|
+
src: raw,
|
|
620
|
+
mime,
|
|
621
|
+
base64
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
if (/^(?:blob|file):/i.test(raw)) {
|
|
625
|
+
if (raw.includes("\\") || /\s/u.test(raw) || hasEncodedControl(raw)) return null;
|
|
626
|
+
try {
|
|
627
|
+
const url = new URL(raw);
|
|
628
|
+
if (url.protocol === "blob:" || url.protocol === "file:") return { src: url.toString() };
|
|
629
|
+
} catch {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const safe = sanitizeDomHref(raw, { allowRelative: true });
|
|
634
|
+
return safe ? { src: safe } : null;
|
|
635
|
+
}
|
|
636
|
+
const BASE64_TABLE = (() => {
|
|
637
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
638
|
+
const table = new Map();
|
|
639
|
+
for (let i = 0; i < chars.length; i++) table.set(chars[i], i);
|
|
640
|
+
return table;
|
|
641
|
+
})();
|
|
642
|
+
function decodeBase64Prefix(value, maxBytes) {
|
|
643
|
+
const bytes = [];
|
|
644
|
+
let buffer = 0;
|
|
645
|
+
let bits = 0;
|
|
646
|
+
for (const ch of value.replace(/\s+/g, "")) {
|
|
647
|
+
if (ch === "=") break;
|
|
648
|
+
const sextet = BASE64_TABLE.get(ch);
|
|
649
|
+
if (sextet == null) continue;
|
|
650
|
+
buffer = buffer << 6 | sextet;
|
|
651
|
+
bits += 6;
|
|
652
|
+
while (bits >= 8) {
|
|
653
|
+
bits -= 8;
|
|
654
|
+
bytes.push(buffer >> bits & 255);
|
|
655
|
+
if (bytes.length >= maxBytes) return Uint8Array.from(bytes);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return Uint8Array.from(bytes);
|
|
659
|
+
}
|
|
660
|
+
function u16be(bytes, offset) {
|
|
661
|
+
return (bytes[offset] ?? 0) << 8 | (bytes[offset + 1] ?? 0);
|
|
662
|
+
}
|
|
663
|
+
function u16le(bytes, offset) {
|
|
664
|
+
return (bytes[offset] ?? 0) | (bytes[offset + 1] ?? 0) << 8;
|
|
665
|
+
}
|
|
666
|
+
function u24le(bytes, offset) {
|
|
667
|
+
return (bytes[offset] ?? 0) | (bytes[offset + 1] ?? 0) << 8 | (bytes[offset + 2] ?? 0) << 16;
|
|
668
|
+
}
|
|
669
|
+
function u32be(bytes, offset) {
|
|
670
|
+
return (bytes[offset] ?? 0) * 16777216 + ((bytes[offset + 1] ?? 0) << 16 | (bytes[offset + 2] ?? 0) << 8 | (bytes[offset + 3] ?? 0));
|
|
671
|
+
}
|
|
672
|
+
function validDimensions(width, height) {
|
|
673
|
+
return Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0 ? {
|
|
674
|
+
width,
|
|
675
|
+
height
|
|
676
|
+
} : null;
|
|
677
|
+
}
|
|
678
|
+
function readMarkdownImageDimensions(base64) {
|
|
679
|
+
if (!base64) return null;
|
|
680
|
+
const bytes = decodeBase64Prefix(base64, 4096);
|
|
681
|
+
if (bytes.length < 10) return null;
|
|
682
|
+
if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[12] === 73 && bytes[13] === 72 && bytes[14] === 68 && bytes[15] === 82) return validDimensions(u32be(bytes, 16), u32be(bytes, 20));
|
|
683
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) return validDimensions(u16le(bytes, 6), u16le(bytes, 8));
|
|
684
|
+
if (bytes[0] === 255 && bytes[1] === 216) for (let offset = 2; offset + 8 < bytes.length;) {
|
|
685
|
+
if (bytes[offset] !== 255) {
|
|
686
|
+
offset++;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const marker = bytes[offset + 1] ?? 0;
|
|
690
|
+
if (marker === 217 || marker === 218) break;
|
|
691
|
+
const length = u16be(bytes, offset + 2);
|
|
692
|
+
if (length < 2) break;
|
|
693
|
+
if (marker >= 192 && marker <= 195 || marker >= 197 && marker <= 199 || marker >= 201 && marker <= 203 || marker >= 205 && marker <= 207) return validDimensions(u16be(bytes, offset + 7), u16be(bytes, offset + 5));
|
|
694
|
+
offset += 2 + length;
|
|
695
|
+
}
|
|
696
|
+
if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
|
|
697
|
+
const format = String.fromCharCode(bytes[12] ?? 0, bytes[13] ?? 0, bytes[14] ?? 0, bytes[15] ?? 0);
|
|
698
|
+
if (format === "VP8X" && bytes.length >= 30) return validDimensions(u24le(bytes, 24) + 1, u24le(bytes, 27) + 1);
|
|
699
|
+
if (format === "VP8 " && bytes.length >= 30) return validDimensions(u16le(bytes, 26) & 16383, u16le(bytes, 28) & 16383);
|
|
700
|
+
if (format === "VP8L" && bytes.length >= 25 && bytes[20] === 47) {
|
|
701
|
+
const b0 = bytes[21] ?? 0;
|
|
702
|
+
const b1 = bytes[22] ?? 0;
|
|
703
|
+
const b2 = bytes[23] ?? 0;
|
|
704
|
+
const b3 = bytes[24] ?? 0;
|
|
705
|
+
return validDimensions(1 + ((b1 & 63) << 8 | b0), 1 + (b3 << 6 | b2 >> 2));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
//#endregion
|
|
712
|
+
//#region src/vue/markdown/math.ts
|
|
713
|
+
const KATEX_TEXT_OPTIONS = Object.freeze({
|
|
714
|
+
output: "mathml",
|
|
715
|
+
throwOnError: false,
|
|
716
|
+
strict: "ignore",
|
|
717
|
+
trust: false,
|
|
718
|
+
maxSize: 20,
|
|
719
|
+
maxExpand: 200
|
|
720
|
+
});
|
|
721
|
+
const KATEX_VALIDATE_OPTIONS = Object.freeze({
|
|
722
|
+
...KATEX_TEXT_OPTIONS,
|
|
723
|
+
throwOnError: true
|
|
724
|
+
});
|
|
725
|
+
const TEX_SYMBOLS = Object.freeze({
|
|
726
|
+
alpha: "α",
|
|
727
|
+
beta: "β",
|
|
728
|
+
gamma: "γ",
|
|
729
|
+
delta: "δ",
|
|
730
|
+
epsilon: "ε",
|
|
731
|
+
theta: "θ",
|
|
732
|
+
lambda: "λ",
|
|
733
|
+
mu: "μ",
|
|
734
|
+
pi: "π",
|
|
735
|
+
rho: "ρ",
|
|
736
|
+
sigma: "σ",
|
|
737
|
+
tau: "τ",
|
|
738
|
+
phi: "φ",
|
|
739
|
+
omega: "ω",
|
|
740
|
+
Gamma: "Γ",
|
|
741
|
+
Delta: "Δ",
|
|
742
|
+
Theta: "Θ",
|
|
743
|
+
Lambda: "Λ",
|
|
744
|
+
Pi: "Π",
|
|
745
|
+
Sigma: "Σ",
|
|
746
|
+
Phi: "Φ",
|
|
747
|
+
Omega: "Ω",
|
|
748
|
+
int: "∫",
|
|
749
|
+
sum: "∑",
|
|
750
|
+
prod: "∏",
|
|
751
|
+
infty: "∞",
|
|
752
|
+
infinity: "∞",
|
|
753
|
+
pm: "±",
|
|
754
|
+
times: "×",
|
|
755
|
+
cdot: "·",
|
|
756
|
+
div: "÷",
|
|
757
|
+
le: "≤",
|
|
758
|
+
leq: "≤",
|
|
759
|
+
ge: "≥",
|
|
760
|
+
geq: "≥",
|
|
761
|
+
neq: "≠",
|
|
762
|
+
ne: "≠",
|
|
763
|
+
approx: "≈",
|
|
764
|
+
equiv: "≡",
|
|
765
|
+
to: "→",
|
|
766
|
+
rightarrow: "→",
|
|
767
|
+
leftarrow: "←",
|
|
768
|
+
leftrightarrow: "↔",
|
|
769
|
+
partial: "∂",
|
|
770
|
+
nabla: "∇",
|
|
771
|
+
sqrt: "√"
|
|
772
|
+
});
|
|
773
|
+
const SUPPORTED_COMMANDS = new Set([
|
|
774
|
+
...Object.keys(TEX_SYMBOLS),
|
|
775
|
+
"frac",
|
|
776
|
+
"sqrt"
|
|
777
|
+
]);
|
|
778
|
+
function decodeXmlEntities(value) {
|
|
779
|
+
return value.replace(/&(?:#(\d+)|#x([0-9a-f]+)|([a-z]+));/gi, (match, dec, hex, named) => {
|
|
780
|
+
if (dec) return String.fromCodePoint(Number(dec));
|
|
781
|
+
if (hex) return String.fromCodePoint(Number.parseInt(hex, 16));
|
|
782
|
+
switch (String(named).toLowerCase()) {
|
|
783
|
+
case "amp": return "&";
|
|
784
|
+
case "lt": return "<";
|
|
785
|
+
case "gt": return ">";
|
|
786
|
+
case "quot": return "\"";
|
|
787
|
+
case "apos": return "'";
|
|
788
|
+
default: return match;
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
function mathmlTagName(tag) {
|
|
793
|
+
const start = tag.startsWith("/") ? 1 : 0;
|
|
794
|
+
let end = start;
|
|
795
|
+
while (end < tag.length) {
|
|
796
|
+
const ch = tag[end];
|
|
797
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "/") break;
|
|
798
|
+
end++;
|
|
799
|
+
}
|
|
800
|
+
return tag.slice(start, end).toLowerCase();
|
|
801
|
+
}
|
|
802
|
+
function textFromKatexMathml(html) {
|
|
803
|
+
let readable = "";
|
|
804
|
+
let offset = 0;
|
|
805
|
+
const lowerHtml = html.toLowerCase();
|
|
806
|
+
while (offset < html.length) {
|
|
807
|
+
const tagStart = html.indexOf("<", offset);
|
|
808
|
+
if (tagStart < 0) {
|
|
809
|
+
readable += html.slice(offset);
|
|
810
|
+
break;
|
|
811
|
+
}
|
|
812
|
+
readable += html.slice(offset, tagStart);
|
|
813
|
+
const tagEnd = html.indexOf(">", tagStart + 1);
|
|
814
|
+
if (tagEnd < 0) break;
|
|
815
|
+
const tag = html.slice(tagStart + 1, tagEnd).trim();
|
|
816
|
+
const tagName = mathmlTagName(tag);
|
|
817
|
+
const closing = tag.startsWith("/");
|
|
818
|
+
if (!closing && tagName === "annotation") {
|
|
819
|
+
const annotationEnd = lowerHtml.indexOf("</annotation>", tagEnd + 1);
|
|
820
|
+
offset = annotationEnd < 0 ? tagEnd + 1 : annotationEnd + 13;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (!closing && tagName === "mfrac") readable += "(";
|
|
824
|
+
else if (closing && tagName === "mfrac") readable += ")";
|
|
825
|
+
else if (!closing && tagName === "mspace") readable += " ";
|
|
826
|
+
offset = tagEnd + 1;
|
|
827
|
+
}
|
|
828
|
+
const text = sanitizeInlineText(decodeXmlEntities(readable).replace(/[<>]/g, "").replace(/\s+/g, " ").trim());
|
|
829
|
+
return text;
|
|
830
|
+
}
|
|
831
|
+
function approximateTexToUnicode(tex) {
|
|
832
|
+
let out = String(tex ?? "");
|
|
833
|
+
out = out.replace(/\\frac\s*\{([^{}]*)\}\s*\{([^{}]*)\}/g, "$1/$2");
|
|
834
|
+
out = out.replace(/\\sqrt\s*\{([^{}]*)\}/g, "√($1)");
|
|
835
|
+
out = out.replace(/\\([A-Za-z]+)\b/g, (_match, command) => TEX_SYMBOLS[command] ?? command);
|
|
836
|
+
out = out.replace(/\\([^A-Za-z])/g, "$1");
|
|
837
|
+
out = out.replace(/[{}]/g, "");
|
|
838
|
+
out = out.replace(/\s+/g, " ").trim();
|
|
839
|
+
return sanitizeInlineText(out.replace(/[<>]/g, ""));
|
|
840
|
+
}
|
|
841
|
+
function canRenderTerminalMath(tex) {
|
|
842
|
+
const source = String(tex ?? "").trim();
|
|
843
|
+
if (!source || source.length > 160) return false;
|
|
844
|
+
if (/\\(?:begin|end|left|right|matrix|cases|array|align|color|href|html|includegraphics|class|style|tag|text|operatorname)\b/.test(source)) return false;
|
|
845
|
+
const commands = source.matchAll(/\\([A-Za-z]+)\b/g);
|
|
846
|
+
for (const match of commands) if (!SUPPORTED_COMMANDS.has(match[1] ?? "")) return false;
|
|
847
|
+
try {
|
|
848
|
+
katex.renderToString(source, KATEX_VALIDATE_OPTIONS);
|
|
849
|
+
} catch {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
function renderMarkdownInlineMathSegment(tex) {
|
|
855
|
+
const source = String(tex ?? "");
|
|
856
|
+
if (!source.trim()) return {
|
|
857
|
+
text: "",
|
|
858
|
+
supported: false
|
|
859
|
+
};
|
|
860
|
+
if (!canRenderTerminalMath(source)) return {
|
|
861
|
+
text: "",
|
|
862
|
+
supported: false
|
|
863
|
+
};
|
|
864
|
+
try {
|
|
865
|
+
const html = katex.renderToString(source, KATEX_TEXT_OPTIONS);
|
|
866
|
+
const rendered = /\\frac\b/.test(source) ? approximateTexToUnicode(source) : textFromKatexMathml(html);
|
|
867
|
+
if (rendered && !rendered.includes("\\")) return {
|
|
868
|
+
text: rendered,
|
|
869
|
+
supported: true
|
|
870
|
+
};
|
|
871
|
+
} catch {
|
|
872
|
+
return {
|
|
873
|
+
text: "",
|
|
874
|
+
supported: false
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
const approximate = approximateTexToUnicode(source);
|
|
878
|
+
return approximate && !approximate.includes("\\") ? {
|
|
879
|
+
text: approximate,
|
|
880
|
+
supported: true
|
|
881
|
+
} : {
|
|
882
|
+
text: "",
|
|
883
|
+
supported: false
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region src/vue/markdown/parser.ts
|
|
889
|
+
const markdownInstanceCache = new Map();
|
|
890
|
+
const MAX_MARKDOWN_INSTANCE_CACHE = 32;
|
|
891
|
+
function sanitizeMarkdownLink(url) {
|
|
892
|
+
return sanitizeDomHref(url, { allowRelative: true });
|
|
893
|
+
}
|
|
894
|
+
function isSafeMarkdownLink(url) {
|
|
895
|
+
return sanitizeMarkdownLink(url) != null;
|
|
896
|
+
}
|
|
897
|
+
function isSafeMarkdownDestination(url) {
|
|
898
|
+
return isSafeMarkdownLink(url) || sanitizeMarkdownImageSource(url) != null;
|
|
899
|
+
}
|
|
900
|
+
function normalizeCustomHtmlTags(config) {
|
|
901
|
+
return Array.from(new Set(config?.customHtmlTags?.filter(Boolean) ?? [])).sort();
|
|
902
|
+
}
|
|
903
|
+
function markdownInstanceCacheKey(customHtmlTags) {
|
|
904
|
+
return JSON.stringify({ customHtmlTags });
|
|
905
|
+
}
|
|
906
|
+
function getCachedMarkdownInstance(customHtmlTags) {
|
|
907
|
+
const cacheKey = markdownInstanceCacheKey(customHtmlTags);
|
|
908
|
+
const cached = markdownInstanceCache.get(cacheKey);
|
|
909
|
+
if (cached) {
|
|
910
|
+
markdownInstanceCache.delete(cacheKey);
|
|
911
|
+
markdownInstanceCache.set(cacheKey, cached);
|
|
912
|
+
return cached;
|
|
913
|
+
}
|
|
914
|
+
const created = getMarkdown(`vue-tui-markdown:${cacheKey}`, { customHtmlTags });
|
|
915
|
+
markdownInstanceCache.set(cacheKey, created);
|
|
916
|
+
while (markdownInstanceCache.size > MAX_MARKDOWN_INSTANCE_CACHE) {
|
|
917
|
+
const oldest = markdownInstanceCache.keys().next().value;
|
|
918
|
+
if (oldest == null) break;
|
|
919
|
+
markdownInstanceCache.delete(oldest);
|
|
920
|
+
}
|
|
921
|
+
return created;
|
|
922
|
+
}
|
|
923
|
+
function createTuiMarkdownParser(config) {
|
|
924
|
+
const customHtmlTags = normalizeCustomHtmlTags(config);
|
|
925
|
+
const md = getCachedMarkdownInstance(customHtmlTags);
|
|
926
|
+
function parse(content, final) {
|
|
927
|
+
return parseMarkdownToStructure(content, md, {
|
|
928
|
+
final,
|
|
929
|
+
customHtmlTags,
|
|
930
|
+
requireClosingStrong: final || !config?.streaming,
|
|
931
|
+
validateLink: isSafeMarkdownDestination
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
return { parse };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region src/vue/markdown/ast.ts
|
|
939
|
+
const EMPTY_PREFIX = Object.freeze([]);
|
|
940
|
+
const HARD_BREAK_SEGMENT = Object.freeze({
|
|
941
|
+
text: "",
|
|
942
|
+
hardBreak: true
|
|
943
|
+
});
|
|
944
|
+
const TERMINAL_CELL_WIDTH_TO_HEIGHT_RATIO = .5;
|
|
945
|
+
function mergeStyle$1(base, overlay) {
|
|
946
|
+
if (!base) return overlay;
|
|
947
|
+
if (!overlay) return base;
|
|
948
|
+
return {
|
|
949
|
+
...base,
|
|
950
|
+
...overlay
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function trimBlockArray(blocks) {
|
|
954
|
+
let start = 0;
|
|
955
|
+
let end = blocks.length;
|
|
956
|
+
while (start < end && blocks[start]?.type === "blank") start++;
|
|
957
|
+
while (end > start && blocks[end - 1]?.type === "blank") end--;
|
|
958
|
+
return blocks.slice(start, end);
|
|
959
|
+
}
|
|
960
|
+
function nodeChildren(node) {
|
|
961
|
+
return "children" in node && Array.isArray(node.children) ? node.children : [];
|
|
962
|
+
}
|
|
963
|
+
function stringProp(node, key) {
|
|
964
|
+
const value = node[key];
|
|
965
|
+
return typeof value === "string" ? value : "";
|
|
966
|
+
}
|
|
967
|
+
function imageSourceFromRaw(raw) {
|
|
968
|
+
if (!raw.startsWith(";
|
|
970
|
+
if (marker < 0 || !raw.endsWith(")")) return "";
|
|
971
|
+
const destination = raw.slice(marker + 2, -1).trim();
|
|
972
|
+
if (destination.startsWith("<")) {
|
|
973
|
+
const end = destination.indexOf(">");
|
|
974
|
+
return end > 1 ? destination.slice(1, end) : "";
|
|
975
|
+
}
|
|
976
|
+
return destination.split(/\s+/u)[0] ?? "";
|
|
977
|
+
}
|
|
978
|
+
function imageFallbackText(raw, rawSource, sourceSrc) {
|
|
979
|
+
if (raw) return raw;
|
|
980
|
+
const src = rawSource || sourceSrc;
|
|
981
|
+
return src ? `` : "image";
|
|
982
|
+
}
|
|
983
|
+
function normalizeImageResolverResult(result) {
|
|
984
|
+
if (typeof result === "string") return { base64: result.replace(/\s+/g, "") };
|
|
985
|
+
if (!result) return {};
|
|
986
|
+
return {
|
|
987
|
+
base64: result.base64.replace(/\s+/g, ""),
|
|
988
|
+
...result.originalBase64 ? { originalBase64: result.originalBase64.replace(/\s+/g, "") } : {},
|
|
989
|
+
...result.mime ? { mime: result.mime } : {},
|
|
990
|
+
...result.originalMime ? { originalMime: result.originalMime } : {}
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
function booleanProp(node, key) {
|
|
994
|
+
return node[key] === true;
|
|
995
|
+
}
|
|
996
|
+
function numberProp(node, key) {
|
|
997
|
+
const value = node[key];
|
|
998
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
999
|
+
}
|
|
1000
|
+
function nodeItems(node) {
|
|
1001
|
+
const value = node.items;
|
|
1002
|
+
return Array.isArray(value) ? value : [];
|
|
1003
|
+
}
|
|
1004
|
+
function clampImageCells(value, min, max) {
|
|
1005
|
+
return Math.max(min, Math.min(max, Math.round(value)));
|
|
1006
|
+
}
|
|
1007
|
+
function resolveImageDisplaySize(alt, size, naturalWidth, naturalHeight) {
|
|
1008
|
+
if (size?.minWidth == null && size?.maxWidth == null && size?.minHeight == null && size?.maxHeight == null) return {};
|
|
1009
|
+
const minWidth = Math.max(1, Math.floor(size?.minWidth ?? 1));
|
|
1010
|
+
const maxWidth = Math.max(minWidth, Math.floor(size?.maxWidth ?? Number.MAX_SAFE_INTEGER));
|
|
1011
|
+
const minHeight = Math.max(1, Math.floor(size?.minHeight ?? 1));
|
|
1012
|
+
const maxHeight = Math.max(minHeight, Math.floor(size?.maxHeight ?? Number.MAX_SAFE_INTEGER));
|
|
1013
|
+
if (size?.preserveAspectRatio !== false && naturalWidth != null && naturalHeight != null && naturalWidth > 0 && naturalHeight > 0) {
|
|
1014
|
+
const cellRatio = naturalWidth / naturalHeight / TERMINAL_CELL_WIDTH_TO_HEIGHT_RATIO;
|
|
1015
|
+
let width = minWidth;
|
|
1016
|
+
let height = Math.max(minHeight, Math.ceil(width / cellRatio));
|
|
1017
|
+
width = Math.max(width, Math.ceil(height * cellRatio));
|
|
1018
|
+
if (width > maxWidth) {
|
|
1019
|
+
width = maxWidth;
|
|
1020
|
+
height = Math.max(minHeight, Math.ceil(width / cellRatio));
|
|
1021
|
+
}
|
|
1022
|
+
if (height > maxHeight) {
|
|
1023
|
+
height = maxHeight;
|
|
1024
|
+
width = Math.max(minWidth, Math.floor(height * cellRatio));
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
displayWidth: clampImageCells(width, minWidth, maxWidth),
|
|
1028
|
+
displayHeight: clampImageCells(height, minHeight, maxHeight)
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
return {
|
|
1032
|
+
displayWidth: size?.maxWidth != null ? Math.min(maxWidth, Math.max(minWidth, textCellWidth(alt))) : Math.max(minWidth, textCellWidth(alt)),
|
|
1033
|
+
displayHeight: size?.maxHeight != null ? Math.min(maxHeight, Math.max(minHeight, 1)) : minHeight
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function pushTextSegments(out, text, style, graphic, mathAction) {
|
|
1037
|
+
if (!text) return;
|
|
1038
|
+
const normalized = sanitizeTextBlock(text);
|
|
1039
|
+
if (!normalized) return;
|
|
1040
|
+
const parts = normalized.split("\n");
|
|
1041
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1042
|
+
const part = sanitizeInlineText(parts[i] ?? "");
|
|
1043
|
+
if (part) out.push({
|
|
1044
|
+
text: part,
|
|
1045
|
+
...style ? { style } : {},
|
|
1046
|
+
...i === 0 && graphic ? { graphic } : {},
|
|
1047
|
+
...mathAction ? { mathAction } : {}
|
|
1048
|
+
});
|
|
1049
|
+
if (i < parts.length - 1) out.push(HARD_BREAK_SEGMENT);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
function sanitizeCodeBlockText(text, tabSize = 4) {
|
|
1053
|
+
const normalized = String(text ?? "").replace(/\r\n?/g, "\n").replace(/\t/g, spaces(tabSize)).replace(/\u00a0/g, " ");
|
|
1054
|
+
return sanitizeTextBlock(normalized);
|
|
1055
|
+
}
|
|
1056
|
+
function codeBlockLanguage(node) {
|
|
1057
|
+
const raw = stringProp(node, "lang") || stringProp(node, "language") || stringProp(node, "info") || stringProp(node, "meta");
|
|
1058
|
+
const language = raw.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
|
|
1059
|
+
return language || void 0;
|
|
1060
|
+
}
|
|
1061
|
+
function inlineNodeSegments(nodes, theme, inheritedStyle, options = {}) {
|
|
1062
|
+
const out = [];
|
|
1063
|
+
for (const node of nodes) switch (node.type) {
|
|
1064
|
+
case "text":
|
|
1065
|
+
pushTextSegments(out, stringProp(node, "content"), inheritedStyle);
|
|
1066
|
+
break;
|
|
1067
|
+
case "inline":
|
|
1068
|
+
case "paragraph":
|
|
1069
|
+
case "heading":
|
|
1070
|
+
case "list_item":
|
|
1071
|
+
case "blockquote":
|
|
1072
|
+
out.push(...inlineNodeSegments(nodeChildren(node), theme, inheritedStyle, options));
|
|
1073
|
+
break;
|
|
1074
|
+
case "strong":
|
|
1075
|
+
out.push(...inlineNodeSegments(nodeChildren(node), theme, mergeStyle$1(inheritedStyle, theme.strong), options));
|
|
1076
|
+
break;
|
|
1077
|
+
case "emphasis":
|
|
1078
|
+
out.push(...inlineNodeSegments(nodeChildren(node), theme, mergeStyle$1(inheritedStyle, theme.emphasis), options));
|
|
1079
|
+
break;
|
|
1080
|
+
case "strikethrough":
|
|
1081
|
+
out.push(...inlineNodeSegments(nodeChildren(node), theme, mergeStyle$1(inheritedStyle, theme.strikethrough), options));
|
|
1082
|
+
break;
|
|
1083
|
+
case "highlight":
|
|
1084
|
+
case "insert":
|
|
1085
|
+
case "subscript":
|
|
1086
|
+
case "superscript":
|
|
1087
|
+
out.push(...inlineNodeSegments(nodeChildren(node), theme, inheritedStyle, options));
|
|
1088
|
+
break;
|
|
1089
|
+
case "inline_code":
|
|
1090
|
+
pushTextSegments(out, stringProp(node, "code"), mergeStyle$1(inheritedStyle, theme.inlineCode));
|
|
1091
|
+
break;
|
|
1092
|
+
case "hardbreak":
|
|
1093
|
+
out.push(HARD_BREAK_SEGMENT);
|
|
1094
|
+
break;
|
|
1095
|
+
case "link": {
|
|
1096
|
+
const href = stringProp(node, "href");
|
|
1097
|
+
const safeHref = href ? sanitizeMarkdownLink(href) : null;
|
|
1098
|
+
const linkStyle = safeHref ? mergeStyle$1(inheritedStyle, {
|
|
1099
|
+
...theme.link,
|
|
1100
|
+
href: safeHref
|
|
1101
|
+
}) : inheritedStyle;
|
|
1102
|
+
const children = nodeChildren(node);
|
|
1103
|
+
if (children.length) out.push(...inlineNodeSegments(children, theme, linkStyle, options));
|
|
1104
|
+
else pushTextSegments(out, stringProp(node, "text"), linkStyle);
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
case "image": {
|
|
1108
|
+
const raw = stringProp(node, "raw");
|
|
1109
|
+
const rawSource = stringProp(node, "src") || imageSourceFromRaw(raw);
|
|
1110
|
+
const source = sanitizeMarkdownImageSource(rawSource);
|
|
1111
|
+
const alt = stringProp(node, "alt") || imageFallbackText(raw, rawSource, source?.src);
|
|
1112
|
+
if (source) {
|
|
1113
|
+
const size = options.imageSize;
|
|
1114
|
+
const imageLinkStyle = mergeStyle$1(inheritedStyle, {
|
|
1115
|
+
...theme.link,
|
|
1116
|
+
href: source.src
|
|
1117
|
+
});
|
|
1118
|
+
const sourceDimensions = readMarkdownImageDimensions(source.base64);
|
|
1119
|
+
const graphicBase = {
|
|
1120
|
+
kind: "image",
|
|
1121
|
+
src: source.src,
|
|
1122
|
+
alt,
|
|
1123
|
+
...source.mime ? { mime: source.mime } : {},
|
|
1124
|
+
...source.base64 ? { base64: source.base64 } : {},
|
|
1125
|
+
...sourceDimensions ? {
|
|
1126
|
+
naturalWidth: sourceDimensions.width,
|
|
1127
|
+
naturalHeight: sourceDimensions.height
|
|
1128
|
+
} : {},
|
|
1129
|
+
...resolveImageDisplaySize(alt, size, sourceDimensions?.width, sourceDimensions?.height)
|
|
1130
|
+
};
|
|
1131
|
+
const resolved = normalizeImageResolverResult(options.imageResolver?.(graphicBase));
|
|
1132
|
+
const base64 = resolved.base64 || source.base64;
|
|
1133
|
+
if (base64) {
|
|
1134
|
+
const dimensions = readMarkdownImageDimensions(base64) ?? sourceDimensions;
|
|
1135
|
+
const graphic = {
|
|
1136
|
+
...graphicBase,
|
|
1137
|
+
base64,
|
|
1138
|
+
...resolved.originalBase64 ? { originalBase64: resolved.originalBase64 } : {},
|
|
1139
|
+
...resolved.mime ? { mime: resolved.mime } : {},
|
|
1140
|
+
...resolved.originalMime ? { originalMime: resolved.originalMime } : {},
|
|
1141
|
+
...dimensions ? {
|
|
1142
|
+
naturalWidth: dimensions.width,
|
|
1143
|
+
naturalHeight: dimensions.height
|
|
1144
|
+
} : {},
|
|
1145
|
+
...resolveImageDisplaySize(alt, size, dimensions?.width, dimensions?.height)
|
|
1146
|
+
};
|
|
1147
|
+
pushTextSegments(out, alt, imageLinkStyle, graphic);
|
|
1148
|
+
} else pushTextSegments(out, alt, imageLinkStyle);
|
|
1149
|
+
} else pushTextSegments(out, alt, mergeStyle$1(inheritedStyle, theme.link));
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
case "checkbox":
|
|
1153
|
+
case "checkbox_input":
|
|
1154
|
+
pushTextSegments(out, booleanProp(node, "checked") ? "[x]" : "[ ]", inheritedStyle);
|
|
1155
|
+
break;
|
|
1156
|
+
case "emoji":
|
|
1157
|
+
pushTextSegments(out, stringProp(node, "markup") || stringProp(node, "name"), inheritedStyle);
|
|
1158
|
+
break;
|
|
1159
|
+
case "math_inline":
|
|
1160
|
+
{
|
|
1161
|
+
const source = stringProp(node, "content");
|
|
1162
|
+
const raw = stringProp(node, "raw") || `$${source}$`;
|
|
1163
|
+
const rendered = renderMarkdownInlineMathSegment(source);
|
|
1164
|
+
pushTextSegments(out, rendered.supported ? rendered.text : raw, mergeStyle$1(inheritedStyle, theme.inlineCode), void 0, {
|
|
1165
|
+
source,
|
|
1166
|
+
raw,
|
|
1167
|
+
rendered: rendered.supported
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
break;
|
|
1171
|
+
case "reference":
|
|
1172
|
+
case "footnote_reference":
|
|
1173
|
+
case "footnote_anchor":
|
|
1174
|
+
pushTextSegments(out, stringProp(node, "raw"), inheritedStyle);
|
|
1175
|
+
break;
|
|
1176
|
+
case "html_inline":
|
|
1177
|
+
pushTextSegments(out, stringProp(node, "content"), mergeStyle$1(inheritedStyle, theme.html));
|
|
1178
|
+
break;
|
|
1179
|
+
case "html_block":
|
|
1180
|
+
pushTextSegments(out, stringProp(node, "content"), mergeStyle$1(inheritedStyle, theme.html));
|
|
1181
|
+
break;
|
|
1182
|
+
default:
|
|
1183
|
+
if (nodeChildren(node).length) {
|
|
1184
|
+
out.push(...inlineNodeSegments(nodeChildren(node), theme, inheritedStyle, options));
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
pushTextSegments(out, stringProp(node, "raw"), inheritedStyle);
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
return out;
|
|
1191
|
+
}
|
|
1192
|
+
function inlineBlock(key, segments, context) {
|
|
1193
|
+
return {
|
|
1194
|
+
type: "inline",
|
|
1195
|
+
key,
|
|
1196
|
+
segments,
|
|
1197
|
+
prefixSegments: context.prefixSegments,
|
|
1198
|
+
continuationPrefixSegments: context.continuationPrefixSegments
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function blockFromParagraph(node, key, context, theme) {
|
|
1202
|
+
return inlineBlock(key, inlineNodeSegments(nodeChildren(node), theme, void 0, {
|
|
1203
|
+
imageResolver: context.imageResolver,
|
|
1204
|
+
imageSize: context.imageSize
|
|
1205
|
+
}), context);
|
|
1206
|
+
}
|
|
1207
|
+
function blockFromHeading(node, key, context, theme) {
|
|
1208
|
+
const level = Math.min(6, Math.max(1, Math.floor(numberProp(node, "level") ?? 1)));
|
|
1209
|
+
return inlineBlock(key, inlineNodeSegments(nodeChildren(node), theme, theme.heading[level - 1], {
|
|
1210
|
+
imageResolver: context.imageResolver,
|
|
1211
|
+
imageSize: context.imageSize
|
|
1212
|
+
}), context);
|
|
1213
|
+
}
|
|
1214
|
+
function blockFromCodeBlock(node, key, context, theme) {
|
|
1215
|
+
return {
|
|
1216
|
+
type: "code_block",
|
|
1217
|
+
key,
|
|
1218
|
+
language: codeBlockLanguage(node),
|
|
1219
|
+
lines: sanitizeCodeBlockText(stringProp(node, "code")).split("\n"),
|
|
1220
|
+
style: theme.codeBlock,
|
|
1221
|
+
prefixSegments: context.prefixSegments,
|
|
1222
|
+
continuationPrefixSegments: context.continuationPrefixSegments
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function blockFromHtmlBlock(node, key, context, theme) {
|
|
1226
|
+
return inlineBlock(key, inlineNodeSegments([{
|
|
1227
|
+
type: "html_block",
|
|
1228
|
+
raw: stringProp(node, "raw"),
|
|
1229
|
+
content: stringProp(node, "content")
|
|
1230
|
+
}], theme, void 0, { imageResolver: context.imageResolver }), context);
|
|
1231
|
+
}
|
|
1232
|
+
function tableRows$1(node, key) {
|
|
1233
|
+
const value = node[key];
|
|
1234
|
+
if (key === "header") return value && typeof value === "object" ? [value] : [];
|
|
1235
|
+
return Array.isArray(value) ? value : [];
|
|
1236
|
+
}
|
|
1237
|
+
function tableCells(row) {
|
|
1238
|
+
const value = row.cells;
|
|
1239
|
+
return Array.isArray(value) ? value : [];
|
|
1240
|
+
}
|
|
1241
|
+
function tableCellAlign(cell) {
|
|
1242
|
+
const align = stringProp(cell, "align");
|
|
1243
|
+
return align === "center" || align === "right" || align === "left" ? align : void 0;
|
|
1244
|
+
}
|
|
1245
|
+
function blockFromTable(node, key, context, theme) {
|
|
1246
|
+
const headerRow = tableRows$1(node, "header")[0];
|
|
1247
|
+
const header = tableCells(headerRow ?? { type: "table_row" }).map((cell) => ({
|
|
1248
|
+
segments: inlineNodeSegments(nodeChildren(cell), theme, theme.strong, {
|
|
1249
|
+
imageResolver: context.imageResolver,
|
|
1250
|
+
imageSize: context.imageSize
|
|
1251
|
+
}),
|
|
1252
|
+
align: tableCellAlign(cell)
|
|
1253
|
+
}));
|
|
1254
|
+
const rows = tableRows$1(node, "rows").map((row) => tableCells(row).map((cell) => ({
|
|
1255
|
+
segments: inlineNodeSegments(nodeChildren(cell), theme, void 0, {
|
|
1256
|
+
imageResolver: context.imageResolver,
|
|
1257
|
+
imageSize: context.imageSize
|
|
1258
|
+
}),
|
|
1259
|
+
align: tableCellAlign(cell)
|
|
1260
|
+
})));
|
|
1261
|
+
return {
|
|
1262
|
+
type: "table",
|
|
1263
|
+
key,
|
|
1264
|
+
header,
|
|
1265
|
+
rows,
|
|
1266
|
+
borderStyle: theme.thematicBreak,
|
|
1267
|
+
prefixSegments: context.prefixSegments
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
function childSequenceToBlocks(nodes, context, theme, keyPrefix) {
|
|
1271
|
+
const out = [];
|
|
1272
|
+
let appended = false;
|
|
1273
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1274
|
+
const childBlocks = trimBlockArray(nodeToBlocks(nodes[i], context, theme, `${keyPrefix}-${i}`));
|
|
1275
|
+
if (!childBlocks.length) continue;
|
|
1276
|
+
if (appended && out[out.length - 1]?.type !== "blank" && childBlocks[0]?.type !== "blank") out.push({
|
|
1277
|
+
type: "blank",
|
|
1278
|
+
key: `${keyPrefix}-${i}-gap`
|
|
1279
|
+
});
|
|
1280
|
+
out.push(...childBlocks);
|
|
1281
|
+
appended = true;
|
|
1282
|
+
}
|
|
1283
|
+
return out;
|
|
1284
|
+
}
|
|
1285
|
+
function listItemBlocks(item, index, ordered, start, context, theme, keyPrefix) {
|
|
1286
|
+
const markerText = ordered ? `${start + index}. ` : "- ";
|
|
1287
|
+
const markerCells = textCellWidth(markerText);
|
|
1288
|
+
const markerSegment = {
|
|
1289
|
+
text: markerText,
|
|
1290
|
+
style: theme.listMarker
|
|
1291
|
+
};
|
|
1292
|
+
const indentSegment = { text: spaces(markerCells) };
|
|
1293
|
+
const firstContext = {
|
|
1294
|
+
prefixSegments: [...context.prefixSegments, markerSegment],
|
|
1295
|
+
continuationPrefixSegments: [...context.continuationPrefixSegments, indentSegment]
|
|
1296
|
+
};
|
|
1297
|
+
const nestedContext = {
|
|
1298
|
+
prefixSegments: [...context.continuationPrefixSegments, indentSegment],
|
|
1299
|
+
continuationPrefixSegments: [...context.continuationPrefixSegments, indentSegment]
|
|
1300
|
+
};
|
|
1301
|
+
const children = nodeChildren(item);
|
|
1302
|
+
if (!children.length) return [inlineBlock(`${keyPrefix}-empty`, [], firstContext)];
|
|
1303
|
+
const out = [];
|
|
1304
|
+
let usedLead = false;
|
|
1305
|
+
for (let i = 0; i < children.length; i++) {
|
|
1306
|
+
const child = children[i];
|
|
1307
|
+
const useLeadContext = !usedLead && (child.type === "paragraph" || child.type === "heading" || child.type === "code_block");
|
|
1308
|
+
if (!usedLead && !useLeadContext) {
|
|
1309
|
+
out.push(inlineBlock(`${keyPrefix}-marker`, [], firstContext));
|
|
1310
|
+
usedLead = true;
|
|
1311
|
+
}
|
|
1312
|
+
const childBlocks = trimBlockArray(nodeToBlocks(child, useLeadContext && !usedLead ? firstContext : nestedContext, theme, `${keyPrefix}-${i}`));
|
|
1313
|
+
if (!childBlocks.length) continue;
|
|
1314
|
+
const startsNestedList = child.type === "list" && out.length > 0 && out[out.length - 1]?.type === "inline" && childBlocks[0]?.type === "inline";
|
|
1315
|
+
if (out.length && out[out.length - 1]?.type !== "blank" && childBlocks[0]?.type !== "blank" && !startsNestedList) out.push({
|
|
1316
|
+
type: "blank",
|
|
1317
|
+
key: `${keyPrefix}-${i}-gap`
|
|
1318
|
+
});
|
|
1319
|
+
out.push(...childBlocks);
|
|
1320
|
+
usedLead = true;
|
|
1321
|
+
}
|
|
1322
|
+
if (!out.length) out.push(inlineBlock(`${keyPrefix}-fallback`, [], firstContext));
|
|
1323
|
+
return out;
|
|
1324
|
+
}
|
|
1325
|
+
function listBlocks(node, context, theme, keyPrefix) {
|
|
1326
|
+
const out = [];
|
|
1327
|
+
const items = nodeItems(node);
|
|
1328
|
+
for (let i = 0; i < items.length; i++) out.push(...listItemBlocks(items[i], i, booleanProp(node, "ordered"), numberProp(node, "start") ?? 1, context, theme, `${keyPrefix}-${i}`));
|
|
1329
|
+
return out;
|
|
1330
|
+
}
|
|
1331
|
+
function nodeToBlocks(node, context, theme, keyPrefix) {
|
|
1332
|
+
switch (node.type) {
|
|
1333
|
+
case "paragraph":
|
|
1334
|
+
if ("children" in node && Array.isArray(node.children)) return [blockFromParagraph(node, keyPrefix, context, theme)];
|
|
1335
|
+
break;
|
|
1336
|
+
case "heading":
|
|
1337
|
+
if ("children" in node && Array.isArray(node.children) && "level" in node) return [blockFromHeading(node, keyPrefix, context, theme)];
|
|
1338
|
+
break;
|
|
1339
|
+
case "code_block":
|
|
1340
|
+
if ("code" in node) return [blockFromCodeBlock(node, keyPrefix, context, theme)];
|
|
1341
|
+
break;
|
|
1342
|
+
case "thematic_break": return [{
|
|
1343
|
+
type: "thematic_break",
|
|
1344
|
+
key: keyPrefix,
|
|
1345
|
+
char: "─",
|
|
1346
|
+
style: theme.thematicBreak,
|
|
1347
|
+
prefixSegments: context.prefixSegments
|
|
1348
|
+
}];
|
|
1349
|
+
case "blockquote": {
|
|
1350
|
+
const quoteSegment = {
|
|
1351
|
+
text: "│ ",
|
|
1352
|
+
style: theme.blockquote
|
|
1353
|
+
};
|
|
1354
|
+
const quoteContext = {
|
|
1355
|
+
prefixSegments: [...context.prefixSegments, quoteSegment],
|
|
1356
|
+
continuationPrefixSegments: [...context.continuationPrefixSegments, quoteSegment],
|
|
1357
|
+
imageResolver: context.imageResolver,
|
|
1358
|
+
imageSize: context.imageSize
|
|
1359
|
+
};
|
|
1360
|
+
return childSequenceToBlocks(nodeChildren(node), quoteContext, theme, keyPrefix);
|
|
1361
|
+
}
|
|
1362
|
+
case "list":
|
|
1363
|
+
if ("items" in node && Array.isArray(node.items)) return listBlocks(node, context, theme, keyPrefix);
|
|
1364
|
+
break;
|
|
1365
|
+
case "table": return [blockFromTable(node, keyPrefix, context, theme)];
|
|
1366
|
+
case "inline": return [inlineBlock(keyPrefix, inlineNodeSegments(nodeChildren(node), theme, void 0, {
|
|
1367
|
+
imageResolver: context.imageResolver,
|
|
1368
|
+
imageSize: context.imageSize
|
|
1369
|
+
}), context)];
|
|
1370
|
+
case "html_block":
|
|
1371
|
+
if ("content" in node) return [blockFromHtmlBlock(node, keyPrefix, context, theme)];
|
|
1372
|
+
break;
|
|
1373
|
+
case "text": return [inlineBlock(keyPrefix, inlineNodeSegments([node], theme, void 0, { imageResolver: context.imageResolver }), context)];
|
|
1374
|
+
}
|
|
1375
|
+
if (nodeChildren(node).length) return childSequenceToBlocks(nodeChildren(node), context, theme, keyPrefix);
|
|
1376
|
+
return [inlineBlock(keyPrefix, inlineNodeSegments([node], theme, void 0, { imageResolver: context.imageResolver }), context)];
|
|
1377
|
+
}
|
|
1378
|
+
function markdownAstToBlocks(nodes, theme, options = {}) {
|
|
1379
|
+
const blocks = trimBlockArray(childSequenceToBlocks(nodes, {
|
|
1380
|
+
prefixSegments: EMPTY_PREFIX,
|
|
1381
|
+
continuationPrefixSegments: EMPTY_PREFIX,
|
|
1382
|
+
imageResolver: options.imageResolver,
|
|
1383
|
+
imageSize: options.imageSize
|
|
1384
|
+
}, theme, "md"));
|
|
1385
|
+
return blocks.length ? blocks : [{
|
|
1386
|
+
type: "blank",
|
|
1387
|
+
key: "md-empty"
|
|
1388
|
+
}];
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
//#endregion
|
|
1392
|
+
//#region src/vue/markdown/layout.ts
|
|
1393
|
+
function toVisualSegment(segment) {
|
|
1394
|
+
if (!segment.text) return null;
|
|
1395
|
+
const cells = textCellWidth(segment.text);
|
|
1396
|
+
if (cells <= 0) return null;
|
|
1397
|
+
return {
|
|
1398
|
+
text: segment.text,
|
|
1399
|
+
style: segment.style,
|
|
1400
|
+
cells,
|
|
1401
|
+
...segment.graphic ? { graphic: segment.graphic } : {},
|
|
1402
|
+
...segment.mathAction ? { mathAction: segment.mathAction } : {}
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function segmentsPlainText(segments) {
|
|
1406
|
+
return segments.map((segment) => segment.fallbackText ?? segment.text).join("");
|
|
1407
|
+
}
|
|
1408
|
+
function segmentsVisualHeight(segments) {
|
|
1409
|
+
let height = 1;
|
|
1410
|
+
for (const segment of segments) {
|
|
1411
|
+
if (!segment.graphic) continue;
|
|
1412
|
+
height = Math.max(height, Math.max(1, Math.floor(segment.graphic.displayHeight ?? 1)));
|
|
1413
|
+
}
|
|
1414
|
+
return height;
|
|
1415
|
+
}
|
|
1416
|
+
function segmentsCellWidth(segments) {
|
|
1417
|
+
let cells = 0;
|
|
1418
|
+
for (const segment of segments) {
|
|
1419
|
+
if (segment.hardBreak || !segment.text) continue;
|
|
1420
|
+
cells += textCellWidth(segment.text);
|
|
1421
|
+
}
|
|
1422
|
+
return cells;
|
|
1423
|
+
}
|
|
1424
|
+
function normalizeInlineSegments(segments) {
|
|
1425
|
+
const lines = [[]];
|
|
1426
|
+
for (const segment of segments) {
|
|
1427
|
+
if (segment.hardBreak) {
|
|
1428
|
+
lines.push([]);
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
lines[lines.length - 1].push(segment);
|
|
1432
|
+
}
|
|
1433
|
+
return lines;
|
|
1434
|
+
}
|
|
1435
|
+
function clipInlineSegmentsToWidth(segments, width) {
|
|
1436
|
+
let remaining = Math.max(0, Math.floor(width));
|
|
1437
|
+
if (remaining <= 0) return [];
|
|
1438
|
+
const out = [];
|
|
1439
|
+
for (const segment of segments) {
|
|
1440
|
+
if (segment.hardBreak || !segment.text || remaining <= 0) continue;
|
|
1441
|
+
const piece = sliceByCellsRange(segment.text, 0, remaining);
|
|
1442
|
+
const cells = textCellWidth(piece);
|
|
1443
|
+
if (!piece || cells <= 0) continue;
|
|
1444
|
+
const clipped = {
|
|
1445
|
+
text: piece,
|
|
1446
|
+
...segment.style ? { style: segment.style } : {},
|
|
1447
|
+
...segment.mathAction ? { mathAction: segment.mathAction } : {}
|
|
1448
|
+
};
|
|
1449
|
+
out.push(piece === segment.text && segment.graphic ? {
|
|
1450
|
+
...clipped,
|
|
1451
|
+
graphic: segment.graphic
|
|
1452
|
+
} : clipped);
|
|
1453
|
+
remaining -= cells;
|
|
1454
|
+
}
|
|
1455
|
+
return out;
|
|
1456
|
+
}
|
|
1457
|
+
function clipVisualSegmentsToWidth(segments, width) {
|
|
1458
|
+
let remaining = Math.max(0, Math.floor(width));
|
|
1459
|
+
if (remaining <= 0) return [];
|
|
1460
|
+
const out = [];
|
|
1461
|
+
for (const segment of segments) {
|
|
1462
|
+
if (!segment.text || remaining <= 0) continue;
|
|
1463
|
+
if (segment.cells <= remaining) {
|
|
1464
|
+
out.push(segment);
|
|
1465
|
+
remaining -= segment.cells;
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
const text = sliceByCellsRange(segment.text, 0, remaining);
|
|
1469
|
+
const cells = textCellWidth(text);
|
|
1470
|
+
if (!text || cells <= 0) continue;
|
|
1471
|
+
out.push({
|
|
1472
|
+
text,
|
|
1473
|
+
style: segment.style,
|
|
1474
|
+
cells,
|
|
1475
|
+
...text === segment.text && segment.graphic ? { graphic: segment.graphic } : {},
|
|
1476
|
+
...segment.mathAction ? { mathAction: segment.mathAction } : {}
|
|
1477
|
+
});
|
|
1478
|
+
remaining -= cells;
|
|
1479
|
+
}
|
|
1480
|
+
return out;
|
|
1481
|
+
}
|
|
1482
|
+
function wrapLineSegments(source, prefixSegments, continuationPrefixSegments, width) {
|
|
1483
|
+
const rows = [];
|
|
1484
|
+
const prefixCells = segmentsCellWidth(prefixSegments);
|
|
1485
|
+
const continuationPrefixCells = segmentsCellWidth(continuationPrefixSegments);
|
|
1486
|
+
const firstPrefix = prefixCells >= width ? clipInlineSegmentsToWidth(prefixSegments, width) : prefixSegments;
|
|
1487
|
+
const firstPrefixCells = segmentsCellWidth(firstPrefix);
|
|
1488
|
+
const continuationPrefix = continuationPrefixCells >= width ? clipInlineSegmentsToWidth(continuationPrefixSegments, Math.max(0, width - 1)) : continuationPrefixSegments;
|
|
1489
|
+
const trimmedContinuationPrefixCells = segmentsCellWidth(continuationPrefix);
|
|
1490
|
+
const maxFirst = Math.max(0, width - firstPrefixCells);
|
|
1491
|
+
const maxNext = Math.max(0, width - trimmedContinuationPrefixCells);
|
|
1492
|
+
const openRow = (useContinuation) => {
|
|
1493
|
+
const prefix = useContinuation ? continuationPrefix : firstPrefix;
|
|
1494
|
+
const renderedPrefix = prefix.map(toVisualSegment).filter(Boolean);
|
|
1495
|
+
return {
|
|
1496
|
+
useContinuation,
|
|
1497
|
+
segments: [...renderedPrefix],
|
|
1498
|
+
remaining: useContinuation ? maxNext : maxFirst
|
|
1499
|
+
};
|
|
1500
|
+
};
|
|
1501
|
+
const openDegradedRow = () => ({
|
|
1502
|
+
useContinuation: true,
|
|
1503
|
+
segments: [],
|
|
1504
|
+
remaining: width
|
|
1505
|
+
});
|
|
1506
|
+
let row = openRow(false);
|
|
1507
|
+
if (!source.length) {
|
|
1508
|
+
rows.push(row.segments);
|
|
1509
|
+
return rows;
|
|
1510
|
+
}
|
|
1511
|
+
const pushRow = () => {
|
|
1512
|
+
rows.push(row.segments);
|
|
1513
|
+
row = openRow(true);
|
|
1514
|
+
};
|
|
1515
|
+
const prefixLengthForRow = () => row.useContinuation ? continuationPrefix.length : firstPrefix.length;
|
|
1516
|
+
const rowHasBody = () => row.segments.length > prefixLengthForRow();
|
|
1517
|
+
for (const segment of source) {
|
|
1518
|
+
if (segment.graphic) {
|
|
1519
|
+
const displayCells = Math.max(1, Math.floor(segment.graphic.displayWidth ?? 1));
|
|
1520
|
+
if (row.remaining <= 0 && !rowHasBody()) {
|
|
1521
|
+
if (!row.useContinuation) {
|
|
1522
|
+
rows.push(row.segments);
|
|
1523
|
+
row = openDegradedRow();
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
const cells = Math.min(displayCells, Math.max(1, row.remaining || width));
|
|
1527
|
+
row.segments.push({
|
|
1528
|
+
text: spaces(cells),
|
|
1529
|
+
style: segment.style,
|
|
1530
|
+
cells,
|
|
1531
|
+
graphic: segment.graphic,
|
|
1532
|
+
fallbackText: segment.text
|
|
1533
|
+
});
|
|
1534
|
+
row.remaining -= cells;
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
let aborted = false;
|
|
1538
|
+
forEachTextCellSegment(segment.text, (piece) => {
|
|
1539
|
+
while (true) {
|
|
1540
|
+
if (row.remaining <= 0) {
|
|
1541
|
+
pushRow();
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
if (piece.cells > row.remaining) {
|
|
1545
|
+
if (rowHasBody()) {
|
|
1546
|
+
pushRow();
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
if (!row.useContinuation) {
|
|
1550
|
+
rows.push(row.segments);
|
|
1551
|
+
row = openDegradedRow();
|
|
1552
|
+
continue;
|
|
1553
|
+
}
|
|
1554
|
+
if (piece.cells <= width) {
|
|
1555
|
+
row = openDegradedRow();
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
aborted = true;
|
|
1559
|
+
return false;
|
|
1560
|
+
}
|
|
1561
|
+
row.segments.push({
|
|
1562
|
+
text: piece.text,
|
|
1563
|
+
style: segment.style,
|
|
1564
|
+
cells: piece.cells,
|
|
1565
|
+
...segment.mathAction ? { mathAction: segment.mathAction } : {}
|
|
1566
|
+
});
|
|
1567
|
+
row.remaining -= piece.cells;
|
|
1568
|
+
return void 0;
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
if (aborted) continue;
|
|
1572
|
+
}
|
|
1573
|
+
rows.push(row.segments);
|
|
1574
|
+
return rows;
|
|
1575
|
+
}
|
|
1576
|
+
function inlineBlockRows(block, width) {
|
|
1577
|
+
const rows = [];
|
|
1578
|
+
const lines = normalizeInlineSegments(block.segments);
|
|
1579
|
+
const prefix = block.prefixSegments ?? [];
|
|
1580
|
+
const continuationPrefix = block.continuationPrefixSegments ?? prefix;
|
|
1581
|
+
let rowInBlock = 0;
|
|
1582
|
+
for (const line of lines) {
|
|
1583
|
+
const wrapped = wrapLineSegments(line, prefix, continuationPrefix, width);
|
|
1584
|
+
for (const visualSegments of wrapped) {
|
|
1585
|
+
rows.push({
|
|
1586
|
+
key: `${block.key}:${rowInBlock}`,
|
|
1587
|
+
blockKey: block.key,
|
|
1588
|
+
rowInBlock,
|
|
1589
|
+
plainText: segmentsPlainText(visualSegments),
|
|
1590
|
+
segments: visualSegments
|
|
1591
|
+
});
|
|
1592
|
+
rowInBlock++;
|
|
1593
|
+
const height = segmentsVisualHeight(visualSegments);
|
|
1594
|
+
for (let extra = 1; extra < height; extra++) {
|
|
1595
|
+
rows.push({
|
|
1596
|
+
key: `${block.key}:${rowInBlock}`,
|
|
1597
|
+
blockKey: block.key,
|
|
1598
|
+
rowInBlock,
|
|
1599
|
+
plainText: "",
|
|
1600
|
+
segments: []
|
|
1601
|
+
});
|
|
1602
|
+
rowInBlock++;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return rows.length ? rows : [{
|
|
1607
|
+
key: `${block.key}:0`,
|
|
1608
|
+
blockKey: block.key,
|
|
1609
|
+
rowInBlock: 0,
|
|
1610
|
+
plainText: "",
|
|
1611
|
+
segments: prefix.map(toVisualSegment).filter(Boolean)
|
|
1612
|
+
}];
|
|
1613
|
+
}
|
|
1614
|
+
function codeBlockRows(block, width) {
|
|
1615
|
+
const rows = [];
|
|
1616
|
+
const prefix = block.prefixSegments ?? [];
|
|
1617
|
+
const continuationPrefix = block.continuationPrefixSegments ?? prefix;
|
|
1618
|
+
const lineSegments = block.lines.length ? block.lines : [""];
|
|
1619
|
+
let rowInBlock = 0;
|
|
1620
|
+
for (const line of lineSegments) {
|
|
1621
|
+
const sourceSegments = [{
|
|
1622
|
+
text: line,
|
|
1623
|
+
style: block.style
|
|
1624
|
+
}];
|
|
1625
|
+
const wrapped = wrapLineSegments(sourceSegments, prefix, continuationPrefix, width);
|
|
1626
|
+
for (const visualSegments of wrapped) {
|
|
1627
|
+
rows.push({
|
|
1628
|
+
key: `${block.key}:${rowInBlock}`,
|
|
1629
|
+
blockKey: block.key,
|
|
1630
|
+
rowInBlock,
|
|
1631
|
+
plainText: segmentsPlainText(visualSegments),
|
|
1632
|
+
segments: visualSegments
|
|
1633
|
+
});
|
|
1634
|
+
rowInBlock++;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return rows;
|
|
1638
|
+
}
|
|
1639
|
+
function thematicBreakRows(block, width) {
|
|
1640
|
+
const prefix = (block.prefixSegments ?? []).map(toVisualSegment).filter(Boolean);
|
|
1641
|
+
const prefixCells = prefix.reduce((sum, segment) => sum + segment.cells, 0);
|
|
1642
|
+
const fill = Math.max(0, width - prefixCells);
|
|
1643
|
+
const bar = fill > 0 ? [{
|
|
1644
|
+
text: repeatChar(block.char ?? "─", fill),
|
|
1645
|
+
style: block.style,
|
|
1646
|
+
cells: fill
|
|
1647
|
+
}] : [];
|
|
1648
|
+
return [{
|
|
1649
|
+
key: `${block.key}:0`,
|
|
1650
|
+
blockKey: block.key,
|
|
1651
|
+
rowInBlock: 0,
|
|
1652
|
+
plainText: segmentsPlainText([...prefix, ...bar]),
|
|
1653
|
+
segments: [...prefix, ...bar]
|
|
1654
|
+
}];
|
|
1655
|
+
}
|
|
1656
|
+
function tableColumnCount(block) {
|
|
1657
|
+
let columns = block.header.length;
|
|
1658
|
+
for (const row of block.rows) columns = Math.max(columns, row.length);
|
|
1659
|
+
return columns;
|
|
1660
|
+
}
|
|
1661
|
+
function tableColumnWidths(block, columns, width) {
|
|
1662
|
+
const widths = Array.from({ length: columns }, () => 0);
|
|
1663
|
+
const measureRow = (row) => {
|
|
1664
|
+
for (let index = 0; index < columns; index++) widths[index] = Math.max(widths[index] ?? 0, segmentsCellWidth(row[index]?.segments ?? []));
|
|
1665
|
+
};
|
|
1666
|
+
measureRow(block.header);
|
|
1667
|
+
for (const row of block.rows) measureRow(row);
|
|
1668
|
+
const prefixCells = segmentsCellWidth(block.prefixSegments ?? []);
|
|
1669
|
+
const available = Math.max(0, width - prefixCells - (columns * 3 + 1));
|
|
1670
|
+
for (let index = 0; index < widths.length; index++) widths[index] = Math.min(widths[index] ?? 0, available);
|
|
1671
|
+
let total = widths.reduce((sum, item) => sum + item, 0);
|
|
1672
|
+
while (total > available) {
|
|
1673
|
+
let widestIndex = 0;
|
|
1674
|
+
for (let index = 1; index < widths.length; index++) if ((widths[index] ?? 0) > (widths[widestIndex] ?? 0)) widestIndex = index;
|
|
1675
|
+
if ((widths[widestIndex] ?? 0) <= 0) break;
|
|
1676
|
+
widths[widestIndex]--;
|
|
1677
|
+
total--;
|
|
1678
|
+
}
|
|
1679
|
+
return widths;
|
|
1680
|
+
}
|
|
1681
|
+
function appendVisualSegment(segments, text, style, graphic, mathAction) {
|
|
1682
|
+
if (!text) return;
|
|
1683
|
+
const cells = textCellWidth(text);
|
|
1684
|
+
if (cells <= 0) return;
|
|
1685
|
+
const prev = segments[segments.length - 1];
|
|
1686
|
+
if (prev && prev.style === style && prev.mathAction === mathAction && !prev.graphic && !graphic) {
|
|
1687
|
+
segments[segments.length - 1] = {
|
|
1688
|
+
text: `${prev.text}${text}`,
|
|
1689
|
+
style,
|
|
1690
|
+
cells: prev.cells + cells,
|
|
1691
|
+
...mathAction ? { mathAction } : {}
|
|
1692
|
+
};
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
segments.push({
|
|
1696
|
+
text,
|
|
1697
|
+
style,
|
|
1698
|
+
cells,
|
|
1699
|
+
...graphic ? { graphic } : {},
|
|
1700
|
+
...mathAction ? { mathAction } : {}
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
function appendInlineVisualSegments(out, segments) {
|
|
1704
|
+
for (const segment of segments) {
|
|
1705
|
+
if (segment.hardBreak) {
|
|
1706
|
+
appendVisualSegment(out, " ");
|
|
1707
|
+
continue;
|
|
1708
|
+
}
|
|
1709
|
+
appendVisualSegment(out, segment.text, segment.style, segment.graphic, segment.mathAction);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
function tableBorderRow(block, widths, width, rowInBlock, left, join, right) {
|
|
1713
|
+
const prefix = (block.prefixSegments ?? []).map(toVisualSegment).filter(Boolean);
|
|
1714
|
+
const segments = [...prefix];
|
|
1715
|
+
let cells = prefix.reduce((sum, segment) => sum + segment.cells, 0);
|
|
1716
|
+
const append = (text) => {
|
|
1717
|
+
if (cells >= width) return;
|
|
1718
|
+
appendVisualSegment(segments, text, block.borderStyle);
|
|
1719
|
+
cells += textCellWidth(text);
|
|
1720
|
+
};
|
|
1721
|
+
append(left);
|
|
1722
|
+
for (let index = 0; index < widths.length && cells < width; index++) {
|
|
1723
|
+
append(repeatChar("─", (widths[index] ?? 0) + 2));
|
|
1724
|
+
append(index === widths.length - 1 ? right : join);
|
|
1725
|
+
}
|
|
1726
|
+
const clipped = clipVisualSegmentsToWidth(segments, width);
|
|
1727
|
+
return {
|
|
1728
|
+
key: `${block.key}:${rowInBlock}`,
|
|
1729
|
+
blockKey: block.key,
|
|
1730
|
+
rowInBlock,
|
|
1731
|
+
plainText: segmentsPlainText(clipped),
|
|
1732
|
+
segments: clipped
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
function tableContentRow(block, row, widths, width, rowInBlock) {
|
|
1736
|
+
const segments = (block.prefixSegments ?? []).map(toVisualSegment).filter(Boolean);
|
|
1737
|
+
let cells = segments.reduce((sum, segment) => sum + segment.cells, 0);
|
|
1738
|
+
const append = (text, style) => {
|
|
1739
|
+
if (cells >= width) return;
|
|
1740
|
+
appendVisualSegment(segments, text, style);
|
|
1741
|
+
cells += textCellWidth(text);
|
|
1742
|
+
};
|
|
1743
|
+
append("│", block.borderStyle);
|
|
1744
|
+
for (let index = 0; index < widths.length && cells < width; index++) {
|
|
1745
|
+
const columnWidth = widths[index] ?? 0;
|
|
1746
|
+
const cell = row[index];
|
|
1747
|
+
const clipped$1 = clipInlineSegmentsToWidth(cell?.segments ?? [], columnWidth);
|
|
1748
|
+
const contentCells = segmentsCellWidth(clipped$1);
|
|
1749
|
+
const remaining = Math.max(0, columnWidth - contentCells);
|
|
1750
|
+
const align = cell?.align ?? "left";
|
|
1751
|
+
const leftPad = align === "right" ? remaining : align === "center" ? Math.floor(remaining / 2) : 0;
|
|
1752
|
+
const rightPad = remaining - leftPad;
|
|
1753
|
+
append(" ");
|
|
1754
|
+
append(repeatChar(" ", leftPad));
|
|
1755
|
+
if (cells < width) {
|
|
1756
|
+
appendInlineVisualSegments(segments, clipped$1);
|
|
1757
|
+
cells += contentCells;
|
|
1758
|
+
}
|
|
1759
|
+
append(repeatChar(" ", rightPad));
|
|
1760
|
+
append(" ");
|
|
1761
|
+
append("│", block.borderStyle);
|
|
1762
|
+
}
|
|
1763
|
+
const clipped = clipVisualSegmentsToWidth(segments, width);
|
|
1764
|
+
return {
|
|
1765
|
+
key: `${block.key}:${rowInBlock}`,
|
|
1766
|
+
blockKey: block.key,
|
|
1767
|
+
rowInBlock,
|
|
1768
|
+
plainText: segmentsPlainText(clipped),
|
|
1769
|
+
segments: clipped
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
function tableRows(block, width) {
|
|
1773
|
+
const columns = tableColumnCount(block);
|
|
1774
|
+
if (columns <= 0) return [];
|
|
1775
|
+
const widths = tableColumnWidths(block, columns, width);
|
|
1776
|
+
const rows = [];
|
|
1777
|
+
rows.push(tableBorderRow(block, widths, width, rows.length, "╭", "┬", "╮"));
|
|
1778
|
+
rows.push(tableContentRow(block, block.header, widths, width, rows.length));
|
|
1779
|
+
rows.push(tableBorderRow(block, widths, width, rows.length, "├", "┼", "┤"));
|
|
1780
|
+
for (const row of block.rows) rows.push(tableContentRow(block, row, widths, width, rows.length));
|
|
1781
|
+
rows.push(tableBorderRow(block, widths, width, rows.length, "╰", "┴", "╯"));
|
|
1782
|
+
return rows;
|
|
1783
|
+
}
|
|
1784
|
+
function styleSignature(style) {
|
|
1785
|
+
if (!style) return "";
|
|
1786
|
+
return [
|
|
1787
|
+
style.fg ?? "",
|
|
1788
|
+
style.bg ?? "",
|
|
1789
|
+
style.bold ? "1" : "0",
|
|
1790
|
+
style.dim ? "1" : "0",
|
|
1791
|
+
style.italic ? "1" : "0",
|
|
1792
|
+
style.underline ? "1" : "0",
|
|
1793
|
+
style.inverse ? "1" : "0",
|
|
1794
|
+
style.href ?? ""
|
|
1795
|
+
].join("");
|
|
1796
|
+
}
|
|
1797
|
+
function inlineSegmentSignature(segment) {
|
|
1798
|
+
const graphic = segment.graphic ? [
|
|
1799
|
+
segment.graphic.kind,
|
|
1800
|
+
segment.graphic.src,
|
|
1801
|
+
segment.graphic.alt ?? "",
|
|
1802
|
+
segment.graphic.mime ?? "",
|
|
1803
|
+
segment.graphic.base64 ?? "",
|
|
1804
|
+
segment.graphic.displayWidth ?? "",
|
|
1805
|
+
segment.graphic.displayHeight ?? ""
|
|
1806
|
+
].join("") : "";
|
|
1807
|
+
const mathAction = segment.mathAction ? [
|
|
1808
|
+
segment.mathAction.source,
|
|
1809
|
+
segment.mathAction.raw,
|
|
1810
|
+
segment.mathAction.rendered ? "1" : "0"
|
|
1811
|
+
].join("") : "";
|
|
1812
|
+
return [
|
|
1813
|
+
segment.text,
|
|
1814
|
+
segment.hardBreak ? "1" : "0",
|
|
1815
|
+
styleSignature(segment.style),
|
|
1816
|
+
graphic,
|
|
1817
|
+
mathAction
|
|
1818
|
+
].join("");
|
|
1819
|
+
}
|
|
1820
|
+
function inlineSegmentsSignature(segments) {
|
|
1821
|
+
return (segments ?? []).map(inlineSegmentSignature).join("");
|
|
1822
|
+
}
|
|
1823
|
+
function blockLayoutSignature(block) {
|
|
1824
|
+
switch (block.type) {
|
|
1825
|
+
case "inline": return [
|
|
1826
|
+
block.type,
|
|
1827
|
+
inlineSegmentsSignature(block.segments),
|
|
1828
|
+
inlineSegmentsSignature(block.prefixSegments),
|
|
1829
|
+
inlineSegmentsSignature(block.continuationPrefixSegments)
|
|
1830
|
+
].join("");
|
|
1831
|
+
case "code_block": return [
|
|
1832
|
+
block.type,
|
|
1833
|
+
block.language ?? "",
|
|
1834
|
+
block.lines.join(""),
|
|
1835
|
+
styleSignature(block.style),
|
|
1836
|
+
inlineSegmentsSignature(block.prefixSegments),
|
|
1837
|
+
inlineSegmentsSignature(block.continuationPrefixSegments)
|
|
1838
|
+
].join("");
|
|
1839
|
+
case "thematic_break": return [
|
|
1840
|
+
block.type,
|
|
1841
|
+
block.char ?? "",
|
|
1842
|
+
styleSignature(block.style),
|
|
1843
|
+
inlineSegmentsSignature(block.prefixSegments)
|
|
1844
|
+
].join("");
|
|
1845
|
+
case "table": return [
|
|
1846
|
+
block.type,
|
|
1847
|
+
inlineSegmentsSignature(block.prefixSegments),
|
|
1848
|
+
styleSignature(block.borderStyle),
|
|
1849
|
+
block.header.map((cell) => `${inlineSegmentsSignature(cell.segments)}\u0005${cell.align ?? ""}`).join(""),
|
|
1850
|
+
block.rows.map((row) => row.map((cell) => `${inlineSegmentsSignature(cell.segments)}\u0005${cell.align ?? ""}`).join("")).join("\x07")
|
|
1851
|
+
].join("");
|
|
1852
|
+
case "blank": return block.type;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
function layoutMarkdownBlock(block, width) {
|
|
1856
|
+
switch (block.type) {
|
|
1857
|
+
case "inline": return inlineBlockRows(block, width);
|
|
1858
|
+
case "code_block": return codeBlockRows(block, width);
|
|
1859
|
+
case "thematic_break": return thematicBreakRows(block, width);
|
|
1860
|
+
case "table": return tableRows(block, width);
|
|
1861
|
+
case "blank": return [{
|
|
1862
|
+
key: `${block.key}:0`,
|
|
1863
|
+
blockKey: block.key,
|
|
1864
|
+
rowInBlock: 0,
|
|
1865
|
+
plainText: "",
|
|
1866
|
+
segments: []
|
|
1867
|
+
}];
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
function layoutMarkdownBlocksCached(blocks, width, previous) {
|
|
1871
|
+
const normalizedWidth = Math.max(0, Math.floor(width));
|
|
1872
|
+
const widthProvider = currentTextWidthProvider();
|
|
1873
|
+
if (normalizedWidth <= 0) return {
|
|
1874
|
+
rows: [],
|
|
1875
|
+
cache: {
|
|
1876
|
+
width: normalizedWidth,
|
|
1877
|
+
widthProvider,
|
|
1878
|
+
entries: new Map()
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
const rows = [];
|
|
1882
|
+
const entries = new Map();
|
|
1883
|
+
const previousEntries = previous?.width === normalizedWidth && Object.is(previous.widthProvider ?? "default", widthProvider) ? previous.entries : void 0;
|
|
1884
|
+
for (let index = 0; index < blocks.length; index++) {
|
|
1885
|
+
const block = blocks[index];
|
|
1886
|
+
const cacheKey = `${index}\u0000${block.key}`;
|
|
1887
|
+
const signature = blockLayoutSignature(block);
|
|
1888
|
+
const cached = previousEntries?.get(cacheKey);
|
|
1889
|
+
const blockRows = cached?.signature === signature ? cached.rows : layoutMarkdownBlock(block, normalizedWidth);
|
|
1890
|
+
if (blockRows.length) {
|
|
1891
|
+
rows.push(...blockRows);
|
|
1892
|
+
entries.set(cacheKey, {
|
|
1893
|
+
signature,
|
|
1894
|
+
rows: blockRows
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
if (!rows.length) {
|
|
1899
|
+
const emptyRows = [{
|
|
1900
|
+
key: "md-empty:0",
|
|
1901
|
+
blockKey: "md-empty",
|
|
1902
|
+
rowInBlock: 0,
|
|
1903
|
+
plainText: "",
|
|
1904
|
+
segments: []
|
|
1905
|
+
}];
|
|
1906
|
+
entries.set("0\0md-empty", {
|
|
1907
|
+
signature: "empty",
|
|
1908
|
+
rows: emptyRows
|
|
1909
|
+
});
|
|
1910
|
+
rows.push(...emptyRows);
|
|
1911
|
+
}
|
|
1912
|
+
return {
|
|
1913
|
+
rows,
|
|
1914
|
+
cache: {
|
|
1915
|
+
width: normalizedWidth,
|
|
1916
|
+
widthProvider,
|
|
1917
|
+
entries
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
function layoutMarkdownBlocks(blocks, width, options) {
|
|
1922
|
+
if (options?.widthProvider !== void 0) return withTextWidthProvider(options.widthProvider, () => layoutMarkdownBlocksCached(blocks, width).rows);
|
|
1923
|
+
return layoutMarkdownBlocksCached(blocks, width).rows;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
//#endregion
|
|
1927
|
+
//#region src/vue/markdown/theme.ts
|
|
1928
|
+
function freezeStyle(style) {
|
|
1929
|
+
return Object.freeze(style);
|
|
1930
|
+
}
|
|
1931
|
+
function mergeFrozenStyle(base, overlay) {
|
|
1932
|
+
if (!overlay) return base;
|
|
1933
|
+
return freezeStyle({
|
|
1934
|
+
...base,
|
|
1935
|
+
...overlay
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
const DEFAULT_HEADING = Object.freeze([
|
|
1939
|
+
freezeStyle({
|
|
1940
|
+
bold: true,
|
|
1941
|
+
fg: "cyanBright"
|
|
1942
|
+
}),
|
|
1943
|
+
freezeStyle({
|
|
1944
|
+
bold: true,
|
|
1945
|
+
fg: "cyanBright"
|
|
1946
|
+
}),
|
|
1947
|
+
freezeStyle({
|
|
1948
|
+
bold: true,
|
|
1949
|
+
fg: "blueBright"
|
|
1950
|
+
}),
|
|
1951
|
+
freezeStyle({
|
|
1952
|
+
bold: true,
|
|
1953
|
+
fg: "blueBright"
|
|
1954
|
+
}),
|
|
1955
|
+
freezeStyle({ bold: true }),
|
|
1956
|
+
freezeStyle({ bold: true })
|
|
1957
|
+
]);
|
|
1958
|
+
const DEFAULT_TUI_MARKDOWN_THEME = Object.freeze({
|
|
1959
|
+
heading: DEFAULT_HEADING,
|
|
1960
|
+
strong: freezeStyle({ bold: true }),
|
|
1961
|
+
emphasis: freezeStyle({ italic: true }),
|
|
1962
|
+
strikethrough: freezeStyle({ dim: true }),
|
|
1963
|
+
inlineCode: freezeStyle({ fg: "yellowBright" }),
|
|
1964
|
+
link: freezeStyle({
|
|
1965
|
+
fg: "blueBright",
|
|
1966
|
+
underline: true
|
|
1967
|
+
}),
|
|
1968
|
+
blockquote: freezeStyle({ dim: true }),
|
|
1969
|
+
listMarker: freezeStyle({
|
|
1970
|
+
fg: "cyanBright",
|
|
1971
|
+
bold: true
|
|
1972
|
+
}),
|
|
1973
|
+
codeBlock: freezeStyle({ fg: "yellowBright" }),
|
|
1974
|
+
thematicBreak: freezeStyle({ dim: true }),
|
|
1975
|
+
html: freezeStyle({ dim: true })
|
|
1976
|
+
});
|
|
1977
|
+
function stableSerialize(value) {
|
|
1978
|
+
if (value == null) return "null";
|
|
1979
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
1980
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
|
|
1981
|
+
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
1982
|
+
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableSerialize(item)}`).join(",")}}`;
|
|
1983
|
+
}
|
|
1984
|
+
function markdownThemeSignature(overrides) {
|
|
1985
|
+
return stableSerialize(overrides ?? null);
|
|
1986
|
+
}
|
|
1987
|
+
function resolveTuiMarkdownTheme(overrides) {
|
|
1988
|
+
if (!overrides) return DEFAULT_TUI_MARKDOWN_THEME;
|
|
1989
|
+
const heading = DEFAULT_TUI_MARKDOWN_THEME.heading.map((style, index) => mergeFrozenStyle(style, overrides.heading?.[index]));
|
|
1990
|
+
return Object.freeze({
|
|
1991
|
+
heading: Object.freeze(heading),
|
|
1992
|
+
strong: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.strong, overrides.strong),
|
|
1993
|
+
emphasis: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.emphasis, overrides.emphasis),
|
|
1994
|
+
strikethrough: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.strikethrough, overrides.strikethrough),
|
|
1995
|
+
inlineCode: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.inlineCode, overrides.inlineCode),
|
|
1996
|
+
link: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.link, overrides.link),
|
|
1997
|
+
blockquote: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.blockquote, overrides.blockquote),
|
|
1998
|
+
listMarker: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.listMarker, overrides.listMarker),
|
|
1999
|
+
codeBlock: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.codeBlock, overrides.codeBlock),
|
|
2000
|
+
thematicBreak: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.thematicBreak, overrides.thematicBreak),
|
|
2001
|
+
html: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.html, overrides.html)
|
|
2002
|
+
});
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
//#endregion
|
|
2006
|
+
//#region src/vue/markdown/document.ts
|
|
2007
|
+
/**
|
|
2008
|
+
* This helper always builds visual rows from the full source string. Components
|
|
2009
|
+
* that need streaming reuse can call buildMarkdownBlocks and cache block layout.
|
|
2010
|
+
*/
|
|
2011
|
+
function buildMarkdownBlocks(content, parser, options) {
|
|
2012
|
+
const theme = resolveTuiMarkdownTheme(options?.theme);
|
|
2013
|
+
try {
|
|
2014
|
+
const nodes = parser.parse(content, options?.final ?? true);
|
|
2015
|
+
const blocks = markdownAstToBlocks(nodes, theme, {
|
|
2016
|
+
imageResolver: options?.imageResolver,
|
|
2017
|
+
imageSize: options?.imageSize
|
|
2018
|
+
});
|
|
2019
|
+
return {
|
|
2020
|
+
nodes,
|
|
2021
|
+
blocks
|
|
2022
|
+
};
|
|
2023
|
+
} catch (error) {
|
|
2024
|
+
console.warn("[vue-tui] Markdown parse failed; falling back to plain text rendering.", error);
|
|
2025
|
+
return {
|
|
2026
|
+
nodes: [],
|
|
2027
|
+
blocks: plainTextFallbackBlocks(content)
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
function buildMarkdownVisualRows(content, width, parser, options) {
|
|
2032
|
+
const { blocks } = buildMarkdownBlocks(content, parser, options);
|
|
2033
|
+
if (options?.widthProvider !== void 0) return layoutMarkdownBlocks(blocks, width, { widthProvider: options.widthProvider });
|
|
2034
|
+
return layoutMarkdownBlocks(blocks, width);
|
|
2035
|
+
}
|
|
2036
|
+
function plainTextFallbackBlocks(content) {
|
|
2037
|
+
const normalized = sanitizeTextBlock(content);
|
|
2038
|
+
const lines = normalized.split("\n");
|
|
2039
|
+
const blocks = [];
|
|
2040
|
+
for (let index = 0; index < lines.length; index++) {
|
|
2041
|
+
const line = sanitizeInlineText(lines[index] ?? "");
|
|
2042
|
+
if (!line) {
|
|
2043
|
+
blocks.push({
|
|
2044
|
+
type: "blank",
|
|
2045
|
+
key: `md-fallback:${index}`
|
|
2046
|
+
});
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
blocks.push({
|
|
2050
|
+
type: "inline",
|
|
2051
|
+
key: `md-fallback:${index}`,
|
|
2052
|
+
segments: [{ text: line }]
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
return blocks.length ? blocks : [{
|
|
2056
|
+
type: "blank",
|
|
2057
|
+
key: "md-fallback:0"
|
|
2058
|
+
}];
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
//#endregion
|
|
2062
|
+
//#region src/vue/markdown/image-actions.ts
|
|
2063
|
+
function imageDisplayWidth(segment) {
|
|
2064
|
+
return Math.max(1, Math.floor(segment.graphic?.displayWidth ?? segment.cells));
|
|
2065
|
+
}
|
|
2066
|
+
function imageDisplayHeight(segment) {
|
|
2067
|
+
return Math.max(1, Math.floor(segment.graphic?.displayHeight ?? 1));
|
|
2068
|
+
}
|
|
2069
|
+
function findMarkdownImageActionAt(rows, point, options) {
|
|
2070
|
+
const r = options.screenRect;
|
|
2071
|
+
if (r.w <= 0 || r.h <= 0) return null;
|
|
2072
|
+
if (point.cellX < r.x || point.cellY < r.y || point.cellX >= r.x + r.w || point.cellY >= r.y + r.h) return null;
|
|
2073
|
+
const clipStart = Math.max(0, Math.floor(options.clipStart));
|
|
2074
|
+
const clipEnd = clipStart + r.w;
|
|
2075
|
+
const firstRow = Math.max(0, Math.floor(options.rowOffset));
|
|
2076
|
+
const lastRow = Math.min(rows.length, firstRow + r.h);
|
|
2077
|
+
for (let rowIndex = firstRow; rowIndex < lastRow; rowIndex++) {
|
|
2078
|
+
const row = rows[rowIndex];
|
|
2079
|
+
if (!row) continue;
|
|
2080
|
+
let logicalX = 0;
|
|
2081
|
+
for (let segmentIndex = 0; segmentIndex < row.segments.length; segmentIndex++) {
|
|
2082
|
+
const segment = row.segments[segmentIndex];
|
|
2083
|
+
const segmentStart = logicalX;
|
|
2084
|
+
const segmentEnd = logicalX + segment.cells;
|
|
2085
|
+
logicalX = segmentEnd;
|
|
2086
|
+
const image = segment.graphic;
|
|
2087
|
+
if (!image) continue;
|
|
2088
|
+
if (segmentEnd <= clipStart || segmentStart >= clipEnd) continue;
|
|
2089
|
+
const imageX = r.x + segmentStart - clipStart;
|
|
2090
|
+
const imageY = r.y + rowIndex - firstRow;
|
|
2091
|
+
const imageRect = {
|
|
2092
|
+
x: imageX,
|
|
2093
|
+
y: imageY,
|
|
2094
|
+
w: imageDisplayWidth(segment),
|
|
2095
|
+
h: imageDisplayHeight(segment)
|
|
2096
|
+
};
|
|
2097
|
+
const visibleLeft = Math.max(r.x, imageRect.x);
|
|
2098
|
+
const visibleTop = Math.max(r.y, imageRect.y);
|
|
2099
|
+
const visibleRight = Math.min(r.x + r.w, imageRect.x + imageRect.w);
|
|
2100
|
+
const visibleBottom = Math.min(r.y + r.h, imageRect.y + imageRect.h);
|
|
2101
|
+
if (point.cellX < visibleLeft || point.cellY < visibleTop || point.cellX >= visibleRight || point.cellY >= visibleBottom) continue;
|
|
2102
|
+
return {
|
|
2103
|
+
image,
|
|
2104
|
+
rect: imageRect,
|
|
2105
|
+
cellX: point.cellX,
|
|
2106
|
+
cellY: point.cellY,
|
|
2107
|
+
rowIndex,
|
|
2108
|
+
segmentIndex
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
//#endregion
|
|
2116
|
+
//#region src/vue/markdown/link-actions.ts
|
|
2117
|
+
function findMarkdownLinkActionAt(rows, point, options) {
|
|
2118
|
+
const r = options.screenRect;
|
|
2119
|
+
if (r.w <= 0 || r.h <= 0) return null;
|
|
2120
|
+
if (point.cellX < r.x || point.cellY < r.y || point.cellX >= r.x + r.w || point.cellY >= r.y + r.h) return null;
|
|
2121
|
+
const clipStart = Math.max(0, Math.floor(options.clipStart));
|
|
2122
|
+
const clipEnd = clipStart + r.w;
|
|
2123
|
+
const firstRow = Math.max(0, Math.floor(options.rowOffset));
|
|
2124
|
+
const lastRow = Math.min(rows.length, firstRow + r.h);
|
|
2125
|
+
for (let rowIndex = firstRow; rowIndex < lastRow; rowIndex++) {
|
|
2126
|
+
const row = rows[rowIndex];
|
|
2127
|
+
if (!row) continue;
|
|
2128
|
+
let logicalX = 0;
|
|
2129
|
+
for (let segmentIndex = 0; segmentIndex < row.segments.length; segmentIndex++) {
|
|
2130
|
+
const segment = row.segments[segmentIndex];
|
|
2131
|
+
const segmentStart = logicalX;
|
|
2132
|
+
const segmentEnd = logicalX + segment.cells;
|
|
2133
|
+
logicalX = segmentEnd;
|
|
2134
|
+
const href = segment.style?.href;
|
|
2135
|
+
if (!href || segmentEnd <= clipStart || segmentStart >= clipEnd) continue;
|
|
2136
|
+
const visibleStart = Math.max(segmentStart, clipStart);
|
|
2137
|
+
const visibleEnd = Math.min(segmentEnd, clipEnd);
|
|
2138
|
+
const rect = {
|
|
2139
|
+
x: r.x + visibleStart - clipStart,
|
|
2140
|
+
y: r.y + rowIndex - firstRow,
|
|
2141
|
+
w: visibleEnd - visibleStart,
|
|
2142
|
+
h: 1
|
|
2143
|
+
};
|
|
2144
|
+
if (point.cellX < rect.x || point.cellY < rect.y || point.cellX >= rect.x + rect.w || point.cellY >= rect.y + rect.h) continue;
|
|
2145
|
+
let groupStart = segmentStart;
|
|
2146
|
+
let groupEnd = segmentEnd;
|
|
2147
|
+
const textParts = [segment.fallbackText ?? segment.text];
|
|
2148
|
+
for (let index = segmentIndex - 1; index >= 0; index--) {
|
|
2149
|
+
const previous = row.segments[index];
|
|
2150
|
+
if (previous.style?.href !== href) break;
|
|
2151
|
+
groupStart -= previous.cells;
|
|
2152
|
+
textParts.unshift(previous.fallbackText ?? previous.text);
|
|
2153
|
+
}
|
|
2154
|
+
for (let index = segmentIndex + 1; index < row.segments.length; index++) {
|
|
2155
|
+
const next = row.segments[index];
|
|
2156
|
+
if (next.style?.href !== href) break;
|
|
2157
|
+
groupEnd += next.cells;
|
|
2158
|
+
textParts.push(next.fallbackText ?? next.text);
|
|
2159
|
+
}
|
|
2160
|
+
const visibleGroupStart = Math.max(groupStart, clipStart);
|
|
2161
|
+
const visibleGroupEnd = Math.min(groupEnd, clipEnd);
|
|
2162
|
+
return {
|
|
2163
|
+
href,
|
|
2164
|
+
text: textParts.join(""),
|
|
2165
|
+
rect: {
|
|
2166
|
+
x: r.x + visibleGroupStart - clipStart,
|
|
2167
|
+
y: rect.y,
|
|
2168
|
+
w: visibleGroupEnd - visibleGroupStart,
|
|
2169
|
+
h: 1
|
|
2170
|
+
},
|
|
2171
|
+
cellX: point.cellX,
|
|
2172
|
+
cellY: point.cellY,
|
|
2173
|
+
rowIndex,
|
|
2174
|
+
segmentIndex
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
return null;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
//#endregion
|
|
2182
|
+
//#region src/vue/markdown/math-actions.ts
|
|
2183
|
+
function findMarkdownMathActionAt(rows, point, options) {
|
|
2184
|
+
const r = options.screenRect;
|
|
2185
|
+
if (r.w <= 0 || r.h <= 0) return null;
|
|
2186
|
+
if (point.cellX < r.x || point.cellY < r.y || point.cellX >= r.x + r.w || point.cellY >= r.y + r.h) return null;
|
|
2187
|
+
const clipStart = Math.max(0, Math.floor(options.clipStart));
|
|
2188
|
+
const clipEnd = clipStart + r.w;
|
|
2189
|
+
const firstRow = Math.max(0, Math.floor(options.rowOffset));
|
|
2190
|
+
const lastRow = Math.min(rows.length, firstRow + r.h);
|
|
2191
|
+
for (let rowIndex = firstRow; rowIndex < lastRow; rowIndex++) {
|
|
2192
|
+
const row = rows[rowIndex];
|
|
2193
|
+
if (!row) continue;
|
|
2194
|
+
let logicalX = 0;
|
|
2195
|
+
for (let segmentIndex = 0; segmentIndex < row.segments.length; segmentIndex++) {
|
|
2196
|
+
const segment = row.segments[segmentIndex];
|
|
2197
|
+
const segmentStart = logicalX;
|
|
2198
|
+
const segmentEnd = logicalX + segment.cells;
|
|
2199
|
+
logicalX = segmentEnd;
|
|
2200
|
+
const math = segment.mathAction;
|
|
2201
|
+
if (!math || segmentEnd <= clipStart || segmentStart >= clipEnd) continue;
|
|
2202
|
+
const visibleStart = Math.max(segmentStart, clipStart);
|
|
2203
|
+
const visibleEnd = Math.min(segmentEnd, clipEnd);
|
|
2204
|
+
const rect = {
|
|
2205
|
+
x: r.x + visibleStart - clipStart,
|
|
2206
|
+
y: r.y + rowIndex - firstRow,
|
|
2207
|
+
w: visibleEnd - visibleStart,
|
|
2208
|
+
h: 1
|
|
2209
|
+
};
|
|
2210
|
+
if (point.cellX < rect.x || point.cellY < rect.y || point.cellX >= rect.x + rect.w || point.cellY >= rect.y + rect.h) continue;
|
|
2211
|
+
return {
|
|
2212
|
+
math,
|
|
2213
|
+
rect,
|
|
2214
|
+
cellX: point.cellX,
|
|
2215
|
+
cellY: point.cellY,
|
|
2216
|
+
rowIndex,
|
|
2217
|
+
segmentIndex
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
return null;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
//#endregion
|
|
2225
|
+
//#region src/vue/markdown/render.ts
|
|
2226
|
+
const mergedStyleCache = new WeakMap();
|
|
2227
|
+
const markdownImageDebugLog = createDebugLogger(isDebugEnabled());
|
|
2228
|
+
const markdownImageDebug = (...parts) => {
|
|
2229
|
+
if (!isDebugEnabled()) return;
|
|
2230
|
+
markdownImageDebugLog.render(`markdown image ${parts.join(" ")}`);
|
|
2231
|
+
};
|
|
2232
|
+
function mergeStyle(base, overlay) {
|
|
2233
|
+
if (!overlay) return base;
|
|
2234
|
+
let bucket = mergedStyleCache.get(base);
|
|
2235
|
+
if (!bucket) {
|
|
2236
|
+
bucket = new WeakMap();
|
|
2237
|
+
mergedStyleCache.set(base, bucket);
|
|
2238
|
+
}
|
|
2239
|
+
const cached = bucket.get(overlay);
|
|
2240
|
+
if (cached) return cached;
|
|
2241
|
+
const merged = Object.freeze({
|
|
2242
|
+
...base,
|
|
2243
|
+
...overlay
|
|
2244
|
+
});
|
|
2245
|
+
bucket.set(overlay, merged);
|
|
2246
|
+
return merged;
|
|
2247
|
+
}
|
|
2248
|
+
const activeMarkdownImageGraphics = new WeakMap();
|
|
2249
|
+
function markdownImageGraphicId(segment, identity) {
|
|
2250
|
+
return `md-image:${stableTerminalGraphicNumericId(`${identity}:${segment.src}:${segment.displayWidth ?? 0}:${segment.displayHeight ?? 0}:${segment.base64 ?? ""}`)}`;
|
|
2251
|
+
}
|
|
2252
|
+
function rememberMarkdownImageGraphic(terminal, id, rect) {
|
|
2253
|
+
let active = activeMarkdownImageGraphics.get(terminal);
|
|
2254
|
+
if (!active) {
|
|
2255
|
+
active = new Map();
|
|
2256
|
+
activeMarkdownImageGraphics.set(terminal, active);
|
|
2257
|
+
}
|
|
2258
|
+
active.set(id, rect);
|
|
2259
|
+
}
|
|
2260
|
+
function clearTrackedMarkdownImageGraphic(terminal, id) {
|
|
2261
|
+
activeMarkdownImageGraphics.get(terminal)?.delete(id);
|
|
2262
|
+
try {
|
|
2263
|
+
getTerminalGraphicsOutput(terminal)?.clear?.(id);
|
|
2264
|
+
} catch {}
|
|
2265
|
+
}
|
|
2266
|
+
function clearStaleMarkdownImageGraphicsForRow(terminal, y, keepIds, keepVisibleIds) {
|
|
2267
|
+
const active = activeMarkdownImageGraphics.get(terminal);
|
|
2268
|
+
if (!active?.size) return;
|
|
2269
|
+
for (const [id, rect] of active) {
|
|
2270
|
+
const intersectsRow = keepVisibleIds ? y >= rect.y && y < rect.y + rect.h : rect.y === y;
|
|
2271
|
+
if (!intersectsRow || keepIds.has(id) || keepVisibleIds?.has(id)) continue;
|
|
2272
|
+
clearTrackedMarkdownImageGraphic(terminal, id);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
function canAttemptMarkdownImageGraphic(terminal, segment) {
|
|
2276
|
+
if (segment.kind !== "image" || !segment.base64) return false;
|
|
2277
|
+
const output = getTerminalGraphicsOutput(terminal);
|
|
2278
|
+
const protocol = output?.capabilities.preferredProtocol;
|
|
2279
|
+
return Boolean(output?.capabilities.supported && isTerminalGraphicsProtocol(protocol));
|
|
2280
|
+
}
|
|
2281
|
+
function clearMarkdownImageGraphics(terminal, rect) {
|
|
2282
|
+
const active = activeMarkdownImageGraphics.get(terminal);
|
|
2283
|
+
if (!active?.size) return;
|
|
2284
|
+
for (const [id, item] of active) {
|
|
2285
|
+
if (rect && (item.x >= rect.x + rect.w || item.x + item.w <= rect.x || item.y >= rect.y + rect.h || item.y + item.h <= rect.y)) continue;
|
|
2286
|
+
clearTrackedMarkdownImageGraphic(terminal, id);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
function queueMarkdownImageGraphic(terminal, segment, identity, rect) {
|
|
2290
|
+
if (segment.kind !== "image" || !segment.base64) return "unavailable";
|
|
2291
|
+
const output = getTerminalGraphicsOutput(terminal);
|
|
2292
|
+
const protocol = output?.capabilities.preferredProtocol;
|
|
2293
|
+
if (!output?.capabilities.supported || !isTerminalGraphicsProtocol(protocol)) return "unavailable";
|
|
2294
|
+
const width = Math.max(1, Math.floor(segment.displayWidth ?? rect.w));
|
|
2295
|
+
const height = Math.max(1, Math.floor(segment.displayHeight ?? 1));
|
|
2296
|
+
const id = markdownImageGraphicId(segment, identity);
|
|
2297
|
+
const imageId = stableTerminalGraphicNumericId(`image:${id}`);
|
|
2298
|
+
const placementId = stableTerminalGraphicNumericId(`placement:${id}`);
|
|
2299
|
+
const sequence = protocol === "kitty" ? createKittyGraphicsSequence(segment.base64, {
|
|
2300
|
+
imageId,
|
|
2301
|
+
placementId,
|
|
2302
|
+
columns: width,
|
|
2303
|
+
rows: height,
|
|
2304
|
+
zIndex: -1
|
|
2305
|
+
}) : protocol === "iterm2" ? createIterm2InlineImageSequence(segment.base64, {
|
|
2306
|
+
width,
|
|
2307
|
+
height,
|
|
2308
|
+
preserveAspectRatio: true,
|
|
2309
|
+
doNotMoveCursor: true
|
|
2310
|
+
}) : "";
|
|
2311
|
+
if (!sequence) return "unavailable";
|
|
2312
|
+
const clearSequence = protocol === "kitty" ? createKittyDeleteGraphicsSequence({
|
|
2313
|
+
imageId,
|
|
2314
|
+
placementId
|
|
2315
|
+
}) : void 0;
|
|
2316
|
+
const resizeSequence = protocol === "kitty" ? createKittyPlacementSequence({
|
|
2317
|
+
imageId,
|
|
2318
|
+
placementId,
|
|
2319
|
+
columns: width,
|
|
2320
|
+
rows: height
|
|
2321
|
+
}) : void 0;
|
|
2322
|
+
const accepted = output.queue({
|
|
2323
|
+
id,
|
|
2324
|
+
x: rect.x,
|
|
2325
|
+
y: rect.y,
|
|
2326
|
+
w: width,
|
|
2327
|
+
h: height,
|
|
2328
|
+
protocol,
|
|
2329
|
+
sequence,
|
|
2330
|
+
resizeSequence,
|
|
2331
|
+
clearSequence,
|
|
2332
|
+
fallbackText: segment.alt ?? "image"
|
|
2333
|
+
});
|
|
2334
|
+
markdownImageDebug(`queue id=${id}`, `accepted=${accepted ? "true" : "false"}`, `x=${rect.x}`, `y=${rect.y}`, `w=${width}`, `h=${height}`, `visibleW=${rect.w}`, `displayW=${segment.displayWidth ?? ""}`, `displayH=${segment.displayHeight ?? ""}`);
|
|
2335
|
+
if (accepted) rememberMarkdownImageGraphic(terminal, id, {
|
|
2336
|
+
x: rect.x,
|
|
2337
|
+
y: rect.y,
|
|
2338
|
+
w: width,
|
|
2339
|
+
h: height
|
|
2340
|
+
});
|
|
2341
|
+
return accepted ? "queued" : "rejected";
|
|
2342
|
+
}
|
|
2343
|
+
function collectVisibleMarkdownImageGraphicIds(rows, options) {
|
|
2344
|
+
const keepIds = new Set();
|
|
2345
|
+
if (options.w <= 0 || options.h <= 0) return keepIds;
|
|
2346
|
+
const clipStart = Math.max(0, Math.floor(options.clipStart));
|
|
2347
|
+
const clipEnd = clipStart + options.w;
|
|
2348
|
+
const firstRow = Math.min(rows.length, Math.max(0, Math.floor(options.rowOffset)));
|
|
2349
|
+
const lastRow = Math.min(rows.length, firstRow + Math.max(0, Math.floor(options.h)));
|
|
2350
|
+
let scanStart = firstRow;
|
|
2351
|
+
while (scanStart > 0 && (rows[scanStart]?.segments.length ?? 0) === 0) scanStart--;
|
|
2352
|
+
for (let rowIndex = scanStart; rowIndex < lastRow; rowIndex++) {
|
|
2353
|
+
const row = rows[rowIndex];
|
|
2354
|
+
if (!row) continue;
|
|
2355
|
+
let logicalX = 0;
|
|
2356
|
+
for (const [segmentIndex, segment] of row.segments.entries()) {
|
|
2357
|
+
const segmentStart = logicalX;
|
|
2358
|
+
const segmentEnd = logicalX + segment.cells;
|
|
2359
|
+
logicalX = segmentEnd;
|
|
2360
|
+
if (!segment.graphic) continue;
|
|
2361
|
+
if (segmentEnd <= clipStart || segmentStart >= clipEnd) continue;
|
|
2362
|
+
if (Math.max(segmentStart, clipStart) !== segmentStart) continue;
|
|
2363
|
+
const height = Math.max(1, Math.floor(segment.graphic.displayHeight ?? 1));
|
|
2364
|
+
if (rowIndex + height <= firstRow || rowIndex >= lastRow) continue;
|
|
2365
|
+
const rect = {
|
|
2366
|
+
x: options.x + segmentStart - clipStart,
|
|
2367
|
+
y: options.y + rowIndex - firstRow,
|
|
2368
|
+
w: Math.max(1, Math.floor(segment.graphic.displayWidth ?? segment.cells)),
|
|
2369
|
+
h: height
|
|
2370
|
+
};
|
|
2371
|
+
if (options.isGraphicCovered?.(rect)) continue;
|
|
2372
|
+
keepIds.add(markdownImageGraphicId(segment.graphic, `${row.key}:${segmentIndex}`));
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
return keepIds;
|
|
2376
|
+
}
|
|
2377
|
+
function paintMarkdownVisualRow(terminal, row, options) {
|
|
2378
|
+
const clipStart = Math.max(0, Math.floor(options.clipStart ?? 0));
|
|
2379
|
+
const clipEnd = clipStart + options.w;
|
|
2380
|
+
if (!row) {
|
|
2381
|
+
if (options.clear !== false) {
|
|
2382
|
+
terminal.write(spaces(options.w), {
|
|
2383
|
+
x: options.x,
|
|
2384
|
+
y: options.y,
|
|
2385
|
+
style: options.baseStyle
|
|
2386
|
+
});
|
|
2387
|
+
clearStaleMarkdownImageGraphicsForRow(terminal, options.y, new Set(), options.keepGraphicIds);
|
|
2388
|
+
}
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
let logicalX = 0;
|
|
2392
|
+
let used = 0;
|
|
2393
|
+
const queuedGraphics = new Set();
|
|
2394
|
+
for (const [segmentIndex, segment] of row.segments.entries()) {
|
|
2395
|
+
if (used >= options.w || !segment.text) {
|
|
2396
|
+
logicalX += segment.cells;
|
|
2397
|
+
continue;
|
|
2398
|
+
}
|
|
2399
|
+
const segmentStart = logicalX;
|
|
2400
|
+
const segmentEnd = logicalX + segment.cells;
|
|
2401
|
+
logicalX += segment.cells;
|
|
2402
|
+
if (segmentEnd <= clipStart || segmentStart >= clipEnd) continue;
|
|
2403
|
+
const segmentStyle = mergeStyle(options.baseStyle, segment.style);
|
|
2404
|
+
if (segment.graphic) {
|
|
2405
|
+
const visibleStart = Math.max(segmentStart, clipStart);
|
|
2406
|
+
const visibleEnd = Math.min(segmentEnd, clipEnd);
|
|
2407
|
+
const visibleCells = visibleEnd - visibleStart;
|
|
2408
|
+
if (visibleCells <= 0) continue;
|
|
2409
|
+
const expectedUsed = visibleStart - clipStart;
|
|
2410
|
+
if (expectedUsed > used) {
|
|
2411
|
+
const pad = Math.min(expectedUsed - used, options.w - used);
|
|
2412
|
+
terminal.write(spaces(pad), {
|
|
2413
|
+
x: options.x + used,
|
|
2414
|
+
y: options.y,
|
|
2415
|
+
style: options.baseStyle
|
|
2416
|
+
});
|
|
2417
|
+
used += pad;
|
|
2418
|
+
}
|
|
2419
|
+
const cellX = options.x + used;
|
|
2420
|
+
const graphicRect = {
|
|
2421
|
+
x: cellX,
|
|
2422
|
+
y: options.y,
|
|
2423
|
+
w: Math.max(1, Math.floor(segment.graphic.displayWidth ?? visibleCells)),
|
|
2424
|
+
h: Math.max(1, Math.floor(segment.graphic.displayHeight ?? 1))
|
|
2425
|
+
};
|
|
2426
|
+
const graphicCovered = options.isGraphicCovered?.(graphicRect) === true;
|
|
2427
|
+
const queueResult = !graphicCovered && visibleStart === segmentStart && visibleCells > 0 && queueMarkdownImageGraphic(terminal, segment.graphic, `${row.key}:${segmentIndex}`, {
|
|
2428
|
+
x: cellX,
|
|
2429
|
+
y: options.y,
|
|
2430
|
+
w: visibleCells
|
|
2431
|
+
});
|
|
2432
|
+
const graphicAvailable = queueResult || canAttemptMarkdownImageGraphic(terminal, segment.graphic);
|
|
2433
|
+
const queuedGraphic = queueResult === "queued";
|
|
2434
|
+
const suppressFallback = queuedGraphic || queueResult === "rejected" || graphicAvailable === true;
|
|
2435
|
+
if (queuedGraphic) queuedGraphics.add(markdownImageGraphicId(segment.graphic, `${row.key}:${segmentIndex}`));
|
|
2436
|
+
terminal.write(suppressFallback ? spaces(visibleCells) : sliceByCellsRange(segment.fallbackText ?? segment.graphic.alt ?? "image", 0, visibleCells) || spaces(visibleCells), {
|
|
2437
|
+
x: cellX,
|
|
2438
|
+
y: options.y,
|
|
2439
|
+
style: suppressFallback ? options.baseStyle : segmentStyle
|
|
2440
|
+
});
|
|
2441
|
+
used += visibleCells;
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
let pieceStart = segmentStart;
|
|
2445
|
+
forEachTextCellSegment(segment.text, (piece) => {
|
|
2446
|
+
if (used >= options.w) return false;
|
|
2447
|
+
const pieceEnd = pieceStart + piece.cells;
|
|
2448
|
+
const visibleStart = Math.max(pieceStart, clipStart);
|
|
2449
|
+
const visibleEnd = Math.min(pieceEnd, clipEnd);
|
|
2450
|
+
pieceStart = pieceEnd;
|
|
2451
|
+
if (visibleEnd <= visibleStart) return;
|
|
2452
|
+
const expectedUsed = visibleStart - clipStart;
|
|
2453
|
+
if (expectedUsed > used) {
|
|
2454
|
+
const pad$1 = Math.min(expectedUsed - used, options.w - used);
|
|
2455
|
+
terminal.write(spaces(pad$1), {
|
|
2456
|
+
x: options.x + used,
|
|
2457
|
+
y: options.y,
|
|
2458
|
+
style: options.baseStyle
|
|
2459
|
+
});
|
|
2460
|
+
used += pad$1;
|
|
2461
|
+
}
|
|
2462
|
+
if (visibleStart === pieceStart - piece.cells && visibleEnd === pieceEnd) {
|
|
2463
|
+
terminal.write(piece.text, {
|
|
2464
|
+
x: options.x + used,
|
|
2465
|
+
y: options.y,
|
|
2466
|
+
style: segmentStyle
|
|
2467
|
+
});
|
|
2468
|
+
used += piece.cells;
|
|
2469
|
+
return;
|
|
2470
|
+
}
|
|
2471
|
+
const pad = Math.min(visibleEnd - visibleStart, options.w - used);
|
|
2472
|
+
if (pad > 0) {
|
|
2473
|
+
const fallback = sliceByCellsRange(piece.text, visibleStart - (pieceStart - piece.cells), visibleEnd - (pieceStart - piece.cells));
|
|
2474
|
+
terminal.write(fallback || spaces(pad), {
|
|
2475
|
+
x: options.x + used,
|
|
2476
|
+
y: options.y,
|
|
2477
|
+
style: segmentStyle
|
|
2478
|
+
});
|
|
2479
|
+
used += pad;
|
|
2480
|
+
}
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
clearStaleMarkdownImageGraphicsForRow(terminal, options.y, queuedGraphics, options.keepGraphicIds);
|
|
2484
|
+
if (used < options.w && options.clear !== false) terminal.write(spaces(options.w - used), {
|
|
2485
|
+
x: options.x + used,
|
|
2486
|
+
y: options.y,
|
|
2487
|
+
style: options.baseStyle
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
//#endregion
|
|
2492
|
+
//#region src/vue/components/TVirtualMarkdown.ts
|
|
2493
|
+
function clamp(n, min, max) {
|
|
2494
|
+
return Math.max(min, Math.min(max, n));
|
|
2495
|
+
}
|
|
2496
|
+
function markdownStyleSignature(style) {
|
|
2497
|
+
if (!style) return "";
|
|
2498
|
+
return [
|
|
2499
|
+
style.fg ?? "",
|
|
2500
|
+
style.bg ?? "",
|
|
2501
|
+
style.bold ? "1" : "0",
|
|
2502
|
+
style.dim ? "1" : "0",
|
|
2503
|
+
style.italic ? "1" : "0",
|
|
2504
|
+
style.underline ? "1" : "0",
|
|
2505
|
+
style.inverse ? "1" : "0",
|
|
2506
|
+
style.href ?? ""
|
|
2507
|
+
].join("");
|
|
2508
|
+
}
|
|
2509
|
+
function markdownRowSignature(row) {
|
|
2510
|
+
if (!row) return "";
|
|
2511
|
+
return [
|
|
2512
|
+
row.key,
|
|
2513
|
+
row.plainText,
|
|
2514
|
+
row.segments.map((segment) => `${segment.text}\u0001${segment.cells}\u0001${markdownStyleSignature(segment.style)}\u0001${segment.graphic?.src ?? ""}\u0001${segment.graphic?.base64 ?? ""}\u0001${segment.fallbackText ?? ""}`).join("")
|
|
2515
|
+
].join("");
|
|
2516
|
+
}
|
|
2517
|
+
function getWheelScrollInput(e) {
|
|
2518
|
+
const deltaY = Number(e.deltaY ?? 0);
|
|
2519
|
+
const deltaMode = typeof e.deltaMode === "number" ? e.deltaMode : void 0;
|
|
2520
|
+
if (Number.isInteger(deltaY) && deltaY !== 0 && Math.abs(deltaY) >= 100 && Math.abs(deltaY) % 100 === 0 && deltaMode == null) return {
|
|
2521
|
+
deltaY: deltaY / 100,
|
|
2522
|
+
mode: "line"
|
|
2523
|
+
};
|
|
2524
|
+
if (deltaMode === 1) return {
|
|
2525
|
+
deltaY,
|
|
2526
|
+
mode: "line"
|
|
2527
|
+
};
|
|
2528
|
+
if (deltaMode === 0) return {
|
|
2529
|
+
deltaY,
|
|
2530
|
+
mode: "pixel"
|
|
2531
|
+
};
|
|
2532
|
+
return {
|
|
2533
|
+
deltaY,
|
|
2534
|
+
mode: "auto"
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
function rowGraphicSignature(row) {
|
|
2538
|
+
if (!row) return "";
|
|
2539
|
+
return row.segments.map((segment) => segment.graphic ? `${segment.graphic.kind}\u0001${segment.graphic.src}\u0001${segment.graphic.base64 ?? ""}` : "").join("");
|
|
2540
|
+
}
|
|
2541
|
+
const TVirtualMarkdown = defineComponent({
|
|
2542
|
+
name: "TVirtualMarkdown",
|
|
2543
|
+
props: {
|
|
2544
|
+
x: {
|
|
2545
|
+
type: Number,
|
|
2546
|
+
required: true
|
|
2547
|
+
},
|
|
2548
|
+
y: {
|
|
2549
|
+
type: Number,
|
|
2550
|
+
required: true
|
|
2551
|
+
},
|
|
2552
|
+
w: {
|
|
2553
|
+
type: Number,
|
|
2554
|
+
required: true
|
|
2555
|
+
},
|
|
2556
|
+
h: {
|
|
2557
|
+
type: Number,
|
|
2558
|
+
required: true
|
|
2559
|
+
},
|
|
2560
|
+
zIndex: {
|
|
2561
|
+
type: Number,
|
|
2562
|
+
default: 0
|
|
2563
|
+
},
|
|
2564
|
+
content: {
|
|
2565
|
+
type: String,
|
|
2566
|
+
default: ""
|
|
2567
|
+
},
|
|
2568
|
+
blocks: {
|
|
2569
|
+
type: Array,
|
|
2570
|
+
default: void 0
|
|
2571
|
+
},
|
|
2572
|
+
scrollTop: {
|
|
2573
|
+
type: Number,
|
|
2574
|
+
default: 0
|
|
2575
|
+
},
|
|
2576
|
+
style: {
|
|
2577
|
+
type: Object,
|
|
2578
|
+
default: void 0
|
|
2579
|
+
},
|
|
2580
|
+
final: {
|
|
2581
|
+
type: Boolean,
|
|
2582
|
+
default: true
|
|
2583
|
+
},
|
|
2584
|
+
streaming: {
|
|
2585
|
+
type: Boolean,
|
|
2586
|
+
default: false
|
|
2587
|
+
},
|
|
2588
|
+
autoFocus: {
|
|
2589
|
+
type: Boolean,
|
|
2590
|
+
default: false
|
|
2591
|
+
},
|
|
2592
|
+
selectable: {
|
|
2593
|
+
type: Boolean,
|
|
2594
|
+
default: true
|
|
2595
|
+
},
|
|
2596
|
+
customHtmlTags: {
|
|
2597
|
+
type: Array,
|
|
2598
|
+
default: void 0
|
|
2599
|
+
},
|
|
2600
|
+
theme: {
|
|
2601
|
+
type: Object,
|
|
2602
|
+
default: void 0
|
|
2603
|
+
},
|
|
2604
|
+
imageRenderer: {
|
|
2605
|
+
type: Function,
|
|
2606
|
+
default: void 0
|
|
2607
|
+
},
|
|
2608
|
+
imageMinWidth: {
|
|
2609
|
+
type: Number,
|
|
2610
|
+
default: void 0
|
|
2611
|
+
},
|
|
2612
|
+
imageMaxWidth: {
|
|
2613
|
+
type: Number,
|
|
2614
|
+
default: void 0
|
|
2615
|
+
},
|
|
2616
|
+
imageMinHeight: {
|
|
2617
|
+
type: Number,
|
|
2618
|
+
default: void 0
|
|
2619
|
+
},
|
|
2620
|
+
imageMaxHeight: {
|
|
2621
|
+
type: Number,
|
|
2622
|
+
default: void 0
|
|
2623
|
+
},
|
|
2624
|
+
imagePreserveAspectRatio: {
|
|
2625
|
+
type: Boolean,
|
|
2626
|
+
default: true
|
|
2627
|
+
},
|
|
2628
|
+
imageActions: {
|
|
2629
|
+
type: Boolean,
|
|
2630
|
+
default: false
|
|
2631
|
+
},
|
|
2632
|
+
mathActions: {
|
|
2633
|
+
type: Boolean,
|
|
2634
|
+
default: false
|
|
2635
|
+
},
|
|
2636
|
+
linkActions: {
|
|
2637
|
+
type: Boolean,
|
|
2638
|
+
default: false
|
|
2639
|
+
},
|
|
2640
|
+
imageOcclusionRects: {
|
|
2641
|
+
type: Array,
|
|
2642
|
+
default: void 0
|
|
2643
|
+
}
|
|
2644
|
+
},
|
|
2645
|
+
emits: {
|
|
2646
|
+
"update:scrollTop": (_value) => true,
|
|
2647
|
+
scroll: (_value) => true,
|
|
2648
|
+
focus: () => true,
|
|
2649
|
+
blur: () => true,
|
|
2650
|
+
keydown: (_event) => true,
|
|
2651
|
+
imageAction: (_payload) => true,
|
|
2652
|
+
mathAction: (_payload) => true,
|
|
2653
|
+
linkAction: (_payload) => true
|
|
2654
|
+
},
|
|
2655
|
+
setup(props, { emit }) {
|
|
2656
|
+
const instance = getCurrentInstance();
|
|
2657
|
+
const { terminal, defaultStyle, events, scheduler, selection, widthProvider } = useTerminal();
|
|
2658
|
+
const layout = useLayout();
|
|
2659
|
+
const { visible, rootProps } = useVisibility();
|
|
2660
|
+
const parentEventZ = inject(EventZIndexContextKey, computed(() => 0));
|
|
2661
|
+
const eventZ = computed(() => (parentEventZ.value ?? 0) + (props.zIndex ?? 0));
|
|
2662
|
+
const internalScrollTop = ref(0);
|
|
2663
|
+
const documentVersion = ref(0);
|
|
2664
|
+
const rows = shallowRef(markRaw([]));
|
|
2665
|
+
let pendingSelectionScrollFocusRemap = false;
|
|
2666
|
+
const wheelState = createWheelScrollState();
|
|
2667
|
+
let builtOnce = false;
|
|
2668
|
+
let rebuildVersion = 0;
|
|
2669
|
+
let layoutCache;
|
|
2670
|
+
let alive = true;
|
|
2671
|
+
const parser = shallowRef(markRaw(createTuiMarkdownParser({
|
|
2672
|
+
streaming: props.streaming,
|
|
2673
|
+
customHtmlTags: props.customHtmlTags
|
|
2674
|
+
})));
|
|
2675
|
+
const graphicsOutputVersion = shallowRef(getTerminalGraphicsOutputVersion(terminal));
|
|
2676
|
+
const unsubscribeGraphicsOutput = subscribeTerminalGraphicsOutput(terminal, () => {
|
|
2677
|
+
graphicsOutputVersion.value = getTerminalGraphicsOutputVersion(terminal);
|
|
2678
|
+
});
|
|
2679
|
+
watch(() => `${props.streaming ? 1 : 0}:${(props.customHtmlTags ?? []).join("\0")}`, () => {
|
|
2680
|
+
parser.value = markRaw(createTuiMarkdownParser({
|
|
2681
|
+
streaming: props.streaming,
|
|
2682
|
+
customHtmlTags: props.customHtmlTags
|
|
2683
|
+
}));
|
|
2684
|
+
});
|
|
2685
|
+
function rebuildRows() {
|
|
2686
|
+
const prevScrollTop = internalScrollTop.value;
|
|
2687
|
+
const prevVisibleRows = visibleRowSignatures(rows.value, prevScrollTop);
|
|
2688
|
+
const prevVisibleGraphics = visibleRowGraphics(rows.value, prevScrollTop);
|
|
2689
|
+
const nextLayout = withTextWidthProvider(widthProvider, () => {
|
|
2690
|
+
const blocks = props.blocks ?? buildMarkdownBlocks(props.content, parser.value, {
|
|
2691
|
+
final: props.final,
|
|
2692
|
+
theme: props.theme,
|
|
2693
|
+
imageResolver: props.imageRenderer,
|
|
2694
|
+
imageSize: {
|
|
2695
|
+
minWidth: props.imageMinWidth,
|
|
2696
|
+
maxWidth: props.imageMaxWidth,
|
|
2697
|
+
minHeight: props.imageMinHeight,
|
|
2698
|
+
maxHeight: props.imageMaxHeight,
|
|
2699
|
+
preserveAspectRatio: props.imagePreserveAspectRatio
|
|
2700
|
+
}
|
|
2701
|
+
}).blocks;
|
|
2702
|
+
return layoutMarkdownBlocksCached(blocks, props.w, layoutCache);
|
|
2703
|
+
});
|
|
2704
|
+
layoutCache = markRaw(nextLayout.cache);
|
|
2705
|
+
const nextRows = nextLayout.rows;
|
|
2706
|
+
rows.value = markRaw(nextRows);
|
|
2707
|
+
reconcileScrollTop();
|
|
2708
|
+
const nextScrollTop = internalScrollTop.value;
|
|
2709
|
+
const nextVisibleRows = visibleRowSignatures(nextRows, nextScrollTop);
|
|
2710
|
+
const nextVisibleGraphics = visibleRowGraphics(nextRows, nextScrollTop);
|
|
2711
|
+
const visibleChanged = prevVisibleRows.length !== nextVisibleRows.length || prevVisibleRows.some((row, index) => row !== nextVisibleRows[index]) || prevVisibleGraphics.some((row, index) => row !== nextVisibleGraphics[index]);
|
|
2712
|
+
if (!builtOnce || prevScrollTop !== nextScrollTop || visibleChanged) {
|
|
2713
|
+
documentVersion.value++;
|
|
2714
|
+
selection.refresh();
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
function scheduleRebuild() {
|
|
2718
|
+
const currentVersion = ++rebuildVersion;
|
|
2719
|
+
if (!builtOnce || !props.streaming) {
|
|
2720
|
+
builtOnce = true;
|
|
2721
|
+
rebuildRows();
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
scheduler.queueFrameTask({
|
|
2725
|
+
id: `TVirtualMarkdown:${instance?.uid ?? "unknown"}:markdown`,
|
|
2726
|
+
reason: props.streaming ? "stream" : "data",
|
|
2727
|
+
priority: props.streaming ? "low" : "normal",
|
|
2728
|
+
sync: false,
|
|
2729
|
+
run: () => {
|
|
2730
|
+
if (!alive) return;
|
|
2731
|
+
if (currentVersion !== rebuildVersion) return;
|
|
2732
|
+
rebuildRows();
|
|
2733
|
+
}
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
const fullRect = computed(() => translateRect({
|
|
2737
|
+
x: props.x,
|
|
2738
|
+
y: props.y,
|
|
2739
|
+
w: props.w,
|
|
2740
|
+
h: props.h
|
|
2741
|
+
}, layout.originX, layout.originY));
|
|
2742
|
+
const absRect = computed(() => {
|
|
2743
|
+
const translated = fullRect.value;
|
|
2744
|
+
if (!layout.clipRect) return translated;
|
|
2745
|
+
return intersectRect(translated, layout.clipRect) ?? {
|
|
2746
|
+
x: 0,
|
|
2747
|
+
y: 0,
|
|
2748
|
+
w: 0,
|
|
2749
|
+
h: 0
|
|
2750
|
+
};
|
|
2751
|
+
});
|
|
2752
|
+
function normalizedRect() {
|
|
2753
|
+
return normalizeCellRect(absRect.value);
|
|
2754
|
+
}
|
|
2755
|
+
function normalizedFullRect() {
|
|
2756
|
+
return normalizeCellRect(fullRect.value);
|
|
2757
|
+
}
|
|
2758
|
+
function clipOffsets() {
|
|
2759
|
+
const full = normalizedFullRect();
|
|
2760
|
+
const clip = normalizedRect();
|
|
2761
|
+
return {
|
|
2762
|
+
x: Math.max(0, clip.x - full.x),
|
|
2763
|
+
y: Math.max(0, clip.y - full.y)
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
function maxScrollTop() {
|
|
2767
|
+
const clip = normalizedRect();
|
|
2768
|
+
const { y: clipY } = clipOffsets();
|
|
2769
|
+
return Math.max(0, rows.value.length - (clipY + clip.h));
|
|
2770
|
+
}
|
|
2771
|
+
function visibleRowSignatures(sourceRows, scrollTop) {
|
|
2772
|
+
const clip = normalizedRect();
|
|
2773
|
+
const { y: clipY } = clipOffsets();
|
|
2774
|
+
const start = Math.max(0, scrollTop + clipY);
|
|
2775
|
+
const end = Math.max(start, start + clip.h);
|
|
2776
|
+
const out = [];
|
|
2777
|
+
for (let index = start; index < end; index++) out.push(markdownRowSignature(sourceRows[index]));
|
|
2778
|
+
return out;
|
|
2779
|
+
}
|
|
2780
|
+
function visibleRowGraphics(sourceRows, scrollTop) {
|
|
2781
|
+
const clip = normalizedRect();
|
|
2782
|
+
const { y: clipY } = clipOffsets();
|
|
2783
|
+
const start = Math.max(0, scrollTop + clipY);
|
|
2784
|
+
const end = Math.max(start, start + clip.h);
|
|
2785
|
+
const out = [];
|
|
2786
|
+
for (let index = start; index < end; index++) out.push(rowGraphicSignature(sourceRows[index]));
|
|
2787
|
+
return out;
|
|
2788
|
+
}
|
|
2789
|
+
function hasControlledScrollTop() {
|
|
2790
|
+
return Object.prototype.hasOwnProperty.call(instance?.vnode.props ?? {}, "scrollTop");
|
|
2791
|
+
}
|
|
2792
|
+
function applyControlledScrollTop(next, remapSelectionFocus = false) {
|
|
2793
|
+
const desired = Math.floor(Number(next) || 0);
|
|
2794
|
+
const clamped = clamp(desired, 0, maxScrollTop());
|
|
2795
|
+
const changed = internalScrollTop.value !== clamped;
|
|
2796
|
+
if (changed) internalScrollTop.value = clamped;
|
|
2797
|
+
if (desired !== clamped) {
|
|
2798
|
+
emit("update:scrollTop", clamped);
|
|
2799
|
+
emit("scroll", clamped);
|
|
2800
|
+
}
|
|
2801
|
+
if (changed || remapSelectionFocus) selection.refresh(remapSelectionFocus ? { remapFocus: true } : void 0);
|
|
2802
|
+
}
|
|
2803
|
+
function setScrollTop(next, emitChange = true, refreshOptions) {
|
|
2804
|
+
const desired = Math.floor(Number(next) || 0);
|
|
2805
|
+
const clamped = clamp(desired, 0, maxScrollTop());
|
|
2806
|
+
const changed = internalScrollTop.value !== clamped;
|
|
2807
|
+
if (changed) internalScrollTop.value = clamped;
|
|
2808
|
+
if (emitChange && (changed || refreshOptions?.emitClampEvenIfUnchanged && desired !== clamped)) {
|
|
2809
|
+
emit("update:scrollTop", clamped);
|
|
2810
|
+
emit("scroll", clamped);
|
|
2811
|
+
}
|
|
2812
|
+
if (changed || refreshOptions?.remapSelectionFocus) selection.refresh(refreshOptions?.remapSelectionFocus ? { remapFocus: true } : void 0);
|
|
2813
|
+
}
|
|
2814
|
+
function reconcileScrollTop() {
|
|
2815
|
+
const desired = hasControlledScrollTop() ? Math.floor(Number(props.scrollTop) || 0) : internalScrollTop.value;
|
|
2816
|
+
const clamped = clamp(desired, 0, maxScrollTop());
|
|
2817
|
+
if (internalScrollTop.value === clamped) return;
|
|
2818
|
+
internalScrollTop.value = clamped;
|
|
2819
|
+
if (desired !== clamped) {
|
|
2820
|
+
emit("update:scrollTop", clamped);
|
|
2821
|
+
emit("scroll", clamped);
|
|
2822
|
+
}
|
|
2823
|
+
selection.refresh();
|
|
2824
|
+
}
|
|
2825
|
+
watch(() => props.scrollTop, () => {
|
|
2826
|
+
if (!hasControlledScrollTop()) return;
|
|
2827
|
+
const remap = pendingSelectionScrollFocusRemap;
|
|
2828
|
+
pendingSelectionScrollFocusRemap = false;
|
|
2829
|
+
applyControlledScrollTop(props.scrollTop, remap);
|
|
2830
|
+
});
|
|
2831
|
+
watch([
|
|
2832
|
+
() => props.content,
|
|
2833
|
+
() => props.blocks,
|
|
2834
|
+
() => props.w,
|
|
2835
|
+
() => parser.value,
|
|
2836
|
+
() => props.final,
|
|
2837
|
+
() => props.imageRenderer,
|
|
2838
|
+
() => markdownThemeSignature(props.theme),
|
|
2839
|
+
() => props.imageMinWidth,
|
|
2840
|
+
() => props.imageMaxWidth,
|
|
2841
|
+
() => props.imageMinHeight,
|
|
2842
|
+
() => props.imageMaxHeight,
|
|
2843
|
+
() => props.imagePreserveAspectRatio
|
|
2844
|
+
], () => {
|
|
2845
|
+
scheduleRebuild();
|
|
2846
|
+
}, { immediate: true });
|
|
2847
|
+
watch([
|
|
2848
|
+
() => props.content,
|
|
2849
|
+
() => props.w,
|
|
2850
|
+
() => props.h,
|
|
2851
|
+
() => absRect.value.h
|
|
2852
|
+
], () => {
|
|
2853
|
+
resetWheelScrollState(wheelState);
|
|
2854
|
+
});
|
|
2855
|
+
watch([
|
|
2856
|
+
() => props.h,
|
|
2857
|
+
() => absRect.value.y,
|
|
2858
|
+
() => absRect.value.h,
|
|
2859
|
+
() => fullRect.value.y,
|
|
2860
|
+
() => fullRect.value.h
|
|
2861
|
+
], () => {
|
|
2862
|
+
reconcileScrollTop();
|
|
2863
|
+
}, { immediate: true });
|
|
2864
|
+
function scrollSelectionBy(delta) {
|
|
2865
|
+
const n = Math.trunc(Number(delta));
|
|
2866
|
+
if (!Number.isFinite(n) || n === 0) return false;
|
|
2867
|
+
if (hasControlledScrollTop()) {
|
|
2868
|
+
const nextTop = clamp(internalScrollTop.value + n, 0, maxScrollTop());
|
|
2869
|
+
if (nextTop === internalScrollTop.value) return false;
|
|
2870
|
+
if (pendingSelectionScrollFocusRemap) return false;
|
|
2871
|
+
pendingSelectionScrollFocusRemap = true;
|
|
2872
|
+
emit("update:scrollTop", nextTop);
|
|
2873
|
+
emit("scroll", nextTop);
|
|
2874
|
+
return true;
|
|
2875
|
+
}
|
|
2876
|
+
const before = internalScrollTop.value;
|
|
2877
|
+
setScrollTop(before + n, true, { remapSelectionFocus: true });
|
|
2878
|
+
return internalScrollTop.value !== before;
|
|
2879
|
+
}
|
|
2880
|
+
function selectionPointForCell(point) {
|
|
2881
|
+
const r = normalizedRect();
|
|
2882
|
+
if (point.x < r.x || point.y < r.y || point.x >= r.x + r.w || point.y >= r.y + r.h) return null;
|
|
2883
|
+
const { x: clipX, y: clipY } = clipOffsets();
|
|
2884
|
+
const visualY = internalScrollTop.value + clipY + (point.y - r.y);
|
|
2885
|
+
if (visualY < 0 || visualY >= rows.value.length) return null;
|
|
2886
|
+
return {
|
|
2887
|
+
x: clamp(clipX + (point.x - r.x), 0, Math.max(0, props.w - 1)),
|
|
2888
|
+
y: visualY
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
function canHandleSelectionRange(range) {
|
|
2892
|
+
return Boolean(selectionPointForCell(range.anchor) && selectionPointForCell(range.focus));
|
|
2893
|
+
}
|
|
2894
|
+
function textForSelectionRange(range) {
|
|
2895
|
+
const cols = Math.max(1, Math.floor(props.w));
|
|
2896
|
+
return withTextWidthProvider(widthProvider, () => terminalSelectionRowSpans(range, cols, rows.value.length).map((span) => {
|
|
2897
|
+
const text = sliceByCellsRange(rows.value[span.y]?.plainText ?? "", span.x0, span.x1);
|
|
2898
|
+
return span.x1 >= cols ? text.trimEnd() : text;
|
|
2899
|
+
}).join("\n"));
|
|
2900
|
+
}
|
|
2901
|
+
function visibleSpansForSelectionRange(providerRange, _screenRange) {
|
|
2902
|
+
const r = normalizedRect();
|
|
2903
|
+
const { x: clipX, y: clipY } = clipOffsets();
|
|
2904
|
+
const cols = Math.max(1, Math.floor(props.w));
|
|
2905
|
+
const top = internalScrollTop.value + clipY;
|
|
2906
|
+
const bottom = top + r.h;
|
|
2907
|
+
const providerSpans = terminalSelectionVisibleRowSpans(providerRange, cols, rows.value.length, top, bottom);
|
|
2908
|
+
const result = [];
|
|
2909
|
+
for (const span of providerSpans) {
|
|
2910
|
+
const screenY = r.y + (span.y - top);
|
|
2911
|
+
const screenX0 = r.x + span.x0 - clipX;
|
|
2912
|
+
const screenX1 = r.x + span.x1 - clipX;
|
|
2913
|
+
const x0 = Math.max(r.x, screenX0);
|
|
2914
|
+
const x1 = Math.min(r.x + r.w, screenX1);
|
|
2915
|
+
if (screenY >= r.y && screenY < r.y + r.h && x1 > x0) result.push({
|
|
2916
|
+
y: screenY,
|
|
2917
|
+
x0,
|
|
2918
|
+
x1
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
return result;
|
|
2922
|
+
}
|
|
2923
|
+
function onKeydown(event) {
|
|
2924
|
+
emit("keydown", event);
|
|
2925
|
+
const page = Math.max(1, normalizedRect().h);
|
|
2926
|
+
if (event.key === "ArrowUp") {
|
|
2927
|
+
event.preventDefault();
|
|
2928
|
+
setScrollTop(internalScrollTop.value - 1);
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
if (event.key === "ArrowDown") {
|
|
2932
|
+
event.preventDefault();
|
|
2933
|
+
setScrollTop(internalScrollTop.value + 1);
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
if (event.key === "PageUp") {
|
|
2937
|
+
event.preventDefault();
|
|
2938
|
+
setScrollTop(internalScrollTop.value - page);
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
2941
|
+
if (event.key === "PageDown") {
|
|
2942
|
+
event.preventDefault();
|
|
2943
|
+
setScrollTop(internalScrollTop.value + page);
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
if (event.key === "Home") {
|
|
2947
|
+
event.preventDefault();
|
|
2948
|
+
setScrollTop(0);
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
if (event.key === "End") {
|
|
2952
|
+
event.preventDefault();
|
|
2953
|
+
setScrollTop(maxScrollTop());
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
const selectionTextProvider = {
|
|
2957
|
+
id: `TVirtualMarkdown:${instance?.uid ?? "unknown"}:selection-text`,
|
|
2958
|
+
get rect() {
|
|
2959
|
+
return normalizedRect();
|
|
2960
|
+
},
|
|
2961
|
+
canHandle: canHandleSelectionRange,
|
|
2962
|
+
pointForCell: selectionPointForCell,
|
|
2963
|
+
getText: textForSelectionRange,
|
|
2964
|
+
getVisibleSpans: visibleSpansForSelectionRange
|
|
2965
|
+
};
|
|
2966
|
+
const unregisterSelectionTextProvider = selection.registerTextProvider(selectionTextProvider);
|
|
2967
|
+
onBeforeUnmount(unregisterSelectionTextProvider);
|
|
2968
|
+
const eventNode = useTerminalNode(() => ({
|
|
2969
|
+
rect: normalizedRect(),
|
|
2970
|
+
zIndex: eventZ.value,
|
|
2971
|
+
visible: visible.value,
|
|
2972
|
+
focusable: true,
|
|
2973
|
+
selectable: props.selectable,
|
|
2974
|
+
selectionScrollBy: scrollSelectionBy,
|
|
2975
|
+
handlers: {
|
|
2976
|
+
click: (event) => {
|
|
2977
|
+
const r = normalizedRect();
|
|
2978
|
+
const { x: clipX, y: clipY } = clipOffsets();
|
|
2979
|
+
const hitOptions = {
|
|
2980
|
+
screenRect: r,
|
|
2981
|
+
rowOffset: internalScrollTop.value + clipY,
|
|
2982
|
+
clipStart: clipX
|
|
2983
|
+
};
|
|
2984
|
+
if (props.imageActions) {
|
|
2985
|
+
const hit = findMarkdownImageActionAt(rows.value, {
|
|
2986
|
+
cellX: event.cellX,
|
|
2987
|
+
cellY: event.cellY
|
|
2988
|
+
}, hitOptions);
|
|
2989
|
+
if (hit) {
|
|
2990
|
+
event.preventDefault();
|
|
2991
|
+
emit("imageAction", hit);
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
if (props.mathActions) {
|
|
2996
|
+
const math = findMarkdownMathActionAt(rows.value, {
|
|
2997
|
+
cellX: event.cellX,
|
|
2998
|
+
cellY: event.cellY
|
|
2999
|
+
}, hitOptions);
|
|
3000
|
+
if (math) {
|
|
3001
|
+
event.preventDefault();
|
|
3002
|
+
emit("mathAction", math);
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
if (!props.linkActions) return;
|
|
3007
|
+
const link = findMarkdownLinkActionAt(rows.value, {
|
|
3008
|
+
cellX: event.cellX,
|
|
3009
|
+
cellY: event.cellY
|
|
3010
|
+
}, hitOptions);
|
|
3011
|
+
if (!link) return;
|
|
3012
|
+
event.preventDefault();
|
|
3013
|
+
emit("linkAction", link);
|
|
3014
|
+
},
|
|
3015
|
+
wheel: (event) => {
|
|
3016
|
+
const { deltaY, mode } = getWheelScrollInput(event);
|
|
3017
|
+
if (!deltaY) return;
|
|
3018
|
+
const now = typeof event.time === "number" ? event.time : typeof event.timeStamp === "number" ? event.timeStamp : Date.now();
|
|
3019
|
+
const { nextTop, dir } = applyWheelScroll(wheelState, deltaY, internalScrollTop.value, maxScrollTop(), now, mode, { disableAcceleration: mode === "pixel" });
|
|
3020
|
+
if (!dir || nextTop === internalScrollTop.value) return;
|
|
3021
|
+
event.preventDefault?.();
|
|
3022
|
+
setScrollTop(nextTop);
|
|
3023
|
+
},
|
|
3024
|
+
focus: () => {
|
|
3025
|
+
emit("focus");
|
|
3026
|
+
},
|
|
3027
|
+
blur: () => {
|
|
3028
|
+
emit("blur");
|
|
3029
|
+
},
|
|
3030
|
+
keydown: onKeydown
|
|
3031
|
+
}
|
|
3032
|
+
}));
|
|
3033
|
+
watchEffect(() => {
|
|
3034
|
+
if (!props.autoFocus || !visible.value) return;
|
|
3035
|
+
const manager = events.value;
|
|
3036
|
+
const nodeId = eventNode.id.value;
|
|
3037
|
+
if (!nodeId) return;
|
|
3038
|
+
if (!manager) return;
|
|
3039
|
+
if (manager.getFocused() === nodeId) return;
|
|
3040
|
+
manager.focus(nodeId);
|
|
3041
|
+
});
|
|
3042
|
+
onBeforeUnmount(() => {
|
|
3043
|
+
alive = false;
|
|
3044
|
+
rebuildVersion++;
|
|
3045
|
+
unsubscribeGraphicsOutput();
|
|
3046
|
+
clearMarkdownImageGraphics(terminal, fullRect.value);
|
|
3047
|
+
});
|
|
3048
|
+
useRenderNode(() => ({
|
|
3049
|
+
zIndex: props.zIndex,
|
|
3050
|
+
rect: visible.value ? normalizedRect() : {
|
|
3051
|
+
x: 0,
|
|
3052
|
+
y: 0,
|
|
3053
|
+
w: 0,
|
|
3054
|
+
h: 0
|
|
3055
|
+
},
|
|
3056
|
+
deps: [
|
|
3057
|
+
visible.value,
|
|
3058
|
+
absRect.value,
|
|
3059
|
+
fullRect.value,
|
|
3060
|
+
internalScrollTop.value,
|
|
3061
|
+
props.style,
|
|
3062
|
+
defaultStyle.value,
|
|
3063
|
+
documentVersion.value,
|
|
3064
|
+
graphicsOutputVersion.value,
|
|
3065
|
+
props.imageOcclusionRects
|
|
3066
|
+
],
|
|
3067
|
+
paint: (dirtyRows) => {
|
|
3068
|
+
withTextWidthProvider(widthProvider, () => {
|
|
3069
|
+
if (!visible.value) return;
|
|
3070
|
+
const r = normalizedRect();
|
|
3071
|
+
if (r.w <= 0 || r.h <= 0) return;
|
|
3072
|
+
const baseStyle = props.style ?? defaultStyle.value;
|
|
3073
|
+
const { x: clipX, y: clipY } = clipOffsets();
|
|
3074
|
+
const isGraphicCovered = (rect) => {
|
|
3075
|
+
return props.imageOcclusionRects?.some((item) => intersectRect(rect, item)) === true;
|
|
3076
|
+
};
|
|
3077
|
+
const keepGraphicIds = collectVisibleMarkdownImageGraphicIds(rows.value, {
|
|
3078
|
+
x: r.x,
|
|
3079
|
+
y: r.y,
|
|
3080
|
+
w: r.w,
|
|
3081
|
+
h: r.h,
|
|
3082
|
+
rowOffset: internalScrollTop.value + clipY,
|
|
3083
|
+
clipStart: clipX,
|
|
3084
|
+
isGraphicCovered
|
|
3085
|
+
});
|
|
3086
|
+
const paintRow = (y) => {
|
|
3087
|
+
if (y < r.y || y >= r.y + r.h) return;
|
|
3088
|
+
const visualIndex = internalScrollTop.value + clipY + (y - r.y);
|
|
3089
|
+
paintMarkdownVisualRow(terminal, rows.value[visualIndex], {
|
|
3090
|
+
x: r.x,
|
|
3091
|
+
y,
|
|
3092
|
+
w: r.w,
|
|
3093
|
+
clipStart: clipX,
|
|
3094
|
+
baseStyle,
|
|
3095
|
+
clear: true,
|
|
3096
|
+
keepGraphicIds,
|
|
3097
|
+
isGraphicCovered
|
|
3098
|
+
});
|
|
3099
|
+
};
|
|
3100
|
+
if (dirtyRows?.length) {
|
|
3101
|
+
for (const y of dirtyRows) paintRow(y);
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
for (let y = r.y; y < r.y + r.h; y++) paintRow(y);
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
}));
|
|
3108
|
+
return () => h("span", rootProps);
|
|
3109
|
+
}
|
|
3110
|
+
});
|
|
3111
|
+
|
|
3112
|
+
//#endregion
|
|
3113
|
+
//#region src/vue/markdown/block-source.ts
|
|
3114
|
+
function withBlockKeyPrefix(block, prefix, index) {
|
|
3115
|
+
const key = `${prefix}:${index}:${block.key}`;
|
|
3116
|
+
switch (block.type) {
|
|
3117
|
+
case "inline": return {
|
|
3118
|
+
...block,
|
|
3119
|
+
key
|
|
3120
|
+
};
|
|
3121
|
+
case "code_block": return {
|
|
3122
|
+
...block,
|
|
3123
|
+
key
|
|
3124
|
+
};
|
|
3125
|
+
case "thematic_break": return {
|
|
3126
|
+
...block,
|
|
3127
|
+
key
|
|
3128
|
+
};
|
|
3129
|
+
case "table": return {
|
|
3130
|
+
...block,
|
|
3131
|
+
key
|
|
3132
|
+
};
|
|
3133
|
+
case "blank": return {
|
|
3134
|
+
...block,
|
|
3135
|
+
key
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
function parseBlockGroup(text, key, parser, theme, final) {
|
|
3140
|
+
const { blocks } = buildMarkdownBlocks(text, parser, {
|
|
3141
|
+
final,
|
|
3142
|
+
theme
|
|
3143
|
+
});
|
|
3144
|
+
return {
|
|
3145
|
+
key,
|
|
3146
|
+
blocks: blocks.map((block, index) => withBlockKeyPrefix(block, key, index))
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
function shouldInsertGap(previous, next) {
|
|
3150
|
+
return previous != null && next != null && previous.type !== "blank" && next.type !== "blank";
|
|
3151
|
+
}
|
|
3152
|
+
function mergeGroups(groups) {
|
|
3153
|
+
const out = [];
|
|
3154
|
+
for (const group of groups) {
|
|
3155
|
+
if (!group.blocks.length) continue;
|
|
3156
|
+
if (shouldInsertGap(out[out.length - 1], group.blocks[0])) out.push({
|
|
3157
|
+
type: "blank",
|
|
3158
|
+
key: `${group.key}:gap`
|
|
3159
|
+
});
|
|
3160
|
+
out.push(...group.blocks);
|
|
3161
|
+
}
|
|
3162
|
+
return out;
|
|
3163
|
+
}
|
|
3164
|
+
function createMarkdownBlockSource(options) {
|
|
3165
|
+
const parser = options?.parser ?? createTuiMarkdownParser({
|
|
3166
|
+
streaming: true,
|
|
3167
|
+
customHtmlTags: options?.customHtmlTags
|
|
3168
|
+
});
|
|
3169
|
+
const theme = options?.theme;
|
|
3170
|
+
const finalizedGroups = [];
|
|
3171
|
+
let tailText = "";
|
|
3172
|
+
let nextGroupIndex = 0;
|
|
3173
|
+
let version = 0;
|
|
3174
|
+
let cachedVersion = -1;
|
|
3175
|
+
let cachedBlocks = [];
|
|
3176
|
+
const invalidate = () => {
|
|
3177
|
+
version++;
|
|
3178
|
+
cachedVersion = -1;
|
|
3179
|
+
};
|
|
3180
|
+
const currentGroups = () => {
|
|
3181
|
+
if (!tailText) return finalizedGroups;
|
|
3182
|
+
return [...finalizedGroups, parseBlockGroup(tailText, `md-tail-${nextGroupIndex}`, parser, theme, false)];
|
|
3183
|
+
};
|
|
3184
|
+
const blocks = () => {
|
|
3185
|
+
if (cachedVersion === version) return cachedBlocks;
|
|
3186
|
+
cachedBlocks = mergeGroups(currentGroups());
|
|
3187
|
+
cachedVersion = version;
|
|
3188
|
+
return cachedBlocks;
|
|
3189
|
+
};
|
|
3190
|
+
const snapshot = () => ({
|
|
3191
|
+
version,
|
|
3192
|
+
blocks: blocks()
|
|
3193
|
+
});
|
|
3194
|
+
return {
|
|
3195
|
+
get version() {
|
|
3196
|
+
return version;
|
|
3197
|
+
},
|
|
3198
|
+
get blocks() {
|
|
3199
|
+
return blocks();
|
|
3200
|
+
},
|
|
3201
|
+
appendDelta(text) {
|
|
3202
|
+
if (!text) return;
|
|
3203
|
+
tailText += text;
|
|
3204
|
+
invalidate();
|
|
3205
|
+
},
|
|
3206
|
+
replaceTailBlock(text) {
|
|
3207
|
+
if (tailText === text) return;
|
|
3208
|
+
tailText = text;
|
|
3209
|
+
invalidate();
|
|
3210
|
+
},
|
|
3211
|
+
finalizeBlock() {
|
|
3212
|
+
if (!tailText) return;
|
|
3213
|
+
const key = `md-block-${nextGroupIndex++}`;
|
|
3214
|
+
finalizedGroups.push(parseBlockGroup(tailText, key, parser, theme, true));
|
|
3215
|
+
tailText = "";
|
|
3216
|
+
invalidate();
|
|
3217
|
+
},
|
|
3218
|
+
clear() {
|
|
3219
|
+
if (!tailText && finalizedGroups.length === 0) return;
|
|
3220
|
+
finalizedGroups.length = 0;
|
|
3221
|
+
tailText = "";
|
|
3222
|
+
nextGroupIndex = 0;
|
|
3223
|
+
invalidate();
|
|
3224
|
+
},
|
|
3225
|
+
snapshot
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
//#endregion
|
|
3230
|
+
export { TVirtualMarkdown, buildMarkdownBlocks, buildMarkdownVisualRows, canDrawTerminalGraphicRect, clearMarkdownImageGraphics, collectVisibleMarkdownImageGraphicIds, createIterm2InlineImageSequence, createKittyDeleteGraphicsSequence, createKittyGraphicsSequence, createKittyPlacementSequence, createMarkdownBlockSource, createTuiMarkdownParser, detectTerminalGraphicsCapabilities, findMarkdownImageActionAt, findMarkdownLinkActionAt, findMarkdownMathActionAt, getTerminalGraphicsOutput, getTerminalGraphicsOutputVersion, hashTerminalGraphicsString, isSafeMarkdownLink, isSafeTerminalGraphicsSequence, isTerminalGraphicsProtocol, layoutMarkdownBlocks, markdownThemeSignature, normalizeTerminalGraphicSize, paintMarkdownVisualRow, sanitizeTerminalFallbackText, stableTerminalGraphicNumericId, subscribeTerminalGraphicsOutput };
|