@mariozechner/pi-tui 0.5.48 → 0.7.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 +166 -475
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +2 -0
- package/dist/autocomplete.js.map +1 -1
- package/dist/components/{text-editor.d.ts → editor.d.ts} +9 -5
- package/dist/components/editor.d.ts.map +1 -0
- package/dist/components/{text-editor.js → editor.js} +125 -70
- package/dist/components/editor.js.map +1 -0
- package/dist/components/input.d.ts +14 -0
- package/dist/components/input.d.ts.map +1 -0
- package/dist/components/input.js +120 -0
- package/dist/components/input.js.map +1 -0
- package/dist/components/{loading-animation.d.ts → loader.d.ts} +5 -5
- package/dist/components/loader.d.ts.map +1 -0
- package/dist/components/{loading-animation.js → loader.js} +13 -10
- package/dist/components/loader.js.map +1 -0
- package/dist/components/markdown.d.ts +46 -0
- package/dist/components/markdown.d.ts.map +1 -0
- package/dist/components/markdown.js +499 -0
- package/dist/components/markdown.js.map +1 -0
- package/dist/components/select-list.d.ts +3 -3
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +24 -16
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/spacer.d.ts +11 -0
- package/dist/components/spacer.d.ts.map +1 -0
- package/dist/components/spacer.js +20 -0
- package/dist/components/spacer.js.map +1 -0
- package/dist/components/text.d.ts +26 -0
- package/dist/components/text.d.ts.map +1 -0
- package/dist/components/text.js +141 -0
- package/dist/components/text.js.map +1 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -12
- package/dist/index.js.map +1 -1
- package/dist/terminal.d.ts +12 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +33 -3
- package/dist/terminal.js.map +1 -1
- package/dist/tui.d.ts +30 -52
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +131 -337
- package/dist/tui.js.map +1 -1
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +15 -0
- package/dist/utils.js.map +1 -0
- package/package.json +6 -5
- package/dist/components/loading-animation.d.ts.map +0 -1
- package/dist/components/loading-animation.js.map +0 -1
- package/dist/components/markdown-component.d.ts +0 -15
- package/dist/components/markdown-component.d.ts.map +0 -1
- package/dist/components/markdown-component.js +0 -247
- package/dist/components/markdown-component.js.map +0 -1
- package/dist/components/text-component.d.ts +0 -14
- package/dist/components/text-component.d.ts.map +0 -1
- package/dist/components/text-component.js +0 -90
- package/dist/components/text-component.js.map +0 -1
- package/dist/components/text-editor.d.ts.map +0 -1
- package/dist/components/text-editor.js.map +0 -1
- package/dist/components/whitespace-component.d.ts +0 -13
- package/dist/components/whitespace-component.d.ts.map +0 -1
- package/dist/components/whitespace-component.js +0 -22
- package/dist/components/whitespace-component.js.map +0 -1
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
2
|
+
import { Text } from "./text.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Simulates the animation component that causes flicker in single-buffer mode
|
|
4
|
+
* Loader component that updates every 80ms with spinning animation
|
|
6
5
|
*/
|
|
7
|
-
export class
|
|
6
|
+
export class Loader extends Text {
|
|
7
|
+
message;
|
|
8
|
+
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
9
|
+
currentFrame = 0;
|
|
10
|
+
intervalId = null;
|
|
11
|
+
ui = null;
|
|
8
12
|
constructor(ui, message = "Loading...") {
|
|
9
|
-
super("",
|
|
13
|
+
super("", 1, 0);
|
|
10
14
|
this.message = message;
|
|
11
|
-
this.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
12
|
-
this.currentFrame = 0;
|
|
13
|
-
this.intervalId = null;
|
|
14
|
-
this.ui = null;
|
|
15
15
|
this.ui = ui;
|
|
16
16
|
this.start();
|
|
17
17
|
}
|
|
18
|
+
render(width) {
|
|
19
|
+
return ["", ...super.render(width)];
|
|
20
|
+
}
|
|
18
21
|
start() {
|
|
19
22
|
this.updateDisplay();
|
|
20
23
|
this.intervalId = setInterval(() => {
|
|
@@ -40,4 +43,4 @@ export class LoadingAnimation extends TextComponent {
|
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
|
-
//# sourceMappingURL=
|
|
46
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/components/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;GAEG;AACH,MAAM,OAAO,MAAO,SAAQ,IAAI;IAQtB,OAAO;IAPR,MAAM,GAAG,CAAC,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,EAAE,KAAG,CAAC,CAAC;IAC5D,YAAY,GAAG,CAAC,CAAC;IACjB,UAAU,GAA0B,IAAI,CAAC;IACzC,EAAE,GAAe,IAAI,CAAC;IAE9B,YACC,EAAO,EACC,OAAO,GAAW,YAAY,EACrC;QACD,KAAK,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;uBAFR,OAAO;QAGf,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,KAAK,EAAE,CAAC;IAAA,CACb;IAED,MAAM,CAAC,KAAa,EAAY;QAC/B,OAAO,CAAC,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAAA,CACpC;IAED,KAAK,GAAG;QACP,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;YACnC,IAAI,CAAC,YAAY,GAAG,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;YACjE,IAAI,CAAC,aAAa,EAAE,CAAC;QAAA,CACrB,EAAE,EAAE,CAAC,CAAC;IAAA,CACP;IAED,IAAI,GAAG;QACN,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC/B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACxB,CAAC;IAAA,CACD;IAED,UAAU,CAAC,OAAe,EAAE;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAEO,aAAa,GAAG;QACvB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC7C,IAAI,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAChE,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;YACb,IAAI,CAAC,EAAE,CAAC,aAAa,EAAE,CAAC;QACzB,CAAC;IAAA,CACD;CACD","sourcesContent":["import chalk from \"chalk\";\nimport type { TUI } from \"../tui.js\";\nimport { Text } from \"./text.js\";\n\n/**\n * Loader component that updates every 80ms with spinning animation\n */\nexport class Loader extends Text {\n\tprivate frames = [\"⠋\", \"⠙\", \"⠹\", \"⠸\", \"⠼\", \"⠴\", \"⠦\", \"⠧\", \"⠇\", \"⠏\"];\n\tprivate currentFrame = 0;\n\tprivate intervalId: NodeJS.Timeout | null = null;\n\tprivate ui: TUI | null = null;\n\n\tconstructor(\n\t\tui: TUI,\n\t\tprivate message: string = \"Loading...\",\n\t) {\n\t\tsuper(\"\", 1, 0);\n\t\tthis.ui = ui;\n\t\tthis.start();\n\t}\n\n\trender(width: number): string[] {\n\t\treturn [\"\", ...super.render(width)];\n\t}\n\n\tstart() {\n\t\tthis.updateDisplay();\n\t\tthis.intervalId = setInterval(() => {\n\t\t\tthis.currentFrame = (this.currentFrame + 1) % this.frames.length;\n\t\t\tthis.updateDisplay();\n\t\t}, 80);\n\t}\n\n\tstop() {\n\t\tif (this.intervalId) {\n\t\t\tclearInterval(this.intervalId);\n\t\t\tthis.intervalId = null;\n\t\t}\n\t}\n\n\tsetMessage(message: string) {\n\t\tthis.message = message;\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate updateDisplay() {\n\t\tconst frame = this.frames[this.currentFrame];\n\t\tthis.setText(`${chalk.cyan(frame)} ${chalk.dim(this.message)}`);\n\t\tif (this.ui) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Component } from "../tui.js";
|
|
2
|
+
type Color = "black" | "red" | "green" | "yellow" | "blue" | "magenta" | "cyan" | "white" | "gray" | "bgBlack" | "bgRed" | "bgGreen" | "bgYellow" | "bgBlue" | "bgMagenta" | "bgCyan" | "bgWhite" | "bgGray";
|
|
3
|
+
export declare class Markdown implements Component {
|
|
4
|
+
private text;
|
|
5
|
+
private bgColor?;
|
|
6
|
+
private fgColor?;
|
|
7
|
+
private customBgRgb?;
|
|
8
|
+
private paddingX;
|
|
9
|
+
private paddingY;
|
|
10
|
+
private cachedText?;
|
|
11
|
+
private cachedWidth?;
|
|
12
|
+
private cachedLines?;
|
|
13
|
+
constructor(text?: string, bgColor?: Color, fgColor?: Color, customBgRgb?: {
|
|
14
|
+
r: number;
|
|
15
|
+
g: number;
|
|
16
|
+
b: number;
|
|
17
|
+
}, paddingX?: number, paddingY?: number);
|
|
18
|
+
setText(text: string): void;
|
|
19
|
+
setBgColor(bgColor?: Color): void;
|
|
20
|
+
setFgColor(fgColor?: Color): void;
|
|
21
|
+
setCustomBgRgb(customBgRgb?: {
|
|
22
|
+
r: number;
|
|
23
|
+
g: number;
|
|
24
|
+
b: number;
|
|
25
|
+
}): void;
|
|
26
|
+
render(width: number): string[];
|
|
27
|
+
private renderToken;
|
|
28
|
+
private renderInlineTokens;
|
|
29
|
+
private wrapLine;
|
|
30
|
+
private wrapSingleLine;
|
|
31
|
+
/**
|
|
32
|
+
* Render a list with proper nesting support
|
|
33
|
+
*/
|
|
34
|
+
private renderList;
|
|
35
|
+
/**
|
|
36
|
+
* Render list item tokens, handling nested lists
|
|
37
|
+
* Returns lines WITHOUT the parent indent (renderList will add it)
|
|
38
|
+
*/
|
|
39
|
+
private renderListItem;
|
|
40
|
+
/**
|
|
41
|
+
* Render a table
|
|
42
|
+
*/
|
|
43
|
+
private renderTable;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAG3C,KAAK,KAAK,GACP,OAAO,GACP,KAAK,GACL,OAAO,GACP,QAAQ,GACR,MAAM,GACN,SAAS,GACT,MAAM,GACN,OAAO,GACP,MAAM,GACN,SAAS,GACT,OAAO,GACP,SAAS,GACT,UAAU,GACV,QAAQ,GACR,WAAW,GACX,QAAQ,GACR,SAAS,GACT,QAAQ,CAAC;AAEZ,qBAAa,QAAS,YAAW,SAAS;IACzC,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,OAAO,CAAC,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAC,CAAQ;IACxB,OAAO,CAAC,WAAW,CAAC,CAAsC;IAC1D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAC,CAAS;IAC7B,OAAO,CAAC,WAAW,CAAC,CAAW;IAE/B,YACC,IAAI,GAAE,MAAW,EACjB,OAAO,CAAC,EAAE,KAAK,EACf,OAAO,CAAC,EAAE,KAAK,EACf,WAAW,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EACjD,QAAQ,GAAE,MAAU,EACpB,QAAQ,GAAE,MAAU,EAQpB;IAED,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAM1B;IAED,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,GAAG,IAAI,CAMhC;IAED,UAAU,CAAC,OAAO,CAAC,EAAE,KAAK,GAAG,IAAI,CAMhC;IAED,cAAc,CAAC,WAAW,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAMtE;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAwG9B;IAED,OAAO,CAAC,WAAW;IAyFnB,OAAO,CAAC,kBAAkB;IAwD1B,OAAO,CAAC,QAAQ;IA0BhB,OAAO,CAAC,cAAc;IAmFtB;;OAEG;IACH,OAAO,CAAC,UAAU;IA6ClB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAsCtB;;OAEG;IACH,OAAO,CAAC,WAAW;CAqDnB","sourcesContent":["import chalk from \"chalk\";\nimport { marked, type Token } from \"marked\";\nimport type { Component } from \"../tui.js\";\nimport { visibleWidth } from \"../utils.js\";\n\ntype Color =\n\t| \"black\"\n\t| \"red\"\n\t| \"green\"\n\t| \"yellow\"\n\t| \"blue\"\n\t| \"magenta\"\n\t| \"cyan\"\n\t| \"white\"\n\t| \"gray\"\n\t| \"bgBlack\"\n\t| \"bgRed\"\n\t| \"bgGreen\"\n\t| \"bgYellow\"\n\t| \"bgBlue\"\n\t| \"bgMagenta\"\n\t| \"bgCyan\"\n\t| \"bgWhite\"\n\t| \"bgGray\";\n\nexport class Markdown implements Component {\n\tprivate text: string;\n\tprivate bgColor?: Color;\n\tprivate fgColor?: Color;\n\tprivate customBgRgb?: { r: number; g: number; b: number };\n\tprivate paddingX: number; // Left/right padding\n\tprivate paddingY: number; // Top/bottom padding\n\n\t// Cache for rendered output\n\tprivate cachedText?: string;\n\tprivate cachedWidth?: number;\n\tprivate cachedLines?: string[];\n\n\tconstructor(\n\t\ttext: string = \"\",\n\t\tbgColor?: Color,\n\t\tfgColor?: Color,\n\t\tcustomBgRgb?: { r: number; g: number; b: number },\n\t\tpaddingX: number = 1,\n\t\tpaddingY: number = 1,\n\t) {\n\t\tthis.text = text;\n\t\tthis.bgColor = bgColor;\n\t\tthis.fgColor = fgColor;\n\t\tthis.customBgRgb = customBgRgb;\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t}\n\n\tsetText(text: string): void {\n\t\tthis.text = text;\n\t\t// Invalidate cache when text changes\n\t\tthis.cachedText = undefined;\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\tsetBgColor(bgColor?: Color): void {\n\t\tthis.bgColor = bgColor;\n\t\t// Invalidate cache when color changes\n\t\tthis.cachedText = undefined;\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\tsetFgColor(fgColor?: Color): void {\n\t\tthis.fgColor = fgColor;\n\t\t// Invalidate cache when color changes\n\t\tthis.cachedText = undefined;\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\tsetCustomBgRgb(customBgRgb?: { r: number; g: number; b: number }): void {\n\t\tthis.customBgRgb = customBgRgb;\n\t\t// Invalidate cache when color changes\n\t\tthis.cachedText = undefined;\n\t\tthis.cachedWidth = undefined;\n\t\tthis.cachedLines = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\t// Check cache\n\t\tif (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\t// Calculate available width for content (subtract horizontal padding)\n\t\tconst contentWidth = Math.max(1, width - this.paddingX * 2);\n\n\t\t// Don't render anything if there's no actual text\n\t\tif (!this.text || this.text.trim() === \"\") {\n\t\t\tconst result: string[] = [];\n\t\t\t// Update cache\n\t\t\tthis.cachedText = this.text;\n\t\t\tthis.cachedWidth = width;\n\t\t\tthis.cachedLines = result;\n\t\t\treturn result;\n\t\t}\n\n\t\t// Replace tabs with 3 spaces for consistent rendering\n\t\tconst normalizedText = this.text.replace(/\\t/g, \" \");\n\n\t\t// Parse markdown to HTML-like tokens\n\t\tconst tokens = marked.lexer(normalizedText);\n\n\t\t// Convert tokens to styled terminal output\n\t\tconst renderedLines: string[] = [];\n\n\t\tfor (let i = 0; i < tokens.length; i++) {\n\t\t\tconst token = tokens[i];\n\t\t\tconst nextToken = tokens[i + 1];\n\t\t\tconst tokenLines = this.renderToken(token, contentWidth, nextToken?.type);\n\t\t\trenderedLines.push(...tokenLines);\n\t\t}\n\n\t\t// Wrap lines to fit content width\n\t\tconst wrappedLines: string[] = [];\n\t\tfor (const line of renderedLines) {\n\t\t\twrappedLines.push(...this.wrapLine(line, contentWidth));\n\t\t}\n\n\t\t// Add padding and apply colors\n\t\tconst leftPad = \" \".repeat(this.paddingX);\n\t\tconst paddedLines: string[] = [];\n\n\t\tfor (const line of wrappedLines) {\n\t\t\t// Calculate visible length\n\t\t\tconst visibleLength = visibleWidth(line);\n\t\t\t// Right padding to fill to width (accounting for left padding and content)\n\t\t\tconst rightPadLength = Math.max(0, width - this.paddingX - visibleLength);\n\t\t\tconst rightPad = \" \".repeat(rightPadLength);\n\n\t\t\t// Add left padding, content, and right padding\n\t\t\tlet paddedLine = leftPad + line + rightPad;\n\n\t\t\t// Apply foreground color if specified\n\t\t\tif (this.fgColor) {\n\t\t\t\tpaddedLine = (chalk as any)[this.fgColor](paddedLine);\n\t\t\t}\n\n\t\t\t// Apply background color if specified\n\t\t\tif (this.customBgRgb) {\n\t\t\t\tpaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(paddedLine);\n\t\t\t} else if (this.bgColor) {\n\t\t\t\tpaddedLine = (chalk as any)[this.bgColor](paddedLine);\n\t\t\t}\n\n\t\t\tpaddedLines.push(paddedLine);\n\t\t}\n\n\t\t// Add top padding (empty lines)\n\t\tconst emptyLine = \" \".repeat(width);\n\t\tconst topPadding: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tlet emptyPaddedLine = emptyLine;\n\t\t\tif (this.customBgRgb) {\n\t\t\t\temptyPaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(emptyPaddedLine);\n\t\t\t} else if (this.bgColor) {\n\t\t\t\temptyPaddedLine = (chalk as any)[this.bgColor](emptyPaddedLine);\n\t\t\t}\n\t\t\ttopPadding.push(emptyPaddedLine);\n\t\t}\n\n\t\t// Add bottom padding (empty lines)\n\t\tconst bottomPadding: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tlet emptyPaddedLine = emptyLine;\n\t\t\tif (this.customBgRgb) {\n\t\t\t\temptyPaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(emptyPaddedLine);\n\t\t\t} else if (this.bgColor) {\n\t\t\t\temptyPaddedLine = (chalk as any)[this.bgColor](emptyPaddedLine);\n\t\t\t}\n\t\t\tbottomPadding.push(emptyPaddedLine);\n\t\t}\n\n\t\t// Combine top padding, content, and bottom padding\n\t\tconst result = [...topPadding, ...paddedLines, ...bottomPadding];\n\n\t\t// Update cache\n\t\tthis.cachedText = this.text;\n\t\tthis.cachedWidth = width;\n\t\tthis.cachedLines = result;\n\n\t\treturn result.length > 0 ? result : [\"\"];\n\t}\n\n\tprivate renderToken(token: Token, width: number, nextTokenType?: string): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tswitch (token.type) {\n\t\t\tcase \"heading\": {\n\t\t\t\tconst headingLevel = token.depth;\n\t\t\t\tconst headingPrefix = \"#\".repeat(headingLevel) + \" \";\n\t\t\t\tconst headingText = this.renderInlineTokens(token.tokens || []);\n\t\t\t\tif (headingLevel === 1) {\n\t\t\t\t\tlines.push(chalk.bold.underline.yellow(headingText));\n\t\t\t\t} else if (headingLevel === 2) {\n\t\t\t\t\tlines.push(chalk.bold.yellow(headingText));\n\t\t\t\t} else {\n\t\t\t\t\tlines.push(chalk.bold(headingPrefix + headingText));\n\t\t\t\t}\n\t\t\t\tlines.push(\"\"); // Add spacing after headings\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"paragraph\": {\n\t\t\t\tconst paragraphText = this.renderInlineTokens(token.tokens || []);\n\t\t\t\tlines.push(paragraphText);\n\t\t\t\t// Don't add spacing if next token is space or list\n\t\t\t\tif (nextTokenType && nextTokenType !== \"list\" && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"code\": {\n\t\t\t\tlines.push(chalk.gray(\"```\" + (token.lang || \"\")));\n\t\t\t\t// Split code by newlines and style each line\n\t\t\t\tconst codeLines = token.text.split(\"\\n\");\n\t\t\t\tfor (const codeLine of codeLines) {\n\t\t\t\t\tlines.push(chalk.dim(\" \") + chalk.green(codeLine));\n\t\t\t\t}\n\t\t\t\tlines.push(chalk.gray(\"```\"));\n\t\t\t\tlines.push(\"\"); // Add spacing after code blocks\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"list\": {\n\t\t\t\tconst listLines = this.renderList(token as any, 0);\n\t\t\t\tlines.push(...listLines);\n\t\t\t\t// Don't add spacing after lists if a space token follows\n\t\t\t\t// (the space token will handle it)\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"table\": {\n\t\t\t\tconst tableLines = this.renderTable(token as any);\n\t\t\t\tlines.push(...tableLines);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"blockquote\": {\n\t\t\t\tconst quoteText = this.renderInlineTokens(token.tokens || []);\n\t\t\t\tconst quoteLines = quoteText.split(\"\\n\");\n\t\t\t\tfor (const quoteLine of quoteLines) {\n\t\t\t\t\tlines.push(chalk.gray(\"│ \") + chalk.italic(quoteLine));\n\t\t\t\t}\n\t\t\t\tlines.push(\"\"); // Add spacing after blockquotes\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"hr\":\n\t\t\t\tlines.push(chalk.gray(\"─\".repeat(Math.min(width, 80))));\n\t\t\t\tlines.push(\"\"); // Add spacing after horizontal rules\n\t\t\t\tbreak;\n\n\t\t\tcase \"html\":\n\t\t\t\t// Skip HTML for terminal output\n\t\t\t\tbreak;\n\n\t\t\tcase \"space\":\n\t\t\t\t// Space tokens represent blank lines in markdown\n\t\t\t\tlines.push(\"\");\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\t// Handle any other token types as plain text\n\t\t\t\tif (\"text\" in token && typeof token.text === \"string\") {\n\t\t\t\t\tlines.push(token.text);\n\t\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\tprivate renderInlineTokens(tokens: Token[]): string {\n\t\tlet result = \"\";\n\n\t\tfor (const token of tokens) {\n\t\t\tswitch (token.type) {\n\t\t\t\tcase \"text\":\n\t\t\t\t\t// Text tokens in list items can have nested tokens for inline formatting\n\t\t\t\t\tif (token.tokens && token.tokens.length > 0) {\n\t\t\t\t\t\tresult += this.renderInlineTokens(token.tokens);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult += token.text;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"strong\":\n\t\t\t\t\tresult += chalk.bold(this.renderInlineTokens(token.tokens || []));\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"em\":\n\t\t\t\t\tresult += chalk.italic(this.renderInlineTokens(token.tokens || []));\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"codespan\":\n\t\t\t\t\tresult += chalk.gray(\"`\") + chalk.cyan(token.text) + chalk.gray(\"`\");\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"link\": {\n\t\t\t\t\tconst linkText = this.renderInlineTokens(token.tokens || []);\n\t\t\t\t\t// If link text matches href, only show the link once\n\t\t\t\t\tif (linkText === token.href) {\n\t\t\t\t\t\tresult += chalk.underline.blue(linkText);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult += chalk.underline.blue(linkText) + chalk.gray(` (${token.href})`);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"br\":\n\t\t\t\t\tresult += \"\\n\";\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"del\":\n\t\t\t\t\tresult += chalk.strikethrough(this.renderInlineTokens(token.tokens || []));\n\t\t\t\t\tbreak;\n\n\t\t\t\tdefault:\n\t\t\t\t\t// Handle any other inline token types as plain text\n\t\t\t\t\tif (\"text\" in token && typeof token.text === \"string\") {\n\t\t\t\t\t\tresult += token.text;\n\t\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\tprivate wrapLine(line: string, width: number): string[] {\n\t\t// Handle ANSI escape codes properly when wrapping\n\t\tconst wrapped: string[] = [];\n\n\t\t// Handle undefined or null lines\n\t\tif (!line) {\n\t\t\treturn [\"\"];\n\t\t}\n\n\t\t// Split by newlines first - wrap each line individually\n\t\tconst splitLines = line.split(\"\\n\");\n\t\tfor (const splitLine of splitLines) {\n\t\t\tconst visibleLength = visibleWidth(splitLine);\n\n\t\t\tif (visibleLength <= width) {\n\t\t\t\twrapped.push(splitLine);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// This line needs wrapping\n\t\t\twrapped.push(...this.wrapSingleLine(splitLine, width));\n\t\t}\n\n\t\treturn wrapped.length > 0 ? wrapped : [\"\"];\n\t}\n\n\tprivate wrapSingleLine(line: string, width: number): string[] {\n\t\tconst wrapped: string[] = [];\n\n\t\t// Track active ANSI codes to preserve them across wrapped lines\n\t\tconst activeAnsiCodes: string[] = [];\n\t\tlet currentLine = \"\";\n\t\tlet currentLength = 0;\n\t\tlet i = 0;\n\n\t\twhile (i < line.length) {\n\t\t\tif (line[i] === \"\\x1b\" && line[i + 1] === \"[\") {\n\t\t\t\t// ANSI escape sequence - parse and track it\n\t\t\t\tlet j = i + 2;\n\t\t\t\twhile (j < line.length && line[j] && !/[mGKHJ]/.test(line[j]!)) {\n\t\t\t\t\tj++;\n\t\t\t\t}\n\t\t\t\tif (j < line.length) {\n\t\t\t\t\tconst ansiCode = line.substring(i, j + 1);\n\t\t\t\t\tcurrentLine += ansiCode;\n\n\t\t\t\t\t// Track styling codes (ending with 'm')\n\t\t\t\t\tif (line[j] === \"m\") {\n\t\t\t\t\t\t// Reset code\n\t\t\t\t\t\tif (ansiCode === \"\\x1b[0m\" || ansiCode === \"\\x1b[m\") {\n\t\t\t\t\t\t\tactiveAnsiCodes.length = 0;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Add to active codes (replacing similar ones)\n\t\t\t\t\t\t\tactiveAnsiCodes.push(ansiCode);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\ti = j + 1;\n\t\t\t\t} else {\n\t\t\t\t\t// Incomplete ANSI sequence at end - don't include it\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Regular character - extract full grapheme cluster\n\t\t\t\t// Handle multi-byte characters (emoji, surrogate pairs, etc.)\n\t\t\t\tlet char: string;\n\t\t\t\tlet charByteLength: number;\n\n\t\t\t\t// Check for surrogate pair (emoji and other multi-byte chars)\n\t\t\t\tconst codePoint = line.charCodeAt(i);\n\t\t\t\tif (codePoint >= 0xd800 && codePoint <= 0xdbff && i + 1 < line.length) {\n\t\t\t\t\t// High surrogate - get the pair\n\t\t\t\t\tchar = line.substring(i, i + 2);\n\t\t\t\t\tcharByteLength = 2;\n\t\t\t\t} else {\n\t\t\t\t\t// Regular character\n\t\t\t\t\tchar = line[i];\n\t\t\t\t\tcharByteLength = 1;\n\t\t\t\t}\n\n\t\t\t\tconst charWidth = visibleWidth(char);\n\n\t\t\t\t// Check if adding this character would exceed width\n\t\t\t\tif (currentLength + charWidth > width) {\n\t\t\t\t\t// Need to wrap - close current line with reset if needed\n\t\t\t\t\tif (activeAnsiCodes.length > 0) {\n\t\t\t\t\t\twrapped.push(currentLine + \"\\x1b[0m\");\n\t\t\t\t\t\t// Start new line with active codes\n\t\t\t\t\t\tcurrentLine = activeAnsiCodes.join(\"\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\twrapped.push(currentLine);\n\t\t\t\t\t\tcurrentLine = \"\";\n\t\t\t\t\t}\n\t\t\t\t\tcurrentLength = 0;\n\t\t\t\t}\n\n\t\t\t\tcurrentLine += char;\n\t\t\t\tcurrentLength += charWidth;\n\t\t\t\ti += charByteLength;\n\t\t\t}\n\t\t}\n\n\t\tif (currentLine) {\n\t\t\twrapped.push(currentLine);\n\t\t}\n\n\t\treturn wrapped.length > 0 ? wrapped : [\"\"];\n\t}\n\n\t/**\n\t * Render a list with proper nesting support\n\t */\n\tprivate renderList(token: Token & { items: any[]; ordered: boolean }, depth: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst indent = \" \".repeat(depth);\n\n\t\tfor (let i = 0; i < token.items.length; i++) {\n\t\t\tconst item = token.items[i];\n\t\t\tconst bullet = token.ordered ? `${i + 1}. ` : \"- \";\n\n\t\t\t// Process item tokens to handle nested lists\n\t\t\tconst itemLines = this.renderListItem(item.tokens || [], depth);\n\n\t\t\tif (itemLines.length > 0) {\n\t\t\t\t// First line - check if it's a nested list (contains cyan ANSI code for bullets)\n\t\t\t\tconst firstLine = itemLines[0];\n\t\t\t\tconst isNestedList = firstLine.includes(\"\\x1b[36m\"); // cyan color code\n\n\t\t\t\tif (isNestedList) {\n\t\t\t\t\t// This is a nested list, just add it as-is (already has full indent)\n\t\t\t\t\tlines.push(firstLine);\n\t\t\t\t} else {\n\t\t\t\t\t// Regular text content - add indent and bullet\n\t\t\t\t\tlines.push(indent + chalk.cyan(bullet) + firstLine);\n\t\t\t\t}\n\n\t\t\t\t// Rest of the lines\n\t\t\t\tfor (let j = 1; j < itemLines.length; j++) {\n\t\t\t\t\tconst line = itemLines[j];\n\t\t\t\t\tconst isNestedListLine = line.includes(\"\\x1b[36m\"); // cyan bullet color\n\n\t\t\t\t\tif (isNestedListLine) {\n\t\t\t\t\t\t// Nested list line - already has full indent\n\t\t\t\t\t\tlines.push(line);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Regular content - add parent indent + 2 spaces for continuation\n\t\t\t\t\t\tlines.push(indent + \" \" + line);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlines.push(indent + chalk.cyan(bullet));\n\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\t/**\n\t * Render list item tokens, handling nested lists\n\t * Returns lines WITHOUT the parent indent (renderList will add it)\n\t */\n\tprivate renderListItem(tokens: Token[], parentDepth: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tfor (const token of tokens) {\n\t\t\tif (token.type === \"list\") {\n\t\t\t\t// Nested list - render with one additional indent level\n\t\t\t\t// These lines will have their own indent, so we just add them as-is\n\t\t\t\tconst nestedLines = this.renderList(token as any, parentDepth + 1);\n\t\t\t\tlines.push(...nestedLines);\n\t\t\t} else if (token.type === \"text\") {\n\t\t\t\t// Text content (may have inline tokens)\n\t\t\t\tconst text =\n\t\t\t\t\ttoken.tokens && token.tokens.length > 0 ? this.renderInlineTokens(token.tokens) : token.text || \"\";\n\t\t\t\tlines.push(text);\n\t\t\t} else if (token.type === \"paragraph\") {\n\t\t\t\t// Paragraph in list item\n\t\t\t\tconst text = this.renderInlineTokens(token.tokens || []);\n\t\t\t\tlines.push(text);\n\t\t\t} else if (token.type === \"code\") {\n\t\t\t\t// Code block in list item\n\t\t\t\tlines.push(chalk.gray(\"```\" + (token.lang || \"\")));\n\t\t\t\tconst codeLines = token.text.split(\"\\n\");\n\t\t\t\tfor (const codeLine of codeLines) {\n\t\t\t\t\tlines.push(chalk.dim(\" \") + chalk.green(codeLine));\n\t\t\t\t}\n\t\t\t\tlines.push(chalk.gray(\"```\"));\n\t\t\t} else {\n\t\t\t\t// Other token types - try to render as inline\n\t\t\t\tconst text = this.renderInlineTokens([token]);\n\t\t\t\tif (text) {\n\t\t\t\t\tlines.push(text);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\t/**\n\t * Render a table\n\t */\n\tprivate renderTable(token: Token & { header: any[]; rows: any[][] }): string[] {\n\t\tconst lines: string[] = [];\n\n\t\t// Calculate column widths\n\t\tconst columnWidths: number[] = [];\n\n\t\t// Check header\n\t\tfor (let i = 0; i < token.header.length; i++) {\n\t\t\tconst headerText = this.renderInlineTokens(token.header[i].tokens || []);\n\t\t\tconst width = visibleWidth(headerText);\n\t\t\tcolumnWidths[i] = Math.max(columnWidths[i] || 0, width);\n\t\t}\n\n\t\t// Check rows\n\t\tfor (const row of token.rows) {\n\t\t\tfor (let i = 0; i < row.length; i++) {\n\t\t\t\tconst cellText = this.renderInlineTokens(row[i].tokens || []);\n\t\t\t\tconst width = visibleWidth(cellText);\n\t\t\t\tcolumnWidths[i] = Math.max(columnWidths[i] || 0, width);\n\t\t\t}\n\t\t}\n\n\t\t// Limit column widths to reasonable max\n\t\tconst maxColWidth = 40;\n\t\tfor (let i = 0; i < columnWidths.length; i++) {\n\t\t\tcolumnWidths[i] = Math.min(columnWidths[i], maxColWidth);\n\t\t}\n\n\t\t// Render header\n\t\tconst headerCells = token.header.map((cell, i) => {\n\t\t\tconst text = this.renderInlineTokens(cell.tokens || []);\n\t\t\treturn chalk.bold(text.padEnd(columnWidths[i]));\n\t\t});\n\t\tlines.push(\"│ \" + headerCells.join(\" │ \") + \" │\");\n\n\t\t// Render separator\n\t\tconst separatorCells = columnWidths.map((width) => \"─\".repeat(width));\n\t\tlines.push(\"├─\" + separatorCells.join(\"─┼─\") + \"─┤\");\n\n\t\t// Render rows\n\t\tfor (const row of token.rows) {\n\t\t\tconst rowCells = row.map((cell, i) => {\n\t\t\t\tconst text = this.renderInlineTokens(cell.tokens || []);\n\t\t\t\tconst visWidth = visibleWidth(text);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, columnWidths[i] - visWidth));\n\t\t\t\treturn text + padding;\n\t\t\t});\n\t\t\tlines.push(\"│ \" + rowCells.join(\" │ \") + \" │\");\n\t\t}\n\n\t\tlines.push(\"\"); // Add spacing after table\n\t\treturn lines;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
import { visibleWidth } from "../utils.js";
|
|
4
|
+
export class Markdown {
|
|
5
|
+
text;
|
|
6
|
+
bgColor;
|
|
7
|
+
fgColor;
|
|
8
|
+
customBgRgb;
|
|
9
|
+
paddingX; // Left/right padding
|
|
10
|
+
paddingY; // Top/bottom padding
|
|
11
|
+
// Cache for rendered output
|
|
12
|
+
cachedText;
|
|
13
|
+
cachedWidth;
|
|
14
|
+
cachedLines;
|
|
15
|
+
constructor(text = "", bgColor, fgColor, customBgRgb, paddingX = 1, paddingY = 1) {
|
|
16
|
+
this.text = text;
|
|
17
|
+
this.bgColor = bgColor;
|
|
18
|
+
this.fgColor = fgColor;
|
|
19
|
+
this.customBgRgb = customBgRgb;
|
|
20
|
+
this.paddingX = paddingX;
|
|
21
|
+
this.paddingY = paddingY;
|
|
22
|
+
}
|
|
23
|
+
setText(text) {
|
|
24
|
+
this.text = text;
|
|
25
|
+
// Invalidate cache when text changes
|
|
26
|
+
this.cachedText = undefined;
|
|
27
|
+
this.cachedWidth = undefined;
|
|
28
|
+
this.cachedLines = undefined;
|
|
29
|
+
}
|
|
30
|
+
setBgColor(bgColor) {
|
|
31
|
+
this.bgColor = bgColor;
|
|
32
|
+
// Invalidate cache when color changes
|
|
33
|
+
this.cachedText = undefined;
|
|
34
|
+
this.cachedWidth = undefined;
|
|
35
|
+
this.cachedLines = undefined;
|
|
36
|
+
}
|
|
37
|
+
setFgColor(fgColor) {
|
|
38
|
+
this.fgColor = fgColor;
|
|
39
|
+
// Invalidate cache when color changes
|
|
40
|
+
this.cachedText = undefined;
|
|
41
|
+
this.cachedWidth = undefined;
|
|
42
|
+
this.cachedLines = undefined;
|
|
43
|
+
}
|
|
44
|
+
setCustomBgRgb(customBgRgb) {
|
|
45
|
+
this.customBgRgb = customBgRgb;
|
|
46
|
+
// Invalidate cache when color changes
|
|
47
|
+
this.cachedText = undefined;
|
|
48
|
+
this.cachedWidth = undefined;
|
|
49
|
+
this.cachedLines = undefined;
|
|
50
|
+
}
|
|
51
|
+
render(width) {
|
|
52
|
+
// Check cache
|
|
53
|
+
if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
|
|
54
|
+
return this.cachedLines;
|
|
55
|
+
}
|
|
56
|
+
// Calculate available width for content (subtract horizontal padding)
|
|
57
|
+
const contentWidth = Math.max(1, width - this.paddingX * 2);
|
|
58
|
+
// Don't render anything if there's no actual text
|
|
59
|
+
if (!this.text || this.text.trim() === "") {
|
|
60
|
+
const result = [];
|
|
61
|
+
// Update cache
|
|
62
|
+
this.cachedText = this.text;
|
|
63
|
+
this.cachedWidth = width;
|
|
64
|
+
this.cachedLines = result;
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
// Replace tabs with 3 spaces for consistent rendering
|
|
68
|
+
const normalizedText = this.text.replace(/\t/g, " ");
|
|
69
|
+
// Parse markdown to HTML-like tokens
|
|
70
|
+
const tokens = marked.lexer(normalizedText);
|
|
71
|
+
// Convert tokens to styled terminal output
|
|
72
|
+
const renderedLines = [];
|
|
73
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
74
|
+
const token = tokens[i];
|
|
75
|
+
const nextToken = tokens[i + 1];
|
|
76
|
+
const tokenLines = this.renderToken(token, contentWidth, nextToken?.type);
|
|
77
|
+
renderedLines.push(...tokenLines);
|
|
78
|
+
}
|
|
79
|
+
// Wrap lines to fit content width
|
|
80
|
+
const wrappedLines = [];
|
|
81
|
+
for (const line of renderedLines) {
|
|
82
|
+
wrappedLines.push(...this.wrapLine(line, contentWidth));
|
|
83
|
+
}
|
|
84
|
+
// Add padding and apply colors
|
|
85
|
+
const leftPad = " ".repeat(this.paddingX);
|
|
86
|
+
const paddedLines = [];
|
|
87
|
+
for (const line of wrappedLines) {
|
|
88
|
+
// Calculate visible length
|
|
89
|
+
const visibleLength = visibleWidth(line);
|
|
90
|
+
// Right padding to fill to width (accounting for left padding and content)
|
|
91
|
+
const rightPadLength = Math.max(0, width - this.paddingX - visibleLength);
|
|
92
|
+
const rightPad = " ".repeat(rightPadLength);
|
|
93
|
+
// Add left padding, content, and right padding
|
|
94
|
+
let paddedLine = leftPad + line + rightPad;
|
|
95
|
+
// Apply foreground color if specified
|
|
96
|
+
if (this.fgColor) {
|
|
97
|
+
paddedLine = chalk[this.fgColor](paddedLine);
|
|
98
|
+
}
|
|
99
|
+
// Apply background color if specified
|
|
100
|
+
if (this.customBgRgb) {
|
|
101
|
+
paddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(paddedLine);
|
|
102
|
+
}
|
|
103
|
+
else if (this.bgColor) {
|
|
104
|
+
paddedLine = chalk[this.bgColor](paddedLine);
|
|
105
|
+
}
|
|
106
|
+
paddedLines.push(paddedLine);
|
|
107
|
+
}
|
|
108
|
+
// Add top padding (empty lines)
|
|
109
|
+
const emptyLine = " ".repeat(width);
|
|
110
|
+
const topPadding = [];
|
|
111
|
+
for (let i = 0; i < this.paddingY; i++) {
|
|
112
|
+
let emptyPaddedLine = emptyLine;
|
|
113
|
+
if (this.customBgRgb) {
|
|
114
|
+
emptyPaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(emptyPaddedLine);
|
|
115
|
+
}
|
|
116
|
+
else if (this.bgColor) {
|
|
117
|
+
emptyPaddedLine = chalk[this.bgColor](emptyPaddedLine);
|
|
118
|
+
}
|
|
119
|
+
topPadding.push(emptyPaddedLine);
|
|
120
|
+
}
|
|
121
|
+
// Add bottom padding (empty lines)
|
|
122
|
+
const bottomPadding = [];
|
|
123
|
+
for (let i = 0; i < this.paddingY; i++) {
|
|
124
|
+
let emptyPaddedLine = emptyLine;
|
|
125
|
+
if (this.customBgRgb) {
|
|
126
|
+
emptyPaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(emptyPaddedLine);
|
|
127
|
+
}
|
|
128
|
+
else if (this.bgColor) {
|
|
129
|
+
emptyPaddedLine = chalk[this.bgColor](emptyPaddedLine);
|
|
130
|
+
}
|
|
131
|
+
bottomPadding.push(emptyPaddedLine);
|
|
132
|
+
}
|
|
133
|
+
// Combine top padding, content, and bottom padding
|
|
134
|
+
const result = [...topPadding, ...paddedLines, ...bottomPadding];
|
|
135
|
+
// Update cache
|
|
136
|
+
this.cachedText = this.text;
|
|
137
|
+
this.cachedWidth = width;
|
|
138
|
+
this.cachedLines = result;
|
|
139
|
+
return result.length > 0 ? result : [""];
|
|
140
|
+
}
|
|
141
|
+
renderToken(token, width, nextTokenType) {
|
|
142
|
+
const lines = [];
|
|
143
|
+
switch (token.type) {
|
|
144
|
+
case "heading": {
|
|
145
|
+
const headingLevel = token.depth;
|
|
146
|
+
const headingPrefix = "#".repeat(headingLevel) + " ";
|
|
147
|
+
const headingText = this.renderInlineTokens(token.tokens || []);
|
|
148
|
+
if (headingLevel === 1) {
|
|
149
|
+
lines.push(chalk.bold.underline.yellow(headingText));
|
|
150
|
+
}
|
|
151
|
+
else if (headingLevel === 2) {
|
|
152
|
+
lines.push(chalk.bold.yellow(headingText));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
lines.push(chalk.bold(headingPrefix + headingText));
|
|
156
|
+
}
|
|
157
|
+
lines.push(""); // Add spacing after headings
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case "paragraph": {
|
|
161
|
+
const paragraphText = this.renderInlineTokens(token.tokens || []);
|
|
162
|
+
lines.push(paragraphText);
|
|
163
|
+
// Don't add spacing if next token is space or list
|
|
164
|
+
if (nextTokenType && nextTokenType !== "list" && nextTokenType !== "space") {
|
|
165
|
+
lines.push("");
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case "code": {
|
|
170
|
+
lines.push(chalk.gray("```" + (token.lang || "")));
|
|
171
|
+
// Split code by newlines and style each line
|
|
172
|
+
const codeLines = token.text.split("\n");
|
|
173
|
+
for (const codeLine of codeLines) {
|
|
174
|
+
lines.push(chalk.dim(" ") + chalk.green(codeLine));
|
|
175
|
+
}
|
|
176
|
+
lines.push(chalk.gray("```"));
|
|
177
|
+
lines.push(""); // Add spacing after code blocks
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case "list": {
|
|
181
|
+
const listLines = this.renderList(token, 0);
|
|
182
|
+
lines.push(...listLines);
|
|
183
|
+
// Don't add spacing after lists if a space token follows
|
|
184
|
+
// (the space token will handle it)
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
case "table": {
|
|
188
|
+
const tableLines = this.renderTable(token);
|
|
189
|
+
lines.push(...tableLines);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case "blockquote": {
|
|
193
|
+
const quoteText = this.renderInlineTokens(token.tokens || []);
|
|
194
|
+
const quoteLines = quoteText.split("\n");
|
|
195
|
+
for (const quoteLine of quoteLines) {
|
|
196
|
+
lines.push(chalk.gray("│ ") + chalk.italic(quoteLine));
|
|
197
|
+
}
|
|
198
|
+
lines.push(""); // Add spacing after blockquotes
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case "hr":
|
|
202
|
+
lines.push(chalk.gray("─".repeat(Math.min(width, 80))));
|
|
203
|
+
lines.push(""); // Add spacing after horizontal rules
|
|
204
|
+
break;
|
|
205
|
+
case "html":
|
|
206
|
+
// Skip HTML for terminal output
|
|
207
|
+
break;
|
|
208
|
+
case "space":
|
|
209
|
+
// Space tokens represent blank lines in markdown
|
|
210
|
+
lines.push("");
|
|
211
|
+
break;
|
|
212
|
+
default:
|
|
213
|
+
// Handle any other token types as plain text
|
|
214
|
+
if ("text" in token && typeof token.text === "string") {
|
|
215
|
+
lines.push(token.text);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return lines;
|
|
219
|
+
}
|
|
220
|
+
renderInlineTokens(tokens) {
|
|
221
|
+
let result = "";
|
|
222
|
+
for (const token of tokens) {
|
|
223
|
+
switch (token.type) {
|
|
224
|
+
case "text":
|
|
225
|
+
// Text tokens in list items can have nested tokens for inline formatting
|
|
226
|
+
if (token.tokens && token.tokens.length > 0) {
|
|
227
|
+
result += this.renderInlineTokens(token.tokens);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
result += token.text;
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
case "strong":
|
|
234
|
+
result += chalk.bold(this.renderInlineTokens(token.tokens || []));
|
|
235
|
+
break;
|
|
236
|
+
case "em":
|
|
237
|
+
result += chalk.italic(this.renderInlineTokens(token.tokens || []));
|
|
238
|
+
break;
|
|
239
|
+
case "codespan":
|
|
240
|
+
result += chalk.gray("`") + chalk.cyan(token.text) + chalk.gray("`");
|
|
241
|
+
break;
|
|
242
|
+
case "link": {
|
|
243
|
+
const linkText = this.renderInlineTokens(token.tokens || []);
|
|
244
|
+
// If link text matches href, only show the link once
|
|
245
|
+
if (linkText === token.href) {
|
|
246
|
+
result += chalk.underline.blue(linkText);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
result += chalk.underline.blue(linkText) + chalk.gray(` (${token.href})`);
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "br":
|
|
254
|
+
result += "\n";
|
|
255
|
+
break;
|
|
256
|
+
case "del":
|
|
257
|
+
result += chalk.strikethrough(this.renderInlineTokens(token.tokens || []));
|
|
258
|
+
break;
|
|
259
|
+
default:
|
|
260
|
+
// Handle any other inline token types as plain text
|
|
261
|
+
if ("text" in token && typeof token.text === "string") {
|
|
262
|
+
result += token.text;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
wrapLine(line, width) {
|
|
269
|
+
// Handle ANSI escape codes properly when wrapping
|
|
270
|
+
const wrapped = [];
|
|
271
|
+
// Handle undefined or null lines
|
|
272
|
+
if (!line) {
|
|
273
|
+
return [""];
|
|
274
|
+
}
|
|
275
|
+
// Split by newlines first - wrap each line individually
|
|
276
|
+
const splitLines = line.split("\n");
|
|
277
|
+
for (const splitLine of splitLines) {
|
|
278
|
+
const visibleLength = visibleWidth(splitLine);
|
|
279
|
+
if (visibleLength <= width) {
|
|
280
|
+
wrapped.push(splitLine);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
// This line needs wrapping
|
|
284
|
+
wrapped.push(...this.wrapSingleLine(splitLine, width));
|
|
285
|
+
}
|
|
286
|
+
return wrapped.length > 0 ? wrapped : [""];
|
|
287
|
+
}
|
|
288
|
+
wrapSingleLine(line, width) {
|
|
289
|
+
const wrapped = [];
|
|
290
|
+
// Track active ANSI codes to preserve them across wrapped lines
|
|
291
|
+
const activeAnsiCodes = [];
|
|
292
|
+
let currentLine = "";
|
|
293
|
+
let currentLength = 0;
|
|
294
|
+
let i = 0;
|
|
295
|
+
while (i < line.length) {
|
|
296
|
+
if (line[i] === "\x1b" && line[i + 1] === "[") {
|
|
297
|
+
// ANSI escape sequence - parse and track it
|
|
298
|
+
let j = i + 2;
|
|
299
|
+
while (j < line.length && line[j] && !/[mGKHJ]/.test(line[j])) {
|
|
300
|
+
j++;
|
|
301
|
+
}
|
|
302
|
+
if (j < line.length) {
|
|
303
|
+
const ansiCode = line.substring(i, j + 1);
|
|
304
|
+
currentLine += ansiCode;
|
|
305
|
+
// Track styling codes (ending with 'm')
|
|
306
|
+
if (line[j] === "m") {
|
|
307
|
+
// Reset code
|
|
308
|
+
if (ansiCode === "\x1b[0m" || ansiCode === "\x1b[m") {
|
|
309
|
+
activeAnsiCodes.length = 0;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
// Add to active codes (replacing similar ones)
|
|
313
|
+
activeAnsiCodes.push(ansiCode);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
i = j + 1;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
// Incomplete ANSI sequence at end - don't include it
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
// Regular character - extract full grapheme cluster
|
|
325
|
+
// Handle multi-byte characters (emoji, surrogate pairs, etc.)
|
|
326
|
+
let char;
|
|
327
|
+
let charByteLength;
|
|
328
|
+
// Check for surrogate pair (emoji and other multi-byte chars)
|
|
329
|
+
const codePoint = line.charCodeAt(i);
|
|
330
|
+
if (codePoint >= 0xd800 && codePoint <= 0xdbff && i + 1 < line.length) {
|
|
331
|
+
// High surrogate - get the pair
|
|
332
|
+
char = line.substring(i, i + 2);
|
|
333
|
+
charByteLength = 2;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// Regular character
|
|
337
|
+
char = line[i];
|
|
338
|
+
charByteLength = 1;
|
|
339
|
+
}
|
|
340
|
+
const charWidth = visibleWidth(char);
|
|
341
|
+
// Check if adding this character would exceed width
|
|
342
|
+
if (currentLength + charWidth > width) {
|
|
343
|
+
// Need to wrap - close current line with reset if needed
|
|
344
|
+
if (activeAnsiCodes.length > 0) {
|
|
345
|
+
wrapped.push(currentLine + "\x1b[0m");
|
|
346
|
+
// Start new line with active codes
|
|
347
|
+
currentLine = activeAnsiCodes.join("");
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
wrapped.push(currentLine);
|
|
351
|
+
currentLine = "";
|
|
352
|
+
}
|
|
353
|
+
currentLength = 0;
|
|
354
|
+
}
|
|
355
|
+
currentLine += char;
|
|
356
|
+
currentLength += charWidth;
|
|
357
|
+
i += charByteLength;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (currentLine) {
|
|
361
|
+
wrapped.push(currentLine);
|
|
362
|
+
}
|
|
363
|
+
return wrapped.length > 0 ? wrapped : [""];
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Render a list with proper nesting support
|
|
367
|
+
*/
|
|
368
|
+
renderList(token, depth) {
|
|
369
|
+
const lines = [];
|
|
370
|
+
const indent = " ".repeat(depth);
|
|
371
|
+
for (let i = 0; i < token.items.length; i++) {
|
|
372
|
+
const item = token.items[i];
|
|
373
|
+
const bullet = token.ordered ? `${i + 1}. ` : "- ";
|
|
374
|
+
// Process item tokens to handle nested lists
|
|
375
|
+
const itemLines = this.renderListItem(item.tokens || [], depth);
|
|
376
|
+
if (itemLines.length > 0) {
|
|
377
|
+
// First line - check if it's a nested list (contains cyan ANSI code for bullets)
|
|
378
|
+
const firstLine = itemLines[0];
|
|
379
|
+
const isNestedList = firstLine.includes("\x1b[36m"); // cyan color code
|
|
380
|
+
if (isNestedList) {
|
|
381
|
+
// This is a nested list, just add it as-is (already has full indent)
|
|
382
|
+
lines.push(firstLine);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// Regular text content - add indent and bullet
|
|
386
|
+
lines.push(indent + chalk.cyan(bullet) + firstLine);
|
|
387
|
+
}
|
|
388
|
+
// Rest of the lines
|
|
389
|
+
for (let j = 1; j < itemLines.length; j++) {
|
|
390
|
+
const line = itemLines[j];
|
|
391
|
+
const isNestedListLine = line.includes("\x1b[36m"); // cyan bullet color
|
|
392
|
+
if (isNestedListLine) {
|
|
393
|
+
// Nested list line - already has full indent
|
|
394
|
+
lines.push(line);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
// Regular content - add parent indent + 2 spaces for continuation
|
|
398
|
+
lines.push(indent + " " + line);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
lines.push(indent + chalk.cyan(bullet));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return lines;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Render list item tokens, handling nested lists
|
|
410
|
+
* Returns lines WITHOUT the parent indent (renderList will add it)
|
|
411
|
+
*/
|
|
412
|
+
renderListItem(tokens, parentDepth) {
|
|
413
|
+
const lines = [];
|
|
414
|
+
for (const token of tokens) {
|
|
415
|
+
if (token.type === "list") {
|
|
416
|
+
// Nested list - render with one additional indent level
|
|
417
|
+
// These lines will have their own indent, so we just add them as-is
|
|
418
|
+
const nestedLines = this.renderList(token, parentDepth + 1);
|
|
419
|
+
lines.push(...nestedLines);
|
|
420
|
+
}
|
|
421
|
+
else if (token.type === "text") {
|
|
422
|
+
// Text content (may have inline tokens)
|
|
423
|
+
const text = token.tokens && token.tokens.length > 0 ? this.renderInlineTokens(token.tokens) : token.text || "";
|
|
424
|
+
lines.push(text);
|
|
425
|
+
}
|
|
426
|
+
else if (token.type === "paragraph") {
|
|
427
|
+
// Paragraph in list item
|
|
428
|
+
const text = this.renderInlineTokens(token.tokens || []);
|
|
429
|
+
lines.push(text);
|
|
430
|
+
}
|
|
431
|
+
else if (token.type === "code") {
|
|
432
|
+
// Code block in list item
|
|
433
|
+
lines.push(chalk.gray("```" + (token.lang || "")));
|
|
434
|
+
const codeLines = token.text.split("\n");
|
|
435
|
+
for (const codeLine of codeLines) {
|
|
436
|
+
lines.push(chalk.dim(" ") + chalk.green(codeLine));
|
|
437
|
+
}
|
|
438
|
+
lines.push(chalk.gray("```"));
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
// Other token types - try to render as inline
|
|
442
|
+
const text = this.renderInlineTokens([token]);
|
|
443
|
+
if (text) {
|
|
444
|
+
lines.push(text);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return lines;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Render a table
|
|
452
|
+
*/
|
|
453
|
+
renderTable(token) {
|
|
454
|
+
const lines = [];
|
|
455
|
+
// Calculate column widths
|
|
456
|
+
const columnWidths = [];
|
|
457
|
+
// Check header
|
|
458
|
+
for (let i = 0; i < token.header.length; i++) {
|
|
459
|
+
const headerText = this.renderInlineTokens(token.header[i].tokens || []);
|
|
460
|
+
const width = visibleWidth(headerText);
|
|
461
|
+
columnWidths[i] = Math.max(columnWidths[i] || 0, width);
|
|
462
|
+
}
|
|
463
|
+
// Check rows
|
|
464
|
+
for (const row of token.rows) {
|
|
465
|
+
for (let i = 0; i < row.length; i++) {
|
|
466
|
+
const cellText = this.renderInlineTokens(row[i].tokens || []);
|
|
467
|
+
const width = visibleWidth(cellText);
|
|
468
|
+
columnWidths[i] = Math.max(columnWidths[i] || 0, width);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// Limit column widths to reasonable max
|
|
472
|
+
const maxColWidth = 40;
|
|
473
|
+
for (let i = 0; i < columnWidths.length; i++) {
|
|
474
|
+
columnWidths[i] = Math.min(columnWidths[i], maxColWidth);
|
|
475
|
+
}
|
|
476
|
+
// Render header
|
|
477
|
+
const headerCells = token.header.map((cell, i) => {
|
|
478
|
+
const text = this.renderInlineTokens(cell.tokens || []);
|
|
479
|
+
return chalk.bold(text.padEnd(columnWidths[i]));
|
|
480
|
+
});
|
|
481
|
+
lines.push("│ " + headerCells.join(" │ ") + " │");
|
|
482
|
+
// Render separator
|
|
483
|
+
const separatorCells = columnWidths.map((width) => "─".repeat(width));
|
|
484
|
+
lines.push("├─" + separatorCells.join("─┼─") + "─┤");
|
|
485
|
+
// Render rows
|
|
486
|
+
for (const row of token.rows) {
|
|
487
|
+
const rowCells = row.map((cell, i) => {
|
|
488
|
+
const text = this.renderInlineTokens(cell.tokens || []);
|
|
489
|
+
const visWidth = visibleWidth(text);
|
|
490
|
+
const padding = " ".repeat(Math.max(0, columnWidths[i] - visWidth));
|
|
491
|
+
return text + padding;
|
|
492
|
+
});
|
|
493
|
+
lines.push("│ " + rowCells.join(" │ ") + " │");
|
|
494
|
+
}
|
|
495
|
+
lines.push(""); // Add spacing after table
|
|
496
|
+
return lines;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
//# sourceMappingURL=markdown.js.map
|