@fractaal/pi-tui 1.4.1 → 1.6.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 +2 -0
- package/dist/components/editor.d.ts +2 -0
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +8 -1
- package/dist/components/editor.js.map +1 -1
- package/dist/components/markdown.d.ts +6 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +136 -52
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/select-list.d.ts +1 -0
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +10 -1
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts +1 -0
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +8 -1
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +3 -0
- package/dist/components/text.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +6 -0
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +15 -0
- package/dist/keybindings.js.map +1 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +60 -6
- package/dist/keys.js.map +1 -1
- package/dist/program-status.d.ts.map +1 -1
- package/dist/program-status.js +2 -2
- package/dist/program-status.js.map +1 -1
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +10 -1
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +11 -4
- package/dist/terminal.js.map +1 -1
- package/dist/tui-alt-screen.d.ts +2 -1
- package/dist/tui-alt-screen.d.ts.map +1 -1
- package/dist/tui-alt-screen.js +5 -2
- package/dist/tui-alt-screen.js.map +1 -1
- package/dist/tui-main-screen.d.ts.map +1 -1
- package/dist/tui-main-screen.js +4 -1
- package/dist/tui-main-screen.js.map +1 -1
- package/dist/tui.d.ts +5 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +5 -3
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +2 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +74 -7
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -36,6 +36,7 @@ export interface MarkdownTheme {
|
|
|
36
36
|
italic: (text: string) => string;
|
|
37
37
|
strikethrough: (text: string) => string;
|
|
38
38
|
underline: (text: string) => string;
|
|
39
|
+
/** Highlighted lines of a code block. A Markdown component reuses the result for the same code until invalidate(). */
|
|
39
40
|
highlightCode?: (code: string, lang?: string) => string[];
|
|
40
41
|
/** Prefix applied to each rendered code block line (default: " ") */
|
|
41
42
|
codeBlockIndent?: string;
|
|
@@ -62,10 +63,15 @@ export declare class Markdown implements Component {
|
|
|
62
63
|
private cachedWidth?;
|
|
63
64
|
private cachedLines?;
|
|
64
65
|
private cachedTokens?;
|
|
66
|
+
private blocks?;
|
|
67
|
+
private blockHighlightedCode?;
|
|
65
68
|
constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, options?: MarkdownOptions);
|
|
66
69
|
setText(text: string): void;
|
|
67
70
|
invalidate(): void;
|
|
71
|
+
private clearRenderCache;
|
|
68
72
|
render(width: number): string[];
|
|
73
|
+
/** Render one top-level block to output lines: wrapped, with margins, and padded or backgrounded to the width. */
|
|
74
|
+
private renderBlock;
|
|
69
75
|
/**
|
|
70
76
|
* Apply default text style to a string.
|
|
71
77
|
* This is the base styling applied to all text content.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,9 +2,18 @@ import { Marked, Tokenizer } from "marked";
|
|
|
2
2
|
import markedCjkFriendly from "marked-cjk-friendly";
|
|
3
3
|
import { renderLatex } from "../latex.js";
|
|
4
4
|
import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.js";
|
|
5
|
-
import { applyBackgroundToLine, flattenLines, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
5
|
+
import { applyBackgroundToLine, cjkPunctuationRegex, flattenLines, visibleWidth, wrapTextWithAnsi } from "../utils.js";
|
|
6
6
|
const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
|
|
7
|
-
class
|
|
7
|
+
class MarkdownTokenizer extends Tokenizer {
|
|
8
|
+
url(src) {
|
|
9
|
+
const token = super.url(src);
|
|
10
|
+
if (!token)
|
|
11
|
+
return undefined;
|
|
12
|
+
// Bare URLs stop at CJK punctuation; explicit links and angle autolinks
|
|
13
|
+
// use separate tokenizers and can still contain these characters.
|
|
14
|
+
const punctuationIndex = token.raw.search(cjkPunctuationRegex);
|
|
15
|
+
return punctuationIndex < 0 ? token : super.url(token.raw.slice(0, punctuationIndex));
|
|
16
|
+
}
|
|
8
17
|
del(src) {
|
|
9
18
|
const match = STRICT_STRIKETHROUGH_REGEX.exec(src);
|
|
10
19
|
if (!match) {
|
|
@@ -159,9 +168,40 @@ function trimPartialClosingFences(tokens) {
|
|
|
159
168
|
}
|
|
160
169
|
token.text = token.text.slice(0, -lastLine.length).replace(/\n$/, "");
|
|
161
170
|
}
|
|
171
|
+
// An unclosed comment runs to the end of an HTML block, as in CommonMark, so streamed comments stay hidden.
|
|
172
|
+
const HTML_COMMENT_REGEX = /<!--[\s\S]*?(?:-->|$)/g;
|
|
173
|
+
function stripHtmlComments(html) {
|
|
174
|
+
return html.replace(HTML_COMMENT_REGEX, "");
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Remove block HTML tokens that hold only comments, so they render no line. A removed comment between two blank
|
|
178
|
+
* lines, or at the start or end, also drops a blank line, so it leaves no extra gap.
|
|
179
|
+
* See https://github.com/earendil-works/pi/issues/11710.
|
|
180
|
+
*/
|
|
181
|
+
function removeHtmlCommentBlocks(tokens) {
|
|
182
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
183
|
+
const token = tokens[i];
|
|
184
|
+
if (token.type === "list") {
|
|
185
|
+
for (const item of token.items)
|
|
186
|
+
removeHtmlCommentBlocks(item.tokens);
|
|
187
|
+
}
|
|
188
|
+
else if (token.type === "blockquote") {
|
|
189
|
+
removeHtmlCommentBlocks(token.tokens ?? []);
|
|
190
|
+
}
|
|
191
|
+
else if (token.type === "html" && stripHtmlComments(token.raw).trim() === "") {
|
|
192
|
+
tokens.splice(i, 1);
|
|
193
|
+
if (tokens[i]?.type === "space" && (i === 0 || tokens[i - 1]?.type === "space")) {
|
|
194
|
+
tokens.splice(i, 1);
|
|
195
|
+
}
|
|
196
|
+
else if (i === tokens.length && tokens[i - 1]?.type === "space") {
|
|
197
|
+
tokens.splice(--i, 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
162
202
|
const markdownParser = new Marked();
|
|
163
203
|
markdownParser.setOptions({
|
|
164
|
-
tokenizer: new
|
|
204
|
+
tokenizer: new MarkdownTokenizer(),
|
|
165
205
|
});
|
|
166
206
|
markdownParser.use({
|
|
167
207
|
tokenizer: { emStrong: markedCjkFriendly().tokenizer?.emStrong },
|
|
@@ -183,6 +223,13 @@ export class Markdown {
|
|
|
183
223
|
// about ten times the size of its source, and every message of a long transcript keeps a Markdown component. The
|
|
184
224
|
// tokens survive a burst of re-renders, such as a theme preview, and are collected afterwards.
|
|
185
225
|
cachedTokens;
|
|
226
|
+
// The last render's top-level blocks. While a message streams only its last block changes, so a render styles and
|
|
227
|
+
// wraps only that block. Each render keeps only the blocks it used, which drops a streamed block's earlier versions.
|
|
228
|
+
// Held weakly like the tokens: the blocks repeat the source in their keys and the code in their highlighting, and
|
|
229
|
+
// only a burst of renders, such as streaming or a resize, needs them.
|
|
230
|
+
blocks;
|
|
231
|
+
// Highlighted code of the block being rendered, only set while renderBlock runs.
|
|
232
|
+
blockHighlightedCode;
|
|
186
233
|
constructor(text, paddingX, paddingY, theme, defaultTextStyle, options) {
|
|
187
234
|
this.text = text;
|
|
188
235
|
this.paddingX = paddingX;
|
|
@@ -193,9 +240,15 @@ export class Markdown {
|
|
|
193
240
|
}
|
|
194
241
|
setText(text) {
|
|
195
242
|
this.text = text;
|
|
196
|
-
this.
|
|
243
|
+
this.clearRenderCache();
|
|
197
244
|
}
|
|
198
245
|
invalidate() {
|
|
246
|
+
this.clearRenderCache();
|
|
247
|
+
// The theme may have changed, which changes every styled line and the default style's prefix.
|
|
248
|
+
this.blocks = undefined;
|
|
249
|
+
this.defaultStylePrefix = undefined;
|
|
250
|
+
}
|
|
251
|
+
clearRenderCache() {
|
|
199
252
|
this.cachedText = undefined;
|
|
200
253
|
this.cachedWidth = undefined;
|
|
201
254
|
this.cachedLines = undefined;
|
|
@@ -210,6 +263,7 @@ export class Markdown {
|
|
|
210
263
|
const text = this.options.transform?.(this.text, contentWidth) ?? this.text;
|
|
211
264
|
// Don't render anything if there's no actual text
|
|
212
265
|
if (!text || text.trim() === "") {
|
|
266
|
+
this.blocks = undefined;
|
|
213
267
|
const result = [];
|
|
214
268
|
// Update cache
|
|
215
269
|
this.cachedText = this.text;
|
|
@@ -225,67 +279,77 @@ export class Markdown {
|
|
|
225
279
|
if (!tokens) {
|
|
226
280
|
tokens = markdownParser.lexer(normalizedText);
|
|
227
281
|
trimPartialClosingFences(tokens);
|
|
282
|
+
removeHtmlCommentBlocks(tokens);
|
|
228
283
|
this.cachedTokens = new WeakRef({ source: normalizedText, tokens });
|
|
229
284
|
}
|
|
230
|
-
//
|
|
231
|
-
|
|
285
|
+
// A reference link resolves through a definition anywhere in the text, so blocks with the same source can render
|
|
286
|
+
// differently; such texts are rare in practice and render without reusing blocks.
|
|
287
|
+
const reuseBlocks = Object.keys(tokens.links).length === 0;
|
|
288
|
+
const previousBlocks = this.blocks?.deref();
|
|
289
|
+
const blocks = new Map();
|
|
290
|
+
this.blocks = new WeakRef(blocks);
|
|
291
|
+
const contentLines = [];
|
|
232
292
|
for (let i = 0; i < tokens.length; i++) {
|
|
233
293
|
const token = tokens[i];
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
// Wrap lines (NO padding, NO background yet)
|
|
241
|
-
const wrappedLines = [];
|
|
242
|
-
for (const line of renderedLines) {
|
|
243
|
-
if (isImageLine(line)) {
|
|
244
|
-
wrappedLines.push(line);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
for (const wrappedLine of wrapTextWithAnsi(line, contentWidth)) {
|
|
248
|
-
wrappedLines.push(wrappedLine);
|
|
249
|
-
}
|
|
294
|
+
const nextTokenType = tokens[i + 1]?.type;
|
|
295
|
+
const reused = reuseBlocks ? (blocks.get(token.raw) ?? previousBlocks?.get(token.raw)) : undefined;
|
|
296
|
+
const block = reused ?? { width: -1, nextTokenType, lines: [], highlightedCode: new Map() };
|
|
297
|
+
if (block.width !== width || block.nextTokenType !== nextTokenType) {
|
|
298
|
+
this.renderBlock(block, token, nextTokenType, width, contentWidth);
|
|
250
299
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const rightMargin = " ".repeat(this.paddingX);
|
|
255
|
-
const bgFn = this.defaultTextStyle?.bgColor;
|
|
256
|
-
const contentLines = [];
|
|
257
|
-
for (const line of wrappedLines) {
|
|
258
|
-
if (isImageLine(line)) {
|
|
300
|
+
if (reuseBlocks)
|
|
301
|
+
blocks.set(token.raw, block);
|
|
302
|
+
for (const line of block.lines) {
|
|
259
303
|
contentLines.push(line);
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
const lineWithMargins = leftMargin + line + rightMargin;
|
|
263
|
-
if (bgFn) {
|
|
264
|
-
contentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
// No background - just pad to width
|
|
268
|
-
const visibleLen = visibleWidth(lineWithMargins);
|
|
269
|
-
const paddingNeeded = Math.max(0, width - visibleLen);
|
|
270
|
-
contentLines.push(lineWithMargins + " ".repeat(paddingNeeded));
|
|
271
304
|
}
|
|
272
305
|
}
|
|
273
306
|
// Add top/bottom padding (empty lines)
|
|
307
|
+
const bgFn = this.defaultTextStyle?.bgColor;
|
|
274
308
|
const emptyLine = " ".repeat(width);
|
|
275
309
|
const emptyLines = [];
|
|
276
310
|
for (let i = 0; i < this.paddingY; i++) {
|
|
277
311
|
const line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;
|
|
278
312
|
emptyLines.push(line);
|
|
279
313
|
}
|
|
314
|
+
flattenLines(emptyLines);
|
|
280
315
|
// Combine top padding, content, and bottom padding
|
|
281
316
|
const result = emptyLines.concat(contentLines, emptyLines);
|
|
282
|
-
flattenLines(result);
|
|
283
317
|
// Update cache
|
|
284
318
|
this.cachedText = this.text;
|
|
285
319
|
this.cachedWidth = width;
|
|
286
320
|
this.cachedLines = result;
|
|
287
321
|
return result.length > 0 ? result : [""];
|
|
288
322
|
}
|
|
323
|
+
/** Render one top-level block to output lines: wrapped, with margins, and padded or backgrounded to the width. */
|
|
324
|
+
renderBlock(block, token, nextTokenType, width, contentWidth) {
|
|
325
|
+
this.blockHighlightedCode = block.highlightedCode;
|
|
326
|
+
const leftMargin = " ".repeat(this.paddingX);
|
|
327
|
+
const rightMargin = " ".repeat(this.paddingX);
|
|
328
|
+
const bgFn = this.defaultTextStyle?.bgColor;
|
|
329
|
+
const lines = [];
|
|
330
|
+
for (const line of this.renderToken(token, contentWidth, nextTokenType)) {
|
|
331
|
+
if (isImageLine(line)) {
|
|
332
|
+
lines.push(line);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
for (const wrappedLine of wrapTextWithAnsi(line, contentWidth)) {
|
|
336
|
+
const lineWithMargins = leftMargin + wrappedLine + rightMargin;
|
|
337
|
+
if (bgFn) {
|
|
338
|
+
lines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// No background - just pad to width
|
|
342
|
+
const paddingNeeded = Math.max(0, width - visibleWidth(lineWithMargins));
|
|
343
|
+
lines.push(lineWithMargins + " ".repeat(paddingNeeded));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
this.blockHighlightedCode = undefined;
|
|
348
|
+
flattenLines(lines);
|
|
349
|
+
block.lines = lines;
|
|
350
|
+
block.width = width;
|
|
351
|
+
block.nextTokenType = nextTokenType;
|
|
352
|
+
}
|
|
289
353
|
/**
|
|
290
354
|
* Apply default text style to a string.
|
|
291
355
|
* This is the base styling applied to all text content.
|
|
@@ -398,9 +462,13 @@ export class Markdown {
|
|
|
398
462
|
break;
|
|
399
463
|
case "latexBlock": {
|
|
400
464
|
const latexToken = token;
|
|
401
|
-
|
|
402
|
-
?
|
|
403
|
-
:
|
|
465
|
+
let rendered = !latexToken.pending && this.options.renderLatex !== false
|
|
466
|
+
? renderLatex(latexToken.text, { display: true })
|
|
467
|
+
: undefined;
|
|
468
|
+
// A 2D layout cannot be wrapped row by row; fall back to the raw source when it does not fit.
|
|
469
|
+
if (rendered === undefined || rendered.split("\n").some((line) => visibleWidth(line) > width)) {
|
|
470
|
+
rendered = latexToken.raw.trim();
|
|
471
|
+
}
|
|
404
472
|
for (const line of rendered.split("\n")) {
|
|
405
473
|
lines.push(this.applyDefaultStyle(line));
|
|
406
474
|
}
|
|
@@ -413,8 +481,13 @@ export class Markdown {
|
|
|
413
481
|
const indent = this.theme.codeBlockIndent ?? " ";
|
|
414
482
|
lines.push(this.theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
|
|
415
483
|
if (this.theme.highlightCode) {
|
|
416
|
-
const
|
|
417
|
-
|
|
484
|
+
const key = `${token.lang ?? ""}\0${token.text}`;
|
|
485
|
+
let highlighted = this.blockHighlightedCode?.get(key);
|
|
486
|
+
if (!highlighted) {
|
|
487
|
+
highlighted = this.theme.highlightCode(token.text, token.lang);
|
|
488
|
+
this.blockHighlightedCode?.set(key, highlighted);
|
|
489
|
+
}
|
|
490
|
+
for (const hlLine of highlighted) {
|
|
418
491
|
lines.push(`${indent}${hlLine}`);
|
|
419
492
|
}
|
|
420
493
|
}
|
|
@@ -506,9 +579,9 @@ export class Markdown {
|
|
|
506
579
|
}
|
|
507
580
|
break;
|
|
508
581
|
case "html":
|
|
509
|
-
// Render HTML as plain text (escaped for terminal)
|
|
582
|
+
// Render HTML as plain text (escaped for terminal), without its comments
|
|
510
583
|
if ("raw" in token && typeof token.raw === "string") {
|
|
511
|
-
lines.push(this.applyDefaultStyle(token.raw.trim()));
|
|
584
|
+
lines.push(this.applyDefaultStyle(stripHtmlComments(token.raw).trim()));
|
|
512
585
|
}
|
|
513
586
|
break;
|
|
514
587
|
case "space":
|
|
@@ -531,7 +604,15 @@ export class Markdown {
|
|
|
531
604
|
const segments = text.split("\n");
|
|
532
605
|
return segments.map((segment) => applyText(segment)).join("\n");
|
|
533
606
|
};
|
|
534
|
-
|
|
607
|
+
// After a skipped HTML comment, drop the next text's leading space if the text before ended with one,
|
|
608
|
+
// so "a <!-- c --> b" renders as "a b".
|
|
609
|
+
let dropLeadingSpace = false;
|
|
610
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
611
|
+
let token = tokens[i];
|
|
612
|
+
if (dropLeadingSpace && token.type === "text" && !token.tokens?.length && /^[ \t]/.test(token.text)) {
|
|
613
|
+
token = { ...token, text: token.text.replace(/^[ \t]+/, "") };
|
|
614
|
+
}
|
|
615
|
+
dropLeadingSpace = false;
|
|
535
616
|
switch (token.type) {
|
|
536
617
|
case "latex": {
|
|
537
618
|
const latexToken = token;
|
|
@@ -602,8 +683,11 @@ export class Markdown {
|
|
|
602
683
|
break;
|
|
603
684
|
}
|
|
604
685
|
case "html":
|
|
605
|
-
// Render inline HTML as plain text
|
|
606
|
-
if (
|
|
686
|
+
// Render inline HTML as plain text; comments are hidden
|
|
687
|
+
if (token.raw.startsWith("<!--")) {
|
|
688
|
+
dropLeadingSpace = i === 0 || /[ \t]$/.test(tokens[i - 1].raw);
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
607
691
|
result += applyTextWithNewlines(token.raw);
|
|
608
692
|
}
|
|
609
693
|
break;
|