@earendil-works/pi-tui 0.74.0 → 0.75.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/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +27 -14
- package/dist/components/image.js.map +1 -1
- package/dist/components/markdown.d.ts +0 -5
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +33 -87
- package/dist/components/markdown.js.map +1 -1
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +22 -0
- package/dist/stdin-buffer.js.map +1 -1
- package/dist/terminal-image.d.ts +5 -0
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +24 -11
- package/dist/terminal-image.js.map +1 -1
- package/package.json +5 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/components/image.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/components/image.ts"],"names":[],"mappings":"AAAA,OAAO,EAKN,KAAK,eAAe,EAGpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,MAAM,WAAW,UAAU;IAC1B,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAED,MAAM,WAAW,YAAY;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,KAAM,YAAW,SAAS;IACtC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,OAAO,CAAC,CAAS;IAEzB,OAAO,CAAC,WAAW,CAAC,CAAW;IAC/B,OAAO,CAAC,WAAW,CAAC,CAAS;IAE7B,YACC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,UAAU,EACjB,OAAO,GAAE,YAAiB,EAC1B,UAAU,CAAC,EAAE,eAAe,EAQ5B;IAED,0DAA0D;IAC1D,UAAU,IAAI,MAAM,GAAG,SAAS,CAE/B;IAED,UAAU,IAAI,IAAI,CAGjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiE9B;CACD","sourcesContent":["import {\n\tallocateImageId,\n\tgetCapabilities,\n\tgetCellDimensions,\n\tgetImageDimensions,\n\ttype ImageDimensions,\n\timageFallback,\n\trenderImage,\n} from \"../terminal-image.js\";\nimport type { Component } from \"../tui.js\";\n\nexport interface ImageTheme {\n\tfallbackColor: (str: string) => string;\n}\n\nexport interface ImageOptions {\n\tmaxWidthCells?: number;\n\tmaxHeightCells?: number;\n\tfilename?: string;\n\t/** Kitty image ID. If provided, reuses this ID (for animations/updates). */\n\timageId?: number;\n}\n\nexport class Image implements Component {\n\tprivate base64Data: string;\n\tprivate mimeType: string;\n\tprivate dimensions: ImageDimensions;\n\tprivate theme: ImageTheme;\n\tprivate options: ImageOptions;\n\tprivate imageId?: number;\n\n\tprivate cachedLines?: string[];\n\tprivate cachedWidth?: number;\n\n\tconstructor(\n\t\tbase64Data: string,\n\t\tmimeType: string,\n\t\ttheme: ImageTheme,\n\t\toptions: ImageOptions = {},\n\t\tdimensions?: ImageDimensions,\n\t) {\n\t\tthis.base64Data = base64Data;\n\t\tthis.mimeType = mimeType;\n\t\tthis.theme = theme;\n\t\tthis.options = options;\n\t\tthis.dimensions = dimensions || getImageDimensions(base64Data, mimeType) || { widthPx: 800, heightPx: 600 };\n\t\tthis.imageId = options.imageId;\n\t}\n\n\t/** Get the Kitty image ID used by this image (if any). */\n\tgetImageId(): number | undefined {\n\t\treturn this.imageId;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedLines = undefined;\n\t\tthis.cachedWidth = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedLines && this.cachedWidth === width) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\tconst maxWidth = Math.max(1, Math.min(width - 2, this.options.maxWidthCells ?? 60));\n\t\tconst cellDimensions = getCellDimensions();\n\t\tconst defaultMaxHeight = Math.max(1, Math.ceil((maxWidth * cellDimensions.widthPx) / cellDimensions.heightPx));\n\t\tconst maxHeight = this.options.maxHeightCells ?? defaultMaxHeight;\n\n\t\tconst caps = getCapabilities();\n\t\tlet lines: string[];\n\n\t\tif (caps.images) {\n\t\t\tif (caps.images === \"kitty\" && this.imageId === undefined) {\n\t\t\t\tthis.imageId = allocateImageId();\n\t\t\t}\n\t\t\tconst result = renderImage(this.base64Data, this.dimensions, {\n\t\t\t\tmaxWidthCells: maxWidth,\n\t\t\t\tmaxHeightCells: maxHeight,\n\t\t\t\timageId: this.imageId,\n\t\t\t\tmoveCursor: false,\n\t\t\t});\n\n\t\t\tif (result) {\n\t\t\t\t// Store the image ID for later cleanup\n\t\t\t\tif (result.imageId) {\n\t\t\t\t\tthis.imageId = result.imageId;\n\t\t\t\t}\n\n\t\t\t\tif (caps.images === \"kitty\") {\n\t\t\t\t\t// For Kitty: C=1 prevents cursor movement.\n\t\t\t\t\t// Don't need the cursor movement.\n\t\t\t\t\tlines = [result.sequence];\n\n\t\t\t\t\t// Return `rows` lines so TUI accounts for image height.\n\t\t\t\t\tfor (let i = 0; i < result.rows - 1; i++) {\n\t\t\t\t\t\tlines.push(\"\");\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Return `rows` lines so TUI accounts for image height.\n\t\t\t\t\t// First (rows-1) lines are empty and cleared before the image is drawn.\n\t\t\t\t\t// Last line: move cursor back up, draw the image, then move back down\n\t\t\t\t\t// so TUI cursor accounting stays inside the scroll area.\n\t\t\t\t\tlines = [];\n\t\t\t\t\tfor (let i = 0; i < result.rows - 1; i++) {\n\t\t\t\t\t\tlines.push(\"\");\n\t\t\t\t\t}\n\t\t\t\t\tconst rowOffset = result.rows - 1;\n\t\t\t\t\tconst moveUp = rowOffset > 0 ? `\\x1b[${rowOffset}A` : \"\";\n\t\t\t\t\tlines.push(moveUp + result.sequence);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);\n\t\t\t\tlines = [this.theme.fallbackColor(fallback)];\n\t\t\t}\n\t\t} else {\n\t\t\tconst fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);\n\t\t\tlines = [this.theme.fallbackColor(fallback)];\n\t\t}\n\n\t\tthis.cachedLines = lines;\n\t\tthis.cachedWidth = width;\n\n\t\treturn lines;\n\t}\n}\n"]}
|
package/dist/components/image.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { allocateImageId, getCapabilities, getImageDimensions, imageFallback, renderImage, } from "../terminal-image.js";
|
|
1
|
+
import { allocateImageId, getCapabilities, getCellDimensions, getImageDimensions, imageFallback, renderImage, } from "../terminal-image.js";
|
|
2
2
|
export class Image {
|
|
3
3
|
base64Data;
|
|
4
4
|
mimeType;
|
|
@@ -28,7 +28,10 @@ export class Image {
|
|
|
28
28
|
if (this.cachedLines && this.cachedWidth === width) {
|
|
29
29
|
return this.cachedLines;
|
|
30
30
|
}
|
|
31
|
-
const maxWidth = Math.min(width - 2, this.options.maxWidthCells ?? 60);
|
|
31
|
+
const maxWidth = Math.max(1, Math.min(width - 2, this.options.maxWidthCells ?? 60));
|
|
32
|
+
const cellDimensions = getCellDimensions();
|
|
33
|
+
const defaultMaxHeight = Math.max(1, Math.ceil((maxWidth * cellDimensions.widthPx) / cellDimensions.heightPx));
|
|
34
|
+
const maxHeight = this.options.maxHeightCells ?? defaultMaxHeight;
|
|
32
35
|
const caps = getCapabilities();
|
|
33
36
|
let lines;
|
|
34
37
|
if (caps.images) {
|
|
@@ -37,6 +40,7 @@ export class Image {
|
|
|
37
40
|
}
|
|
38
41
|
const result = renderImage(this.base64Data, this.dimensions, {
|
|
39
42
|
maxWidthCells: maxWidth,
|
|
43
|
+
maxHeightCells: maxHeight,
|
|
40
44
|
imageId: this.imageId,
|
|
41
45
|
moveCursor: false,
|
|
42
46
|
});
|
|
@@ -45,19 +49,28 @@ export class Image {
|
|
|
45
49
|
if (result.imageId) {
|
|
46
50
|
this.imageId = result.imageId;
|
|
47
51
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
if (caps.images === "kitty") {
|
|
53
|
+
// For Kitty: C=1 prevents cursor movement.
|
|
54
|
+
// Don't need the cursor movement.
|
|
55
|
+
lines = [result.sequence];
|
|
56
|
+
// Return `rows` lines so TUI accounts for image height.
|
|
57
|
+
for (let i = 0; i < result.rows - 1; i++) {
|
|
58
|
+
lines.push("");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Return `rows` lines so TUI accounts for image height.
|
|
63
|
+
// First (rows-1) lines are empty and cleared before the image is drawn.
|
|
64
|
+
// Last line: move cursor back up, draw the image, then move back down
|
|
65
|
+
// so TUI cursor accounting stays inside the scroll area.
|
|
66
|
+
lines = [];
|
|
67
|
+
for (let i = 0; i < result.rows - 1; i++) {
|
|
68
|
+
lines.push("");
|
|
69
|
+
}
|
|
70
|
+
const rowOffset = result.rows - 1;
|
|
71
|
+
const moveUp = rowOffset > 0 ? `\x1b[${rowOffset}A` : "";
|
|
72
|
+
lines.push(moveUp + result.sequence);
|
|
56
73
|
}
|
|
57
|
-
const rowOffset = result.rows - 1;
|
|
58
|
-
const moveUp = rowOffset > 0 ? `\x1b[${rowOffset}A` : "";
|
|
59
|
-
const moveDown = caps.images === "kitty" && rowOffset > 0 ? `\x1b[${rowOffset}B` : "";
|
|
60
|
-
lines.push(moveUp + result.sequence + moveDown);
|
|
61
74
|
}
|
|
62
75
|
else {
|
|
63
76
|
const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/components/image.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,eAAe,EACf,eAAe,EACf,kBAAkB,EAElB,aAAa,EACb,WAAW,GACX,MAAM,sBAAsB,CAAC;AAe9B,MAAM,OAAO,KAAK;IACT,UAAU,CAAS;IACnB,QAAQ,CAAS;IACjB,UAAU,CAAkB;IAC5B,KAAK,CAAa;IAClB,OAAO,CAAe;IACtB,OAAO,CAAU;IAEjB,WAAW,CAAY;IACvB,WAAW,CAAU;IAE7B,YACC,UAAkB,EAClB,QAAgB,EAChB,KAAiB,EACjB,OAAO,GAAiB,EAAE,EAC1B,UAA4B,EAC3B;QACD,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,UAAU,IAAI,kBAAkB,CAAC,UAAU,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;QAC5G,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAAA,CAC/B;IAED,0DAA0D;IAC1D,UAAU,GAAuB;QAChC,OAAO,IAAI,CAAC,OAAO,CAAC;IAAA,CACpB;IAED,UAAU,GAAS;QAClB,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;IAAA,CAC7B;IAED,MAAM,CAAC,KAAa,EAAY;QAC/B,IAAI,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,WAAW,KAAK,KAAK,EAAE,CAAC;YACpD,OAAO,IAAI,CAAC,WAAW,CAAC;QACzB,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"image.js","sourceRoot":"","sources":["../../src/components/image.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tallocateImageId,\n\tgetCapabilities,\n\tgetCellDimensions,\n\tgetImageDimensions,\n\ttype ImageDimensions,\n\timageFallback,\n\trenderImage,\n} from \"../terminal-image.js\";\nimport type { Component } from \"../tui.js\";\n\nexport interface ImageTheme {\n\tfallbackColor: (str: string) => string;\n}\n\nexport interface ImageOptions {\n\tmaxWidthCells?: number;\n\tmaxHeightCells?: number;\n\tfilename?: string;\n\t/** Kitty image ID. If provided, reuses this ID (for animations/updates). */\n\timageId?: number;\n}\n\nexport class Image implements Component {\n\tprivate base64Data: string;\n\tprivate mimeType: string;\n\tprivate dimensions: ImageDimensions;\n\tprivate theme: ImageTheme;\n\tprivate options: ImageOptions;\n\tprivate imageId?: number;\n\n\tprivate cachedLines?: string[];\n\tprivate cachedWidth?: number;\n\n\tconstructor(\n\t\tbase64Data: string,\n\t\tmimeType: string,\n\t\ttheme: ImageTheme,\n\t\toptions: ImageOptions = {},\n\t\tdimensions?: ImageDimensions,\n\t) {\n\t\tthis.base64Data = base64Data;\n\t\tthis.mimeType = mimeType;\n\t\tthis.theme = theme;\n\t\tthis.options = options;\n\t\tthis.dimensions = dimensions || getImageDimensions(base64Data, mimeType) || { widthPx: 800, heightPx: 600 };\n\t\tthis.imageId = options.imageId;\n\t}\n\n\t/** Get the Kitty image ID used by this image (if any). */\n\tgetImageId(): number | undefined {\n\t\treturn this.imageId;\n\t}\n\n\tinvalidate(): void {\n\t\tthis.cachedLines = undefined;\n\t\tthis.cachedWidth = undefined;\n\t}\n\n\trender(width: number): string[] {\n\t\tif (this.cachedLines && this.cachedWidth === width) {\n\t\t\treturn this.cachedLines;\n\t\t}\n\n\t\tconst maxWidth = Math.max(1, Math.min(width - 2, this.options.maxWidthCells ?? 60));\n\t\tconst cellDimensions = getCellDimensions();\n\t\tconst defaultMaxHeight = Math.max(1, Math.ceil((maxWidth * cellDimensions.widthPx) / cellDimensions.heightPx));\n\t\tconst maxHeight = this.options.maxHeightCells ?? defaultMaxHeight;\n\n\t\tconst caps = getCapabilities();\n\t\tlet lines: string[];\n\n\t\tif (caps.images) {\n\t\t\tif (caps.images === \"kitty\" && this.imageId === undefined) {\n\t\t\t\tthis.imageId = allocateImageId();\n\t\t\t}\n\t\t\tconst result = renderImage(this.base64Data, this.dimensions, {\n\t\t\t\tmaxWidthCells: maxWidth,\n\t\t\t\tmaxHeightCells: maxHeight,\n\t\t\t\timageId: this.imageId,\n\t\t\t\tmoveCursor: false,\n\t\t\t});\n\n\t\t\tif (result) {\n\t\t\t\t// Store the image ID for later cleanup\n\t\t\t\tif (result.imageId) {\n\t\t\t\t\tthis.imageId = result.imageId;\n\t\t\t\t}\n\n\t\t\t\tif (caps.images === \"kitty\") {\n\t\t\t\t\t// For Kitty: C=1 prevents cursor movement.\n\t\t\t\t\t// Don't need the cursor movement.\n\t\t\t\t\tlines = [result.sequence];\n\n\t\t\t\t\t// Return `rows` lines so TUI accounts for image height.\n\t\t\t\t\tfor (let i = 0; i < result.rows - 1; i++) {\n\t\t\t\t\t\tlines.push(\"\");\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Return `rows` lines so TUI accounts for image height.\n\t\t\t\t\t// First (rows-1) lines are empty and cleared before the image is drawn.\n\t\t\t\t\t// Last line: move cursor back up, draw the image, then move back down\n\t\t\t\t\t// so TUI cursor accounting stays inside the scroll area.\n\t\t\t\t\tlines = [];\n\t\t\t\t\tfor (let i = 0; i < result.rows - 1; i++) {\n\t\t\t\t\t\tlines.push(\"\");\n\t\t\t\t\t}\n\t\t\t\t\tconst rowOffset = result.rows - 1;\n\t\t\t\t\tconst moveUp = rowOffset > 0 ? `\\x1b[${rowOffset}A` : \"\";\n\t\t\t\t\tlines.push(moveUp + result.sequence);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);\n\t\t\t\tlines = [this.theme.fallbackColor(fallback)];\n\t\t\t}\n\t\t} else {\n\t\t\tconst fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);\n\t\t\tlines = [this.theme.fallbackColor(fallback)];\n\t\t}\n\n\t\tthis.cachedLines = lines;\n\t\tthis.cachedWidth = width;\n\n\t\treturn lines;\n\t}\n}\n"]}
|
|
@@ -70,11 +70,6 @@ export declare class Markdown implements Component {
|
|
|
70
70
|
* Render a list with proper nesting support
|
|
71
71
|
*/
|
|
72
72
|
private renderList;
|
|
73
|
-
/**
|
|
74
|
-
* Render list item tokens, handling nested lists
|
|
75
|
-
* Returns lines WITHOUT the parent indent (renderList will add it)
|
|
76
|
-
*/
|
|
77
|
-
private renderListItem;
|
|
78
73
|
/**
|
|
79
74
|
* Get the visible width of the longest word in a string.
|
|
80
75
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["import { Marked, type Token, Tokenizer, type Tokens } from \"marked\";\nimport { getCapabilities, hyperlink, isImageLine } from \"../terminal-image.js\";\nimport type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from \"../utils.js\";\n\nconst STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\\s~])((?:\\\\.|[^\\\\])*?(?:\\\\.|[^\\s~\\\\]))\\1(?=[^~]|$)/;\n\nclass StrictStrikethroughTokenizer extends Tokenizer {\n\toverride del(src: string): Tokens.Del | undefined {\n\t\tconst match = STRICT_STRIKETHROUGH_REGEX.exec(src);\n\t\tif (!match) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst text = match[2];\n\t\treturn {\n\t\t\ttype: \"del\",\n\t\t\traw: match[0],\n\t\t\ttext,\n\t\t\ttokens: this.lexer.inlineTokens(text),\n\t\t};\n\t}\n}\n\nconst markdownParser = new Marked();\nmarkdownParser.setOptions({\n\ttokenizer: new StrictStrikethroughTokenizer(),\n});\n\n/**\n * Default text styling for markdown content.\n * Applied to all text unless overridden by markdown formatting.\n */\nexport interface DefaultTextStyle {\n\t/** Foreground color function */\n\tcolor?: (text: string) => string;\n\t/** Background color function */\n\tbgColor?: (text: string) => string;\n\t/** Bold text */\n\tbold?: boolean;\n\t/** Italic text */\n\titalic?: boolean;\n\t/** Strikethrough text */\n\tstrikethrough?: boolean;\n\t/** Underline text */\n\tunderline?: boolean;\n}\n\n/**\n * Theme functions for markdown elements.\n * Each function takes text and returns styled text with ANSI codes.\n */\nexport interface MarkdownTheme {\n\theading: (text: string) => string;\n\tlink: (text: string) => string;\n\tlinkUrl: (text: string) => string;\n\tcode: (text: string) => string;\n\tcodeBlock: (text: string) => string;\n\tcodeBlockBorder: (text: string) => string;\n\tquote: (text: string) => string;\n\tquoteBorder: (text: string) => string;\n\thr: (text: string) => string;\n\tlistBullet: (text: string) => string;\n\tbold: (text: string) => string;\n\titalic: (text: string) => string;\n\tstrikethrough: (text: string) => string;\n\tunderline: (text: string) => string;\n\thighlightCode?: (code: string, lang?: string) => string[];\n\t/** Prefix applied to each rendered code block line (default: \" \") */\n\tcodeBlockIndent?: string;\n}\n\ninterface InlineStyleContext {\n\tapplyText: (text: string) => string;\n\tstylePrefix: string;\n}\n\nexport class Markdown implements Component {\n\tprivate text: string;\n\tprivate paddingX: number; // Left/right padding\n\tprivate paddingY: number; // Top/bottom padding\n\tprivate defaultTextStyle?: DefaultTextStyle;\n\tprivate theme: MarkdownTheme;\n\tprivate defaultStylePrefix?: string;\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\tpaddingX: number,\n\t\tpaddingY: number,\n\t\ttheme: MarkdownTheme,\n\t\tdefaultTextStyle?: DefaultTextStyle,\n\t) {\n\t\tthis.text = text;\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.theme = theme;\n\t\tthis.defaultTextStyle = defaultTextStyle;\n\t}\n\n\tsetText(text: string): void {\n\t\tthis.text = text;\n\t\tthis.invalidate();\n\t}\n\n\tinvalidate(): void {\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 = markdownParser.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 (NO padding, NO background yet)\n\t\tconst wrappedLines: string[] = [];\n\t\tfor (const line of renderedLines) {\n\t\t\tif (isImageLine(line)) {\n\t\t\t\twrappedLines.push(line);\n\t\t\t} else {\n\t\t\t\twrappedLines.push(...wrapTextWithAnsi(line, contentWidth));\n\t\t\t}\n\t\t}\n\n\t\t// Add margins and background to each wrapped line\n\t\tconst leftMargin = \" \".repeat(this.paddingX);\n\t\tconst rightMargin = \" \".repeat(this.paddingX);\n\t\tconst bgFn = this.defaultTextStyle?.bgColor;\n\t\tconst contentLines: string[] = [];\n\n\t\tfor (const line of wrappedLines) {\n\t\t\tif (isImageLine(line)) {\n\t\t\t\tcontentLines.push(line);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst lineWithMargins = leftMargin + line + rightMargin;\n\n\t\t\tif (bgFn) {\n\t\t\t\tcontentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));\n\t\t\t} else {\n\t\t\t\t// No background - just pad to width\n\t\t\t\tconst visibleLen = visibleWidth(lineWithMargins);\n\t\t\t\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\t\t\t\tcontentLines.push(lineWithMargins + \" \".repeat(paddingNeeded));\n\t\t\t}\n\t\t}\n\n\t\t// Add top/bottom padding (empty lines)\n\t\tconst emptyLine = \" \".repeat(width);\n\t\tconst emptyLines: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tconst line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;\n\t\t\temptyLines.push(line);\n\t\t}\n\n\t\t// Combine top padding, content, and bottom padding\n\t\tconst result = [...emptyLines, ...contentLines, ...emptyLines];\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\t/**\n\t * Apply default text style to a string.\n\t * This is the base styling applied to all text content.\n\t * NOTE: Background color is NOT applied here - it's applied at the padding stage\n\t * to ensure it extends to the full line width.\n\t */\n\tprivate applyDefaultStyle(text: string): string {\n\t\tif (!this.defaultTextStyle) {\n\t\t\treturn text;\n\t\t}\n\n\t\tlet styled = text;\n\n\t\t// Apply foreground color (NOT background - that's applied at padding stage)\n\t\tif (this.defaultTextStyle.color) {\n\t\t\tstyled = this.defaultTextStyle.color(styled);\n\t\t}\n\n\t\t// Apply text decorations using this.theme\n\t\tif (this.defaultTextStyle.bold) {\n\t\t\tstyled = this.theme.bold(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.italic) {\n\t\t\tstyled = this.theme.italic(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.strikethrough) {\n\t\t\tstyled = this.theme.strikethrough(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.underline) {\n\t\t\tstyled = this.theme.underline(styled);\n\t\t}\n\n\t\treturn styled;\n\t}\n\n\tprivate getDefaultStylePrefix(): string {\n\t\tif (!this.defaultTextStyle) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tif (this.defaultStylePrefix !== undefined) {\n\t\t\treturn this.defaultStylePrefix;\n\t\t}\n\n\t\tconst sentinel = \"\\u0000\";\n\t\tlet styled = sentinel;\n\n\t\tif (this.defaultTextStyle.color) {\n\t\t\tstyled = this.defaultTextStyle.color(styled);\n\t\t}\n\n\t\tif (this.defaultTextStyle.bold) {\n\t\t\tstyled = this.theme.bold(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.italic) {\n\t\t\tstyled = this.theme.italic(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.strikethrough) {\n\t\t\tstyled = this.theme.strikethrough(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.underline) {\n\t\t\tstyled = this.theme.underline(styled);\n\t\t}\n\n\t\tconst sentinelIndex = styled.indexOf(sentinel);\n\t\tthis.defaultStylePrefix = sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : \"\";\n\t\treturn this.defaultStylePrefix;\n\t}\n\n\tprivate getStylePrefix(styleFn: (text: string) => string): string {\n\t\tconst sentinel = \"\\u0000\";\n\t\tconst styled = styleFn(sentinel);\n\t\tconst sentinelIndex = styled.indexOf(sentinel);\n\t\treturn sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : \"\";\n\t}\n\n\tprivate getDefaultInlineStyleContext(): InlineStyleContext {\n\t\treturn {\n\t\t\tapplyText: (text: string) => this.applyDefaultStyle(text),\n\t\t\tstylePrefix: this.getDefaultStylePrefix(),\n\t\t};\n\t}\n\n\tprivate renderToken(\n\t\ttoken: Token,\n\t\twidth: number,\n\t\tnextTokenType?: string,\n\t\tstyleContext?: InlineStyleContext,\n\t): 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\n\t\t\t\t// Build a heading-specific style context so inline tokens (codespan, bold, etc.)\n\t\t\t\t// restore heading styling after their own ANSI resets instead of falling back to\n\t\t\t\t// the default text style.\n\t\t\t\tlet headingStyleFn: (text: string) => string;\n\t\t\t\tif (headingLevel === 1) {\n\t\t\t\t\theadingStyleFn = (text: string) => this.theme.heading(this.theme.bold(this.theme.underline(text)));\n\t\t\t\t} else {\n\t\t\t\t\theadingStyleFn = (text: string) => this.theme.heading(this.theme.bold(text));\n\t\t\t\t}\n\n\t\t\t\tconst headingStyleContext: InlineStyleContext = {\n\t\t\t\t\tapplyText: headingStyleFn,\n\t\t\t\t\tstylePrefix: this.getStylePrefix(headingStyleFn),\n\t\t\t\t};\n\n\t\t\t\tconst headingText = this.renderInlineTokens(token.tokens || [], headingStyleContext);\n\t\t\t\tconst styledHeading = headingLevel >= 3 ? headingStyleFn(headingPrefix) + headingText : headingText;\n\t\t\t\tlines.push(styledHeading);\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after headings (unless space token follows)\n\t\t\t\t}\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 || [], styleContext);\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\tconst indent = this.theme.codeBlockIndent ?? \" \";\n\t\t\t\tlines.push(this.theme.codeBlockBorder(`\\`\\`\\`${token.lang || \"\"}`));\n\t\t\t\tif (this.theme.highlightCode) {\n\t\t\t\t\tconst highlightedLines = this.theme.highlightCode(token.text, token.lang);\n\t\t\t\t\tfor (const hlLine of highlightedLines) {\n\t\t\t\t\t\tlines.push(`${indent}${hlLine}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Split code by newlines and style each line\n\t\t\t\t\tconst codeLines = token.text.split(\"\\n\");\n\t\t\t\t\tfor (const codeLine of codeLines) {\n\t\t\t\t\t\tlines.push(`${indent}${this.theme.codeBlock(codeLine)}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlines.push(this.theme.codeBlockBorder(\"```\"));\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after code blocks (unless space token follows)\n\t\t\t\t}\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, styleContext);\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, width, nextTokenType, styleContext);\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 quoteStyle = (text: string) => this.theme.quote(this.theme.italic(text));\n\t\t\t\tconst quoteStylePrefix = this.getStylePrefix(quoteStyle);\n\t\t\t\tconst applyQuoteStyle = (line: string): string => {\n\t\t\t\t\tif (!quoteStylePrefix) {\n\t\t\t\t\t\treturn quoteStyle(line);\n\t\t\t\t\t}\n\t\t\t\t\tconst lineWithReappliedStyle = line.replace(/\\x1b\\[0m/g, `\\x1b[0m${quoteStylePrefix}`);\n\t\t\t\t\treturn quoteStyle(lineWithReappliedStyle);\n\t\t\t\t};\n\n\t\t\t\t// Calculate available width for quote content (subtract border \"│ \" = 2 chars)\n\t\t\t\tconst quoteContentWidth = Math.max(1, width - 2);\n\n\t\t\t\t// Blockquotes contain block-level tokens (paragraph, list, code, etc.), so render\n\t\t\t\t// children with renderToken() instead of renderInlineTokens().\n\t\t\t\t// Default message style should not apply inside blockquotes.\n\t\t\t\tconst quoteInlineStyleContext: InlineStyleContext = {\n\t\t\t\t\tapplyText: (text: string) => text,\n\t\t\t\t\tstylePrefix: quoteStylePrefix,\n\t\t\t\t};\n\t\t\t\tconst quoteTokens = token.tokens || [];\n\t\t\t\tconst renderedQuoteLines: string[] = [];\n\t\t\t\tfor (let i = 0; i < quoteTokens.length; i++) {\n\t\t\t\t\tconst quoteToken = quoteTokens[i];\n\t\t\t\t\tconst nextQuoteToken = quoteTokens[i + 1];\n\t\t\t\t\trenderedQuoteLines.push(\n\t\t\t\t\t\t...this.renderToken(quoteToken, quoteContentWidth, nextQuoteToken?.type, quoteInlineStyleContext),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// Avoid rendering an extra empty quote line before the outer blockquote spacing.\n\t\t\t\twhile (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1] === \"\") {\n\t\t\t\t\trenderedQuoteLines.pop();\n\t\t\t\t}\n\n\t\t\t\tfor (const quoteLine of renderedQuoteLines) {\n\t\t\t\t\tconst styledLine = applyQuoteStyle(quoteLine);\n\t\t\t\t\tconst wrappedLines = wrapTextWithAnsi(styledLine, quoteContentWidth);\n\t\t\t\t\tfor (const wrappedLine of wrappedLines) {\n\t\t\t\t\t\tlines.push(this.theme.quoteBorder(\"│ \") + wrappedLine);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after blockquotes (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"hr\":\n\t\t\t\tlines.push(this.theme.hr(\"─\".repeat(Math.min(width, 80))));\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after horizontal rules (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"html\":\n\t\t\t\t// Render HTML as plain text (escaped for terminal)\n\t\t\t\tif (\"raw\" in token && typeof token.raw === \"string\") {\n\t\t\t\t\tlines.push(this.applyDefaultStyle(token.raw.trim()));\n\t\t\t\t}\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[], styleContext?: InlineStyleContext): string {\n\t\tlet result = \"\";\n\t\tconst resolvedStyleContext = styleContext ?? this.getDefaultInlineStyleContext();\n\t\tconst { applyText, stylePrefix } = resolvedStyleContext;\n\t\tconst applyTextWithNewlines = (text: string): string => {\n\t\t\tconst segments: string[] = text.split(\"\\n\");\n\t\t\treturn segments.map((segment: string) => applyText(segment)).join(\"\\n\");\n\t\t};\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, resolvedStyleContext);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult += applyTextWithNewlines(token.text);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"paragraph\":\n\t\t\t\t\t// Paragraph tokens contain nested inline tokens\n\t\t\t\t\tresult += this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"strong\": {\n\t\t\t\t\tconst boldContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.bold(boldContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"em\": {\n\t\t\t\t\tconst italicContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.italic(italicContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"codespan\":\n\t\t\t\t\tresult += this.theme.code(token.text) + stylePrefix;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"link\": {\n\t\t\t\t\tconst linkText = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tconst styledLink = this.theme.link(this.theme.underline(linkText));\n\t\t\t\t\tif (getCapabilities().hyperlinks) {\n\t\t\t\t\t\t// OSC 8: render as a clickable hyperlink. The URL is not printed inline,\n\t\t\t\t\t\t// so we always show only the link text regardless of whether it matches href.\n\t\t\t\t\t\tresult += hyperlink(styledLink, token.href) + stylePrefix;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Fallback: print URL in parentheses when text differs from href.\n\t\t\t\t\t\t// Compare raw token.text (not styled) against href for the equality check.\n\t\t\t\t\t\t// For mailto: links strip the prefix (autolinked emails use text=\"foo@bar.com\"\n\t\t\t\t\t\t// but href=\"mailto:foo@bar.com\").\n\t\t\t\t\t\tconst hrefForComparison = token.href.startsWith(\"mailto:\") ? token.href.slice(7) : token.href;\n\t\t\t\t\t\tif (token.text === token.href || token.text === hrefForComparison) {\n\t\t\t\t\t\t\tresult += styledLink + stylePrefix;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresult += styledLink + this.theme.linkUrl(` (${token.href})`) + stylePrefix;\n\t\t\t\t\t\t}\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\tconst delContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.strikethrough(delContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"html\":\n\t\t\t\t\t// Render inline HTML as plain text\n\t\t\t\t\tif (\"raw\" in token && typeof token.raw === \"string\") {\n\t\t\t\t\t\tresult += applyTextWithNewlines(token.raw);\n\t\t\t\t\t}\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 += applyTextWithNewlines(token.text);\n\t\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\twhile (stylePrefix && result.endsWith(stylePrefix)) {\n\t\t\tresult = result.slice(0, -stylePrefix.length);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Render a list with proper nesting support\n\t */\n\tprivate renderList(\n\t\ttoken: Token & { items: any[]; ordered: boolean; start?: number },\n\t\tdepth: number,\n\t\tstyleContext?: InlineStyleContext,\n\t): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst indent = \" \".repeat(depth);\n\t\t// Use the list's start property (defaults to 1 for ordered lists)\n\t\tconst startNumber = token.start ?? 1;\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 ? `${startNumber + i}. ` : \"- \";\n\n\t\t\t// Process item tokens to handle nested lists\n\t\t\tconst itemLines = this.renderListItem(item.tokens || [], depth, styleContext);\n\n\t\t\tif (itemLines.length > 0) {\n\t\t\t\t// First line - check if it's a nested list\n\t\t\t\t// A nested list will start with indent (spaces) followed by cyan bullet\n\t\t\t\tconst firstLine = itemLines[0];\n\t\t\t\tconst isNestedList = /^\\s+\\x1b\\[36m[-\\d]/.test(firstLine); // starts with spaces + cyan + bullet char\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 + this.theme.listBullet(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 = /^\\s+\\x1b\\[36m[-\\d]/.test(line); // starts with spaces + cyan + bullet char\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 + this.theme.listBullet(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, styleContext?: InlineStyleContext): 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, styleContext);\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\n\t\t\t\t\t\t? this.renderInlineTokens(token.tokens, styleContext)\n\t\t\t\t\t\t: 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 || [], styleContext);\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\tconst indent = this.theme.codeBlockIndent ?? \" \";\n\t\t\t\tlines.push(this.theme.codeBlockBorder(`\\`\\`\\`${token.lang || \"\"}`));\n\t\t\t\tif (this.theme.highlightCode) {\n\t\t\t\t\tconst highlightedLines = this.theme.highlightCode(token.text, token.lang);\n\t\t\t\t\tfor (const hlLine of highlightedLines) {\n\t\t\t\t\t\tlines.push(`${indent}${hlLine}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst codeLines = token.text.split(\"\\n\");\n\t\t\t\t\tfor (const codeLine of codeLines) {\n\t\t\t\t\t\tlines.push(`${indent}${this.theme.codeBlock(codeLine)}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlines.push(this.theme.codeBlockBorder(\"```\"));\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], styleContext);\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 * Get the visible width of the longest word in a string.\n\t */\n\tprivate getLongestWordWidth(text: string, maxWidth?: number): number {\n\t\tconst words = text.split(/\\s+/).filter((word) => word.length > 0);\n\t\tlet longest = 0;\n\t\tfor (const word of words) {\n\t\t\tlongest = Math.max(longest, visibleWidth(word));\n\t\t}\n\t\tif (maxWidth === undefined) {\n\t\t\treturn longest;\n\t\t}\n\t\treturn Math.min(longest, maxWidth);\n\t}\n\n\t/**\n\t * Wrap a table cell to fit into a column.\n\t *\n\t * Delegates to wrapTextWithAnsi() so ANSI codes + long tokens are handled\n\t * consistently with the rest of the renderer.\n\t */\n\tprivate wrapCellText(text: string, maxWidth: number): string[] {\n\t\treturn wrapTextWithAnsi(text, Math.max(1, maxWidth));\n\t}\n\n\t/**\n\t * Render a table with width-aware cell wrapping.\n\t * Cells that don't fit are wrapped to multiple lines.\n\t */\n\tprivate renderTable(\n\t\ttoken: Token & { header: any[]; rows: any[][]; raw?: string },\n\t\tavailableWidth: number,\n\t\tnextTokenType?: string,\n\t\tstyleContext?: InlineStyleContext,\n\t): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst numCols = token.header.length;\n\n\t\tif (numCols === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate border overhead: \"│ \" + (n-1) * \" │ \" + \" │\"\n\t\t// = 2 + (n-1) * 3 + 2 = 3n + 1\n\t\tconst borderOverhead = 3 * numCols + 1;\n\t\tconst availableForCells = availableWidth - borderOverhead;\n\t\tif (availableForCells < numCols) {\n\t\t\t// Too narrow to render a stable table. Fall back to raw markdown.\n\t\t\tconst fallbackLines = token.raw ? wrapTextWithAnsi(token.raw, availableWidth) : [];\n\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\tfallbackLines.push(\"\");\n\t\t\t}\n\t\t\treturn fallbackLines;\n\t\t}\n\n\t\tconst maxUnbrokenWordWidth = 30;\n\n\t\t// Calculate natural column widths (what each column needs without constraints)\n\t\tconst naturalWidths: number[] = [];\n\t\tconst minWordWidths: number[] = [];\n\t\tfor (let i = 0; i < numCols; i++) {\n\t\t\tconst headerText = this.renderInlineTokens(token.header[i].tokens || [], styleContext);\n\t\t\tnaturalWidths[i] = visibleWidth(headerText);\n\t\t\tminWordWidths[i] = Math.max(1, this.getLongestWordWidth(headerText, maxUnbrokenWordWidth));\n\t\t}\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 || [], styleContext);\n\t\t\t\tnaturalWidths[i] = Math.max(naturalWidths[i] || 0, visibleWidth(cellText));\n\t\t\t\tminWordWidths[i] = Math.max(\n\t\t\t\t\tminWordWidths[i] || 1,\n\t\t\t\t\tthis.getLongestWordWidth(cellText, maxUnbrokenWordWidth),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tlet minColumnWidths = minWordWidths;\n\t\tlet minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);\n\n\t\tif (minCellsWidth > availableForCells) {\n\t\t\tminColumnWidths = new Array(numCols).fill(1);\n\t\t\tconst remaining = availableForCells - numCols;\n\n\t\t\tif (remaining > 0) {\n\t\t\t\tconst totalWeight = minWordWidths.reduce((total, width) => total + Math.max(0, width - 1), 0);\n\t\t\t\tconst growth = minWordWidths.map((width) => {\n\t\t\t\t\tconst weight = Math.max(0, width - 1);\n\t\t\t\t\treturn totalWeight > 0 ? Math.floor((weight / totalWeight) * remaining) : 0;\n\t\t\t\t});\n\n\t\t\t\tfor (let i = 0; i < numCols; i++) {\n\t\t\t\t\tminColumnWidths[i] += growth[i] ?? 0;\n\t\t\t\t}\n\n\t\t\t\tconst allocated = growth.reduce((total, width) => total + width, 0);\n\t\t\t\tlet leftover = remaining - allocated;\n\t\t\t\tfor (let i = 0; leftover > 0 && i < numCols; i++) {\n\t\t\t\t\tminColumnWidths[i]++;\n\t\t\t\t\tleftover--;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tminCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);\n\t\t}\n\n\t\t// Calculate column widths that fit within available width\n\t\tconst totalNaturalWidth = naturalWidths.reduce((a, b) => a + b, 0) + borderOverhead;\n\t\tlet columnWidths: number[];\n\n\t\tif (totalNaturalWidth <= availableWidth) {\n\t\t\t// Everything fits naturally\n\t\t\tcolumnWidths = naturalWidths.map((width, index) => Math.max(width, minColumnWidths[index]));\n\t\t} else {\n\t\t\t// Need to shrink columns to fit\n\t\t\tconst totalGrowPotential = naturalWidths.reduce((total, width, index) => {\n\t\t\t\treturn total + Math.max(0, width - minColumnWidths[index]);\n\t\t\t}, 0);\n\t\t\tconst extraWidth = Math.max(0, availableForCells - minCellsWidth);\n\t\t\tcolumnWidths = minColumnWidths.map((minWidth, index) => {\n\t\t\t\tconst naturalWidth = naturalWidths[index];\n\t\t\t\tconst minWidthDelta = Math.max(0, naturalWidth - minWidth);\n\t\t\t\tlet grow = 0;\n\t\t\t\tif (totalGrowPotential > 0) {\n\t\t\t\t\tgrow = Math.floor((minWidthDelta / totalGrowPotential) * extraWidth);\n\t\t\t\t}\n\t\t\t\treturn minWidth + grow;\n\t\t\t});\n\n\t\t\t// Adjust for rounding errors - distribute remaining space\n\t\t\tconst allocated = columnWidths.reduce((a, b) => a + b, 0);\n\t\t\tlet remaining = availableForCells - allocated;\n\t\t\twhile (remaining > 0) {\n\t\t\t\tlet grew = false;\n\t\t\t\tfor (let i = 0; i < numCols && remaining > 0; i++) {\n\t\t\t\t\tif (columnWidths[i] < naturalWidths[i]) {\n\t\t\t\t\t\tcolumnWidths[i]++;\n\t\t\t\t\t\tremaining--;\n\t\t\t\t\t\tgrew = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!grew) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Render top border\n\t\tconst topBorderCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tlines.push(`┌─${topBorderCells.join(\"─┬─\")}─┐`);\n\n\t\t// Render header with wrapping\n\t\tconst headerCellLines: string[][] = token.header.map((cell, i) => {\n\t\t\tconst text = this.renderInlineTokens(cell.tokens || [], styleContext);\n\t\t\treturn this.wrapCellText(text, columnWidths[i]);\n\t\t});\n\t\tconst headerLineCount = Math.max(...headerCellLines.map((c) => c.length));\n\n\t\tfor (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {\n\t\t\tconst rowParts = headerCellLines.map((cellLines, colIdx) => {\n\t\t\t\tconst text = cellLines[lineIdx] || \"\";\n\t\t\t\tconst padded = text + \" \".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));\n\t\t\t\treturn this.theme.bold(padded);\n\t\t\t});\n\t\t\tlines.push(`│ ${rowParts.join(\" │ \")} │`);\n\t\t}\n\n\t\t// Render separator\n\t\tconst separatorCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tconst separatorLine = `├─${separatorCells.join(\"─┼─\")}─┤`;\n\t\tlines.push(separatorLine);\n\n\t\t// Render rows with wrapping\n\t\tfor (let rowIndex = 0; rowIndex < token.rows.length; rowIndex++) {\n\t\t\tconst row = token.rows[rowIndex];\n\t\t\tconst rowCellLines: string[][] = row.map((cell, i) => {\n\t\t\t\tconst text = this.renderInlineTokens(cell.tokens || [], styleContext);\n\t\t\t\treturn this.wrapCellText(text, columnWidths[i]);\n\t\t\t});\n\t\t\tconst rowLineCount = Math.max(...rowCellLines.map((c) => c.length));\n\n\t\t\tfor (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {\n\t\t\t\tconst rowParts = rowCellLines.map((cellLines, colIdx) => {\n\t\t\t\t\tconst text = cellLines[lineIdx] || \"\";\n\t\t\t\t\treturn text + \" \".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));\n\t\t\t\t});\n\t\t\t\tlines.push(`│ ${rowParts.join(\" │ \")} │`);\n\t\t\t}\n\n\t\t\tif (rowIndex < token.rows.length - 1) {\n\t\t\t\tlines.push(separatorLine);\n\t\t\t}\n\t\t}\n\n\t\t// Render bottom border\n\t\tconst bottomBorderCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tlines.push(`└─${bottomBorderCells.join(\"─┴─\")}─┘`);\n\n\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\tlines.push(\"\"); // Add spacing after table\n\t\t}\n\t\treturn lines;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"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","sourcesContent":["import { Marked, type Token, Tokenizer, type Tokens } from \"marked\";\nimport { getCapabilities, hyperlink, isImageLine } from \"../terminal-image.js\";\nimport type { Component } from \"../tui.js\";\nimport { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from \"../utils.js\";\n\nconst STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\\s~])((?:\\\\.|[^\\\\])*?(?:\\\\.|[^\\s~\\\\]))\\1(?=[^~]|$)/;\n\nclass StrictStrikethroughTokenizer extends Tokenizer {\n\toverride del(src: string): Tokens.Del | undefined {\n\t\tconst match = STRICT_STRIKETHROUGH_REGEX.exec(src);\n\t\tif (!match) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst text = match[2];\n\t\treturn {\n\t\t\ttype: \"del\",\n\t\t\traw: match[0],\n\t\t\ttext,\n\t\t\ttokens: this.lexer.inlineTokens(text),\n\t\t};\n\t}\n}\n\nconst markdownParser = new Marked();\nmarkdownParser.setOptions({\n\ttokenizer: new StrictStrikethroughTokenizer(),\n});\n\n/**\n * Default text styling for markdown content.\n * Applied to all text unless overridden by markdown formatting.\n */\nexport interface DefaultTextStyle {\n\t/** Foreground color function */\n\tcolor?: (text: string) => string;\n\t/** Background color function */\n\tbgColor?: (text: string) => string;\n\t/** Bold text */\n\tbold?: boolean;\n\t/** Italic text */\n\titalic?: boolean;\n\t/** Strikethrough text */\n\tstrikethrough?: boolean;\n\t/** Underline text */\n\tunderline?: boolean;\n}\n\n/**\n * Theme functions for markdown elements.\n * Each function takes text and returns styled text with ANSI codes.\n */\nexport interface MarkdownTheme {\n\theading: (text: string) => string;\n\tlink: (text: string) => string;\n\tlinkUrl: (text: string) => string;\n\tcode: (text: string) => string;\n\tcodeBlock: (text: string) => string;\n\tcodeBlockBorder: (text: string) => string;\n\tquote: (text: string) => string;\n\tquoteBorder: (text: string) => string;\n\thr: (text: string) => string;\n\tlistBullet: (text: string) => string;\n\tbold: (text: string) => string;\n\titalic: (text: string) => string;\n\tstrikethrough: (text: string) => string;\n\tunderline: (text: string) => string;\n\thighlightCode?: (code: string, lang?: string) => string[];\n\t/** Prefix applied to each rendered code block line (default: \" \") */\n\tcodeBlockIndent?: string;\n}\n\ninterface InlineStyleContext {\n\tapplyText: (text: string) => string;\n\tstylePrefix: string;\n}\n\nexport class Markdown implements Component {\n\tprivate text: string;\n\tprivate paddingX: number; // Left/right padding\n\tprivate paddingY: number; // Top/bottom padding\n\tprivate defaultTextStyle?: DefaultTextStyle;\n\tprivate theme: MarkdownTheme;\n\tprivate defaultStylePrefix?: string;\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\tpaddingX: number,\n\t\tpaddingY: number,\n\t\ttheme: MarkdownTheme,\n\t\tdefaultTextStyle?: DefaultTextStyle,\n\t) {\n\t\tthis.text = text;\n\t\tthis.paddingX = paddingX;\n\t\tthis.paddingY = paddingY;\n\t\tthis.theme = theme;\n\t\tthis.defaultTextStyle = defaultTextStyle;\n\t}\n\n\tsetText(text: string): void {\n\t\tthis.text = text;\n\t\tthis.invalidate();\n\t}\n\n\tinvalidate(): void {\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 = markdownParser.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\tfor (const tokenLine of tokenLines) {\n\t\t\t\trenderedLines.push(tokenLine);\n\t\t\t}\n\t\t}\n\n\t\t// Wrap lines (NO padding, NO background yet)\n\t\tconst wrappedLines: string[] = [];\n\t\tfor (const line of renderedLines) {\n\t\t\tif (isImageLine(line)) {\n\t\t\t\twrappedLines.push(line);\n\t\t\t} else {\n\t\t\t\tfor (const wrappedLine of wrapTextWithAnsi(line, contentWidth)) {\n\t\t\t\t\twrappedLines.push(wrappedLine);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add margins and background to each wrapped line\n\t\tconst leftMargin = \" \".repeat(this.paddingX);\n\t\tconst rightMargin = \" \".repeat(this.paddingX);\n\t\tconst bgFn = this.defaultTextStyle?.bgColor;\n\t\tconst contentLines: string[] = [];\n\n\t\tfor (const line of wrappedLines) {\n\t\t\tif (isImageLine(line)) {\n\t\t\t\tcontentLines.push(line);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst lineWithMargins = leftMargin + line + rightMargin;\n\n\t\t\tif (bgFn) {\n\t\t\t\tcontentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));\n\t\t\t} else {\n\t\t\t\t// No background - just pad to width\n\t\t\t\tconst visibleLen = visibleWidth(lineWithMargins);\n\t\t\t\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\t\t\t\tcontentLines.push(lineWithMargins + \" \".repeat(paddingNeeded));\n\t\t\t}\n\t\t}\n\n\t\t// Add top/bottom padding (empty lines)\n\t\tconst emptyLine = \" \".repeat(width);\n\t\tconst emptyLines: string[] = [];\n\t\tfor (let i = 0; i < this.paddingY; i++) {\n\t\t\tconst line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;\n\t\t\temptyLines.push(line);\n\t\t}\n\n\t\t// Combine top padding, content, and bottom padding\n\t\tconst result = emptyLines.concat(contentLines, emptyLines);\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\t/**\n\t * Apply default text style to a string.\n\t * This is the base styling applied to all text content.\n\t * NOTE: Background color is NOT applied here - it's applied at the padding stage\n\t * to ensure it extends to the full line width.\n\t */\n\tprivate applyDefaultStyle(text: string): string {\n\t\tif (!this.defaultTextStyle) {\n\t\t\treturn text;\n\t\t}\n\n\t\tlet styled = text;\n\n\t\t// Apply foreground color (NOT background - that's applied at padding stage)\n\t\tif (this.defaultTextStyle.color) {\n\t\t\tstyled = this.defaultTextStyle.color(styled);\n\t\t}\n\n\t\t// Apply text decorations using this.theme\n\t\tif (this.defaultTextStyle.bold) {\n\t\t\tstyled = this.theme.bold(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.italic) {\n\t\t\tstyled = this.theme.italic(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.strikethrough) {\n\t\t\tstyled = this.theme.strikethrough(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.underline) {\n\t\t\tstyled = this.theme.underline(styled);\n\t\t}\n\n\t\treturn styled;\n\t}\n\n\tprivate getDefaultStylePrefix(): string {\n\t\tif (!this.defaultTextStyle) {\n\t\t\treturn \"\";\n\t\t}\n\n\t\tif (this.defaultStylePrefix !== undefined) {\n\t\t\treturn this.defaultStylePrefix;\n\t\t}\n\n\t\tconst sentinel = \"\\u0000\";\n\t\tlet styled = sentinel;\n\n\t\tif (this.defaultTextStyle.color) {\n\t\t\tstyled = this.defaultTextStyle.color(styled);\n\t\t}\n\n\t\tif (this.defaultTextStyle.bold) {\n\t\t\tstyled = this.theme.bold(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.italic) {\n\t\t\tstyled = this.theme.italic(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.strikethrough) {\n\t\t\tstyled = this.theme.strikethrough(styled);\n\t\t}\n\t\tif (this.defaultTextStyle.underline) {\n\t\t\tstyled = this.theme.underline(styled);\n\t\t}\n\n\t\tconst sentinelIndex = styled.indexOf(sentinel);\n\t\tthis.defaultStylePrefix = sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : \"\";\n\t\treturn this.defaultStylePrefix;\n\t}\n\n\tprivate getStylePrefix(styleFn: (text: string) => string): string {\n\t\tconst sentinel = \"\\u0000\";\n\t\tconst styled = styleFn(sentinel);\n\t\tconst sentinelIndex = styled.indexOf(sentinel);\n\t\treturn sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : \"\";\n\t}\n\n\tprivate getDefaultInlineStyleContext(): InlineStyleContext {\n\t\treturn {\n\t\t\tapplyText: (text: string) => this.applyDefaultStyle(text),\n\t\t\tstylePrefix: this.getDefaultStylePrefix(),\n\t\t};\n\t}\n\n\tprivate renderToken(\n\t\ttoken: Token,\n\t\twidth: number,\n\t\tnextTokenType?: string,\n\t\tstyleContext?: InlineStyleContext,\n\t): 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\n\t\t\t\t// Build a heading-specific style context so inline tokens (codespan, bold, etc.)\n\t\t\t\t// restore heading styling after their own ANSI resets instead of falling back to\n\t\t\t\t// the default text style.\n\t\t\t\tlet headingStyleFn: (text: string) => string;\n\t\t\t\tif (headingLevel === 1) {\n\t\t\t\t\theadingStyleFn = (text: string) => this.theme.heading(this.theme.bold(this.theme.underline(text)));\n\t\t\t\t} else {\n\t\t\t\t\theadingStyleFn = (text: string) => this.theme.heading(this.theme.bold(text));\n\t\t\t\t}\n\n\t\t\t\tconst headingStyleContext: InlineStyleContext = {\n\t\t\t\t\tapplyText: headingStyleFn,\n\t\t\t\t\tstylePrefix: this.getStylePrefix(headingStyleFn),\n\t\t\t\t};\n\n\t\t\t\tconst headingText = this.renderInlineTokens(token.tokens || [], headingStyleContext);\n\t\t\t\tconst styledHeading = headingLevel >= 3 ? headingStyleFn(headingPrefix) + headingText : headingText;\n\t\t\t\tlines.push(styledHeading);\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after headings (unless space token follows)\n\t\t\t\t}\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 || [], styleContext);\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 \"text\":\n\t\t\t\tlines.push(this.renderInlineTokens([token], styleContext));\n\t\t\t\tbreak;\n\n\t\t\tcase \"code\": {\n\t\t\t\tconst indent = this.theme.codeBlockIndent ?? \" \";\n\t\t\t\tlines.push(this.theme.codeBlockBorder(`\\`\\`\\`${token.lang || \"\"}`));\n\t\t\t\tif (this.theme.highlightCode) {\n\t\t\t\t\tconst highlightedLines = this.theme.highlightCode(token.text, token.lang);\n\t\t\t\t\tfor (const hlLine of highlightedLines) {\n\t\t\t\t\t\tlines.push(`${indent}${hlLine}`);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Split code by newlines and style each line\n\t\t\t\t\tconst codeLines = token.text.split(\"\\n\");\n\t\t\t\t\tfor (const codeLine of codeLines) {\n\t\t\t\t\t\tlines.push(`${indent}${this.theme.codeBlock(codeLine)}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tlines.push(this.theme.codeBlockBorder(\"```\"));\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after code blocks (unless space token follows)\n\t\t\t\t}\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 Tokens.List, 0, width, styleContext);\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 Tokens.Table, width, nextTokenType, styleContext);\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 quoteStyle = (text: string) => this.theme.quote(this.theme.italic(text));\n\t\t\t\tconst quoteStylePrefix = this.getStylePrefix(quoteStyle);\n\t\t\t\tconst applyQuoteStyle = (line: string): string => {\n\t\t\t\t\tif (!quoteStylePrefix) {\n\t\t\t\t\t\treturn quoteStyle(line);\n\t\t\t\t\t}\n\t\t\t\t\tconst lineWithReappliedStyle = line.replace(/\\x1b\\[0m/g, `\\x1b[0m${quoteStylePrefix}`);\n\t\t\t\t\treturn quoteStyle(lineWithReappliedStyle);\n\t\t\t\t};\n\n\t\t\t\t// Calculate available width for quote content (subtract border \"│ \" = 2 chars)\n\t\t\t\tconst quoteContentWidth = Math.max(1, width - 2);\n\n\t\t\t\t// Blockquotes contain block-level tokens (paragraph, list, code, etc.), so render\n\t\t\t\t// children with renderToken() instead of renderInlineTokens().\n\t\t\t\t// Default message style should not apply inside blockquotes.\n\t\t\t\tconst quoteInlineStyleContext: InlineStyleContext = {\n\t\t\t\t\tapplyText: (text: string) => text,\n\t\t\t\t\tstylePrefix: quoteStylePrefix,\n\t\t\t\t};\n\t\t\t\tconst quoteTokens = token.tokens || [];\n\t\t\t\tconst renderedQuoteLines: string[] = [];\n\t\t\t\tfor (let i = 0; i < quoteTokens.length; i++) {\n\t\t\t\t\tconst quoteToken = quoteTokens[i];\n\t\t\t\t\tconst nextQuoteToken = quoteTokens[i + 1];\n\t\t\t\t\trenderedQuoteLines.push(\n\t\t\t\t\t\t...this.renderToken(quoteToken, quoteContentWidth, nextQuoteToken?.type, quoteInlineStyleContext),\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\t// Avoid rendering an extra empty quote line before the outer blockquote spacing.\n\t\t\t\twhile (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1] === \"\") {\n\t\t\t\t\trenderedQuoteLines.pop();\n\t\t\t\t}\n\n\t\t\t\tfor (const quoteLine of renderedQuoteLines) {\n\t\t\t\t\tconst styledLine = applyQuoteStyle(quoteLine);\n\t\t\t\t\tconst wrappedLines = wrapTextWithAnsi(styledLine, quoteContentWidth);\n\t\t\t\t\tfor (const wrappedLine of wrappedLines) {\n\t\t\t\t\t\tlines.push(this.theme.quoteBorder(\"│ \") + wrappedLine);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after blockquotes (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcase \"hr\":\n\t\t\t\tlines.push(this.theme.hr(\"─\".repeat(Math.min(width, 80))));\n\t\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\t\tlines.push(\"\"); // Add spacing after horizontal rules (unless space token follows)\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"html\":\n\t\t\t\t// Render HTML as plain text (escaped for terminal)\n\t\t\t\tif (\"raw\" in token && typeof token.raw === \"string\") {\n\t\t\t\t\tlines.push(this.applyDefaultStyle(token.raw.trim()));\n\t\t\t\t}\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[], styleContext?: InlineStyleContext): string {\n\t\tlet result = \"\";\n\t\tconst resolvedStyleContext = styleContext ?? this.getDefaultInlineStyleContext();\n\t\tconst { applyText, stylePrefix } = resolvedStyleContext;\n\t\tconst applyTextWithNewlines = (text: string): string => {\n\t\t\tconst segments: string[] = text.split(\"\\n\");\n\t\t\treturn segments.map((segment: string) => applyText(segment)).join(\"\\n\");\n\t\t};\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, resolvedStyleContext);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tresult += applyTextWithNewlines(token.text);\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"paragraph\":\n\t\t\t\t\t// Paragraph tokens contain nested inline tokens\n\t\t\t\t\tresult += this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"strong\": {\n\t\t\t\t\tconst boldContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.bold(boldContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"em\": {\n\t\t\t\t\tconst italicContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.italic(italicContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"codespan\":\n\t\t\t\t\tresult += this.theme.code(token.text) + stylePrefix;\n\t\t\t\t\tbreak;\n\n\t\t\t\tcase \"link\": {\n\t\t\t\t\tconst linkText = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tconst styledLink = this.theme.link(this.theme.underline(linkText));\n\t\t\t\t\tif (getCapabilities().hyperlinks) {\n\t\t\t\t\t\t// OSC 8: render as a clickable hyperlink. The URL is not printed inline,\n\t\t\t\t\t\t// so we always show only the link text regardless of whether it matches href.\n\t\t\t\t\t\tresult += hyperlink(styledLink, token.href) + stylePrefix;\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Fallback: print URL in parentheses when text differs from href.\n\t\t\t\t\t\t// Compare raw token.text (not styled) against href for the equality check.\n\t\t\t\t\t\t// For mailto: links strip the prefix (autolinked emails use text=\"foo@bar.com\"\n\t\t\t\t\t\t// but href=\"mailto:foo@bar.com\").\n\t\t\t\t\t\tconst hrefForComparison = token.href.startsWith(\"mailto:\") ? token.href.slice(7) : token.href;\n\t\t\t\t\t\tif (token.text === token.href || token.text === hrefForComparison) {\n\t\t\t\t\t\t\tresult += styledLink + stylePrefix;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tresult += styledLink + this.theme.linkUrl(` (${token.href})`) + stylePrefix;\n\t\t\t\t\t\t}\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\tconst delContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);\n\t\t\t\t\tresult += this.theme.strikethrough(delContent) + stylePrefix;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tcase \"html\":\n\t\t\t\t\t// Render inline HTML as plain text\n\t\t\t\t\tif (\"raw\" in token && typeof token.raw === \"string\") {\n\t\t\t\t\t\tresult += applyTextWithNewlines(token.raw);\n\t\t\t\t\t}\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 += applyTextWithNewlines(token.text);\n\t\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\twhile (stylePrefix && result.endsWith(stylePrefix)) {\n\t\t\tresult = result.slice(0, -stylePrefix.length);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Render a list with proper nesting support\n\t */\n\tprivate renderList(token: Tokens.List, depth: number, width: number, styleContext?: InlineStyleContext): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst indent = \" \".repeat(depth);\n\t\t// Use the list's start property (defaults to 1 for ordered lists)\n\t\tconst startNumber = typeof token.start === \"number\" ? token.start : 1;\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 ? `${startNumber + i}. ` : \"- \";\n\t\t\tconst taskMarker = item.task ? `[${item.checked ? \"x\" : \" \"}] ` : \"\";\n\t\t\tconst marker = bullet + taskMarker;\n\t\t\tconst firstPrefix = indent + this.theme.listBullet(marker);\n\t\t\tconst continuationPrefix = indent + \" \".repeat(visibleWidth(marker));\n\t\t\tconst itemWidth = Math.max(1, width - visibleWidth(firstPrefix));\n\t\t\tlet renderedAnyLine = false;\n\n\t\t\tfor (const itemToken of item.tokens) {\n\t\t\t\tif (itemToken.type === \"list\") {\n\t\t\t\t\tlines.push(...this.renderList(itemToken as Tokens.List, depth + 1, width, styleContext));\n\t\t\t\t\trenderedAnyLine = true;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tconst itemLines = this.renderToken(itemToken, itemWidth, undefined, styleContext);\n\t\t\t\tfor (const line of itemLines) {\n\t\t\t\t\tfor (const wrappedLine of wrapTextWithAnsi(line, itemWidth)) {\n\t\t\t\t\t\tconst linePrefix = renderedAnyLine ? continuationPrefix : firstPrefix;\n\t\t\t\t\t\tlines.push(linePrefix + wrappedLine);\n\t\t\t\t\t\trenderedAnyLine = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!renderedAnyLine) {\n\t\t\t\tlines.push(firstPrefix);\n\t\t\t}\n\t\t}\n\n\t\treturn lines;\n\t}\n\n\t/**\n\t * Get the visible width of the longest word in a string.\n\t */\n\tprivate getLongestWordWidth(text: string, maxWidth?: number): number {\n\t\tconst words = text.split(/\\s+/).filter((word) => word.length > 0);\n\t\tlet longest = 0;\n\t\tfor (const word of words) {\n\t\t\tlongest = Math.max(longest, visibleWidth(word));\n\t\t}\n\t\tif (maxWidth === undefined) {\n\t\t\treturn longest;\n\t\t}\n\t\treturn Math.min(longest, maxWidth);\n\t}\n\n\t/**\n\t * Wrap a table cell to fit into a column.\n\t *\n\t * Delegates to wrapTextWithAnsi() so ANSI codes + long tokens are handled\n\t * consistently with the rest of the renderer.\n\t */\n\tprivate wrapCellText(text: string, maxWidth: number): string[] {\n\t\treturn wrapTextWithAnsi(text, Math.max(1, maxWidth));\n\t}\n\n\t/**\n\t * Render a table with width-aware cell wrapping.\n\t * Cells that don't fit are wrapped to multiple lines.\n\t */\n\tprivate renderTable(\n\t\ttoken: Tokens.Table,\n\t\tavailableWidth: number,\n\t\tnextTokenType?: string,\n\t\tstyleContext?: InlineStyleContext,\n\t): string[] {\n\t\tconst lines: string[] = [];\n\t\tconst numCols = token.header.length;\n\n\t\tif (numCols === 0) {\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Calculate border overhead: \"│ \" + (n-1) * \" │ \" + \" │\"\n\t\t// = 2 + (n-1) * 3 + 2 = 3n + 1\n\t\tconst borderOverhead = 3 * numCols + 1;\n\t\tconst availableForCells = availableWidth - borderOverhead;\n\t\tif (availableForCells < numCols) {\n\t\t\t// Too narrow to render a stable table. Fall back to raw markdown.\n\t\t\tconst fallbackLines = token.raw ? wrapTextWithAnsi(token.raw, availableWidth) : [];\n\t\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\t\tfallbackLines.push(\"\");\n\t\t\t}\n\t\t\treturn fallbackLines;\n\t\t}\n\n\t\tconst maxUnbrokenWordWidth = 30;\n\n\t\t// Calculate natural column widths (what each column needs without constraints)\n\t\tconst naturalWidths: number[] = [];\n\t\tconst minWordWidths: number[] = [];\n\t\tfor (let i = 0; i < numCols; i++) {\n\t\t\tconst headerText = this.renderInlineTokens(token.header[i].tokens || [], styleContext);\n\t\t\tnaturalWidths[i] = visibleWidth(headerText);\n\t\t\tminWordWidths[i] = Math.max(1, this.getLongestWordWidth(headerText, maxUnbrokenWordWidth));\n\t\t}\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 || [], styleContext);\n\t\t\t\tnaturalWidths[i] = Math.max(naturalWidths[i] || 0, visibleWidth(cellText));\n\t\t\t\tminWordWidths[i] = Math.max(\n\t\t\t\t\tminWordWidths[i] || 1,\n\t\t\t\t\tthis.getLongestWordWidth(cellText, maxUnbrokenWordWidth),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tlet minColumnWidths = minWordWidths;\n\t\tlet minCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);\n\n\t\tif (minCellsWidth > availableForCells) {\n\t\t\tminColumnWidths = new Array(numCols).fill(1);\n\t\t\tconst remaining = availableForCells - numCols;\n\n\t\t\tif (remaining > 0) {\n\t\t\t\tconst totalWeight = minWordWidths.reduce((total, width) => total + Math.max(0, width - 1), 0);\n\t\t\t\tconst growth = minWordWidths.map((width) => {\n\t\t\t\t\tconst weight = Math.max(0, width - 1);\n\t\t\t\t\treturn totalWeight > 0 ? Math.floor((weight / totalWeight) * remaining) : 0;\n\t\t\t\t});\n\n\t\t\t\tfor (let i = 0; i < numCols; i++) {\n\t\t\t\t\tminColumnWidths[i] += growth[i] ?? 0;\n\t\t\t\t}\n\n\t\t\t\tconst allocated = growth.reduce((total, width) => total + width, 0);\n\t\t\t\tlet leftover = remaining - allocated;\n\t\t\t\tfor (let i = 0; leftover > 0 && i < numCols; i++) {\n\t\t\t\t\tminColumnWidths[i]++;\n\t\t\t\t\tleftover--;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tminCellsWidth = minColumnWidths.reduce((a, b) => a + b, 0);\n\t\t}\n\n\t\t// Calculate column widths that fit within available width\n\t\tconst totalNaturalWidth = naturalWidths.reduce((a, b) => a + b, 0) + borderOverhead;\n\t\tlet columnWidths: number[];\n\n\t\tif (totalNaturalWidth <= availableWidth) {\n\t\t\t// Everything fits naturally\n\t\t\tcolumnWidths = naturalWidths.map((width, index) => Math.max(width, minColumnWidths[index]));\n\t\t} else {\n\t\t\t// Need to shrink columns to fit\n\t\t\tconst totalGrowPotential = naturalWidths.reduce((total, width, index) => {\n\t\t\t\treturn total + Math.max(0, width - minColumnWidths[index]);\n\t\t\t}, 0);\n\t\t\tconst extraWidth = Math.max(0, availableForCells - minCellsWidth);\n\t\t\tcolumnWidths = minColumnWidths.map((minWidth, index) => {\n\t\t\t\tconst naturalWidth = naturalWidths[index];\n\t\t\t\tconst minWidthDelta = Math.max(0, naturalWidth - minWidth);\n\t\t\t\tlet grow = 0;\n\t\t\t\tif (totalGrowPotential > 0) {\n\t\t\t\t\tgrow = Math.floor((minWidthDelta / totalGrowPotential) * extraWidth);\n\t\t\t\t}\n\t\t\t\treturn minWidth + grow;\n\t\t\t});\n\n\t\t\t// Adjust for rounding errors - distribute remaining space\n\t\t\tconst allocated = columnWidths.reduce((a, b) => a + b, 0);\n\t\t\tlet remaining = availableForCells - allocated;\n\t\t\twhile (remaining > 0) {\n\t\t\t\tlet grew = false;\n\t\t\t\tfor (let i = 0; i < numCols && remaining > 0; i++) {\n\t\t\t\t\tif (columnWidths[i] < naturalWidths[i]) {\n\t\t\t\t\t\tcolumnWidths[i]++;\n\t\t\t\t\t\tremaining--;\n\t\t\t\t\t\tgrew = true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!grew) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Render top border\n\t\tconst topBorderCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tlines.push(`┌─${topBorderCells.join(\"─┬─\")}─┐`);\n\n\t\t// Render header with wrapping\n\t\tconst headerCellLines: string[][] = token.header.map((cell, i) => {\n\t\t\tconst text = this.renderInlineTokens(cell.tokens || [], styleContext);\n\t\t\treturn this.wrapCellText(text, columnWidths[i]);\n\t\t});\n\t\tconst headerLineCount = Math.max(...headerCellLines.map((c) => c.length));\n\n\t\tfor (let lineIdx = 0; lineIdx < headerLineCount; lineIdx++) {\n\t\t\tconst rowParts = headerCellLines.map((cellLines, colIdx) => {\n\t\t\t\tconst text = cellLines[lineIdx] || \"\";\n\t\t\t\tconst padded = text + \" \".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));\n\t\t\t\treturn this.theme.bold(padded);\n\t\t\t});\n\t\t\tlines.push(`│ ${rowParts.join(\" │ \")} │`);\n\t\t}\n\n\t\t// Render separator\n\t\tconst separatorCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tconst separatorLine = `├─${separatorCells.join(\"─┼─\")}─┤`;\n\t\tlines.push(separatorLine);\n\n\t\t// Render rows with wrapping\n\t\tfor (let rowIndex = 0; rowIndex < token.rows.length; rowIndex++) {\n\t\t\tconst row = token.rows[rowIndex];\n\t\t\tconst rowCellLines: string[][] = row.map((cell, i) => {\n\t\t\t\tconst text = this.renderInlineTokens(cell.tokens || [], styleContext);\n\t\t\t\treturn this.wrapCellText(text, columnWidths[i]);\n\t\t\t});\n\t\t\tconst rowLineCount = Math.max(...rowCellLines.map((c) => c.length));\n\n\t\t\tfor (let lineIdx = 0; lineIdx < rowLineCount; lineIdx++) {\n\t\t\t\tconst rowParts = rowCellLines.map((cellLines, colIdx) => {\n\t\t\t\t\tconst text = cellLines[lineIdx] || \"\";\n\t\t\t\t\treturn text + \" \".repeat(Math.max(0, columnWidths[colIdx] - visibleWidth(text)));\n\t\t\t\t});\n\t\t\t\tlines.push(`│ ${rowParts.join(\" │ \")} │`);\n\t\t\t}\n\n\t\t\tif (rowIndex < token.rows.length - 1) {\n\t\t\t\tlines.push(separatorLine);\n\t\t\t}\n\t\t}\n\n\t\t// Render bottom border\n\t\tconst bottomBorderCells = columnWidths.map((w) => \"─\".repeat(w));\n\t\tlines.push(`└─${bottomBorderCells.join(\"─┴─\")}─┘`);\n\n\t\tif (nextTokenType && nextTokenType !== \"space\") {\n\t\t\tlines.push(\"\"); // Add spacing after table\n\t\t}\n\t\treturn lines;\n\t}\n}\n"]}
|
|
@@ -74,7 +74,9 @@ export class Markdown {
|
|
|
74
74
|
const token = tokens[i];
|
|
75
75
|
const nextToken = tokens[i + 1];
|
|
76
76
|
const tokenLines = this.renderToken(token, contentWidth, nextToken?.type);
|
|
77
|
-
|
|
77
|
+
for (const tokenLine of tokenLines) {
|
|
78
|
+
renderedLines.push(tokenLine);
|
|
79
|
+
}
|
|
78
80
|
}
|
|
79
81
|
// Wrap lines (NO padding, NO background yet)
|
|
80
82
|
const wrappedLines = [];
|
|
@@ -83,7 +85,9 @@ export class Markdown {
|
|
|
83
85
|
wrappedLines.push(line);
|
|
84
86
|
}
|
|
85
87
|
else {
|
|
86
|
-
|
|
88
|
+
for (const wrappedLine of wrapTextWithAnsi(line, contentWidth)) {
|
|
89
|
+
wrappedLines.push(wrappedLine);
|
|
90
|
+
}
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
// Add margins and background to each wrapped line
|
|
@@ -115,7 +119,7 @@ export class Markdown {
|
|
|
115
119
|
emptyLines.push(line);
|
|
116
120
|
}
|
|
117
121
|
// Combine top padding, content, and bottom padding
|
|
118
|
-
const result =
|
|
122
|
+
const result = emptyLines.concat(contentLines, emptyLines);
|
|
119
123
|
// Update cache
|
|
120
124
|
this.cachedText = this.text;
|
|
121
125
|
this.cachedWidth = width;
|
|
@@ -229,6 +233,9 @@ export class Markdown {
|
|
|
229
233
|
}
|
|
230
234
|
break;
|
|
231
235
|
}
|
|
236
|
+
case "text":
|
|
237
|
+
lines.push(this.renderInlineTokens([token], styleContext));
|
|
238
|
+
break;
|
|
232
239
|
case "code": {
|
|
233
240
|
const indent = this.theme.codeBlockIndent ?? " ";
|
|
234
241
|
lines.push(this.theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
|
|
@@ -252,7 +259,7 @@ export class Markdown {
|
|
|
252
259
|
break;
|
|
253
260
|
}
|
|
254
261
|
case "list": {
|
|
255
|
-
const listLines = this.renderList(token, 0, styleContext);
|
|
262
|
+
const listLines = this.renderList(token, 0, width, styleContext);
|
|
256
263
|
lines.push(...listLines);
|
|
257
264
|
// Don't add spacing after lists if a space token follows
|
|
258
265
|
// (the space token will handle it)
|
|
@@ -417,98 +424,37 @@ export class Markdown {
|
|
|
417
424
|
/**
|
|
418
425
|
* Render a list with proper nesting support
|
|
419
426
|
*/
|
|
420
|
-
renderList(token, depth, styleContext) {
|
|
427
|
+
renderList(token, depth, width, styleContext) {
|
|
421
428
|
const lines = [];
|
|
422
|
-
const indent = "
|
|
429
|
+
const indent = " ".repeat(depth);
|
|
423
430
|
// Use the list's start property (defaults to 1 for ordered lists)
|
|
424
|
-
const startNumber = token.start
|
|
431
|
+
const startNumber = typeof token.start === "number" ? token.start : 1;
|
|
425
432
|
for (let i = 0; i < token.items.length; i++) {
|
|
426
433
|
const item = token.items[i];
|
|
427
434
|
const bullet = token.ordered ? `${startNumber + i}. ` : "- ";
|
|
428
|
-
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
if (
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
else {
|
|
440
|
-
// Regular text content - add indent and bullet
|
|
441
|
-
lines.push(indent + this.theme.listBullet(bullet) + firstLine);
|
|
435
|
+
const taskMarker = item.task ? `[${item.checked ? "x" : " "}] ` : "";
|
|
436
|
+
const marker = bullet + taskMarker;
|
|
437
|
+
const firstPrefix = indent + this.theme.listBullet(marker);
|
|
438
|
+
const continuationPrefix = indent + " ".repeat(visibleWidth(marker));
|
|
439
|
+
const itemWidth = Math.max(1, width - visibleWidth(firstPrefix));
|
|
440
|
+
let renderedAnyLine = false;
|
|
441
|
+
for (const itemToken of item.tokens) {
|
|
442
|
+
if (itemToken.type === "list") {
|
|
443
|
+
lines.push(...this.renderList(itemToken, depth + 1, width, styleContext));
|
|
444
|
+
renderedAnyLine = true;
|
|
445
|
+
continue;
|
|
442
446
|
}
|
|
443
|
-
|
|
444
|
-
for (
|
|
445
|
-
const line
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
lines.push(line);
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
// Regular content - add parent indent + 2 spaces for continuation
|
|
453
|
-
lines.push(`${indent} ${line}`);
|
|
447
|
+
const itemLines = this.renderToken(itemToken, itemWidth, undefined, styleContext);
|
|
448
|
+
for (const line of itemLines) {
|
|
449
|
+
for (const wrappedLine of wrapTextWithAnsi(line, itemWidth)) {
|
|
450
|
+
const linePrefix = renderedAnyLine ? continuationPrefix : firstPrefix;
|
|
451
|
+
lines.push(linePrefix + wrappedLine);
|
|
452
|
+
renderedAnyLine = true;
|
|
454
453
|
}
|
|
455
454
|
}
|
|
456
455
|
}
|
|
457
|
-
|
|
458
|
-
lines.push(
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
return lines;
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Render list item tokens, handling nested lists
|
|
465
|
-
* Returns lines WITHOUT the parent indent (renderList will add it)
|
|
466
|
-
*/
|
|
467
|
-
renderListItem(tokens, parentDepth, styleContext) {
|
|
468
|
-
const lines = [];
|
|
469
|
-
for (const token of tokens) {
|
|
470
|
-
if (token.type === "list") {
|
|
471
|
-
// Nested list - render with one additional indent level
|
|
472
|
-
// These lines will have their own indent, so we just add them as-is
|
|
473
|
-
const nestedLines = this.renderList(token, parentDepth + 1, styleContext);
|
|
474
|
-
lines.push(...nestedLines);
|
|
475
|
-
}
|
|
476
|
-
else if (token.type === "text") {
|
|
477
|
-
// Text content (may have inline tokens)
|
|
478
|
-
const text = token.tokens && token.tokens.length > 0
|
|
479
|
-
? this.renderInlineTokens(token.tokens, styleContext)
|
|
480
|
-
: token.text || "";
|
|
481
|
-
lines.push(text);
|
|
482
|
-
}
|
|
483
|
-
else if (token.type === "paragraph") {
|
|
484
|
-
// Paragraph in list item
|
|
485
|
-
const text = this.renderInlineTokens(token.tokens || [], styleContext);
|
|
486
|
-
lines.push(text);
|
|
487
|
-
}
|
|
488
|
-
else if (token.type === "code") {
|
|
489
|
-
// Code block in list item
|
|
490
|
-
const indent = this.theme.codeBlockIndent ?? " ";
|
|
491
|
-
lines.push(this.theme.codeBlockBorder(`\`\`\`${token.lang || ""}`));
|
|
492
|
-
if (this.theme.highlightCode) {
|
|
493
|
-
const highlightedLines = this.theme.highlightCode(token.text, token.lang);
|
|
494
|
-
for (const hlLine of highlightedLines) {
|
|
495
|
-
lines.push(`${indent}${hlLine}`);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
else {
|
|
499
|
-
const codeLines = token.text.split("\n");
|
|
500
|
-
for (const codeLine of codeLines) {
|
|
501
|
-
lines.push(`${indent}${this.theme.codeBlock(codeLine)}`);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
lines.push(this.theme.codeBlockBorder("```"));
|
|
505
|
-
}
|
|
506
|
-
else {
|
|
507
|
-
// Other token types - try to render as inline
|
|
508
|
-
const text = this.renderInlineTokens([token], styleContext);
|
|
509
|
-
if (text) {
|
|
510
|
-
lines.push(text);
|
|
511
|
-
}
|
|
456
|
+
if (!renderedAnyLine) {
|
|
457
|
+
lines.push(firstPrefix);
|
|
512
458
|
}
|
|
513
459
|
}
|
|
514
460
|
return lines;
|