@fleetagent/pi-tui 0.1.9 → 0.2.0
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 +47 -7
- package/dist/autocomplete.d.ts +17 -23
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +129 -195
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/alt-screen-flash.d.ts +13 -0
- package/dist/components/alt-screen-flash.d.ts.map +1 -0
- package/dist/components/alt-screen-flash.js +37 -0
- package/dist/components/alt-screen-flash.js.map +1 -0
- package/dist/components/editor.d.ts +47 -4
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +615 -585
- package/dist/components/editor.js.map +1 -1
- package/dist/components/h-stack.d.ts +7 -0
- package/dist/components/h-stack.d.ts.map +1 -0
- package/dist/components/h-stack.js +43 -0
- package/dist/components/h-stack.js.map +1 -0
- package/dist/components/image.d.ts +4 -0
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +38 -51
- package/dist/components/image.js.map +1 -1
- package/dist/components/input.d.ts +9 -0
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +121 -147
- package/dist/components/input.js.map +1 -1
- package/dist/components/markdown.d.ts +34 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +491 -429
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/scroll-view.d.ts +53 -0
- package/dist/components/scroll-view.d.ts.map +1 -0
- package/dist/components/scroll-view.js +173 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts +2 -0
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +31 -29
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/stack.d.ts +32 -0
- package/dist/components/stack.d.ts.map +1 -0
- package/dist/components/stack.js +122 -0
- package/dist/components/stack.js.map +1 -0
- package/dist/components/v-stack.d.ts +8 -0
- package/dist/components/v-stack.d.ts.map +1 -0
- package/dist/components/v-stack.js +27 -0
- package/dist/components/v-stack.js.map +1 -0
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +37 -46
- package/dist/fuzzy.js.map +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +54 -4
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +39 -4
- package/dist/keybindings.js.map +1 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +359 -479
- package/dist/keys.js.map +1 -1
- package/dist/kill-ring.d.ts +6 -4
- package/dist/kill-ring.d.ts.map +1 -1
- package/dist/kill-ring.js +0 -7
- package/dist/kill-ring.js.map +1 -1
- package/dist/latex.d.ts +10 -0
- package/dist/latex.d.ts.map +1 -0
- package/dist/latex.js +1406 -0
- package/dist/latex.js.map +1 -0
- package/dist/layout-node.d.ts +43 -0
- package/dist/layout-node.d.ts.map +1 -0
- package/dist/layout-node.js +6 -0
- package/dist/layout-node.js.map +1 -0
- package/dist/layout.d.ts +40 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +386 -0
- package/dist/layout.js.map +1 -0
- package/dist/stdin-buffer.d.ts +8 -0
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +116 -159
- package/dist/stdin-buffer.js.map +1 -1
- package/dist/terminal-image.d.ts +41 -19
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +171 -32
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal.d.ts +3 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +27 -23
- package/dist/terminal.js.map +1 -1
- package/dist/tui-alt-screen.d.ts +114 -0
- package/dist/tui-alt-screen.d.ts.map +1 -0
- package/dist/tui-alt-screen.js +950 -0
- package/dist/tui-alt-screen.js.map +1 -0
- package/dist/tui-main-screen.d.ts +52 -0
- package/dist/tui-main-screen.d.ts.map +1 -0
- package/dist/tui-main-screen.js +511 -0
- package/dist/tui-main-screen.js.map +1 -0
- package/dist/tui.d.ts +112 -44
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +489 -699
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +29 -17
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +605 -528
- package/dist/utils.js.map +1 -1
- package/dist/word-navigation.d.ts.map +1 -1
- package/dist/word-navigation.js +37 -39
- package/dist/word-navigation.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Marked, Tokenizer } from "marked";
|
|
2
|
+
import { renderLatex } from "../latex.js";
|
|
2
3
|
import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.js";
|
|
3
4
|
import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
4
5
|
const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
|
|
@@ -21,10 +22,115 @@ class StrictStrikethroughTokenizer extends Tokenizer {
|
|
|
21
22
|
};
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
function isEscaped(source, index) {
|
|
26
|
+
let backslashes = 0;
|
|
27
|
+
for (let position = index - 1; position >= 0 && source[position] === "\\"; position--) {
|
|
28
|
+
backslashes++;
|
|
29
|
+
}
|
|
30
|
+
return backslashes % 2 === 1;
|
|
31
|
+
}
|
|
32
|
+
function findClosingDelimiter(source, closing, start) {
|
|
33
|
+
let index = source.indexOf(closing, start);
|
|
34
|
+
while (index >= 0 && isEscaped(source, index)) {
|
|
35
|
+
index = source.indexOf(closing, index + closing.length);
|
|
36
|
+
}
|
|
37
|
+
return index;
|
|
38
|
+
}
|
|
39
|
+
function looksLikePendingDollarMath(source) {
|
|
40
|
+
return /\\[A-Za-z]+|[_^=+*/<>()[\]|±≤≥≠≈∈→⇒∞∫∑√-]/.test(source);
|
|
41
|
+
}
|
|
42
|
+
function looksLikeShellVariableChain(content, following) {
|
|
43
|
+
return /^[A-Z_][A-Z0-9_]*(?:[/:.-][A-Za-z0-9_./:-]*)*$/.test(content) && /^[A-Za-z_][A-Za-z0-9_]*/.test(following);
|
|
44
|
+
}
|
|
45
|
+
function tokenizeInlineLatex(source) {
|
|
46
|
+
let opening = "";
|
|
47
|
+
let closing = "";
|
|
48
|
+
if (source.startsWith("$$")) {
|
|
49
|
+
opening = "$$";
|
|
50
|
+
closing = "$$";
|
|
51
|
+
}
|
|
52
|
+
else if (source.startsWith("\\(")) {
|
|
53
|
+
opening = "\\(";
|
|
54
|
+
closing = "\\)";
|
|
55
|
+
}
|
|
56
|
+
else if (source.startsWith("\\[")) {
|
|
57
|
+
opening = "\\[";
|
|
58
|
+
closing = "\\]";
|
|
59
|
+
}
|
|
60
|
+
else if (source.startsWith("$") && !/^\$\s/.test(source)) {
|
|
61
|
+
opening = "$";
|
|
62
|
+
closing = "$";
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const closingIndex = findClosingDelimiter(source, closing, opening.length);
|
|
68
|
+
if (closingIndex >= 0 &&
|
|
69
|
+
opening === "$" &&
|
|
70
|
+
(/\s$/.test(source.slice(opening.length, closingIndex)) ||
|
|
71
|
+
/^\d/.test(source.slice(closingIndex + 1)) ||
|
|
72
|
+
looksLikeShellVariableChain(source.slice(opening.length, closingIndex), source.slice(closingIndex + closing.length)) ||
|
|
73
|
+
source.slice(opening.length, closingIndex).includes("`"))) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
if (closingIndex < 0) {
|
|
77
|
+
const pendingSource = source.slice(opening.length);
|
|
78
|
+
if (opening.startsWith("\\") || looksLikePendingDollarMath(pendingSource)) {
|
|
79
|
+
return { type: "latex", raw: source, text: pendingSource, pending: true };
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const text = source.slice(opening.length, closingIndex);
|
|
84
|
+
if (!text || text.includes("\n")) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const raw = source.slice(0, closingIndex + closing.length);
|
|
88
|
+
return { type: "latex", raw, text };
|
|
89
|
+
}
|
|
90
|
+
function tokenizeBlockLatex(source) {
|
|
91
|
+
const dollarMatch = /^ {0,3}\$\$[ \t]*(?:\n)?([\s\S]*?)\$\$[ \t]*(?:\n|$)/.exec(source);
|
|
92
|
+
if (dollarMatch?.[1]) {
|
|
93
|
+
return { type: "latexBlock", raw: dollarMatch[0], text: dollarMatch[1].trim() };
|
|
94
|
+
}
|
|
95
|
+
const bracketMatch = /^ {0,3}\\\[[ \t]*(?:\n)?([\s\S]*?)\\\][ \t]*(?:\n|$)/.exec(source);
|
|
96
|
+
if (bracketMatch?.[1]) {
|
|
97
|
+
return { type: "latexBlock", raw: bracketMatch[0], text: bracketMatch[1].trim() };
|
|
98
|
+
}
|
|
99
|
+
const pendingBracket = /^ {0,3}\\\[[ \t]*(?:\n)?([\s\S]*)$/.exec(source);
|
|
100
|
+
if (pendingBracket) {
|
|
101
|
+
return { type: "latexBlock", raw: pendingBracket[0], text: pendingBracket[1], pending: true };
|
|
102
|
+
}
|
|
103
|
+
const pendingDollar = /^ {0,3}\$\$[ \t]*(?:\n)?([\s\S]*)$/.exec(source);
|
|
104
|
+
if (pendingDollar?.[1] && looksLikePendingDollarMath(pendingDollar[1])) {
|
|
105
|
+
return { type: "latexBlock", raw: pendingDollar[0], text: pendingDollar[1], pending: true };
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const LATEX_MARKDOWN_EXTENSIONS = [
|
|
110
|
+
{
|
|
111
|
+
name: "latexBlock",
|
|
112
|
+
level: "block",
|
|
113
|
+
start(source) {
|
|
114
|
+
const match = /(?:^|\n) {0,3}(?:\$\$|\\\[)/.exec(source);
|
|
115
|
+
return match ? match.index + (match[0].startsWith("\n") ? 1 : 0) : undefined;
|
|
116
|
+
},
|
|
117
|
+
tokenizer: tokenizeBlockLatex,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "latex",
|
|
121
|
+
level: "inline",
|
|
122
|
+
start(source) {
|
|
123
|
+
const indices = [source.indexOf("$"), source.indexOf("\\("), source.indexOf("\\[")].filter((index) => index >= 0);
|
|
124
|
+
return indices.length > 0 ? Math.min(...indices) : undefined;
|
|
125
|
+
},
|
|
126
|
+
tokenizer: tokenizeInlineLatex,
|
|
127
|
+
},
|
|
128
|
+
];
|
|
24
129
|
const markdownParser = new Marked();
|
|
25
130
|
markdownParser.setOptions({
|
|
26
131
|
tokenizer: new StrictStrikethroughTokenizer(),
|
|
27
132
|
});
|
|
133
|
+
markdownParser.use({ extensions: [...LATEX_MARKDOWN_EXTENSIONS] });
|
|
28
134
|
export class Markdown {
|
|
29
135
|
text;
|
|
30
136
|
paddingX; // Left/right padding
|
|
@@ -54,82 +160,80 @@ export class Markdown {
|
|
|
54
160
|
this.cachedWidth = undefined;
|
|
55
161
|
this.cachedLines = undefined;
|
|
56
162
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
|
|
60
|
-
return this.cachedLines;
|
|
61
|
-
}
|
|
62
|
-
// Calculate available width for content (subtract horizontal padding)
|
|
63
|
-
const contentWidth = Math.max(1, width - this.paddingX * 2);
|
|
64
|
-
// Don't render anything if there's no actual text
|
|
65
|
-
if (!this.text || this.text.trim() === "") {
|
|
66
|
-
const result = [];
|
|
67
|
-
// Update cache
|
|
68
|
-
this.cachedText = this.text;
|
|
69
|
-
this.cachedWidth = width;
|
|
70
|
-
this.cachedLines = result;
|
|
71
|
-
return result;
|
|
72
|
-
}
|
|
73
|
-
// Replace tabs with 3 spaces for consistent rendering
|
|
74
|
-
const normalizedText = this.text.replace(/\t/g, " ");
|
|
75
|
-
// Parse markdown to HTML-like tokens
|
|
76
|
-
const tokens = markdownParser.lexer(normalizedText);
|
|
77
|
-
// Convert tokens to styled terminal output
|
|
163
|
+
renderMarkdownTokens(text, contentWidth) {
|
|
164
|
+
const tokens = markdownParser.lexer(text);
|
|
78
165
|
const renderedLines = [];
|
|
79
|
-
for (let
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const tokenLines = this.renderToken(token, contentWidth, nextToken?.type);
|
|
83
|
-
for (const tokenLine of tokenLines) {
|
|
166
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
167
|
+
const tokenLines = this.renderToken(tokens[index], contentWidth, tokens[index + 1]?.type);
|
|
168
|
+
for (const tokenLine of tokenLines)
|
|
84
169
|
renderedLines.push(tokenLine);
|
|
85
|
-
}
|
|
86
170
|
}
|
|
87
|
-
|
|
171
|
+
return renderedLines;
|
|
172
|
+
}
|
|
173
|
+
wrapRenderedLines(renderedLines, contentWidth) {
|
|
88
174
|
const wrappedLines = [];
|
|
89
175
|
for (const line of renderedLines) {
|
|
90
176
|
if (isImageLine(line)) {
|
|
91
177
|
wrappedLines.push(line);
|
|
178
|
+
continue;
|
|
92
179
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
wrappedLines.push(wrappedLine);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
180
|
+
for (const wrappedLine of wrapTextWithAnsi(line, contentWidth))
|
|
181
|
+
wrappedLines.push(wrappedLine);
|
|
98
182
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
183
|
+
return wrappedLines;
|
|
184
|
+
}
|
|
185
|
+
padRenderedLines(lines, width, paddingX) {
|
|
186
|
+
const margin = " ".repeat(paddingX);
|
|
102
187
|
const bgFn = this.defaultTextStyle?.bgColor;
|
|
103
188
|
const contentLines = [];
|
|
104
|
-
for (const line of
|
|
189
|
+
for (const line of lines) {
|
|
105
190
|
if (isImageLine(line)) {
|
|
106
191
|
contentLines.push(line);
|
|
107
192
|
continue;
|
|
108
193
|
}
|
|
109
|
-
const lineWithMargins =
|
|
194
|
+
const lineWithMargins = margin + line + margin;
|
|
110
195
|
if (bgFn) {
|
|
111
196
|
contentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
|
|
197
|
+
continue;
|
|
112
198
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const visibleLen = visibleWidth(lineWithMargins);
|
|
116
|
-
const paddingNeeded = Math.max(0, width - visibleLen);
|
|
117
|
-
contentLines.push(lineWithMargins + " ".repeat(paddingNeeded));
|
|
118
|
-
}
|
|
199
|
+
const paddingNeeded = Math.max(0, width - visibleWidth(lineWithMargins));
|
|
200
|
+
contentLines.push(lineWithMargins + " ".repeat(paddingNeeded));
|
|
119
201
|
}
|
|
120
|
-
|
|
202
|
+
return contentLines;
|
|
203
|
+
}
|
|
204
|
+
createVerticalPadding(width) {
|
|
205
|
+
const bgFn = this.defaultTextStyle?.bgColor;
|
|
121
206
|
const emptyLine = " ".repeat(width);
|
|
122
207
|
const emptyLines = [];
|
|
123
|
-
for (let
|
|
124
|
-
|
|
125
|
-
emptyLines.push(line);
|
|
208
|
+
for (let index = 0; index < this.paddingY; index++) {
|
|
209
|
+
emptyLines.push(bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine);
|
|
126
210
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
211
|
+
return emptyLines;
|
|
212
|
+
}
|
|
213
|
+
cacheRenderedLines(width, lines) {
|
|
130
214
|
this.cachedText = this.text;
|
|
131
215
|
this.cachedWidth = width;
|
|
132
|
-
this.cachedLines =
|
|
216
|
+
this.cachedLines = lines;
|
|
217
|
+
}
|
|
218
|
+
render(width) {
|
|
219
|
+
if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
|
|
220
|
+
return this.cachedLines;
|
|
221
|
+
}
|
|
222
|
+
const effectivePaddingX = Math.min(this.paddingX, Math.floor(Math.max(0, width - 1) / 2));
|
|
223
|
+
const contentWidth = Math.max(1, width - effectivePaddingX * 2);
|
|
224
|
+
const text = this.options.transform?.(this.text, contentWidth) ?? this.text;
|
|
225
|
+
if (!text || text.trim() === "") {
|
|
226
|
+
const result = [];
|
|
227
|
+
this.cacheRenderedLines(width, result);
|
|
228
|
+
return result;
|
|
229
|
+
}
|
|
230
|
+
const normalizedText = text.replace(/\t/g, " ");
|
|
231
|
+
const renderedLines = this.renderMarkdownTokens(normalizedText, contentWidth);
|
|
232
|
+
const wrappedLines = this.wrapRenderedLines(renderedLines, contentWidth);
|
|
233
|
+
const contentLines = this.padRenderedLines(wrappedLines, width, effectivePaddingX);
|
|
234
|
+
const verticalPadding = this.createVerticalPadding(width);
|
|
235
|
+
const result = verticalPadding.concat(contentLines, verticalPadding);
|
|
236
|
+
this.cacheRenderedLines(width, result);
|
|
133
237
|
return result.length > 0 ? result : [""];
|
|
134
238
|
}
|
|
135
239
|
/**
|
|
@@ -202,235 +306,210 @@ export class Markdown {
|
|
|
202
306
|
stylePrefix: this.getDefaultStylePrefix(),
|
|
203
307
|
};
|
|
204
308
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
lines.push(this.renderInlineTokens([token], styleContext));
|
|
244
|
-
break;
|
|
245
|
-
case "code": {
|
|
246
|
-
const indent = this.theme.codeBlockIndent ?? " ";
|
|
247
|
-
lines.push(this.theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
|
|
248
|
-
if (this.theme.highlightCode) {
|
|
249
|
-
const highlightedLines = this.theme.highlightCode(token.text, token.lang);
|
|
250
|
-
for (const hlLine of highlightedLines) {
|
|
251
|
-
lines.push(`${indent}${hlLine}`);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
// Split code by newlines and style each line
|
|
256
|
-
const codeLines = token.text.split("\n");
|
|
257
|
-
for (const codeLine of codeLines) {
|
|
258
|
-
lines.push(`${indent}${this.theme.codeBlock(codeLine)}`);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
lines.push(this.theme.codeBlockBorder("```"));
|
|
262
|
-
if (nextTokenType && nextTokenType !== "space") {
|
|
263
|
-
lines.push(""); // Add spacing after code blocks (unless space token follows)
|
|
264
|
-
}
|
|
265
|
-
break;
|
|
266
|
-
}
|
|
267
|
-
case "list": {
|
|
268
|
-
const listLines = this.renderList(token, 0, width, styleContext);
|
|
269
|
-
lines.push(...listLines);
|
|
270
|
-
// Don't add spacing after lists if a space token follows
|
|
271
|
-
// (the space token will handle it)
|
|
272
|
-
break;
|
|
309
|
+
shouldAppendBlockSpacing(nextTokenType) {
|
|
310
|
+
return Boolean(nextTokenType && nextTokenType !== "space");
|
|
311
|
+
}
|
|
312
|
+
renderHeadingToken(token, nextTokenType) {
|
|
313
|
+
const headingPrefix = `${"#".repeat(token.depth)} `;
|
|
314
|
+
const headingStyle = token.depth === 1
|
|
315
|
+
? (text) => this.theme.heading(this.theme.bold(this.theme.underline(text)))
|
|
316
|
+
: (text) => this.theme.heading(this.theme.bold(text));
|
|
317
|
+
const styleContext = {
|
|
318
|
+
applyText: headingStyle,
|
|
319
|
+
stylePrefix: this.getStylePrefix(headingStyle),
|
|
320
|
+
};
|
|
321
|
+
const headingText = this.renderInlineTokens(token.tokens || [], styleContext);
|
|
322
|
+
const styledHeading = token.depth >= 3 ? headingStyle(headingPrefix) + headingText : headingText;
|
|
323
|
+
return this.shouldAppendBlockSpacing(nextTokenType) ? [styledHeading, ""] : [styledHeading];
|
|
324
|
+
}
|
|
325
|
+
renderParagraphToken(token, nextTokenType, styleContext) {
|
|
326
|
+
const lines = [this.renderInlineTokens(token.tokens || [], styleContext)];
|
|
327
|
+
if (nextTokenType && nextTokenType !== "list" && nextTokenType !== "space")
|
|
328
|
+
lines.push("");
|
|
329
|
+
return lines;
|
|
330
|
+
}
|
|
331
|
+
renderLatexBlockToken(token, width, nextTokenType) {
|
|
332
|
+
const candidate = !token.pending && this.options.renderLatex !== false ? renderLatex(token.text, { display: true }) : undefined;
|
|
333
|
+
const rendered = candidate?.split("\n").every((line) => visibleWidth(line) <= width)
|
|
334
|
+
? candidate
|
|
335
|
+
: token.raw.trim();
|
|
336
|
+
const lines = rendered.split("\n").map((line) => this.applyDefaultStyle(line));
|
|
337
|
+
if (this.shouldAppendBlockSpacing(nextTokenType))
|
|
338
|
+
lines.push("");
|
|
339
|
+
return lines;
|
|
340
|
+
}
|
|
341
|
+
renderCodeToken(token, nextTokenType) {
|
|
342
|
+
const indent = this.theme.codeBlockIndent ?? " ";
|
|
343
|
+
const lines = [this.theme.codeBlockBorder(`\`\`\`${token.lang || ""}`)];
|
|
344
|
+
if (this.theme.highlightCode) {
|
|
345
|
+
for (const highlightedLine of this.theme.highlightCode(token.text, token.lang)) {
|
|
346
|
+
lines.push(`${indent}${highlightedLine}`);
|
|
273
347
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
for (const codeLine of token.text.split("\n")) {
|
|
351
|
+
lines.push(`${indent}${this.theme.codeBlock(codeLine)}`);
|
|
278
352
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
for (const quoteLine of renderedQuoteLines) {
|
|
310
|
-
const styledLine = applyQuoteStyle(quoteLine);
|
|
311
|
-
const wrappedLines = wrapTextWithAnsi(styledLine, quoteContentWidth);
|
|
312
|
-
for (const wrappedLine of wrappedLines) {
|
|
313
|
-
lines.push(this.theme.quoteBorder("│ ") + wrappedLine);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (nextTokenType && nextTokenType !== "space") {
|
|
317
|
-
lines.push(""); // Add spacing after blockquotes (unless space token follows)
|
|
318
|
-
}
|
|
319
|
-
break;
|
|
353
|
+
}
|
|
354
|
+
lines.push(this.theme.codeBlockBorder("```"));
|
|
355
|
+
if (this.shouldAppendBlockSpacing(nextTokenType))
|
|
356
|
+
lines.push("");
|
|
357
|
+
return lines;
|
|
358
|
+
}
|
|
359
|
+
renderBlockquoteToken(token, width, nextTokenType) {
|
|
360
|
+
const quoteStyle = (text) => this.theme.quote(this.theme.italic(text));
|
|
361
|
+
const quoteStylePrefix = this.getStylePrefix(quoteStyle);
|
|
362
|
+
const quoteContentWidth = Math.max(1, width - 2);
|
|
363
|
+
const quoteStyleContext = {
|
|
364
|
+
applyText: (text) => text,
|
|
365
|
+
stylePrefix: quoteStylePrefix,
|
|
366
|
+
};
|
|
367
|
+
const renderedQuoteLines = [];
|
|
368
|
+
const quoteTokens = token.tokens || [];
|
|
369
|
+
for (let index = 0; index < quoteTokens.length; index++) {
|
|
370
|
+
renderedQuoteLines.push(...this.renderToken(quoteTokens[index], quoteContentWidth, quoteTokens[index + 1]?.type, quoteStyleContext));
|
|
371
|
+
}
|
|
372
|
+
while (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1] === "") {
|
|
373
|
+
renderedQuoteLines.pop();
|
|
374
|
+
}
|
|
375
|
+
const lines = [];
|
|
376
|
+
for (const quoteLine of renderedQuoteLines) {
|
|
377
|
+
const lineWithReappliedStyle = quoteStylePrefix
|
|
378
|
+
? quoteLine.replace(/\x1b\[0m/g, `\x1b[0m${quoteStylePrefix}`)
|
|
379
|
+
: quoteLine;
|
|
380
|
+
for (const wrappedLine of wrapTextWithAnsi(quoteStyle(lineWithReappliedStyle), quoteContentWidth)) {
|
|
381
|
+
lines.push(this.theme.quoteBorder("│ ") + wrappedLine);
|
|
320
382
|
}
|
|
383
|
+
}
|
|
384
|
+
if (this.shouldAppendBlockSpacing(nextTokenType))
|
|
385
|
+
lines.push("");
|
|
386
|
+
return lines;
|
|
387
|
+
}
|
|
388
|
+
renderHorizontalRule(width, nextTokenType) {
|
|
389
|
+
const lines = [this.theme.hr("─".repeat(Math.min(width, 80)))];
|
|
390
|
+
if (this.shouldAppendBlockSpacing(nextTokenType))
|
|
391
|
+
lines.push("");
|
|
392
|
+
return lines;
|
|
393
|
+
}
|
|
394
|
+
renderHtmlBlock(token) {
|
|
395
|
+
if (!("raw" in token) || typeof token.raw !== "string")
|
|
396
|
+
return [];
|
|
397
|
+
const visibleHtml = stripHtmlComments(token.raw).trim();
|
|
398
|
+
return visibleHtml ? [this.applyDefaultStyle(visibleHtml)] : [];
|
|
399
|
+
}
|
|
400
|
+
renderFallbackToken(token) {
|
|
401
|
+
return "text" in token && typeof token.text === "string" ? [token.text] : [];
|
|
402
|
+
}
|
|
403
|
+
renderToken(token, width, nextTokenType, styleContext) {
|
|
404
|
+
switch (token.type) {
|
|
405
|
+
case "heading":
|
|
406
|
+
return this.renderHeadingToken(token, nextTokenType);
|
|
407
|
+
case "paragraph":
|
|
408
|
+
return this.renderParagraphToken(token, nextTokenType, styleContext);
|
|
409
|
+
case "text":
|
|
410
|
+
return [this.renderInlineTokens([token], styleContext)];
|
|
411
|
+
case "latexBlock":
|
|
412
|
+
return this.renderLatexBlockToken(token, width, nextTokenType);
|
|
413
|
+
case "code":
|
|
414
|
+
return this.renderCodeToken(token, nextTokenType);
|
|
415
|
+
case "list":
|
|
416
|
+
return this.renderList(token, 0, width, styleContext);
|
|
417
|
+
case "table":
|
|
418
|
+
return this.renderTable(token, width, nextTokenType, styleContext);
|
|
419
|
+
case "blockquote":
|
|
420
|
+
return this.renderBlockquoteToken(token, width, nextTokenType);
|
|
321
421
|
case "hr":
|
|
322
|
-
|
|
323
|
-
if (nextTokenType && nextTokenType !== "space") {
|
|
324
|
-
lines.push(""); // Add spacing after horizontal rules (unless space token follows)
|
|
325
|
-
}
|
|
326
|
-
break;
|
|
422
|
+
return this.renderHorizontalRule(width, nextTokenType);
|
|
327
423
|
case "html":
|
|
328
|
-
|
|
329
|
-
if ("raw" in token && typeof token.raw === "string") {
|
|
330
|
-
const visibleHtml = stripHtmlComments(token.raw).trim();
|
|
331
|
-
if (visibleHtml)
|
|
332
|
-
lines.push(this.applyDefaultStyle(visibleHtml));
|
|
333
|
-
}
|
|
334
|
-
break;
|
|
424
|
+
return this.renderHtmlBlock(token);
|
|
335
425
|
case "space":
|
|
336
|
-
|
|
337
|
-
lines.push("");
|
|
338
|
-
break;
|
|
426
|
+
return [""];
|
|
339
427
|
default:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
428
|
+
return this.renderFallbackToken(token);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
applyInlineText(text, styleContext) {
|
|
432
|
+
return text
|
|
433
|
+
.split("\n")
|
|
434
|
+
.map((segment) => styleContext.applyText(segment))
|
|
435
|
+
.join("\n");
|
|
436
|
+
}
|
|
437
|
+
renderInlineLinkToken(token, styleContext) {
|
|
438
|
+
const linkText = this.renderInlineTokens(token.tokens || [], styleContext);
|
|
439
|
+
const styledLink = this.theme.link(this.theme.underline(linkText));
|
|
440
|
+
if (getCapabilities().hyperlinks) {
|
|
441
|
+
// OSC 8: render as a clickable hyperlink. The URL is not printed inline,
|
|
442
|
+
// so we always show only the link text regardless of whether it matches href.
|
|
443
|
+
return hyperlink(styledLink, token.href) + styleContext.stylePrefix;
|
|
444
|
+
}
|
|
445
|
+
// Fallback: print URL in parentheses when text differs from href.
|
|
446
|
+
// Compare raw token.text (not styled) against href for the equality check.
|
|
447
|
+
// For mailto: links strip the prefix (autolinked emails use text="foo@bar.com"
|
|
448
|
+
// but href="mailto:foo@bar.com").
|
|
449
|
+
const hrefForComparison = token.href.startsWith("mailto:") ? token.href.slice(7) : token.href;
|
|
450
|
+
if (token.text === token.href || token.text === hrefForComparison) {
|
|
451
|
+
return styledLink + styleContext.stylePrefix;
|
|
452
|
+
}
|
|
453
|
+
return styledLink + this.theme.linkUrl(` (${token.href})`) + styleContext.stylePrefix;
|
|
454
|
+
}
|
|
455
|
+
renderInlineLatexToken(token, styleContext) {
|
|
456
|
+
const rendered = !token.pending && this.options.renderLatex !== false ? (renderLatex(token.text) ?? token.raw) : token.raw;
|
|
457
|
+
return this.applyInlineText(rendered, styleContext);
|
|
458
|
+
}
|
|
459
|
+
renderInlineTextToken(token, styleContext) {
|
|
460
|
+
// Text tokens in list items can have nested tokens for inline formatting.
|
|
461
|
+
return token.tokens?.length
|
|
462
|
+
? this.renderInlineTokens(token.tokens, styleContext)
|
|
463
|
+
: this.applyInlineText(token.text, styleContext);
|
|
464
|
+
}
|
|
465
|
+
renderInlineHtmlToken(token, styleContext) {
|
|
466
|
+
if (!("raw" in token) || typeof token.raw !== "string")
|
|
467
|
+
return "";
|
|
468
|
+
const visibleHtml = stripHtmlComments(token.raw);
|
|
469
|
+
return visibleHtml ? this.applyInlineText(visibleHtml, styleContext) : "";
|
|
470
|
+
}
|
|
471
|
+
renderInlineFallbackToken(token, styleContext) {
|
|
472
|
+
if (!("text" in token) || typeof token.text !== "string")
|
|
473
|
+
return "";
|
|
474
|
+
return this.applyInlineText(token.text, styleContext);
|
|
475
|
+
}
|
|
476
|
+
renderInlineToken(token, styleContext) {
|
|
477
|
+
const { stylePrefix } = styleContext;
|
|
478
|
+
switch (token.type) {
|
|
479
|
+
case "latex":
|
|
480
|
+
return this.renderInlineLatexToken(token, styleContext);
|
|
481
|
+
case "escape":
|
|
482
|
+
return this.applyInlineText(this.options.preserveBackslashEscapes ? token.raw : token.text, styleContext);
|
|
483
|
+
case "text":
|
|
484
|
+
return this.renderInlineTextToken(token, styleContext);
|
|
485
|
+
case "paragraph":
|
|
486
|
+
return this.renderInlineTokens(token.tokens || [], styleContext);
|
|
487
|
+
case "strong":
|
|
488
|
+
return this.theme.bold(this.renderInlineTokens(token.tokens || [], styleContext)) + stylePrefix;
|
|
489
|
+
case "em":
|
|
490
|
+
return this.theme.italic(this.renderInlineTokens(token.tokens || [], styleContext)) + stylePrefix;
|
|
491
|
+
case "codespan":
|
|
492
|
+
return this.theme.code(token.text) + stylePrefix;
|
|
493
|
+
case "link":
|
|
494
|
+
return this.renderInlineLinkToken(token, styleContext);
|
|
495
|
+
case "br":
|
|
496
|
+
return "\n";
|
|
497
|
+
case "del":
|
|
498
|
+
return this.theme.strikethrough(this.renderInlineTokens(token.tokens || [], styleContext)) + stylePrefix;
|
|
499
|
+
case "html":
|
|
500
|
+
return this.renderInlineHtmlToken(token, styleContext);
|
|
501
|
+
default:
|
|
502
|
+
return this.renderInlineFallbackToken(token, styleContext);
|
|
344
503
|
}
|
|
345
|
-
return lines;
|
|
346
504
|
}
|
|
347
505
|
renderInlineTokens(tokens, styleContext) {
|
|
348
|
-
let result = "";
|
|
349
506
|
const resolvedStyleContext = styleContext ?? this.getDefaultInlineStyleContext();
|
|
350
|
-
|
|
351
|
-
const applyTextWithNewlines = (text) => {
|
|
352
|
-
const segments = text.split("\n");
|
|
353
|
-
return segments.map((segment) => applyText(segment)).join("\n");
|
|
354
|
-
};
|
|
507
|
+
let result = "";
|
|
355
508
|
for (const token of tokens) {
|
|
356
|
-
|
|
357
|
-
case "escape":
|
|
358
|
-
result += applyTextWithNewlines(this.options.preserveBackslashEscapes ? token.raw : token.text);
|
|
359
|
-
break;
|
|
360
|
-
case "text":
|
|
361
|
-
// Text tokens in list items can have nested tokens for inline formatting
|
|
362
|
-
if (token.tokens && token.tokens.length > 0) {
|
|
363
|
-
result += this.renderInlineTokens(token.tokens, resolvedStyleContext);
|
|
364
|
-
}
|
|
365
|
-
else {
|
|
366
|
-
result += applyTextWithNewlines(token.text);
|
|
367
|
-
}
|
|
368
|
-
break;
|
|
369
|
-
case "paragraph":
|
|
370
|
-
// Paragraph tokens contain nested inline tokens
|
|
371
|
-
result += this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
372
|
-
break;
|
|
373
|
-
case "strong": {
|
|
374
|
-
const boldContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
375
|
-
result += this.theme.bold(boldContent) + stylePrefix;
|
|
376
|
-
break;
|
|
377
|
-
}
|
|
378
|
-
case "em": {
|
|
379
|
-
const italicContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
380
|
-
result += this.theme.italic(italicContent) + stylePrefix;
|
|
381
|
-
break;
|
|
382
|
-
}
|
|
383
|
-
case "codespan":
|
|
384
|
-
result += this.theme.code(token.text) + stylePrefix;
|
|
385
|
-
break;
|
|
386
|
-
case "link": {
|
|
387
|
-
const linkText = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
388
|
-
const styledLink = this.theme.link(this.theme.underline(linkText));
|
|
389
|
-
if (getCapabilities().hyperlinks) {
|
|
390
|
-
// OSC 8: render as a clickable hyperlink. The URL is not printed inline,
|
|
391
|
-
// so we always show only the link text regardless of whether it matches href.
|
|
392
|
-
result += hyperlink(styledLink, token.href) + stylePrefix;
|
|
393
|
-
}
|
|
394
|
-
else {
|
|
395
|
-
// Fallback: print URL in parentheses when text differs from href.
|
|
396
|
-
// Compare raw token.text (not styled) against href for the equality check.
|
|
397
|
-
// For mailto: links strip the prefix (autolinked emails use text="foo@bar.com"
|
|
398
|
-
// but href="mailto:foo@bar.com").
|
|
399
|
-
const hrefForComparison = token.href.startsWith("mailto:") ? token.href.slice(7) : token.href;
|
|
400
|
-
if (token.text === token.href || token.text === hrefForComparison) {
|
|
401
|
-
result += styledLink + stylePrefix;
|
|
402
|
-
}
|
|
403
|
-
else {
|
|
404
|
-
result += styledLink + this.theme.linkUrl(` (${token.href})`) + stylePrefix;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
break;
|
|
408
|
-
}
|
|
409
|
-
case "br":
|
|
410
|
-
result += "\n";
|
|
411
|
-
break;
|
|
412
|
-
case "del": {
|
|
413
|
-
const delContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
|
|
414
|
-
result += this.theme.strikethrough(delContent) + stylePrefix;
|
|
415
|
-
break;
|
|
416
|
-
}
|
|
417
|
-
case "html":
|
|
418
|
-
// HTML comments are invisible in Markdown; render other inline HTML as plain text.
|
|
419
|
-
if ("raw" in token && typeof token.raw === "string") {
|
|
420
|
-
const visibleHtml = stripHtmlComments(token.raw);
|
|
421
|
-
if (visibleHtml)
|
|
422
|
-
result += applyTextWithNewlines(visibleHtml);
|
|
423
|
-
}
|
|
424
|
-
break;
|
|
425
|
-
default:
|
|
426
|
-
// Handle any other inline token types as plain text
|
|
427
|
-
if ("text" in token && typeof token.text === "string") {
|
|
428
|
-
result += applyTextWithNewlines(token.text);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
509
|
+
result += this.renderInlineToken(token, resolvedStyleContext);
|
|
431
510
|
}
|
|
432
|
-
while (stylePrefix && result.endsWith(stylePrefix)) {
|
|
433
|
-
result = result.slice(0, -stylePrefix.length);
|
|
511
|
+
while (resolvedStyleContext.stylePrefix && result.endsWith(resolvedStyleContext.stylePrefix)) {
|
|
512
|
+
result = result.slice(0, -resolvedStyleContext.stylePrefix.length);
|
|
434
513
|
}
|
|
435
514
|
return result;
|
|
436
515
|
}
|
|
@@ -438,45 +517,50 @@ export class Markdown {
|
|
|
438
517
|
const match = /^(?: {0,3})(\d{1,9}[.)])[ \t]+/.exec(item.raw);
|
|
439
518
|
return match ? `${match[1]} ` : undefined;
|
|
440
519
|
}
|
|
520
|
+
getListItemMarker(token, item, index, startNumber) {
|
|
521
|
+
if (!token.ordered)
|
|
522
|
+
return "- ";
|
|
523
|
+
if (this.options.preserveOrderedListMarkers) {
|
|
524
|
+
return this.getOrderedListMarker(item) ?? `${startNumber + index}. `;
|
|
525
|
+
}
|
|
526
|
+
return `${startNumber + index}. `;
|
|
527
|
+
}
|
|
528
|
+
renderListItem(token, item, index, startNumber, depth, width, styleContext) {
|
|
529
|
+
const indent = " ".repeat(depth);
|
|
530
|
+
const bullet = this.getListItemMarker(token, item, index, startNumber);
|
|
531
|
+
const taskMarker = item.task ? `[${item.checked ? "x" : " "}] ` : "";
|
|
532
|
+
const marker = bullet + taskMarker;
|
|
533
|
+
const firstPrefix = indent + this.theme.listBullet(marker);
|
|
534
|
+
const continuationPrefix = indent + " ".repeat(visibleWidth(marker));
|
|
535
|
+
const itemWidth = Math.max(1, width - visibleWidth(firstPrefix));
|
|
536
|
+
const lines = [];
|
|
537
|
+
let renderedAnyLine = false;
|
|
538
|
+
for (const itemToken of item.tokens) {
|
|
539
|
+
if (itemToken.type === "list") {
|
|
540
|
+
lines.push(...this.renderList(itemToken, depth + 1, width, styleContext));
|
|
541
|
+
renderedAnyLine = true;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const itemLines = this.renderToken(itemToken, itemWidth, undefined, styleContext);
|
|
545
|
+
const wrappedItemLines = itemLines.flatMap((line) => wrapTextWithAnsi(line, itemWidth));
|
|
546
|
+
for (const wrappedLine of wrappedItemLines) {
|
|
547
|
+
const linePrefix = renderedAnyLine ? continuationPrefix : firstPrefix;
|
|
548
|
+
lines.push(linePrefix + wrappedLine);
|
|
549
|
+
renderedAnyLine = true;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (!renderedAnyLine)
|
|
553
|
+
lines.push(firstPrefix);
|
|
554
|
+
return lines;
|
|
555
|
+
}
|
|
441
556
|
/**
|
|
442
557
|
* Render a list with proper nesting support
|
|
443
558
|
*/
|
|
444
559
|
renderList(token, depth, width, styleContext) {
|
|
445
560
|
const lines = [];
|
|
446
|
-
const indent = " ".repeat(depth);
|
|
447
|
-
// Use the list's start property (defaults to 1 for ordered lists)
|
|
448
561
|
const startNumber = typeof token.start === "number" ? token.start : 1;
|
|
449
|
-
for (let
|
|
450
|
-
|
|
451
|
-
const bullet = token.ordered
|
|
452
|
-
? this.options.preserveOrderedListMarkers
|
|
453
|
-
? (this.getOrderedListMarker(item) ?? `${startNumber + i}. `)
|
|
454
|
-
: `${startNumber + i}. `
|
|
455
|
-
: "- ";
|
|
456
|
-
const taskMarker = item.task ? `[${item.checked ? "x" : " "}] ` : "";
|
|
457
|
-
const marker = bullet + taskMarker;
|
|
458
|
-
const firstPrefix = indent + this.theme.listBullet(marker);
|
|
459
|
-
const continuationPrefix = indent + " ".repeat(visibleWidth(marker));
|
|
460
|
-
const itemWidth = Math.max(1, width - visibleWidth(firstPrefix));
|
|
461
|
-
let renderedAnyLine = false;
|
|
462
|
-
for (const itemToken of item.tokens) {
|
|
463
|
-
if (itemToken.type === "list") {
|
|
464
|
-
lines.push(...this.renderList(itemToken, depth + 1, width, styleContext));
|
|
465
|
-
renderedAnyLine = true;
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
const itemLines = this.renderToken(itemToken, itemWidth, undefined, styleContext);
|
|
469
|
-
for (const line of itemLines) {
|
|
470
|
-
for (const wrappedLine of wrapTextWithAnsi(line, itemWidth)) {
|
|
471
|
-
const linePrefix = renderedAnyLine ? continuationPrefix : firstPrefix;
|
|
472
|
-
lines.push(linePrefix + wrappedLine);
|
|
473
|
-
renderedAnyLine = true;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
if (!renderedAnyLine) {
|
|
478
|
-
lines.push(firstPrefix);
|
|
479
|
-
}
|
|
562
|
+
for (let index = 0; index < token.items.length; index++) {
|
|
563
|
+
lines.push(...this.renderListItem(token, token.items[index], index, startNumber, depth, width, styleContext));
|
|
480
564
|
}
|
|
481
565
|
return lines;
|
|
482
566
|
}
|
|
@@ -507,148 +591,126 @@ export class Markdown {
|
|
|
507
591
|
* Render a table with width-aware cell wrapping.
|
|
508
592
|
* Cells that don't fit are wrapped to multiple lines.
|
|
509
593
|
*/
|
|
510
|
-
|
|
511
|
-
const lines = [];
|
|
512
|
-
const numCols = token.header.length;
|
|
513
|
-
if (numCols === 0) {
|
|
514
|
-
return lines;
|
|
515
|
-
}
|
|
516
|
-
// Calculate border overhead: "│ " + (n-1) * " │ " + " │"
|
|
517
|
-
// = 2 + (n-1) * 3 + 2 = 3n + 1
|
|
518
|
-
const borderOverhead = 3 * numCols + 1;
|
|
519
|
-
const availableForCells = availableWidth - borderOverhead;
|
|
520
|
-
if (availableForCells < numCols) {
|
|
521
|
-
// Too narrow to render a stable table. Fall back to raw markdown.
|
|
522
|
-
const fallbackLines = token.raw ? wrapTextWithAnsi(token.raw, availableWidth) : [];
|
|
523
|
-
if (nextTokenType && nextTokenType !== "space") {
|
|
524
|
-
fallbackLines.push("");
|
|
525
|
-
}
|
|
526
|
-
return fallbackLines;
|
|
527
|
-
}
|
|
528
|
-
const maxUnbrokenWordWidth = 30;
|
|
529
|
-
// Calculate natural column widths (what each column needs without constraints)
|
|
594
|
+
measureTableColumns(token, styleContext) {
|
|
530
595
|
const naturalWidths = [];
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
596
|
+
const minimumWidths = [];
|
|
597
|
+
const maxUnbrokenWordWidth = 30;
|
|
598
|
+
for (let column = 0; column < token.header.length; column++) {
|
|
599
|
+
const headerText = this.renderInlineTokens(token.header[column].tokens || [], styleContext);
|
|
600
|
+
naturalWidths[column] = visibleWidth(headerText);
|
|
601
|
+
minimumWidths[column] = Math.max(1, this.getLongestWordWidth(headerText, maxUnbrokenWordWidth));
|
|
536
602
|
}
|
|
537
603
|
for (const row of token.rows) {
|
|
538
|
-
for (let
|
|
539
|
-
const cellText = this.renderInlineTokens(row[
|
|
540
|
-
naturalWidths[
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
let minColumnWidths = minWordWidths;
|
|
545
|
-
let minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
|
|
546
|
-
if (minCellsWidth > availableForCells) {
|
|
547
|
-
minColumnWidths = new Array(numCols).fill(1);
|
|
548
|
-
const remaining = availableForCells - numCols;
|
|
549
|
-
if (remaining > 0) {
|
|
550
|
-
const totalWeight = minWordWidths.reduce((total, width) => total + Math.max(0, width - 1), 0);
|
|
551
|
-
const growth = minWordWidths.map((width) => {
|
|
552
|
-
const weight = Math.max(0, width - 1);
|
|
553
|
-
return totalWeight > 0 ? Math.floor((weight / totalWeight) * remaining) : 0;
|
|
554
|
-
});
|
|
555
|
-
for (let i = 0; i < numCols; i++) {
|
|
556
|
-
minColumnWidths[i] += growth[i] ?? 0;
|
|
557
|
-
}
|
|
558
|
-
const allocated = growth.reduce((total, width) => total + width, 0);
|
|
559
|
-
let leftover = remaining - allocated;
|
|
560
|
-
for (let i = 0; leftover > 0 && i < numCols; i++) {
|
|
561
|
-
minColumnWidths[i]++;
|
|
562
|
-
leftover--;
|
|
563
|
-
}
|
|
604
|
+
for (let column = 0; column < row.length; column++) {
|
|
605
|
+
const cellText = this.renderInlineTokens(row[column].tokens || [], styleContext);
|
|
606
|
+
naturalWidths[column] = Math.max(naturalWidths[column] || 0, visibleWidth(cellText));
|
|
607
|
+
minimumWidths[column] = Math.max(minimumWidths[column] || 1, this.getLongestWordWidth(cellText, maxUnbrokenWordWidth));
|
|
564
608
|
}
|
|
565
|
-
minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);
|
|
566
609
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
if (!grew) {
|
|
602
|
-
break;
|
|
603
|
-
}
|
|
610
|
+
return { naturalWidths, minimumWidths };
|
|
611
|
+
}
|
|
612
|
+
fitMinimumTableColumnWidths(minimumWidths, availableForCells) {
|
|
613
|
+
if (minimumWidths.reduce((total, width) => total + width, 0) <= availableForCells)
|
|
614
|
+
return minimumWidths;
|
|
615
|
+
const fittedWidths = new Array(minimumWidths.length).fill(1);
|
|
616
|
+
const remaining = availableForCells - minimumWidths.length;
|
|
617
|
+
if (remaining <= 0)
|
|
618
|
+
return fittedWidths;
|
|
619
|
+
const totalWeight = minimumWidths.reduce((total, width) => total + Math.max(0, width - 1), 0);
|
|
620
|
+
const growth = minimumWidths.map((width) => {
|
|
621
|
+
const weight = Math.max(0, width - 1);
|
|
622
|
+
return totalWeight > 0 ? Math.floor((weight / totalWeight) * remaining) : 0;
|
|
623
|
+
});
|
|
624
|
+
for (let column = 0; column < minimumWidths.length; column++)
|
|
625
|
+
fittedWidths[column] += growth[column] ?? 0;
|
|
626
|
+
const allocated = growth.reduce((total, width) => total + width, 0);
|
|
627
|
+
let leftover = remaining - allocated;
|
|
628
|
+
for (let column = 0; leftover > 0 && column < minimumWidths.length; column++) {
|
|
629
|
+
fittedWidths[column]++;
|
|
630
|
+
leftover--;
|
|
631
|
+
}
|
|
632
|
+
return fittedWidths;
|
|
633
|
+
}
|
|
634
|
+
distributeRemainingTableWidth(columnWidths, naturalWidths, remainingWidth) {
|
|
635
|
+
let remaining = remainingWidth;
|
|
636
|
+
while (remaining > 0) {
|
|
637
|
+
let grew = false;
|
|
638
|
+
for (let column = 0; column < columnWidths.length && remaining > 0; column++) {
|
|
639
|
+
if (columnWidths[column] >= naturalWidths[column])
|
|
640
|
+
continue;
|
|
641
|
+
columnWidths[column]++;
|
|
642
|
+
remaining--;
|
|
643
|
+
grew = true;
|
|
604
644
|
}
|
|
645
|
+
if (!grew)
|
|
646
|
+
break;
|
|
605
647
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
648
|
+
}
|
|
649
|
+
calculateTableColumnWidths(naturalWidths, minimumWidths, availableForCells, borderOverhead) {
|
|
650
|
+
const fittedMinimumWidths = this.fitMinimumTableColumnWidths(minimumWidths, availableForCells);
|
|
651
|
+
const totalNaturalWidth = naturalWidths.reduce((total, width) => total + width, 0) + borderOverhead;
|
|
652
|
+
if (totalNaturalWidth <= availableForCells + borderOverhead) {
|
|
653
|
+
return naturalWidths.map((width, column) => Math.max(width, fittedMinimumWidths[column]));
|
|
654
|
+
}
|
|
655
|
+
const minimumCellsWidth = fittedMinimumWidths.reduce((total, width) => total + width, 0);
|
|
656
|
+
const totalGrowPotential = naturalWidths.reduce((total, width, column) => total + Math.max(0, width - fittedMinimumWidths[column]), 0);
|
|
657
|
+
const extraWidth = Math.max(0, availableForCells - minimumCellsWidth);
|
|
658
|
+
const columnWidths = fittedMinimumWidths.map((minimumWidth, column) => {
|
|
659
|
+
const growthPotential = Math.max(0, naturalWidths[column] - minimumWidth);
|
|
660
|
+
const growth = totalGrowPotential > 0 ? Math.floor((growthPotential / totalGrowPotential) * extraWidth) : 0;
|
|
661
|
+
return minimumWidth + growth;
|
|
662
|
+
});
|
|
663
|
+
const allocated = columnWidths.reduce((total, width) => total + width, 0);
|
|
664
|
+
this.distributeRemainingTableWidth(columnWidths, naturalWidths, availableForCells - allocated);
|
|
665
|
+
return columnWidths;
|
|
666
|
+
}
|
|
667
|
+
renderTableBorder(columnWidths, left, junction, right) {
|
|
668
|
+
return `${left}─${columnWidths.map((width) => "─".repeat(width)).join(`─${junction}─`)}─${right}`;
|
|
669
|
+
}
|
|
670
|
+
renderTableCellLines(cells, columnWidths, styleContext) {
|
|
671
|
+
return cells.map((cell, column) => {
|
|
611
672
|
const text = this.renderInlineTokens(cell.tokens || [], styleContext);
|
|
612
|
-
return this.wrapCellText(text, columnWidths[
|
|
673
|
+
return this.wrapCellText(text, columnWidths[column]);
|
|
613
674
|
});
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
675
|
+
}
|
|
676
|
+
// pi-ignore noExcessiveCollectionIterations: Every rendered table line must format each column once, so this rectangular traversal is linear in the emitted cell slots.
|
|
677
|
+
appendTableRow(lines, cellLines, columnWidths, bold) {
|
|
678
|
+
const lineCount = Math.max(...cellLines.map((cell) => cell.length));
|
|
679
|
+
for (let lineIndex = 0; lineIndex < lineCount; lineIndex++) {
|
|
680
|
+
const rowParts = cellLines.map((columnLines, column) => {
|
|
681
|
+
const text = columnLines[lineIndex] || "";
|
|
682
|
+
const padded = text + " ".repeat(Math.max(0, columnWidths[column] - visibleWidth(text)));
|
|
683
|
+
return bold ? this.theme.bold(padded) : padded;
|
|
620
684
|
});
|
|
621
685
|
lines.push(`│ ${rowParts.join(" │ ")} │`);
|
|
622
686
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
for (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {
|
|
636
|
-
const rowParts = rowCellLines.map((cellLines, colIdx) => {
|
|
637
|
-
const text = cellLines[lineIdx] || "";
|
|
638
|
-
return text + " ".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));
|
|
639
|
-
});
|
|
640
|
-
lines.push(`│ ${rowParts.join(" │ ")} │`);
|
|
641
|
-
}
|
|
642
|
-
if (rowIndex < token.rows.length - 1) {
|
|
643
|
-
lines.push(separatorLine);
|
|
644
|
-
}
|
|
687
|
+
}
|
|
688
|
+
renderTable(token, availableWidth, nextTokenType, styleContext) {
|
|
689
|
+
const columnCount = token.header.length;
|
|
690
|
+
if (columnCount === 0)
|
|
691
|
+
return [];
|
|
692
|
+
const borderOverhead = 3 * columnCount + 1;
|
|
693
|
+
const availableForCells = availableWidth - borderOverhead;
|
|
694
|
+
if (availableForCells < columnCount) {
|
|
695
|
+
const fallbackLines = token.raw ? wrapTextWithAnsi(token.raw, availableWidth) : [];
|
|
696
|
+
if (nextTokenType && nextTokenType !== "space")
|
|
697
|
+
fallbackLines.push("");
|
|
698
|
+
return fallbackLines;
|
|
645
699
|
}
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
lines.
|
|
649
|
-
|
|
650
|
-
|
|
700
|
+
const measurements = this.measureTableColumns(token, styleContext);
|
|
701
|
+
const columnWidths = this.calculateTableColumnWidths(measurements.naturalWidths, measurements.minimumWidths, availableForCells, borderOverhead);
|
|
702
|
+
const lines = [this.renderTableBorder(columnWidths, "┌", "┬", "┐")];
|
|
703
|
+
this.appendTableRow(lines, this.renderTableCellLines(token.header, columnWidths, styleContext), columnWidths, true);
|
|
704
|
+
const separator = this.renderTableBorder(columnWidths, "├", "┼", "┤");
|
|
705
|
+
lines.push(separator);
|
|
706
|
+
for (let rowIndex = 0; rowIndex < token.rows.length; rowIndex++) {
|
|
707
|
+
this.appendTableRow(lines, this.renderTableCellLines(token.rows[rowIndex], columnWidths, styleContext), columnWidths, false);
|
|
708
|
+
if (rowIndex < token.rows.length - 1)
|
|
709
|
+
lines.push(separator);
|
|
651
710
|
}
|
|
711
|
+
lines.push(this.renderTableBorder(columnWidths, "└", "┴", "┘"));
|
|
712
|
+
if (nextTokenType && nextTokenType !== "space")
|
|
713
|
+
lines.push("");
|
|
652
714
|
return lines;
|
|
653
715
|
}
|
|
654
716
|
}
|