@simon_he/vue-tui 0.0.6 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1581 @@
1
+ import { EventZIndexContextKey, applyWheelScroll, createWheelScrollState, forEachTextCellSegment, intersectRect, normalizeCellRect, repeatChar, resetWheelScrollState, sanitizeInlineText, sanitizeTextBlock, sliceByCellsRange, spaces, textCellWidth, translateRect, useLayout, useRenderNode, useTerminal, useTerminalNode, useVisibility } from "./wheel-scroll-CiNsasLf.js";
2
+ import { computed, defineComponent, getCurrentInstance, h, inject, markRaw, onBeforeUnmount, ref, shallowRef, watch, watchEffect } from "vue";
3
+ import { getMarkdown, parseMarkdownToStructure } from "stream-markdown-parser";
4
+
5
+ //#region src/vue/markdown/parser.ts
6
+ const RELATIVE_LINK_PREFIXES = [
7
+ "#",
8
+ "/",
9
+ "./",
10
+ "../"
11
+ ];
12
+ const SAFE_PROTOCOLS = new Set([
13
+ "http:",
14
+ "https:",
15
+ "mailto:"
16
+ ]);
17
+ const markdownInstanceCache = new Map();
18
+ const MAX_MARKDOWN_INSTANCE_CACHE = 32;
19
+ const SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
20
+ function hasControlChars(value) {
21
+ for (const ch of value) {
22
+ const code = ch.codePointAt(0);
23
+ if (code >= 0 && code <= 31 || code >= 127 && code <= 159) return true;
24
+ }
25
+ return false;
26
+ }
27
+ function isSafeMarkdownLink(url) {
28
+ const raw = String(url ?? "").trim();
29
+ if (!raw) return false;
30
+ if (hasControlChars(raw)) return false;
31
+ if (raw.startsWith("//")) return false;
32
+ const value = raw.toLowerCase();
33
+ if (value.startsWith("javascript:")) return false;
34
+ if (value.startsWith("data:")) return false;
35
+ if (RELATIVE_LINK_PREFIXES.some((prefix) => raw.startsWith(prefix))) return true;
36
+ if (!SCHEME_RE.test(raw)) return true;
37
+ try {
38
+ const parsed = new URL(raw);
39
+ return SAFE_PROTOCOLS.has(parsed.protocol);
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+ function markdownInstanceCacheKey(config) {
45
+ var _config$customHtmlTag;
46
+ return JSON.stringify({ customHtmlTags: [...(config === null || config === void 0 || (_config$customHtmlTag = config.customHtmlTags) === null || _config$customHtmlTag === void 0 ? void 0 : _config$customHtmlTag.filter(Boolean)) ?? []].sort() });
47
+ }
48
+ function getCachedMarkdownInstance(config) {
49
+ var _config$customHtmlTag2;
50
+ const cacheKey = markdownInstanceCacheKey(config);
51
+ const cached = markdownInstanceCache.get(cacheKey);
52
+ if (cached) return cached;
53
+ const customHtmlTags = config === null || config === void 0 || (_config$customHtmlTag2 = config.customHtmlTags) === null || _config$customHtmlTag2 === void 0 ? void 0 : _config$customHtmlTag2.filter(Boolean);
54
+ const created = getMarkdown(`vue-tui-markdown:${cacheKey}`, { customHtmlTags });
55
+ if (markdownInstanceCache.size >= MAX_MARKDOWN_INSTANCE_CACHE) markdownInstanceCache.clear();
56
+ markdownInstanceCache.set(cacheKey, created);
57
+ return created;
58
+ }
59
+ function createTuiMarkdownParser(config) {
60
+ var _config$customHtmlTag3;
61
+ const customHtmlTags = config === null || config === void 0 || (_config$customHtmlTag3 = config.customHtmlTags) === null || _config$customHtmlTag3 === void 0 ? void 0 : _config$customHtmlTag3.filter(Boolean);
62
+ const md = getCachedMarkdownInstance(config);
63
+ function parse(content, final) {
64
+ return parseMarkdownToStructure(content, md, {
65
+ final,
66
+ customHtmlTags,
67
+ requireClosingStrong: final || !(config === null || config === void 0 ? void 0 : config.streaming),
68
+ validateLink: isSafeMarkdownLink
69
+ });
70
+ }
71
+ return { parse };
72
+ }
73
+
74
+ //#endregion
75
+ //#region src/vue/markdown/ast.ts
76
+ const EMPTY_PREFIX = Object.freeze([]);
77
+ const HARD_BREAK_SEGMENT = Object.freeze({
78
+ text: "",
79
+ hardBreak: true
80
+ });
81
+ function mergeStyle$1(base, overlay) {
82
+ if (!base) return overlay;
83
+ if (!overlay) return base;
84
+ return {
85
+ ...base,
86
+ ...overlay
87
+ };
88
+ }
89
+ function trimBlockArray(blocks) {
90
+ var _blocks$start, _blocks;
91
+ let start = 0;
92
+ let end = blocks.length;
93
+ while (start < end && ((_blocks$start = blocks[start]) === null || _blocks$start === void 0 ? void 0 : _blocks$start.type) === "blank") start++;
94
+ while (end > start && ((_blocks = blocks[end - 1]) === null || _blocks === void 0 ? void 0 : _blocks.type) === "blank") end--;
95
+ return blocks.slice(start, end);
96
+ }
97
+ function nodeChildren(node) {
98
+ return "children" in node && Array.isArray(node.children) ? node.children : [];
99
+ }
100
+ function stringProp(node, key) {
101
+ const value = node[key];
102
+ return typeof value === "string" ? value : "";
103
+ }
104
+ function booleanProp(node, key) {
105
+ return node[key] === true;
106
+ }
107
+ function pushTextSegments(out, text, style) {
108
+ if (!text) return;
109
+ const normalized = sanitizeTextBlock(text);
110
+ if (!normalized) return;
111
+ const parts = normalized.split("\n");
112
+ for (let i = 0; i < parts.length; i++) {
113
+ const part = sanitizeInlineText(parts[i] ?? "");
114
+ if (part) out.push(style ? {
115
+ text: part,
116
+ style
117
+ } : { text: part });
118
+ if (i < parts.length - 1) out.push(HARD_BREAK_SEGMENT);
119
+ }
120
+ }
121
+ function sanitizeCodeBlockText(text, tabSize = 4) {
122
+ const normalized = String(text ?? "").replace(/\r\n?/g, "\n").replace(/\t/g, spaces(tabSize)).replace(/\u00a0/g, " ");
123
+ let out = "";
124
+ for (const ch of normalized) {
125
+ const code = ch.charCodeAt(0);
126
+ if (code >= 0 && code <= 8 || code === 11 || code === 12) continue;
127
+ if (code >= 14 && code <= 31 || code === 127) continue;
128
+ out += ch;
129
+ }
130
+ return out;
131
+ }
132
+ function inlineNodeSegments(nodes, theme, inheritedStyle) {
133
+ const out = [];
134
+ for (const node of nodes) switch (node.type) {
135
+ case "text":
136
+ pushTextSegments(out, stringProp(node, "content"), inheritedStyle);
137
+ break;
138
+ case "inline":
139
+ case "paragraph":
140
+ case "heading":
141
+ case "list_item":
142
+ case "blockquote":
143
+ out.push(...inlineNodeSegments(nodeChildren(node), theme, inheritedStyle));
144
+ break;
145
+ case "strong":
146
+ out.push(...inlineNodeSegments(nodeChildren(node), theme, mergeStyle$1(inheritedStyle, theme.strong)));
147
+ break;
148
+ case "emphasis":
149
+ out.push(...inlineNodeSegments(nodeChildren(node), theme, mergeStyle$1(inheritedStyle, theme.emphasis)));
150
+ break;
151
+ case "strikethrough":
152
+ out.push(...inlineNodeSegments(nodeChildren(node), theme, mergeStyle$1(inheritedStyle, theme.strikethrough)));
153
+ break;
154
+ case "highlight":
155
+ case "insert":
156
+ case "subscript":
157
+ case "superscript":
158
+ out.push(...inlineNodeSegments(nodeChildren(node), theme, inheritedStyle));
159
+ break;
160
+ case "inline_code":
161
+ pushTextSegments(out, stringProp(node, "code"), mergeStyle$1(inheritedStyle, theme.inlineCode));
162
+ break;
163
+ case "hardbreak":
164
+ out.push(HARD_BREAK_SEGMENT);
165
+ break;
166
+ case "link": {
167
+ const href = stringProp(node, "href");
168
+ const safeHref = href && isSafeMarkdownLink(href) ? href : "";
169
+ const linkStyle = safeHref ? mergeStyle$1(inheritedStyle, {
170
+ ...theme.link,
171
+ href: safeHref
172
+ }) : inheritedStyle;
173
+ const children = nodeChildren(node);
174
+ if (children.length) out.push(...inlineNodeSegments(children, theme, linkStyle));
175
+ else pushTextSegments(out, stringProp(node, "text"), linkStyle);
176
+ break;
177
+ }
178
+ case "image":
179
+ pushTextSegments(out, stringProp(node, "alt") || stringProp(node, "src"), mergeStyle$1(inheritedStyle, theme.link));
180
+ break;
181
+ case "checkbox":
182
+ case "checkbox_input":
183
+ pushTextSegments(out, booleanProp(node, "checked") ? "[x]" : "[ ]", inheritedStyle);
184
+ break;
185
+ case "emoji":
186
+ pushTextSegments(out, stringProp(node, "markup") || stringProp(node, "name"), inheritedStyle);
187
+ break;
188
+ case "math_inline":
189
+ pushTextSegments(out, stringProp(node, "content"), mergeStyle$1(inheritedStyle, theme.inlineCode));
190
+ break;
191
+ case "reference":
192
+ case "footnote_reference":
193
+ case "footnote_anchor":
194
+ pushTextSegments(out, node.raw, inheritedStyle);
195
+ break;
196
+ case "html_inline":
197
+ pushTextSegments(out, stringProp(node, "content"), mergeStyle$1(inheritedStyle, theme.html));
198
+ break;
199
+ case "html_block":
200
+ pushTextSegments(out, stringProp(node, "content"), mergeStyle$1(inheritedStyle, theme.html));
201
+ break;
202
+ default:
203
+ if (nodeChildren(node).length) {
204
+ out.push(...inlineNodeSegments(nodeChildren(node), theme, inheritedStyle));
205
+ break;
206
+ }
207
+ pushTextSegments(out, node.raw, inheritedStyle);
208
+ break;
209
+ }
210
+ return out;
211
+ }
212
+ function inlineBlock(key, segments, context) {
213
+ return {
214
+ type: "inline",
215
+ key,
216
+ segments,
217
+ prefixSegments: context.prefixSegments,
218
+ continuationPrefixSegments: context.continuationPrefixSegments
219
+ };
220
+ }
221
+ function blockFromParagraph(node, key, context, theme) {
222
+ return inlineBlock(key, inlineNodeSegments(node.children, theme), context);
223
+ }
224
+ function blockFromHeading(node, key, context, theme) {
225
+ const level = Math.min(6, Math.max(1, Math.floor(node.level || 1)));
226
+ return inlineBlock(key, inlineNodeSegments(node.children, theme, theme.heading[level - 1]), context);
227
+ }
228
+ function blockFromCodeBlock(node, key, context, theme) {
229
+ return {
230
+ type: "code_block",
231
+ key,
232
+ lines: sanitizeCodeBlockText(node.code ?? "").split("\n"),
233
+ style: theme.codeBlock,
234
+ prefixSegments: context.prefixSegments,
235
+ continuationPrefixSegments: context.continuationPrefixSegments
236
+ };
237
+ }
238
+ function blockFromHtmlBlock(node, key, context, theme) {
239
+ return inlineBlock(key, inlineNodeSegments([{
240
+ type: "html_block",
241
+ raw: node.raw,
242
+ content: node.content
243
+ }], theme), context);
244
+ }
245
+ function childSequenceToBlocks(nodes, context, theme, keyPrefix) {
246
+ const out = [];
247
+ let appended = false;
248
+ for (let i = 0; i < nodes.length; i++) {
249
+ var _out, _childBlocks$;
250
+ const childBlocks = trimBlockArray(nodeToBlocks(nodes[i], context, theme, `${keyPrefix}-${i}`));
251
+ if (!childBlocks.length) continue;
252
+ if (appended && ((_out = out[out.length - 1]) === null || _out === void 0 ? void 0 : _out.type) !== "blank" && ((_childBlocks$ = childBlocks[0]) === null || _childBlocks$ === void 0 ? void 0 : _childBlocks$.type) !== "blank") out.push({
253
+ type: "blank",
254
+ key: `${keyPrefix}-${i}-gap`
255
+ });
256
+ out.push(...childBlocks);
257
+ appended = true;
258
+ }
259
+ return out;
260
+ }
261
+ function listItemBlocks(item, index, ordered, start, context, theme, keyPrefix) {
262
+ const markerText = ordered ? `${start + index}. ` : "- ";
263
+ const markerCells = textCellWidth(markerText);
264
+ const markerSegment = {
265
+ text: markerText,
266
+ style: theme.listMarker
267
+ };
268
+ const indentSegment = { text: spaces(markerCells) };
269
+ const firstContext = {
270
+ prefixSegments: [...context.prefixSegments, markerSegment],
271
+ continuationPrefixSegments: [...context.continuationPrefixSegments, indentSegment]
272
+ };
273
+ const nestedContext = {
274
+ prefixSegments: [...context.continuationPrefixSegments, indentSegment],
275
+ continuationPrefixSegments: [...context.continuationPrefixSegments, indentSegment]
276
+ };
277
+ if (!item.children.length) return [inlineBlock(`${keyPrefix}-empty`, [], firstContext)];
278
+ const out = [];
279
+ let usedLead = false;
280
+ for (let i = 0; i < item.children.length; i++) {
281
+ var _out2, _childBlocks$2, _out3, _childBlocks$3;
282
+ const child = item.children[i];
283
+ const useLeadContext = !usedLead && (child.type === "paragraph" || child.type === "heading" || child.type === "code_block");
284
+ if (!usedLead && !useLeadContext) {
285
+ out.push(inlineBlock(`${keyPrefix}-marker`, [], firstContext));
286
+ usedLead = true;
287
+ }
288
+ const childBlocks = trimBlockArray(nodeToBlocks(child, useLeadContext && !usedLead ? firstContext : nestedContext, theme, `${keyPrefix}-${i}`));
289
+ if (!childBlocks.length) continue;
290
+ const startsNestedList = child.type === "list" && out.length > 0 && ((_out2 = out[out.length - 1]) === null || _out2 === void 0 ? void 0 : _out2.type) === "inline" && ((_childBlocks$2 = childBlocks[0]) === null || _childBlocks$2 === void 0 ? void 0 : _childBlocks$2.type) === "inline";
291
+ if (out.length && ((_out3 = out[out.length - 1]) === null || _out3 === void 0 ? void 0 : _out3.type) !== "blank" && ((_childBlocks$3 = childBlocks[0]) === null || _childBlocks$3 === void 0 ? void 0 : _childBlocks$3.type) !== "blank" && !startsNestedList) out.push({
292
+ type: "blank",
293
+ key: `${keyPrefix}-${i}-gap`
294
+ });
295
+ out.push(...childBlocks);
296
+ usedLead = true;
297
+ }
298
+ if (!out.length) out.push(inlineBlock(`${keyPrefix}-fallback`, [], firstContext));
299
+ return out;
300
+ }
301
+ function listBlocks(node, context, theme, keyPrefix) {
302
+ const out = [];
303
+ for (let i = 0; i < node.items.length; i++) out.push(...listItemBlocks(node.items[i], i, node.ordered, node.start ?? 1, context, theme, `${keyPrefix}-${i}`));
304
+ return out;
305
+ }
306
+ function nodeToBlocks(node, context, theme, keyPrefix) {
307
+ switch (node.type) {
308
+ case "paragraph":
309
+ if ("children" in node && Array.isArray(node.children)) return [blockFromParagraph(node, keyPrefix, context, theme)];
310
+ break;
311
+ case "heading":
312
+ if ("children" in node && Array.isArray(node.children) && "level" in node) return [blockFromHeading(node, keyPrefix, context, theme)];
313
+ break;
314
+ case "code_block":
315
+ if ("code" in node) return [blockFromCodeBlock(node, keyPrefix, context, theme)];
316
+ break;
317
+ case "thematic_break": return [{
318
+ type: "thematic_break",
319
+ key: keyPrefix,
320
+ char: "─",
321
+ style: theme.thematicBreak,
322
+ prefixSegments: context.prefixSegments
323
+ }];
324
+ case "blockquote": {
325
+ const quoteSegment = {
326
+ text: "│ ",
327
+ style: theme.blockquote
328
+ };
329
+ const quoteContext = {
330
+ prefixSegments: [...context.prefixSegments, quoteSegment],
331
+ continuationPrefixSegments: [...context.continuationPrefixSegments, quoteSegment]
332
+ };
333
+ return childSequenceToBlocks(nodeChildren(node), quoteContext, theme, keyPrefix);
334
+ }
335
+ case "list":
336
+ if ("items" in node && Array.isArray(node.items)) return listBlocks(node, context, theme, keyPrefix);
337
+ break;
338
+ case "inline": return [inlineBlock(keyPrefix, inlineNodeSegments(nodeChildren(node), theme), context)];
339
+ case "html_block":
340
+ if ("content" in node) return [blockFromHtmlBlock(node, keyPrefix, context, theme)];
341
+ break;
342
+ case "text": return [inlineBlock(keyPrefix, inlineNodeSegments([node], theme), context)];
343
+ }
344
+ if (nodeChildren(node).length) return childSequenceToBlocks(nodeChildren(node), context, theme, keyPrefix);
345
+ return [inlineBlock(keyPrefix, inlineNodeSegments([node], theme), context)];
346
+ }
347
+ function markdownAstToBlocks(nodes, theme) {
348
+ const blocks = trimBlockArray(childSequenceToBlocks(nodes, {
349
+ prefixSegments: EMPTY_PREFIX,
350
+ continuationPrefixSegments: EMPTY_PREFIX
351
+ }, theme, "md"));
352
+ return blocks.length ? blocks : [{
353
+ type: "blank",
354
+ key: "md-empty"
355
+ }];
356
+ }
357
+
358
+ //#endregion
359
+ //#region src/vue/markdown/layout.ts
360
+ function toVisualSegment(segment) {
361
+ if (!segment.text) return null;
362
+ const cells = textCellWidth(segment.text);
363
+ if (cells <= 0) return null;
364
+ return {
365
+ text: segment.text,
366
+ style: segment.style,
367
+ cells
368
+ };
369
+ }
370
+ function segmentsPlainText(segments) {
371
+ return segments.map((segment) => segment.text).join("");
372
+ }
373
+ function segmentsCellWidth(segments) {
374
+ let cells = 0;
375
+ for (const segment of segments) {
376
+ if (segment.hardBreak || !segment.text) continue;
377
+ cells += textCellWidth(segment.text);
378
+ }
379
+ return cells;
380
+ }
381
+ function normalizeInlineSegments(segments) {
382
+ const lines = [[]];
383
+ for (const segment of segments) {
384
+ if (segment.hardBreak) {
385
+ lines.push([]);
386
+ continue;
387
+ }
388
+ lines[lines.length - 1].push(segment);
389
+ }
390
+ return lines;
391
+ }
392
+ function clipInlineSegmentsToWidth(segments, width) {
393
+ let remaining = Math.max(0, Math.floor(width));
394
+ if (remaining <= 0) return [];
395
+ const out = [];
396
+ for (const segment of segments) {
397
+ if (segment.hardBreak || !segment.text || remaining <= 0) continue;
398
+ const piece = sliceByCellsRange(segment.text, 0, remaining);
399
+ const cells = textCellWidth(piece);
400
+ if (!piece || cells <= 0) continue;
401
+ out.push(segment.style ? {
402
+ text: piece,
403
+ style: segment.style
404
+ } : { text: piece });
405
+ remaining -= cells;
406
+ }
407
+ return out;
408
+ }
409
+ function wrapLineSegments(source, prefixSegments, continuationPrefixSegments, width) {
410
+ const rows = [];
411
+ const prefixCells = segmentsCellWidth(prefixSegments);
412
+ const continuationPrefixCells = segmentsCellWidth(continuationPrefixSegments);
413
+ const firstPrefix = prefixCells >= width ? clipInlineSegmentsToWidth(prefixSegments, width) : prefixSegments;
414
+ const firstPrefixCells = segmentsCellWidth(firstPrefix);
415
+ const continuationPrefix = continuationPrefixCells >= width ? clipInlineSegmentsToWidth(continuationPrefixSegments, Math.max(0, width - 1)) : continuationPrefixSegments;
416
+ const trimmedContinuationPrefixCells = segmentsCellWidth(continuationPrefix);
417
+ const maxFirst = Math.max(0, width - firstPrefixCells);
418
+ const maxNext = Math.max(0, width - trimmedContinuationPrefixCells);
419
+ const openRow = (useContinuation) => {
420
+ const prefix = useContinuation ? continuationPrefix : firstPrefix;
421
+ const renderedPrefix = prefix.map(toVisualSegment).filter(Boolean);
422
+ return {
423
+ useContinuation,
424
+ segments: [...renderedPrefix],
425
+ remaining: useContinuation ? maxNext : maxFirst
426
+ };
427
+ };
428
+ const openDegradedRow = () => ({
429
+ useContinuation: true,
430
+ segments: [],
431
+ remaining: width
432
+ });
433
+ let row = openRow(false);
434
+ if (!source.length) {
435
+ rows.push(row.segments);
436
+ return rows;
437
+ }
438
+ const pushRow = () => {
439
+ rows.push(row.segments);
440
+ row = openRow(true);
441
+ };
442
+ const prefixLengthForRow = () => row.useContinuation ? continuationPrefix.length : firstPrefix.length;
443
+ const rowHasBody = () => row.segments.length > prefixLengthForRow();
444
+ for (const segment of source) {
445
+ let aborted = false;
446
+ forEachTextCellSegment(segment.text, (piece) => {
447
+ while (true) {
448
+ if (row.remaining <= 0) {
449
+ pushRow();
450
+ continue;
451
+ }
452
+ if (piece.cells > row.remaining) {
453
+ if (rowHasBody()) {
454
+ pushRow();
455
+ continue;
456
+ }
457
+ if (!row.useContinuation) {
458
+ rows.push(row.segments);
459
+ row = openDegradedRow();
460
+ continue;
461
+ }
462
+ if (piece.cells <= width) {
463
+ row = openDegradedRow();
464
+ continue;
465
+ }
466
+ aborted = true;
467
+ return false;
468
+ }
469
+ row.segments.push({
470
+ text: piece.text,
471
+ style: segment.style,
472
+ cells: piece.cells
473
+ });
474
+ row.remaining -= piece.cells;
475
+ return void 0;
476
+ }
477
+ });
478
+ if (aborted) continue;
479
+ }
480
+ rows.push(row.segments);
481
+ return rows;
482
+ }
483
+ function inlineBlockRows(block, width) {
484
+ const rows = [];
485
+ const lines = normalizeInlineSegments(block.segments);
486
+ const prefix = block.prefixSegments ?? [];
487
+ const continuationPrefix = block.continuationPrefixSegments ?? prefix;
488
+ let rowInBlock = 0;
489
+ for (const line of lines) {
490
+ const wrapped = wrapLineSegments(line, prefix, continuationPrefix, width);
491
+ for (const visualSegments of wrapped) {
492
+ rows.push({
493
+ key: `${block.key}:${rowInBlock}`,
494
+ blockKey: block.key,
495
+ rowInBlock,
496
+ plainText: segmentsPlainText(visualSegments),
497
+ segments: visualSegments
498
+ });
499
+ rowInBlock++;
500
+ }
501
+ }
502
+ return rows.length ? rows : [{
503
+ key: `${block.key}:0`,
504
+ blockKey: block.key,
505
+ rowInBlock: 0,
506
+ plainText: "",
507
+ segments: prefix.map(toVisualSegment).filter(Boolean)
508
+ }];
509
+ }
510
+ function codeBlockRows(block, width) {
511
+ const rows = [];
512
+ const prefix = block.prefixSegments ?? [];
513
+ const continuationPrefix = block.continuationPrefixSegments ?? prefix;
514
+ const lineSegments = block.lines.length ? block.lines : [""];
515
+ let rowInBlock = 0;
516
+ for (const line of lineSegments) {
517
+ const sourceSegments = [{
518
+ text: line,
519
+ style: block.style
520
+ }];
521
+ const wrapped = wrapLineSegments(sourceSegments, prefix, continuationPrefix, width);
522
+ for (const visualSegments of wrapped) {
523
+ rows.push({
524
+ key: `${block.key}:${rowInBlock}`,
525
+ blockKey: block.key,
526
+ rowInBlock,
527
+ plainText: segmentsPlainText(visualSegments),
528
+ segments: visualSegments
529
+ });
530
+ rowInBlock++;
531
+ }
532
+ }
533
+ return rows;
534
+ }
535
+ function thematicBreakRows(block, width) {
536
+ const prefix = (block.prefixSegments ?? []).map(toVisualSegment).filter(Boolean);
537
+ const prefixCells = prefix.reduce((sum, segment) => sum + segment.cells, 0);
538
+ const fill = Math.max(0, width - prefixCells);
539
+ const bar = fill > 0 ? [{
540
+ text: repeatChar(block.char ?? "─", fill),
541
+ style: block.style,
542
+ cells: fill
543
+ }] : [];
544
+ return [{
545
+ key: `${block.key}:0`,
546
+ blockKey: block.key,
547
+ rowInBlock: 0,
548
+ plainText: segmentsPlainText([...prefix, ...bar]),
549
+ segments: [...prefix, ...bar]
550
+ }];
551
+ }
552
+ function styleSignature(style) {
553
+ if (!style) return "";
554
+ return [
555
+ style.fg ?? "",
556
+ style.bg ?? "",
557
+ style.bold ? "1" : "0",
558
+ style.dim ? "1" : "0",
559
+ style.italic ? "1" : "0",
560
+ style.underline ? "1" : "0",
561
+ style.inverse ? "1" : "0",
562
+ style.href ?? ""
563
+ ].join("");
564
+ }
565
+ function inlineSegmentSignature(segment) {
566
+ return [
567
+ segment.text,
568
+ segment.hardBreak ? "1" : "0",
569
+ styleSignature(segment.style)
570
+ ].join("");
571
+ }
572
+ function inlineSegmentsSignature(segments) {
573
+ return (segments ?? []).map(inlineSegmentSignature).join("");
574
+ }
575
+ function blockLayoutSignature(block) {
576
+ switch (block.type) {
577
+ case "inline": return [
578
+ block.type,
579
+ inlineSegmentsSignature(block.segments),
580
+ inlineSegmentsSignature(block.prefixSegments),
581
+ inlineSegmentsSignature(block.continuationPrefixSegments)
582
+ ].join("");
583
+ case "code_block": return [
584
+ block.type,
585
+ block.lines.join(""),
586
+ styleSignature(block.style),
587
+ inlineSegmentsSignature(block.prefixSegments),
588
+ inlineSegmentsSignature(block.continuationPrefixSegments)
589
+ ].join("");
590
+ case "thematic_break": return [
591
+ block.type,
592
+ block.char ?? "",
593
+ styleSignature(block.style),
594
+ inlineSegmentsSignature(block.prefixSegments)
595
+ ].join("");
596
+ case "blank": return block.type;
597
+ }
598
+ }
599
+ function layoutMarkdownBlock(block, width) {
600
+ switch (block.type) {
601
+ case "inline": return inlineBlockRows(block, width);
602
+ case "code_block": return codeBlockRows(block, width);
603
+ case "thematic_break": return thematicBreakRows(block, width);
604
+ case "blank": return [{
605
+ key: `${block.key}:0`,
606
+ blockKey: block.key,
607
+ rowInBlock: 0,
608
+ plainText: "",
609
+ segments: []
610
+ }];
611
+ }
612
+ }
613
+ function layoutMarkdownBlocksCached(blocks, width, previous) {
614
+ const normalizedWidth = Math.max(0, Math.floor(width));
615
+ if (normalizedWidth <= 0) return {
616
+ rows: [],
617
+ cache: {
618
+ width: normalizedWidth,
619
+ entries: new Map()
620
+ }
621
+ };
622
+ const rows = [];
623
+ const entries = new Map();
624
+ const previousEntries = (previous === null || previous === void 0 ? void 0 : previous.width) === normalizedWidth ? previous.entries : void 0;
625
+ for (let index = 0; index < blocks.length; index++) {
626
+ const block = blocks[index];
627
+ const cacheKey = `${index}\u0000${block.key}`;
628
+ const signature = blockLayoutSignature(block);
629
+ const cached = previousEntries === null || previousEntries === void 0 ? void 0 : previousEntries.get(cacheKey);
630
+ const blockRows = (cached === null || cached === void 0 ? void 0 : cached.signature) === signature ? cached.rows : layoutMarkdownBlock(block, normalizedWidth);
631
+ if (blockRows.length) {
632
+ rows.push(...blockRows);
633
+ entries.set(cacheKey, {
634
+ signature,
635
+ rows: blockRows
636
+ });
637
+ }
638
+ }
639
+ if (!rows.length) {
640
+ const emptyRows = [{
641
+ key: "md-empty:0",
642
+ blockKey: "md-empty",
643
+ rowInBlock: 0,
644
+ plainText: "",
645
+ segments: []
646
+ }];
647
+ entries.set("0\0md-empty", {
648
+ signature: "empty",
649
+ rows: emptyRows
650
+ });
651
+ rows.push(...emptyRows);
652
+ }
653
+ return {
654
+ rows,
655
+ cache: {
656
+ width: normalizedWidth,
657
+ entries
658
+ }
659
+ };
660
+ }
661
+ function layoutMarkdownBlocks(blocks, width) {
662
+ return layoutMarkdownBlocksCached(blocks, width).rows;
663
+ }
664
+
665
+ //#endregion
666
+ //#region src/vue/markdown/theme.ts
667
+ function freezeStyle(style) {
668
+ return Object.freeze(style);
669
+ }
670
+ function mergeFrozenStyle(base, overlay) {
671
+ if (!overlay) return base;
672
+ return freezeStyle({
673
+ ...base,
674
+ ...overlay
675
+ });
676
+ }
677
+ const DEFAULT_HEADING = Object.freeze([
678
+ freezeStyle({
679
+ bold: true,
680
+ fg: "cyanBright"
681
+ }),
682
+ freezeStyle({
683
+ bold: true,
684
+ fg: "cyanBright"
685
+ }),
686
+ freezeStyle({
687
+ bold: true,
688
+ fg: "blueBright"
689
+ }),
690
+ freezeStyle({
691
+ bold: true,
692
+ fg: "blueBright"
693
+ }),
694
+ freezeStyle({ bold: true }),
695
+ freezeStyle({ bold: true })
696
+ ]);
697
+ const DEFAULT_TUI_MARKDOWN_THEME = Object.freeze({
698
+ heading: DEFAULT_HEADING,
699
+ strong: freezeStyle({ bold: true }),
700
+ emphasis: freezeStyle({ italic: true }),
701
+ strikethrough: freezeStyle({ dim: true }),
702
+ inlineCode: freezeStyle({ fg: "yellowBright" }),
703
+ link: freezeStyle({
704
+ fg: "blueBright",
705
+ underline: true
706
+ }),
707
+ blockquote: freezeStyle({ dim: true }),
708
+ listMarker: freezeStyle({
709
+ fg: "cyanBright",
710
+ bold: true
711
+ }),
712
+ codeBlock: freezeStyle({ fg: "yellowBright" }),
713
+ thematicBreak: freezeStyle({ dim: true }),
714
+ html: freezeStyle({ dim: true })
715
+ });
716
+ function stableSerialize(value) {
717
+ if (value == null) return "null";
718
+ if (typeof value !== "object") return JSON.stringify(value);
719
+ if (Array.isArray(value)) return `[${value.map((item) => stableSerialize(item)).join(",")}]`;
720
+ const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
721
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableSerialize(item)}`).join(",")}}`;
722
+ }
723
+ function markdownThemeSignature(overrides) {
724
+ return stableSerialize(overrides ?? null);
725
+ }
726
+ function resolveTuiMarkdownTheme(overrides) {
727
+ if (!overrides) return DEFAULT_TUI_MARKDOWN_THEME;
728
+ const heading = DEFAULT_TUI_MARKDOWN_THEME.heading.map((style, index) => {
729
+ var _overrides$heading;
730
+ return mergeFrozenStyle(style, (_overrides$heading = overrides.heading) === null || _overrides$heading === void 0 ? void 0 : _overrides$heading[index]);
731
+ });
732
+ return Object.freeze({
733
+ heading: Object.freeze(heading),
734
+ strong: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.strong, overrides.strong),
735
+ emphasis: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.emphasis, overrides.emphasis),
736
+ strikethrough: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.strikethrough, overrides.strikethrough),
737
+ inlineCode: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.inlineCode, overrides.inlineCode),
738
+ link: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.link, overrides.link),
739
+ blockquote: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.blockquote, overrides.blockquote),
740
+ listMarker: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.listMarker, overrides.listMarker),
741
+ codeBlock: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.codeBlock, overrides.codeBlock),
742
+ thematicBreak: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.thematicBreak, overrides.thematicBreak),
743
+ html: mergeFrozenStyle(DEFAULT_TUI_MARKDOWN_THEME.html, overrides.html)
744
+ });
745
+ }
746
+
747
+ //#endregion
748
+ //#region src/vue/markdown/document.ts
749
+ /**
750
+ * This helper always builds visual rows from the full source string. Components
751
+ * that need streaming reuse can call buildMarkdownBlocks and cache block layout.
752
+ */
753
+ function buildMarkdownBlocks(content, parser, options) {
754
+ const theme = resolveTuiMarkdownTheme(options === null || options === void 0 ? void 0 : options.theme);
755
+ try {
756
+ const nodes = parser.parse(content, (options === null || options === void 0 ? void 0 : options.final) ?? true);
757
+ const blocks = markdownAstToBlocks(nodes, theme);
758
+ return {
759
+ nodes,
760
+ blocks
761
+ };
762
+ } catch (error) {
763
+ console.warn("[vue-tui] Markdown parse failed; falling back to plain text rendering.", error);
764
+ return {
765
+ nodes: [],
766
+ blocks: plainTextFallbackBlocks(content)
767
+ };
768
+ }
769
+ }
770
+ function buildMarkdownVisualRows(content, width, parser, options) {
771
+ const { blocks } = buildMarkdownBlocks(content, parser, options);
772
+ return layoutMarkdownBlocks(blocks, width);
773
+ }
774
+ function plainTextFallbackBlocks(content) {
775
+ const normalized = sanitizeTextBlock(content);
776
+ const lines = normalized.split("\n");
777
+ const blocks = [];
778
+ for (let index = 0; index < lines.length; index++) {
779
+ const line = sanitizeInlineText(lines[index] ?? "");
780
+ if (!line) {
781
+ blocks.push({
782
+ type: "blank",
783
+ key: `md-fallback:${index}`
784
+ });
785
+ continue;
786
+ }
787
+ blocks.push({
788
+ type: "inline",
789
+ key: `md-fallback:${index}`,
790
+ segments: [{ text: line }]
791
+ });
792
+ }
793
+ return blocks.length ? blocks : [{
794
+ type: "blank",
795
+ key: "md-fallback:0"
796
+ }];
797
+ }
798
+
799
+ //#endregion
800
+ //#region src/vue/markdown/render.ts
801
+ const mergedStyleCache = new WeakMap();
802
+ function mergeStyle(base, overlay) {
803
+ if (!overlay) return base;
804
+ let bucket = mergedStyleCache.get(base);
805
+ if (!bucket) {
806
+ bucket = new WeakMap();
807
+ mergedStyleCache.set(base, bucket);
808
+ }
809
+ const cached = bucket.get(overlay);
810
+ if (cached) return cached;
811
+ const merged = Object.freeze({
812
+ ...base,
813
+ ...overlay
814
+ });
815
+ bucket.set(overlay, merged);
816
+ return merged;
817
+ }
818
+ function paintMarkdownVisualRow(terminal, row, options) {
819
+ const clipStart = Math.max(0, Math.floor(options.clipStart ?? 0));
820
+ const clipEnd = clipStart + options.w;
821
+ if (!row) {
822
+ if (options.clear !== false) terminal.write(spaces(options.w), {
823
+ x: options.x,
824
+ y: options.y,
825
+ style: options.baseStyle
826
+ });
827
+ return;
828
+ }
829
+ let logicalX = 0;
830
+ let used = 0;
831
+ for (const segment of row.segments) {
832
+ if (used >= options.w || !segment.text) {
833
+ logicalX += segment.cells;
834
+ continue;
835
+ }
836
+ const segmentStart = logicalX;
837
+ const segmentEnd = logicalX + segment.cells;
838
+ logicalX += segment.cells;
839
+ if (segmentEnd <= clipStart || segmentStart >= clipEnd) continue;
840
+ const segmentStyle = mergeStyle(options.baseStyle, segment.style);
841
+ let pieceStart = segmentStart;
842
+ forEachTextCellSegment(segment.text, (piece) => {
843
+ if (used >= options.w) return false;
844
+ const pieceEnd = pieceStart + piece.cells;
845
+ const visibleStart = Math.max(pieceStart, clipStart);
846
+ const visibleEnd = Math.min(pieceEnd, clipEnd);
847
+ pieceStart = pieceEnd;
848
+ if (visibleEnd <= visibleStart) return;
849
+ const expectedUsed = visibleStart - clipStart;
850
+ if (expectedUsed > used) {
851
+ const pad$1 = Math.min(expectedUsed - used, options.w - used);
852
+ terminal.write(spaces(pad$1), {
853
+ x: options.x + used,
854
+ y: options.y,
855
+ style: options.baseStyle
856
+ });
857
+ used += pad$1;
858
+ }
859
+ if (visibleStart === pieceStart - piece.cells && visibleEnd === pieceEnd) {
860
+ terminal.write(piece.text, {
861
+ x: options.x + used,
862
+ y: options.y,
863
+ style: segmentStyle
864
+ });
865
+ used += piece.cells;
866
+ return;
867
+ }
868
+ const pad = Math.min(visibleEnd - visibleStart, options.w - used);
869
+ if (pad > 0) {
870
+ terminal.write(spaces(pad), {
871
+ x: options.x + used,
872
+ y: options.y,
873
+ style: segmentStyle
874
+ });
875
+ used += pad;
876
+ }
877
+ });
878
+ }
879
+ if (used < options.w && options.clear !== false) terminal.write(spaces(options.w - used), {
880
+ x: options.x + used,
881
+ y: options.y,
882
+ style: options.baseStyle
883
+ });
884
+ }
885
+
886
+ //#endregion
887
+ //#region src/vue/components/TMarkdownText.ts
888
+ const TMarkdownText = defineComponent({
889
+ name: "TMarkdownText",
890
+ props: {
891
+ x: {
892
+ type: Number,
893
+ required: true
894
+ },
895
+ y: {
896
+ type: Number,
897
+ required: true
898
+ },
899
+ zIndex: {
900
+ type: Number,
901
+ default: 0
902
+ },
903
+ content: {
904
+ type: String,
905
+ required: true
906
+ },
907
+ w: {
908
+ type: Number,
909
+ required: true
910
+ },
911
+ h: {
912
+ type: Number,
913
+ default: void 0
914
+ },
915
+ style: {
916
+ type: Object,
917
+ default: void 0
918
+ },
919
+ final: {
920
+ type: Boolean,
921
+ default: true
922
+ },
923
+ streaming: {
924
+ type: Boolean,
925
+ default: false
926
+ },
927
+ clear: {
928
+ type: Boolean,
929
+ default: true
930
+ },
931
+ customHtmlTags: {
932
+ type: Array,
933
+ default: void 0
934
+ },
935
+ theme: {
936
+ type: Object,
937
+ default: void 0
938
+ }
939
+ },
940
+ setup(props) {
941
+ const instance = getCurrentInstance();
942
+ const { terminal, defaultStyle, scheduler } = useTerminal();
943
+ const layout = useLayout();
944
+ const { visible, rootProps } = useVisibility();
945
+ const rows = shallowRef(markRaw([]));
946
+ const documentVersion = shallowRef(0);
947
+ let builtOnce = false;
948
+ let rebuildVersion = 0;
949
+ let alive = true;
950
+ const parser = shallowRef(markRaw(createTuiMarkdownParser({
951
+ streaming: props.streaming,
952
+ customHtmlTags: props.customHtmlTags
953
+ })));
954
+ watch(() => `${props.streaming ? 1 : 0}:${(props.customHtmlTags ?? []).join("\0")}`, () => {
955
+ parser.value = markRaw(createTuiMarkdownParser({
956
+ streaming: props.streaming,
957
+ customHtmlTags: props.customHtmlTags
958
+ }));
959
+ });
960
+ function rebuildRows() {
961
+ rows.value = markRaw(buildMarkdownVisualRows(props.content, props.w, parser.value, {
962
+ final: props.final,
963
+ theme: props.theme
964
+ }));
965
+ documentVersion.value++;
966
+ }
967
+ function scheduleRebuild() {
968
+ const currentVersion = ++rebuildVersion;
969
+ if (!builtOnce || !props.streaming) {
970
+ builtOnce = true;
971
+ rebuildRows();
972
+ return;
973
+ }
974
+ scheduler.queueFrameTask({
975
+ id: `TMarkdownText:${(instance === null || instance === void 0 ? void 0 : instance.uid) ?? "unknown"}:markdown`,
976
+ reason: props.streaming ? "stream" : "data",
977
+ priority: props.streaming ? "low" : "normal",
978
+ sync: false,
979
+ run: () => {
980
+ if (!alive) return;
981
+ if (currentVersion !== rebuildVersion) return;
982
+ rebuildRows();
983
+ }
984
+ });
985
+ }
986
+ watch([
987
+ () => props.content,
988
+ () => props.w,
989
+ () => parser.value,
990
+ () => props.final,
991
+ () => markdownThemeSignature(props.theme)
992
+ ], () => {
993
+ scheduleRebuild();
994
+ }, { immediate: true });
995
+ onBeforeUnmount(() => {
996
+ alive = false;
997
+ rebuildVersion++;
998
+ });
999
+ const fullRect = computed(() => {
1000
+ const height = props.h ?? Math.max(1, rows.value.length);
1001
+ return translateRect({
1002
+ x: props.x,
1003
+ y: props.y,
1004
+ w: props.w,
1005
+ h: height
1006
+ }, layout.originX, layout.originY);
1007
+ });
1008
+ const absRect = computed(() => {
1009
+ const translated = fullRect.value;
1010
+ if (!layout.clipRect) return translated;
1011
+ return intersectRect(translated, layout.clipRect) ?? {
1012
+ x: 0,
1013
+ y: 0,
1014
+ w: 0,
1015
+ h: 0
1016
+ };
1017
+ });
1018
+ useRenderNode(() => ({
1019
+ zIndex: props.zIndex,
1020
+ rect: visible.value ? absRect.value : {
1021
+ x: 0,
1022
+ y: 0,
1023
+ w: 0,
1024
+ h: 0
1025
+ },
1026
+ deps: [
1027
+ visible.value,
1028
+ absRect.value,
1029
+ fullRect.value,
1030
+ props.style,
1031
+ props.clear,
1032
+ documentVersion.value,
1033
+ defaultStyle.value
1034
+ ],
1035
+ paint: (dirtyRows) => {
1036
+ if (!visible.value) return;
1037
+ const r = absRect.value;
1038
+ const full = fullRect.value;
1039
+ if (r.w <= 0 || r.h <= 0) return;
1040
+ const baseStyle = props.style ?? defaultStyle.value;
1041
+ const clipStart = Math.max(0, Math.floor(r.x - full.x));
1042
+ const paintRow = (y) => {
1043
+ if (y < r.y || y >= r.y + r.h) return;
1044
+ const rowIndex = Math.floor(y - full.y);
1045
+ const row = rows.value[rowIndex];
1046
+ if (!row && !props.clear) return;
1047
+ paintMarkdownVisualRow(terminal, row, {
1048
+ x: r.x,
1049
+ y,
1050
+ w: r.w,
1051
+ clipStart,
1052
+ baseStyle,
1053
+ clear: props.clear
1054
+ });
1055
+ };
1056
+ if (dirtyRows === null || dirtyRows === void 0 ? void 0 : dirtyRows.length) {
1057
+ for (const y of dirtyRows) paintRow(y);
1058
+ return;
1059
+ }
1060
+ for (let y = r.y; y < r.y + r.h; y++) paintRow(y);
1061
+ }
1062
+ }));
1063
+ return () => h("span", rootProps);
1064
+ }
1065
+ });
1066
+
1067
+ //#endregion
1068
+ //#region src/vue/components/TVirtualMarkdown.ts
1069
+ function clamp(n, min, max) {
1070
+ return Math.max(min, Math.min(max, n));
1071
+ }
1072
+ function markdownStyleSignature(style) {
1073
+ if (!style) return "";
1074
+ return [
1075
+ style.fg ?? "",
1076
+ style.bg ?? "",
1077
+ style.bold ? "1" : "0",
1078
+ style.dim ? "1" : "0",
1079
+ style.italic ? "1" : "0",
1080
+ style.underline ? "1" : "0",
1081
+ style.inverse ? "1" : "0",
1082
+ style.href ?? ""
1083
+ ].join("");
1084
+ }
1085
+ function markdownRowSignature(row) {
1086
+ if (!row) return "";
1087
+ return [
1088
+ row.key,
1089
+ row.plainText,
1090
+ row.segments.map((segment) => `${segment.text}\u0001${segment.cells}\u0001${markdownStyleSignature(segment.style)}`).join("")
1091
+ ].join("");
1092
+ }
1093
+ function getWheelScrollInput(e) {
1094
+ const deltaY = Number(e.deltaY ?? 0);
1095
+ const deltaMode = typeof e.deltaMode === "number" ? e.deltaMode : void 0;
1096
+ if (Number.isInteger(deltaY) && deltaY !== 0 && Math.abs(deltaY) >= 100 && Math.abs(deltaY) % 100 === 0 && deltaMode == null) return {
1097
+ deltaY: deltaY / 100,
1098
+ mode: "line"
1099
+ };
1100
+ if (deltaMode === 1) return {
1101
+ deltaY,
1102
+ mode: "line"
1103
+ };
1104
+ if (deltaMode === 0) return {
1105
+ deltaY,
1106
+ mode: "pixel"
1107
+ };
1108
+ return {
1109
+ deltaY,
1110
+ mode: "auto"
1111
+ };
1112
+ }
1113
+ const TVirtualMarkdown = defineComponent({
1114
+ name: "TVirtualMarkdown",
1115
+ props: {
1116
+ x: {
1117
+ type: Number,
1118
+ required: true
1119
+ },
1120
+ y: {
1121
+ type: Number,
1122
+ required: true
1123
+ },
1124
+ w: {
1125
+ type: Number,
1126
+ required: true
1127
+ },
1128
+ h: {
1129
+ type: Number,
1130
+ required: true
1131
+ },
1132
+ zIndex: {
1133
+ type: Number,
1134
+ default: 0
1135
+ },
1136
+ content: {
1137
+ type: String,
1138
+ default: ""
1139
+ },
1140
+ blocks: {
1141
+ type: Array,
1142
+ default: void 0
1143
+ },
1144
+ scrollTop: {
1145
+ type: Number,
1146
+ default: 0
1147
+ },
1148
+ style: {
1149
+ type: Object,
1150
+ default: void 0
1151
+ },
1152
+ final: {
1153
+ type: Boolean,
1154
+ default: true
1155
+ },
1156
+ streaming: {
1157
+ type: Boolean,
1158
+ default: false
1159
+ },
1160
+ autoFocus: {
1161
+ type: Boolean,
1162
+ default: false
1163
+ },
1164
+ selectable: {
1165
+ type: Boolean,
1166
+ default: true
1167
+ },
1168
+ customHtmlTags: {
1169
+ type: Array,
1170
+ default: void 0
1171
+ },
1172
+ theme: {
1173
+ type: Object,
1174
+ default: void 0
1175
+ }
1176
+ },
1177
+ emits: [
1178
+ "update:scrollTop",
1179
+ "scroll",
1180
+ "focus",
1181
+ "blur",
1182
+ "keydown"
1183
+ ],
1184
+ setup(props, { emit }) {
1185
+ const instance = getCurrentInstance();
1186
+ const { terminal, defaultStyle, events, scheduler } = useTerminal();
1187
+ const layout = useLayout();
1188
+ const { visible, rootProps } = useVisibility();
1189
+ const parentEventZ = inject(EventZIndexContextKey, computed(() => 0));
1190
+ const eventZ = computed(() => (parentEventZ.value ?? 0) + (props.zIndex ?? 0));
1191
+ const internalScrollTop = ref(0);
1192
+ const documentVersion = ref(0);
1193
+ const rows = shallowRef(markRaw([]));
1194
+ const wheelState = createWheelScrollState();
1195
+ let builtOnce = false;
1196
+ let rebuildVersion = 0;
1197
+ let layoutCache;
1198
+ let alive = true;
1199
+ const parser = shallowRef(markRaw(createTuiMarkdownParser({
1200
+ streaming: props.streaming,
1201
+ customHtmlTags: props.customHtmlTags
1202
+ })));
1203
+ watch(() => `${props.streaming ? 1 : 0}:${(props.customHtmlTags ?? []).join("\0")}`, () => {
1204
+ parser.value = markRaw(createTuiMarkdownParser({
1205
+ streaming: props.streaming,
1206
+ customHtmlTags: props.customHtmlTags
1207
+ }));
1208
+ });
1209
+ function rebuildRows() {
1210
+ const prevScrollTop = internalScrollTop.value;
1211
+ const prevVisibleRows = visibleRowSignatures(rows.value, prevScrollTop);
1212
+ const blocks = props.blocks ?? buildMarkdownBlocks(props.content, parser.value, {
1213
+ final: props.final,
1214
+ theme: props.theme
1215
+ }).blocks;
1216
+ const nextLayout = layoutMarkdownBlocksCached(blocks, props.w, layoutCache);
1217
+ layoutCache = markRaw(nextLayout.cache);
1218
+ const nextRows = nextLayout.rows;
1219
+ rows.value = markRaw(nextRows);
1220
+ reconcileScrollTop();
1221
+ const nextScrollTop = internalScrollTop.value;
1222
+ const nextVisibleRows = visibleRowSignatures(nextRows, nextScrollTop);
1223
+ const visibleChanged = prevVisibleRows.length !== nextVisibleRows.length || prevVisibleRows.some((row, index) => row !== nextVisibleRows[index]);
1224
+ if (!builtOnce || prevScrollTop !== nextScrollTop || visibleChanged) documentVersion.value++;
1225
+ }
1226
+ function scheduleRebuild() {
1227
+ const currentVersion = ++rebuildVersion;
1228
+ if (!builtOnce || !props.streaming) {
1229
+ builtOnce = true;
1230
+ rebuildRows();
1231
+ return;
1232
+ }
1233
+ scheduler.queueFrameTask({
1234
+ id: `TVirtualMarkdown:${(instance === null || instance === void 0 ? void 0 : instance.uid) ?? "unknown"}:markdown`,
1235
+ reason: props.streaming ? "stream" : "data",
1236
+ priority: props.streaming ? "low" : "normal",
1237
+ sync: false,
1238
+ run: () => {
1239
+ if (!alive) return;
1240
+ if (currentVersion !== rebuildVersion) return;
1241
+ rebuildRows();
1242
+ }
1243
+ });
1244
+ }
1245
+ const fullRect = computed(() => translateRect({
1246
+ x: props.x,
1247
+ y: props.y,
1248
+ w: props.w,
1249
+ h: props.h
1250
+ }, layout.originX, layout.originY));
1251
+ const absRect = computed(() => {
1252
+ const translated = fullRect.value;
1253
+ if (!layout.clipRect) return translated;
1254
+ return intersectRect(translated, layout.clipRect) ?? {
1255
+ x: 0,
1256
+ y: 0,
1257
+ w: 0,
1258
+ h: 0
1259
+ };
1260
+ });
1261
+ function normalizedRect() {
1262
+ return normalizeCellRect(absRect.value);
1263
+ }
1264
+ function normalizedFullRect() {
1265
+ return normalizeCellRect(fullRect.value);
1266
+ }
1267
+ function clipOffsets() {
1268
+ const full = normalizedFullRect();
1269
+ const clip = normalizedRect();
1270
+ return {
1271
+ x: Math.max(0, clip.x - full.x),
1272
+ y: Math.max(0, clip.y - full.y)
1273
+ };
1274
+ }
1275
+ function maxScrollTop() {
1276
+ const clip = normalizedRect();
1277
+ const { y: clipY } = clipOffsets();
1278
+ return Math.max(0, rows.value.length - (clipY + clip.h));
1279
+ }
1280
+ function visibleRowSignatures(sourceRows, scrollTop) {
1281
+ const clip = normalizedRect();
1282
+ const { y: clipY } = clipOffsets();
1283
+ const start = Math.max(0, scrollTop + clipY);
1284
+ const end = Math.max(start, start + clip.h);
1285
+ const out = [];
1286
+ for (let index = start; index < end; index++) out.push(markdownRowSignature(sourceRows[index]));
1287
+ return out;
1288
+ }
1289
+ function hasControlledScrollTop() {
1290
+ return Object.prototype.hasOwnProperty.call((instance === null || instance === void 0 ? void 0 : instance.vnode.props) ?? {}, "scrollTop");
1291
+ }
1292
+ function setScrollTop(next, emitChange = true) {
1293
+ const clamped = clamp(Math.floor(Number(next) || 0), 0, maxScrollTop());
1294
+ if (internalScrollTop.value === clamped) return;
1295
+ internalScrollTop.value = clamped;
1296
+ if (emitChange) {
1297
+ emit("update:scrollTop", clamped);
1298
+ emit("scroll", clamped);
1299
+ }
1300
+ }
1301
+ function reconcileScrollTop() {
1302
+ const desired = hasControlledScrollTop() ? Math.floor(Number(props.scrollTop) || 0) : internalScrollTop.value;
1303
+ const clamped = clamp(desired, 0, maxScrollTop());
1304
+ if (internalScrollTop.value === clamped) return;
1305
+ internalScrollTop.value = clamped;
1306
+ if (desired !== clamped) {
1307
+ emit("update:scrollTop", clamped);
1308
+ emit("scroll", clamped);
1309
+ }
1310
+ }
1311
+ watch(() => props.scrollTop, () => {
1312
+ if (!hasControlledScrollTop()) return;
1313
+ const desired = Math.floor(Number(props.scrollTop) || 0);
1314
+ const clamped = clamp(desired, 0, maxScrollTop());
1315
+ internalScrollTop.value = clamped;
1316
+ if (desired !== clamped) {
1317
+ emit("update:scrollTop", clamped);
1318
+ emit("scroll", clamped);
1319
+ }
1320
+ });
1321
+ watch([
1322
+ () => props.content,
1323
+ () => props.blocks,
1324
+ () => props.w,
1325
+ () => parser.value,
1326
+ () => props.final,
1327
+ () => markdownThemeSignature(props.theme)
1328
+ ], () => {
1329
+ scheduleRebuild();
1330
+ }, { immediate: true });
1331
+ watch([
1332
+ () => props.content,
1333
+ () => props.w,
1334
+ () => props.h,
1335
+ () => absRect.value.h
1336
+ ], () => {
1337
+ resetWheelScrollState(wheelState);
1338
+ });
1339
+ watch([
1340
+ () => props.h,
1341
+ () => absRect.value.y,
1342
+ () => absRect.value.h,
1343
+ () => fullRect.value.y,
1344
+ () => fullRect.value.h
1345
+ ], () => {
1346
+ reconcileScrollTop();
1347
+ }, { immediate: true });
1348
+ function onKeydown(event) {
1349
+ emit("keydown", event);
1350
+ const page = Math.max(1, normalizedRect().h);
1351
+ if (event.key === "ArrowUp") {
1352
+ event.preventDefault();
1353
+ setScrollTop(internalScrollTop.value - 1);
1354
+ return;
1355
+ }
1356
+ if (event.key === "ArrowDown") {
1357
+ event.preventDefault();
1358
+ setScrollTop(internalScrollTop.value + 1);
1359
+ return;
1360
+ }
1361
+ if (event.key === "PageUp") {
1362
+ event.preventDefault();
1363
+ setScrollTop(internalScrollTop.value - page);
1364
+ return;
1365
+ }
1366
+ if (event.key === "PageDown") {
1367
+ event.preventDefault();
1368
+ setScrollTop(internalScrollTop.value + page);
1369
+ return;
1370
+ }
1371
+ if (event.key === "Home") {
1372
+ event.preventDefault();
1373
+ setScrollTop(0);
1374
+ return;
1375
+ }
1376
+ if (event.key === "End") {
1377
+ event.preventDefault();
1378
+ setScrollTop(maxScrollTop());
1379
+ }
1380
+ }
1381
+ const eventNode = useTerminalNode(() => ({
1382
+ rect: normalizedRect(),
1383
+ zIndex: eventZ.value,
1384
+ visible: visible.value,
1385
+ focusable: true,
1386
+ selectable: props.selectable,
1387
+ handlers: {
1388
+ click: (_event) => {},
1389
+ wheel: (event) => {
1390
+ var _event$preventDefault;
1391
+ const { deltaY, mode } = getWheelScrollInput(event);
1392
+ if (!deltaY) return;
1393
+ const now = typeof event.time === "number" ? event.time : typeof event.timeStamp === "number" ? event.timeStamp : Date.now();
1394
+ const { nextTop, dir } = applyWheelScroll(wheelState, deltaY, internalScrollTop.value, maxScrollTop(), now, mode, { disableAcceleration: mode === "pixel" });
1395
+ if (!dir || nextTop === internalScrollTop.value) return;
1396
+ (_event$preventDefault = event.preventDefault) === null || _event$preventDefault === void 0 || _event$preventDefault.call(event);
1397
+ setScrollTop(nextTop);
1398
+ },
1399
+ focus: () => {
1400
+ emit("focus");
1401
+ },
1402
+ blur: () => {
1403
+ emit("blur");
1404
+ },
1405
+ keydown: onKeydown
1406
+ }
1407
+ }));
1408
+ watchEffect(() => {
1409
+ if (!props.autoFocus || !visible.value) return;
1410
+ const manager = events.value;
1411
+ const nodeId = eventNode.id.value;
1412
+ if (!nodeId) return;
1413
+ if (!manager) return;
1414
+ if (manager.getFocused() === nodeId) return;
1415
+ manager.focus(nodeId);
1416
+ });
1417
+ onBeforeUnmount(() => {
1418
+ alive = false;
1419
+ rebuildVersion++;
1420
+ });
1421
+ useRenderNode(() => ({
1422
+ zIndex: props.zIndex,
1423
+ rect: visible.value ? normalizedRect() : {
1424
+ x: 0,
1425
+ y: 0,
1426
+ w: 0,
1427
+ h: 0
1428
+ },
1429
+ deps: [
1430
+ visible.value,
1431
+ absRect.value,
1432
+ fullRect.value,
1433
+ internalScrollTop.value,
1434
+ props.style,
1435
+ defaultStyle.value,
1436
+ documentVersion.value
1437
+ ],
1438
+ paint: (dirtyRows) => {
1439
+ if (!visible.value) return;
1440
+ const r = normalizedRect();
1441
+ if (r.w <= 0 || r.h <= 0) return;
1442
+ const baseStyle = props.style ?? defaultStyle.value;
1443
+ const { x: clipX, y: clipY } = clipOffsets();
1444
+ const paintRow = (y) => {
1445
+ if (y < r.y || y >= r.y + r.h) return;
1446
+ const visualIndex = internalScrollTop.value + clipY + (y - r.y);
1447
+ paintMarkdownVisualRow(terminal, rows.value[visualIndex], {
1448
+ x: r.x,
1449
+ y,
1450
+ w: r.w,
1451
+ clipStart: clipX,
1452
+ baseStyle,
1453
+ clear: true
1454
+ });
1455
+ };
1456
+ if (dirtyRows === null || dirtyRows === void 0 ? void 0 : dirtyRows.length) {
1457
+ for (const y of dirtyRows) paintRow(y);
1458
+ return;
1459
+ }
1460
+ for (let y = r.y; y < r.y + r.h; y++) paintRow(y);
1461
+ }
1462
+ }));
1463
+ return () => h("span", rootProps);
1464
+ }
1465
+ });
1466
+
1467
+ //#endregion
1468
+ //#region src/vue/markdown/block-source.ts
1469
+ function withBlockKeyPrefix(block, prefix, index) {
1470
+ const key = `${prefix}:${index}:${block.key}`;
1471
+ switch (block.type) {
1472
+ case "inline": return {
1473
+ ...block,
1474
+ key
1475
+ };
1476
+ case "code_block": return {
1477
+ ...block,
1478
+ key
1479
+ };
1480
+ case "thematic_break": return {
1481
+ ...block,
1482
+ key
1483
+ };
1484
+ case "blank": return {
1485
+ ...block,
1486
+ key
1487
+ };
1488
+ }
1489
+ }
1490
+ function parseBlockGroup(text, key, parser, theme, final) {
1491
+ const { blocks } = buildMarkdownBlocks(text, parser, {
1492
+ final,
1493
+ theme
1494
+ });
1495
+ return {
1496
+ key,
1497
+ blocks: blocks.map((block, index) => withBlockKeyPrefix(block, key, index))
1498
+ };
1499
+ }
1500
+ function shouldInsertGap(previous, next) {
1501
+ return previous != null && next != null && previous.type !== "blank" && next.type !== "blank";
1502
+ }
1503
+ function mergeGroups(groups) {
1504
+ const out = [];
1505
+ for (const group of groups) {
1506
+ if (!group.blocks.length) continue;
1507
+ if (shouldInsertGap(out[out.length - 1], group.blocks[0])) out.push({
1508
+ type: "blank",
1509
+ key: `${group.key}:gap`
1510
+ });
1511
+ out.push(...group.blocks);
1512
+ }
1513
+ return out;
1514
+ }
1515
+ function createMarkdownBlockSource(options) {
1516
+ const parser = (options === null || options === void 0 ? void 0 : options.parser) ?? createTuiMarkdownParser({
1517
+ streaming: true,
1518
+ customHtmlTags: options === null || options === void 0 ? void 0 : options.customHtmlTags
1519
+ });
1520
+ const theme = options === null || options === void 0 ? void 0 : options.theme;
1521
+ const finalizedGroups = [];
1522
+ let tailText = "";
1523
+ let nextGroupIndex = 0;
1524
+ let version = 0;
1525
+ let cachedVersion = -1;
1526
+ let cachedBlocks = [];
1527
+ const invalidate = () => {
1528
+ version++;
1529
+ cachedVersion = -1;
1530
+ };
1531
+ const currentGroups = () => {
1532
+ if (!tailText) return finalizedGroups;
1533
+ return [...finalizedGroups, parseBlockGroup(tailText, `md-tail-${nextGroupIndex}`, parser, theme, false)];
1534
+ };
1535
+ const blocks = () => {
1536
+ if (cachedVersion === version) return cachedBlocks;
1537
+ cachedBlocks = mergeGroups(currentGroups());
1538
+ cachedVersion = version;
1539
+ return cachedBlocks;
1540
+ };
1541
+ const snapshot = () => ({
1542
+ version,
1543
+ blocks: blocks()
1544
+ });
1545
+ return {
1546
+ get version() {
1547
+ return version;
1548
+ },
1549
+ get blocks() {
1550
+ return blocks();
1551
+ },
1552
+ appendDelta(text) {
1553
+ if (!text) return;
1554
+ tailText += text;
1555
+ invalidate();
1556
+ },
1557
+ replaceTailBlock(text) {
1558
+ if (tailText === text) return;
1559
+ tailText = text;
1560
+ invalidate();
1561
+ },
1562
+ finalizeBlock() {
1563
+ if (!tailText) return;
1564
+ const key = `md-block-${nextGroupIndex++}`;
1565
+ finalizedGroups.push(parseBlockGroup(tailText, key, parser, theme, true));
1566
+ tailText = "";
1567
+ invalidate();
1568
+ },
1569
+ clear() {
1570
+ if (!tailText && finalizedGroups.length === 0) return;
1571
+ finalizedGroups.length = 0;
1572
+ tailText = "";
1573
+ nextGroupIndex = 0;
1574
+ invalidate();
1575
+ },
1576
+ snapshot
1577
+ };
1578
+ }
1579
+
1580
+ //#endregion
1581
+ export { TMarkdownText, TVirtualMarkdown, buildMarkdownBlocks, buildMarkdownVisualRows, createMarkdownBlockSource, createTuiMarkdownParser, isSafeMarkdownLink, layoutMarkdownBlocks };