@asm-agent/tui 0.8.2

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.
Files changed (130) hide show
  1. package/README.md +795 -0
  2. package/dist/autocomplete.d.ts +61 -0
  3. package/dist/autocomplete.d.ts.map +1 -0
  4. package/dist/autocomplete.js +601 -0
  5. package/dist/autocomplete.js.map +1 -0
  6. package/dist/components/box.d.ts +24 -0
  7. package/dist/components/box.d.ts.map +1 -0
  8. package/dist/components/box.js +114 -0
  9. package/dist/components/box.js.map +1 -0
  10. package/dist/components/cancellable-loader.d.ts +22 -0
  11. package/dist/components/cancellable-loader.d.ts.map +1 -0
  12. package/dist/components/cancellable-loader.js +35 -0
  13. package/dist/components/cancellable-loader.js.map +1 -0
  14. package/dist/components/editor.d.ts +275 -0
  15. package/dist/components/editor.d.ts.map +1 -0
  16. package/dist/components/editor.js +1968 -0
  17. package/dist/components/editor.js.map +1 -0
  18. package/dist/components/image.d.ts +34 -0
  19. package/dist/components/image.d.ts.map +1 -0
  20. package/dist/components/image.js +96 -0
  21. package/dist/components/image.js.map +1 -0
  22. package/dist/components/input.d.ts +37 -0
  23. package/dist/components/input.d.ts.map +1 -0
  24. package/dist/components/input.js +385 -0
  25. package/dist/components/input.js.map +1 -0
  26. package/dist/components/loader.d.ts +29 -0
  27. package/dist/components/loader.d.ts.map +1 -0
  28. package/dist/components/loader.js +69 -0
  29. package/dist/components/loader.js.map +1 -0
  30. package/dist/components/markdown.d.ts +100 -0
  31. package/dist/components/markdown.d.ts.map +1 -0
  32. package/dist/components/markdown.js +773 -0
  33. package/dist/components/markdown.js.map +1 -0
  34. package/dist/components/select-list.d.ts +61 -0
  35. package/dist/components/select-list.d.ts.map +1 -0
  36. package/dist/components/select-list.js +206 -0
  37. package/dist/components/select-list.js.map +1 -0
  38. package/dist/components/settings-list.d.ts +49 -0
  39. package/dist/components/settings-list.d.ts.map +1 -0
  40. package/dist/components/settings-list.js +170 -0
  41. package/dist/components/settings-list.js.map +1 -0
  42. package/dist/components/spacer.d.ts +12 -0
  43. package/dist/components/spacer.d.ts.map +1 -0
  44. package/dist/components/spacer.js +21 -0
  45. package/dist/components/spacer.js.map +1 -0
  46. package/dist/components/text.d.ts +19 -0
  47. package/dist/components/text.d.ts.map +1 -0
  48. package/dist/components/text.js +77 -0
  49. package/dist/components/text.js.map +1 -0
  50. package/dist/components/truncated-text.d.ts +14 -0
  51. package/dist/components/truncated-text.d.ts.map +1 -0
  52. package/dist/components/truncated-text.js +44 -0
  53. package/dist/components/truncated-text.js.map +1 -0
  54. package/dist/editor-component.d.ts +51 -0
  55. package/dist/editor-component.d.ts.map +1 -0
  56. package/dist/editor-component.js +2 -0
  57. package/dist/editor-component.js.map +1 -0
  58. package/dist/fullscreen.d.ts +105 -0
  59. package/dist/fullscreen.d.ts.map +1 -0
  60. package/dist/fullscreen.js +597 -0
  61. package/dist/fullscreen.js.map +1 -0
  62. package/dist/fuzzy.d.ts +26 -0
  63. package/dist/fuzzy.d.ts.map +1 -0
  64. package/dist/fuzzy.js +114 -0
  65. package/dist/fuzzy.js.map +1 -0
  66. package/dist/index.d.ts +29 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +27 -0
  69. package/dist/index.js.map +1 -0
  70. package/dist/keybindings.d.ts +239 -0
  71. package/dist/keybindings.d.ts.map +1 -0
  72. package/dist/keybindings.js +222 -0
  73. package/dist/keybindings.js.map +1 -0
  74. package/dist/keys.d.ts +184 -0
  75. package/dist/keys.d.ts.map +1 -0
  76. package/dist/keys.js +1163 -0
  77. package/dist/keys.js.map +1 -0
  78. package/dist/kill-ring.d.ts +26 -0
  79. package/dist/kill-ring.d.ts.map +1 -0
  80. package/dist/kill-ring.js +42 -0
  81. package/dist/kill-ring.js.map +1 -0
  82. package/dist/latex.d.ts +21 -0
  83. package/dist/latex.d.ts.map +1 -0
  84. package/dist/latex.js +797 -0
  85. package/dist/latex.js.map +1 -0
  86. package/dist/mouse.d.ts +24 -0
  87. package/dist/mouse.d.ts.map +1 -0
  88. package/dist/mouse.js +34 -0
  89. package/dist/mouse.js.map +1 -0
  90. package/dist/render-cache.d.ts +9 -0
  91. package/dist/render-cache.d.ts.map +1 -0
  92. package/dist/render-cache.js +23 -0
  93. package/dist/render-cache.js.map +1 -0
  94. package/dist/selection-metadata.d.ts +22 -0
  95. package/dist/selection-metadata.d.ts.map +1 -0
  96. package/dist/selection-metadata.js +141 -0
  97. package/dist/selection-metadata.js.map +1 -0
  98. package/dist/slash-command-context.d.ts +12 -0
  99. package/dist/slash-command-context.d.ts.map +1 -0
  100. package/dist/slash-command-context.js +32 -0
  101. package/dist/slash-command-context.js.map +1 -0
  102. package/dist/stdin-buffer.d.ts +50 -0
  103. package/dist/stdin-buffer.d.ts.map +1 -0
  104. package/dist/stdin-buffer.js +313 -0
  105. package/dist/stdin-buffer.js.map +1 -0
  106. package/dist/terminal-colors.d.ts +31 -0
  107. package/dist/terminal-colors.d.ts.map +1 -0
  108. package/dist/terminal-colors.js +161 -0
  109. package/dist/terminal-colors.js.map +1 -0
  110. package/dist/terminal-image.d.ts +85 -0
  111. package/dist/terminal-image.d.ts.map +1 -0
  112. package/dist/terminal-image.js +324 -0
  113. package/dist/terminal-image.js.map +1 -0
  114. package/dist/terminal.d.ts +113 -0
  115. package/dist/terminal.d.ts.map +1 -0
  116. package/dist/terminal.js +486 -0
  117. package/dist/terminal.js.map +1 -0
  118. package/dist/tui.d.ts +310 -0
  119. package/dist/tui.d.ts.map +1 -0
  120. package/dist/tui.js +1673 -0
  121. package/dist/tui.js.map +1 -0
  122. package/dist/undo-stack.d.ts +16 -0
  123. package/dist/undo-stack.d.ts.map +1 -0
  124. package/dist/undo-stack.js +26 -0
  125. package/dist/undo-stack.js.map +1 -0
  126. package/dist/utils.d.ts +98 -0
  127. package/dist/utils.d.ts.map +1 -0
  128. package/dist/utils.js +1233 -0
  129. package/dist/utils.js.map +1 -0
  130. package/package.json +52 -0
@@ -0,0 +1,773 @@
1
+ import { Marked, Tokenizer } from "marked";
2
+ import { latexToUnicode } from "../latex.js";
3
+ import { extractTableCellSelectionRegions, markTableCell, markTableEnd, markTableStart, } from "../selection-metadata.js";
4
+ import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.js";
5
+ import { applyBackgroundToLine, stripAnsi, visibleWidth, wrapTextWithAnsi } from "../utils.js";
6
+ const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
7
+ class StrictStrikethroughTokenizer extends Tokenizer {
8
+ del(src) {
9
+ const match = STRICT_STRIKETHROUGH_REGEX.exec(src);
10
+ if (!match) {
11
+ return undefined;
12
+ }
13
+ const text = match[2];
14
+ return {
15
+ type: "del",
16
+ raw: match[0],
17
+ text,
18
+ tokens: this.lexer.inlineTokens(text),
19
+ };
20
+ }
21
+ }
22
+ // Math must tokenize before marked's escape/emphasis handling, or \[ collapses
23
+ // to [ and underscores inside formulas become italics. Unterminated delimiters
24
+ // never match, so partially streamed math stays plain text until the closing
25
+ // delimiter arrives. Leading indentation is consumed because models often
26
+ // indent display math, which would otherwise lex as an indented code block.
27
+ const BLOCK_MATH_REGEX = /^[ \t]*(?:\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\])[ \t]*(?:\n|$)/;
28
+ function minIndex(a, b) {
29
+ if (a === -1) {
30
+ return b === -1 ? undefined : b;
31
+ }
32
+ return b === -1 ? a : Math.min(a, b);
33
+ }
34
+ const blockMathExtension = {
35
+ name: "blockMath",
36
+ level: "block",
37
+ // start() runs on every paragraph continuation; scanning only the current
38
+ // paragraph keeps it cheap, and later math is caught at its block boundary.
39
+ start: (src) => {
40
+ const paragraphEnd = src.indexOf("\n\n");
41
+ const window = paragraphEnd === -1 ? src : src.slice(0, paragraphEnd);
42
+ return minIndex(window.indexOf("$$"), window.indexOf("\\["));
43
+ },
44
+ tokenizer(src) {
45
+ const first = src.charCodeAt(0);
46
+ if (first !== 0x24 /* $ */ && first !== 0x5c /* \ */ && first !== 0x20 /* space */ && first !== 0x09 /* tab */) {
47
+ return undefined;
48
+ }
49
+ const match = BLOCK_MATH_REGEX.exec(src);
50
+ if (!match) {
51
+ return undefined;
52
+ }
53
+ const token = { type: "blockMath", raw: match[0], text: (match[1] ?? match[2]).trim() };
54
+ return token;
55
+ },
56
+ };
57
+ // $...$ uses the pandoc/GitHub rules to avoid matching prose dollar amounts:
58
+ // the opening $ must be followed by a non-space, the closing $ preceded by a
59
+ // non-space and not followed by a digit ("between $5 and $10" never matches).
60
+ const INLINE_MATH_PATTERNS = [
61
+ /^\$\$([\s\S]+?)\$\$/, // display math used mid-paragraph
62
+ /^\\\[([\s\S]+?)\\\]/,
63
+ /^\\\(([\s\S]+?)\\\)/,
64
+ /^\$([^\s$](?:[^$\n]*[^\s$])?)\$(?!\d)/,
65
+ ];
66
+ const inlineMathExtension = {
67
+ name: "inlineMath",
68
+ level: "inline",
69
+ // Only "$" needs a start() hint: backslashes already terminate text runs,
70
+ // but the text tokenizer would swallow a bare "$" without one.
71
+ start: (src) => {
72
+ const index = src.indexOf("$");
73
+ return index === -1 ? undefined : index;
74
+ },
75
+ tokenizer(src) {
76
+ const first = src.charCodeAt(0);
77
+ if (first !== 0x24 /* $ */ && first !== 0x5c /* \ */) {
78
+ return undefined;
79
+ }
80
+ for (const pattern of INLINE_MATH_PATTERNS) {
81
+ const match = pattern.exec(src);
82
+ if (match) {
83
+ const token = { type: "inlineMath", raw: match[0], text: match[1].trim() };
84
+ return token;
85
+ }
86
+ }
87
+ return undefined;
88
+ },
89
+ };
90
+ const markdownParser = new Marked();
91
+ markdownParser.setOptions({
92
+ tokenizer: new StrictStrikethroughTokenizer(),
93
+ });
94
+ // Registered extensions measurably slow marked's lexing even when they never
95
+ // match, so math-free text (the common case) uses a parser without them.
96
+ const mathMarkdownParser = new Marked();
97
+ mathMarkdownParser.setOptions({
98
+ tokenizer: new StrictStrikethroughTokenizer(),
99
+ });
100
+ mathMarkdownParser.use({ extensions: [blockMathExtension, inlineMathExtension] });
101
+ function pickMarkdownParser(text) {
102
+ return text.includes("$") || text.includes("\\(") || text.includes("\\[") ? mathMarkdownParser : markdownParser;
103
+ }
104
+ export class Markdown {
105
+ text;
106
+ paddingX; // Left/right padding
107
+ paddingY; // Top/bottom padding
108
+ defaultTextStyle;
109
+ theme;
110
+ defaultStylePrefix;
111
+ cachedText;
112
+ cachedWidth;
113
+ cachedLines;
114
+ selectionRegions = [];
115
+ tableIdentities = [];
116
+ // Per-block render cache so streaming appends only re-render the changing
117
+ // final block instead of the whole document. Keyed by width/type/nextType/raw;
118
+ // rebuilt each render so it stays bounded to the current document's blocks.
119
+ blockCache = new Map();
120
+ constructor(text, paddingX, paddingY, theme, defaultTextStyle) {
121
+ this.text = text;
122
+ this.paddingX = paddingX;
123
+ this.paddingY = paddingY;
124
+ this.theme = theme;
125
+ this.defaultTextStyle = defaultTextStyle;
126
+ }
127
+ setText(text) {
128
+ this.text = text;
129
+ // Only the whole-result cache is dropped; the per-block cache stays so a
130
+ // streaming append re-renders just the blocks that actually changed.
131
+ this.cachedText = undefined;
132
+ this.cachedWidth = undefined;
133
+ this.cachedLines = undefined;
134
+ this.selectionRegions = [];
135
+ }
136
+ invalidate() {
137
+ this.cachedText = undefined;
138
+ this.cachedWidth = undefined;
139
+ this.cachedLines = undefined;
140
+ this.selectionRegions = [];
141
+ // External invalidation (e.g. theme change) affects rendered output, so
142
+ // the per-block cache must go too.
143
+ this.blockCache = new Map();
144
+ }
145
+ render(width) {
146
+ if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
147
+ return this.cachedLines;
148
+ }
149
+ const contentWidth = Math.max(1, width - this.paddingX * 2);
150
+ if (!this.text || this.text.trim() === "") {
151
+ const result = [];
152
+ this.selectionRegions = [];
153
+ this.cachedText = this.text;
154
+ this.cachedWidth = width;
155
+ this.cachedLines = result;
156
+ return result;
157
+ }
158
+ const normalizedText = this.text.replace(/\t/g, " ");
159
+ // Parse markdown to HTML-like tokens
160
+ const tokens = pickMarkdownParser(normalizedText).lexer(normalizedText);
161
+ // Reference-link definitions make a block's rendering depend on other
162
+ // blocks, so per-block caching is disabled when any are present.
163
+ const cacheable = Object.keys(tokens.links).length === 0;
164
+ // Render, wrap, and pad per top-level block so unchanged blocks can be
165
+ // served from the cache. The final block is never cached: while streaming,
166
+ // appended text can reinterpret it (unterminated fences, growing lists);
167
+ // once a block is no longer last, its raw text is final.
168
+ const nextCache = new Map();
169
+ const contentLines = [];
170
+ for (let i = 0; i < tokens.length; i++) {
171
+ const token = tokens[i];
172
+ const nextTokenType = tokens[i + 1]?.type;
173
+ const useCache = cacheable && i < tokens.length - 1;
174
+ const key = useCache ? `${width}|${token.type}|${nextTokenType ?? ""}|${token.raw}` : "";
175
+ let blockLines = useCache ? (nextCache.get(key) ?? this.blockCache.get(key)) : undefined;
176
+ if (!blockLines) {
177
+ blockLines = this.renderBlock(token, nextTokenType, width, contentWidth);
178
+ }
179
+ if (useCache) {
180
+ nextCache.set(key, blockLines);
181
+ }
182
+ contentLines.push(...blockLines);
183
+ }
184
+ this.blockCache = nextCache;
185
+ const bgFn = this.defaultTextStyle?.bgColor;
186
+ const emptyLine = " ".repeat(width);
187
+ const emptyLines = [];
188
+ for (let i = 0; i < this.paddingY; i++) {
189
+ const line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;
190
+ emptyLines.push(line);
191
+ }
192
+ const markedResult = [...emptyLines, ...contentLines, ...emptyLines];
193
+ const { lines: result, regions } = extractTableCellSelectionRegions(markedResult, (index) => {
194
+ this.tableIdentities[index] ??= {};
195
+ return this.tableIdentities[index];
196
+ });
197
+ this.selectionRegions = regions;
198
+ this.cachedText = this.text;
199
+ this.cachedWidth = width;
200
+ this.cachedLines = result;
201
+ return result.length > 0 ? result : [""];
202
+ }
203
+ getSelectionRegions() {
204
+ return this.selectionRegions;
205
+ }
206
+ /** Render one top-level block: token lines, wrapping, margins, background. */
207
+ renderBlock(token, nextTokenType, width, contentWidth) {
208
+ const tokenLines = this.renderToken(token, contentWidth, nextTokenType);
209
+ const leftMargin = " ".repeat(this.paddingX);
210
+ const rightMargin = " ".repeat(this.paddingX);
211
+ const bgFn = this.defaultTextStyle?.bgColor;
212
+ const blockLines = [];
213
+ for (const line of tokenLines) {
214
+ if (isImageLine(line)) {
215
+ blockLines.push(line);
216
+ continue;
217
+ }
218
+ for (const wrapped of wrapTextWithAnsi(line, contentWidth)) {
219
+ const lineWithMargins = leftMargin + wrapped + rightMargin;
220
+ if (bgFn) {
221
+ blockLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
222
+ }
223
+ else {
224
+ const visibleLen = visibleWidth(lineWithMargins);
225
+ const paddingNeeded = Math.max(0, width - visibleLen);
226
+ blockLines.push(lineWithMargins + " ".repeat(paddingNeeded));
227
+ }
228
+ }
229
+ }
230
+ return blockLines;
231
+ }
232
+ /**
233
+ * Apply default text style to a string.
234
+ * This is the base styling applied to all text content.
235
+ * NOTE: Background color is NOT applied here - it's applied at the padding stage
236
+ * to ensure it extends to the full line width.
237
+ */
238
+ applyDefaultStyle(text) {
239
+ if (!this.defaultTextStyle) {
240
+ return text;
241
+ }
242
+ let styled = text;
243
+ if (this.defaultTextStyle.color) {
244
+ styled = this.defaultTextStyle.color(styled);
245
+ }
246
+ if (this.defaultTextStyle.bold) {
247
+ styled = this.theme.bold(styled);
248
+ }
249
+ if (this.defaultTextStyle.italic) {
250
+ styled = this.theme.italic(styled);
251
+ }
252
+ if (this.defaultTextStyle.strikethrough) {
253
+ styled = this.theme.strikethrough(styled);
254
+ }
255
+ if (this.defaultTextStyle.underline) {
256
+ styled = this.theme.underline(styled);
257
+ }
258
+ return styled;
259
+ }
260
+ getDefaultStylePrefix() {
261
+ if (!this.defaultTextStyle) {
262
+ return "";
263
+ }
264
+ if (this.defaultStylePrefix !== undefined) {
265
+ return this.defaultStylePrefix;
266
+ }
267
+ const sentinel = "\u0000";
268
+ let styled = sentinel;
269
+ if (this.defaultTextStyle.color) {
270
+ styled = this.defaultTextStyle.color(styled);
271
+ }
272
+ if (this.defaultTextStyle.bold) {
273
+ styled = this.theme.bold(styled);
274
+ }
275
+ if (this.defaultTextStyle.italic) {
276
+ styled = this.theme.italic(styled);
277
+ }
278
+ if (this.defaultTextStyle.strikethrough) {
279
+ styled = this.theme.strikethrough(styled);
280
+ }
281
+ if (this.defaultTextStyle.underline) {
282
+ styled = this.theme.underline(styled);
283
+ }
284
+ const sentinelIndex = styled.indexOf(sentinel);
285
+ this.defaultStylePrefix = sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : "";
286
+ return this.defaultStylePrefix;
287
+ }
288
+ getStylePrefix(styleFn) {
289
+ const sentinel = "\u0000";
290
+ const styled = styleFn(sentinel);
291
+ const sentinelIndex = styled.indexOf(sentinel);
292
+ return sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : "";
293
+ }
294
+ getDefaultInlineStyleContext() {
295
+ return {
296
+ applyText: (text) => this.applyDefaultStyle(text),
297
+ stylePrefix: this.getDefaultStylePrefix(),
298
+ };
299
+ }
300
+ renderToken(token, width, nextTokenType, styleContext) {
301
+ const lines = [];
302
+ switch (token.type) {
303
+ case "heading": {
304
+ const headingLevel = token.depth;
305
+ const headingPrefix = `${"#".repeat(headingLevel)} `;
306
+ // Build a heading-specific style context so inline tokens (codespan, bold, etc.)
307
+ // restore heading styling after their own ANSI resets instead of falling back to
308
+ // the default text style.
309
+ let headingStyleFn;
310
+ if (headingLevel === 1) {
311
+ headingStyleFn = (text) => this.theme.heading(this.theme.bold(this.theme.underline(text)));
312
+ }
313
+ else {
314
+ headingStyleFn = (text) => this.theme.heading(this.theme.bold(text));
315
+ }
316
+ const headingStyleContext = {
317
+ applyText: headingStyleFn,
318
+ stylePrefix: this.getStylePrefix(headingStyleFn),
319
+ };
320
+ const headingText = this.renderInlineTokens(token.tokens || [], headingStyleContext);
321
+ const styledHeading = headingLevel >= 3 ? headingStyleFn(headingPrefix) + headingText : headingText;
322
+ lines.push(styledHeading);
323
+ if (nextTokenType && nextTokenType !== "space") {
324
+ lines.push(""); // Add spacing after headings (unless space token follows)
325
+ }
326
+ break;
327
+ }
328
+ case "paragraph": {
329
+ const paragraphText = this.renderInlineTokens(token.tokens || [], styleContext);
330
+ lines.push(paragraphText);
331
+ if (nextTokenType && nextTokenType !== "list" && nextTokenType !== "space") {
332
+ lines.push("");
333
+ }
334
+ break;
335
+ }
336
+ case "code": {
337
+ lines.push(...this.renderCodeBlock(token));
338
+ if (nextTokenType && nextTokenType !== "space") {
339
+ lines.push(""); // Add spacing after code blocks (unless space token follows)
340
+ }
341
+ break;
342
+ }
343
+ case "blockMath": {
344
+ lines.push(...this.renderMathBlock(token));
345
+ if (nextTokenType && nextTokenType !== "space") {
346
+ lines.push(""); // Add spacing after math blocks (unless space token follows)
347
+ }
348
+ break;
349
+ }
350
+ case "list": {
351
+ const listLines = this.renderList(token, 0, styleContext);
352
+ lines.push(...listLines);
353
+ break;
354
+ }
355
+ case "table": {
356
+ const tableLines = this.renderTable(token, width, nextTokenType, styleContext);
357
+ lines.push(...tableLines);
358
+ break;
359
+ }
360
+ case "blockquote": {
361
+ const quoteStyle = (text) => this.theme.quote(this.theme.italic(text));
362
+ const quoteStylePrefix = this.getStylePrefix(quoteStyle);
363
+ const applyQuoteStyle = (line) => {
364
+ if (!quoteStylePrefix) {
365
+ return quoteStyle(line);
366
+ }
367
+ const lineWithReappliedStyle = line.replace(/\x1b\[0m/g, `\x1b[0m${quoteStylePrefix}`);
368
+ return quoteStyle(lineWithReappliedStyle);
369
+ };
370
+ const quoteContentWidth = Math.max(1, width - 2);
371
+ // Blockquotes contain block-level tokens (paragraph, list, code, etc.), so render
372
+ // children with renderToken() instead of renderInlineTokens().
373
+ // Default message style should not apply inside blockquotes.
374
+ const quoteInlineStyleContext = {
375
+ applyText: (text) => text,
376
+ stylePrefix: quoteStylePrefix,
377
+ };
378
+ const quoteTokens = token.tokens || [];
379
+ const renderedQuoteLines = [];
380
+ for (let i = 0; i < quoteTokens.length; i++) {
381
+ const quoteToken = quoteTokens[i];
382
+ const nextQuoteToken = quoteTokens[i + 1];
383
+ renderedQuoteLines.push(...this.renderToken(quoteToken, quoteContentWidth, nextQuoteToken?.type, quoteInlineStyleContext));
384
+ }
385
+ while (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1] === "") {
386
+ renderedQuoteLines.pop();
387
+ }
388
+ for (const quoteLine of renderedQuoteLines) {
389
+ const styledLine = applyQuoteStyle(quoteLine);
390
+ const wrappedLines = wrapTextWithAnsi(styledLine, quoteContentWidth);
391
+ for (const wrappedLine of wrappedLines) {
392
+ lines.push(this.theme.quoteBorder("│ ") + wrappedLine);
393
+ }
394
+ }
395
+ if (nextTokenType && nextTokenType !== "space") {
396
+ lines.push(""); // Add spacing after blockquotes (unless space token follows)
397
+ }
398
+ break;
399
+ }
400
+ case "hr":
401
+ lines.push(this.theme.hr("─".repeat(Math.min(width, 80))));
402
+ if (nextTokenType && nextTokenType !== "space") {
403
+ lines.push(""); // Add spacing after horizontal rules (unless space token follows)
404
+ }
405
+ break;
406
+ case "html":
407
+ if ("raw" in token && typeof token.raw === "string") {
408
+ lines.push(this.applyDefaultStyle(token.raw.trim()));
409
+ }
410
+ break;
411
+ case "space":
412
+ lines.push("");
413
+ break;
414
+ default:
415
+ if ("text" in token && typeof token.text === "string") {
416
+ lines.push(token.text);
417
+ }
418
+ }
419
+ return lines;
420
+ }
421
+ renderInlineTokens(tokens, styleContext) {
422
+ let result = "";
423
+ const resolvedStyleContext = styleContext ?? this.getDefaultInlineStyleContext();
424
+ const { applyText, stylePrefix } = resolvedStyleContext;
425
+ const applyTextWithNewlines = (text) => {
426
+ const segments = text.split("\n");
427
+ return segments.map((segment) => applyText(segment)).join("\n");
428
+ };
429
+ for (const token of tokens) {
430
+ switch (token.type) {
431
+ case "text":
432
+ if (token.tokens && token.tokens.length > 0) {
433
+ result += this.renderInlineTokens(token.tokens, resolvedStyleContext);
434
+ }
435
+ else {
436
+ result += applyTextWithNewlines(token.text);
437
+ }
438
+ break;
439
+ case "paragraph":
440
+ result += this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
441
+ break;
442
+ case "strong": {
443
+ const boldContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
444
+ result += this.theme.bold(boldContent) + stylePrefix;
445
+ break;
446
+ }
447
+ case "em": {
448
+ const italicContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
449
+ result += this.theme.italic(italicContent) + stylePrefix;
450
+ break;
451
+ }
452
+ case "codespan":
453
+ result += this.theme.code(token.text) + stylePrefix;
454
+ break;
455
+ case "inlineMath": {
456
+ const mathStyle = this.theme.math ?? this.theme.code;
457
+ const converted = latexToUnicode(token.text).replace(/\s*\n\s*/g, " ");
458
+ result += mathStyle(converted) + stylePrefix;
459
+ break;
460
+ }
461
+ case "link": {
462
+ const linkText = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
463
+ const styledLink = this.theme.link(this.theme.underline(linkText));
464
+ if (getCapabilities().hyperlinks) {
465
+ // OSC 8: render as a clickable hyperlink. The URL is not printed inline,
466
+ // so we always show only the link text regardless of whether it matches href.
467
+ result += hyperlink(styledLink, token.href) + stylePrefix;
468
+ }
469
+ else {
470
+ // Compare raw token.text (not styled) against href for the equality check.
471
+ // For mailto: links strip the prefix (autolinked emails use text="foo@bar.com"
472
+ // but href="mailto:foo@bar.com").
473
+ const hrefForComparison = token.href.startsWith("mailto:") ? token.href.slice(7) : token.href;
474
+ if (token.text === token.href || token.text === hrefForComparison) {
475
+ result += styledLink + stylePrefix;
476
+ }
477
+ else {
478
+ result += styledLink + this.theme.linkUrl(` (${token.href})`) + stylePrefix;
479
+ }
480
+ }
481
+ break;
482
+ }
483
+ case "br":
484
+ result += "\n";
485
+ break;
486
+ case "del": {
487
+ const delContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
488
+ result += this.theme.strikethrough(delContent) + stylePrefix;
489
+ break;
490
+ }
491
+ case "html":
492
+ if ("raw" in token && typeof token.raw === "string") {
493
+ result += applyTextWithNewlines(token.raw);
494
+ }
495
+ break;
496
+ default:
497
+ if ("text" in token && typeof token.text === "string") {
498
+ result += applyTextWithNewlines(token.text);
499
+ }
500
+ }
501
+ }
502
+ while (stylePrefix && result.endsWith(stylePrefix)) {
503
+ result = result.slice(0, -stylePrefix.length);
504
+ }
505
+ return result;
506
+ }
507
+ /**
508
+ * Render a list with proper nesting support
509
+ */
510
+ renderList(token, depth, styleContext) {
511
+ const lines = [];
512
+ const indent = " ".repeat(depth);
513
+ const startNumber = token.start ?? 1;
514
+ for (let i = 0; i < token.items.length; i++) {
515
+ const item = token.items[i];
516
+ const bullet = token.ordered ? `${startNumber + i}. ` : "- ";
517
+ const itemLines = this.renderListItem(item.tokens || [], depth, styleContext);
518
+ if (itemLines.length > 0) {
519
+ // A nested list will start with indent (spaces) followed by cyan bullet
520
+ const firstLine = itemLines[0];
521
+ const isNestedList = /^\s+\x1b\[36m[-\d]/.test(firstLine); // starts with spaces + cyan + bullet char
522
+ if (isNestedList) {
523
+ lines.push(firstLine);
524
+ }
525
+ else {
526
+ lines.push(indent + this.theme.listBullet(bullet) + firstLine);
527
+ }
528
+ for (let j = 1; j < itemLines.length; j++) {
529
+ const line = itemLines[j];
530
+ const isNestedListLine = /^\s+\x1b\[36m[-\d]/.test(line); // starts with spaces + cyan + bullet char
531
+ if (isNestedListLine) {
532
+ lines.push(line);
533
+ }
534
+ else {
535
+ lines.push(`${indent} ${line}`);
536
+ }
537
+ }
538
+ }
539
+ else {
540
+ lines.push(indent + this.theme.listBullet(bullet));
541
+ }
542
+ }
543
+ return lines;
544
+ }
545
+ /**
546
+ * Render list item tokens, handling nested lists
547
+ * Returns lines WITHOUT the parent indent (renderList will add it)
548
+ */
549
+ renderListItem(tokens, parentDepth, styleContext) {
550
+ const lines = [];
551
+ for (const token of tokens) {
552
+ if (token.type === "list") {
553
+ // Nested list - render with one additional indent level
554
+ // These lines will have their own indent, so we just add them as-is
555
+ const nestedLines = this.renderList(token, parentDepth + 1, styleContext);
556
+ lines.push(...nestedLines);
557
+ }
558
+ else if (token.type === "text") {
559
+ // Text content (may have inline tokens)
560
+ const text = token.tokens && token.tokens.length > 0
561
+ ? this.renderInlineTokens(token.tokens, styleContext)
562
+ : token.text || "";
563
+ lines.push(text);
564
+ }
565
+ else if (token.type === "paragraph") {
566
+ // Paragraph in list item
567
+ const text = this.renderInlineTokens(token.tokens || [], styleContext);
568
+ lines.push(text);
569
+ }
570
+ else if (token.type === "code") {
571
+ // Code block in list item
572
+ lines.push(...this.renderCodeBlock(token));
573
+ }
574
+ else if (token.type === "blockMath") {
575
+ // Display math in list item
576
+ lines.push(...this.renderMathBlock(token));
577
+ }
578
+ else {
579
+ // Other token types - try to render as inline
580
+ const text = this.renderInlineTokens([token], styleContext);
581
+ if (text) {
582
+ lines.push(text);
583
+ }
584
+ }
585
+ }
586
+ return lines;
587
+ }
588
+ renderCodeBlock(token) {
589
+ if (!("text" in token) || typeof token.text !== "string") {
590
+ return [];
591
+ }
592
+ const indent = this.theme.codeBlockIndent ?? " ";
593
+ const lang = "lang" in token && typeof token.lang === "string" ? token.lang : undefined;
594
+ const renderedCodeLines = this.theme.highlightCode
595
+ ? this.theme.highlightCode(token.text, lang)
596
+ : token.text.split("\n").map((codeLine) => this.theme.codeBlock(codeLine));
597
+ const codeLines = renderedCodeLines.length > 0 ? renderedCodeLines : [this.theme.codeBlock("")];
598
+ return codeLines.map((codeLine) => `${indent}${codeLine}`);
599
+ }
600
+ /** Render display math: converted to Unicode, indented like a code block. */
601
+ renderMathBlock(token) {
602
+ const indent = this.theme.codeBlockIndent ?? " ";
603
+ const style = this.theme.mathBlock ?? this.theme.codeBlock;
604
+ const mathLines = latexToUnicode(token.text)
605
+ .split("\n")
606
+ .map((line) => line.trim())
607
+ .filter((line) => line.length > 0);
608
+ return mathLines.map((line) => indent + style(line));
609
+ }
610
+ /**
611
+ * Get the visible width of the longest word in a string.
612
+ */
613
+ getLongestWordWidth(text, maxWidth) {
614
+ const words = text.split(/\s+/).filter((word) => word.length > 0);
615
+ let longest = 0;
616
+ for (const word of words) {
617
+ longest = Math.max(longest, visibleWidth(word));
618
+ }
619
+ if (maxWidth === undefined) {
620
+ return longest;
621
+ }
622
+ return Math.min(longest, maxWidth);
623
+ }
624
+ /**
625
+ * Wrap a table cell to fit into a column.
626
+ *
627
+ * Delegates to wrapTextWithAnsi() so ANSI codes + long tokens are handled
628
+ * consistently with the rest of the renderer.
629
+ */
630
+ wrapCellText(text, maxWidth) {
631
+ return wrapTextWithAnsi(text, Math.max(1, maxWidth));
632
+ }
633
+ /**
634
+ * Render a table with width-aware cell wrapping.
635
+ * Cells that don't fit are wrapped to multiple lines.
636
+ */
637
+ renderTable(token, availableWidth, nextTokenType, styleContext) {
638
+ const lines = [];
639
+ const numCols = token.header.length;
640
+ if (numCols === 0) {
641
+ return lines;
642
+ }
643
+ // = 2 + (n-1) * 3 + 2 = 3n + 1
644
+ const borderOverhead = 3 * numCols + 1;
645
+ const availableForCells = availableWidth - borderOverhead;
646
+ if (availableForCells < numCols) {
647
+ // Too narrow to render a stable table. Fall back to raw markdown.
648
+ const fallbackLines = token.raw ? wrapTextWithAnsi(token.raw, availableWidth) : [];
649
+ if (nextTokenType && nextTokenType !== "space") {
650
+ fallbackLines.push("");
651
+ }
652
+ return fallbackLines;
653
+ }
654
+ const maxUnbrokenWordWidth = 30;
655
+ const naturalWidths = [];
656
+ const minWordWidths = [];
657
+ for (let i = 0; i < numCols; i++) {
658
+ const headerText = this.renderInlineTokens(token.header[i].tokens || [], styleContext);
659
+ naturalWidths[i] = visibleWidth(headerText);
660
+ minWordWidths[i] = Math.max(1, this.getLongestWordWidth(headerText, maxUnbrokenWordWidth));
661
+ }
662
+ for (const row of token.rows) {
663
+ for (let i = 0; i < row.length; i++) {
664
+ const cellText = this.renderInlineTokens(row[i].tokens || [], styleContext);
665
+ naturalWidths[i] = Math.max(naturalWidths[i] || 0, visibleWidth(cellText));
666
+ minWordWidths[i] = Math.max(minWordWidths[i] || 1, this.getLongestWordWidth(cellText, maxUnbrokenWordWidth));
667
+ }
668
+ }
669
+ let minColumnWidths = minWordWidths;
670
+ let minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
671
+ if (minCellsWidth > availableForCells) {
672
+ minColumnWidths = new Array(numCols).fill(1);
673
+ const remaining = availableForCells - numCols;
674
+ if (remaining > 0) {
675
+ const totalWeight = minWordWidths.reduce((total, width) => total + Math.max(0, width - 1), 0);
676
+ const growth = minWordWidths.map((width) => {
677
+ const weight = Math.max(0, width - 1);
678
+ return totalWeight > 0 ? Math.floor((weight / totalWeight) * remaining) : 0;
679
+ });
680
+ for (let i = 0; i < numCols; i++) {
681
+ minColumnWidths[i] += growth[i] ?? 0;
682
+ }
683
+ const allocated = growth.reduce((total, width) => total + width, 0);
684
+ let leftover = remaining - allocated;
685
+ for (let i = 0; leftover > 0 && i < numCols; i++) {
686
+ minColumnWidths[i]++;
687
+ leftover--;
688
+ }
689
+ }
690
+ minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
691
+ }
692
+ const totalNaturalWidth = naturalWidths.reduce((a, b) => a + b, 0) + borderOverhead;
693
+ let columnWidths;
694
+ if (totalNaturalWidth <= availableWidth) {
695
+ columnWidths = naturalWidths.map((width, index) => Math.max(width, minColumnWidths[index]));
696
+ }
697
+ else {
698
+ const totalGrowPotential = naturalWidths.reduce((total, width, index) => {
699
+ return total + Math.max(0, width - minColumnWidths[index]);
700
+ }, 0);
701
+ const extraWidth = Math.max(0, availableForCells - minCellsWidth);
702
+ columnWidths = minColumnWidths.map((minWidth, index) => {
703
+ const naturalWidth = naturalWidths[index];
704
+ const minWidthDelta = Math.max(0, naturalWidth - minWidth);
705
+ let grow = 0;
706
+ if (totalGrowPotential > 0) {
707
+ grow = Math.floor((minWidthDelta / totalGrowPotential) * extraWidth);
708
+ }
709
+ return minWidth + grow;
710
+ });
711
+ // Adjust for rounding errors - distribute remaining space
712
+ const allocated = columnWidths.reduce((a, b) => a + b, 0);
713
+ let remaining = availableForCells - allocated;
714
+ while (remaining > 0) {
715
+ let grew = false;
716
+ for (let i = 0; i < numCols && remaining > 0; i++) {
717
+ if (columnWidths[i] < naturalWidths[i]) {
718
+ columnWidths[i]++;
719
+ remaining--;
720
+ grew = true;
721
+ }
722
+ }
723
+ if (!grew) {
724
+ break;
725
+ }
726
+ }
727
+ }
728
+ const topBorderCells = columnWidths.map((w) => "─".repeat(w));
729
+ lines.push(markTableStart(`┌─${topBorderCells.join("─┬─")}─┐`));
730
+ const headerCells = token.header.map((cell, i) => {
731
+ const text = this.renderInlineTokens(cell.tokens || [], styleContext);
732
+ return { lines: this.wrapCellText(text, columnWidths[i]), content: stripAnsi(text) };
733
+ });
734
+ const headerLineCount = Math.max(...headerCells.map((cell) => cell.lines.length));
735
+ for (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {
736
+ const rowParts = headerCells.map((cell, colIdx) => {
737
+ const text = cell.lines[lineIdx] || "";
738
+ const padded = text + " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
739
+ return markTableCell(this.theme.bold(padded), 0, colIdx, lineIdx, cell.content);
740
+ });
741
+ lines.push(`│ ${rowParts.join(" │ ")} │`);
742
+ }
743
+ const separatorCells = columnWidths.map((w) => "─".repeat(w));
744
+ const separatorLine = `├─${separatorCells.join("─┼─")}─┤`;
745
+ lines.push(separatorLine);
746
+ for (let rowIndex = 0; rowIndex < token.rows.length; rowIndex++) {
747
+ const row = token.rows[rowIndex];
748
+ const rowCells = row.map((cell, i) => {
749
+ const text = this.renderInlineTokens(cell.tokens || [], styleContext);
750
+ return { lines: this.wrapCellText(text, columnWidths[i]), content: stripAnsi(text) };
751
+ });
752
+ const rowLineCount = Math.max(...rowCells.map((cell) => cell.lines.length));
753
+ for (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {
754
+ const rowParts = rowCells.map((cell, colIdx) => {
755
+ const text = cell.lines[lineIdx] || "";
756
+ const padded = text + " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
757
+ return markTableCell(padded, rowIndex + 1, colIdx, lineIdx, cell.content);
758
+ });
759
+ lines.push(`│ ${rowParts.join(" │ ")} │`);
760
+ }
761
+ if (rowIndex < token.rows.length - 1) {
762
+ lines.push(separatorLine);
763
+ }
764
+ }
765
+ const bottomBorderCells = columnWidths.map((w) => "─".repeat(w));
766
+ lines.push(markTableEnd(`└─${bottomBorderCells.join("─┴─")}─┘`));
767
+ if (nextTokenType && nextTokenType !== "space") {
768
+ lines.push(""); // Add spacing after table
769
+ }
770
+ return lines;
771
+ }
772
+ }
773
+ //# sourceMappingURL=markdown.js.map