@mariozechner/pi-tui 0.5.48 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +166 -475
  2. package/dist/autocomplete.d.ts.map +1 -1
  3. package/dist/autocomplete.js +2 -0
  4. package/dist/autocomplete.js.map +1 -1
  5. package/dist/components/{text-editor.d.ts → editor.d.ts} +9 -5
  6. package/dist/components/editor.d.ts.map +1 -0
  7. package/dist/components/{text-editor.js → editor.js} +125 -70
  8. package/dist/components/editor.js.map +1 -0
  9. package/dist/components/input.d.ts +14 -0
  10. package/dist/components/input.d.ts.map +1 -0
  11. package/dist/components/input.js +120 -0
  12. package/dist/components/input.js.map +1 -0
  13. package/dist/components/{loading-animation.d.ts → loader.d.ts} +5 -5
  14. package/dist/components/loader.d.ts.map +1 -0
  15. package/dist/components/{loading-animation.js → loader.js} +13 -10
  16. package/dist/components/loader.js.map +1 -0
  17. package/dist/components/markdown.d.ts +46 -0
  18. package/dist/components/markdown.d.ts.map +1 -0
  19. package/dist/components/markdown.js +499 -0
  20. package/dist/components/markdown.js.map +1 -0
  21. package/dist/components/select-list.d.ts +3 -3
  22. package/dist/components/select-list.d.ts.map +1 -1
  23. package/dist/components/select-list.js +24 -16
  24. package/dist/components/select-list.js.map +1 -1
  25. package/dist/components/spacer.d.ts +11 -0
  26. package/dist/components/spacer.d.ts.map +1 -0
  27. package/dist/components/spacer.js +20 -0
  28. package/dist/components/spacer.js.map +1 -0
  29. package/dist/components/text.d.ts +26 -0
  30. package/dist/components/text.d.ts.map +1 -0
  31. package/dist/components/text.js +141 -0
  32. package/dist/components/text.js.map +1 -0
  33. package/dist/index.d.ts +8 -6
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +10 -12
  36. package/dist/index.js.map +1 -1
  37. package/dist/terminal.d.ts +12 -0
  38. package/dist/terminal.d.ts.map +1 -1
  39. package/dist/terminal.js +33 -3
  40. package/dist/terminal.js.map +1 -1
  41. package/dist/tui.d.ts +30 -52
  42. package/dist/tui.d.ts.map +1 -1
  43. package/dist/tui.js +131 -337
  44. package/dist/tui.js.map +1 -1
  45. package/dist/utils.d.ts +10 -0
  46. package/dist/utils.d.ts.map +1 -0
  47. package/dist/utils.js +15 -0
  48. package/dist/utils.js.map +1 -0
  49. package/package.json +6 -5
  50. package/dist/components/loading-animation.d.ts.map +0 -1
  51. package/dist/components/loading-animation.js.map +0 -1
  52. package/dist/components/markdown-component.d.ts +0 -15
  53. package/dist/components/markdown-component.d.ts.map +0 -1
  54. package/dist/components/markdown-component.js +0 -247
  55. package/dist/components/markdown-component.js.map +0 -1
  56. package/dist/components/text-component.d.ts +0 -14
  57. package/dist/components/text-component.d.ts.map +0 -1
  58. package/dist/components/text-component.js +0 -90
  59. package/dist/components/text-component.js.map +0 -1
  60. package/dist/components/text-editor.d.ts.map +0 -1
  61. package/dist/components/text-editor.js.map +0 -1
  62. package/dist/components/whitespace-component.d.ts +0 -13
  63. package/dist/components/whitespace-component.d.ts.map +0 -1
  64. package/dist/components/whitespace-component.js +0 -22
  65. package/dist/components/whitespace-component.js.map +0 -1
@@ -1,20 +1,23 @@
1
1
  import chalk from "chalk";
2
- import { TextComponent } from "./text-component.js";
2
+ import { Text } from "./text.js";
3
3
  /**
4
- * LoadingAnimation component that updates every 80ms
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 LoadingAnimation extends TextComponent {
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("", { bottom: 1 });
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=loading-animation.js.map
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