@caupulican/pi-tui 0.81.42 → 0.84.1

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 (104) hide show
  1. package/dist/autocomplete.d.ts +1 -1
  2. package/dist/autocomplete.d.ts.map +1 -1
  3. package/dist/autocomplete.js +15 -23
  4. package/dist/autocomplete.js.map +1 -1
  5. package/dist/bracketed-paste.d.ts +33 -0
  6. package/dist/bracketed-paste.d.ts.map +1 -0
  7. package/dist/bracketed-paste.js +96 -0
  8. package/dist/bracketed-paste.js.map +1 -0
  9. package/dist/components/box.d.ts.map +1 -1
  10. package/dist/components/box.js +2 -7
  11. package/dist/components/box.js.map +1 -1
  12. package/dist/components/cancellable-loader.d.ts.map +1 -1
  13. package/dist/components/cancellable-loader.js +4 -3
  14. package/dist/components/cancellable-loader.js.map +1 -1
  15. package/dist/components/editor.d.ts +20 -8
  16. package/dist/components/editor.d.ts.map +1 -1
  17. package/dist/components/editor.js +172 -310
  18. package/dist/components/editor.js.map +1 -1
  19. package/dist/components/image.d.ts.map +1 -1
  20. package/dist/components/image.js +12 -35
  21. package/dist/components/image.js.map +1 -1
  22. package/dist/components/input.d.ts +8 -8
  23. package/dist/components/input.d.ts.map +1 -1
  24. package/dist/components/input.js +29 -75
  25. package/dist/components/input.js.map +1 -1
  26. package/dist/components/loader.d.ts.map +1 -1
  27. package/dist/components/loader.js +7 -9
  28. package/dist/components/loader.js.map +1 -1
  29. package/dist/components/markdown.d.ts +3 -11
  30. package/dist/components/markdown.d.ts.map +1 -1
  31. package/dist/components/markdown.js +48 -124
  32. package/dist/components/markdown.js.map +1 -1
  33. package/dist/components/select-list.d.ts.map +1 -1
  34. package/dist/components/select-list.js +4 -9
  35. package/dist/components/select-list.js.map +1 -1
  36. package/dist/components/settings-list.d.ts.map +1 -1
  37. package/dist/components/settings-list.js +4 -12
  38. package/dist/components/settings-list.js.map +1 -1
  39. package/dist/components/spacer.d.ts.map +1 -1
  40. package/dist/components/spacer.js +0 -1
  41. package/dist/components/spacer.js.map +1 -1
  42. package/dist/components/text-layout.d.ts +49 -0
  43. package/dist/components/text-layout.d.ts.map +1 -0
  44. package/dist/components/text-layout.js +103 -0
  45. package/dist/components/text-layout.js.map +1 -0
  46. package/dist/components/text.d.ts +2 -11
  47. package/dist/components/text.d.ts.map +1 -1
  48. package/dist/components/text.js +16 -83
  49. package/dist/components/text.js.map +1 -1
  50. package/dist/components/truncated-text.d.ts.map +1 -1
  51. package/dist/components/truncated-text.js +0 -3
  52. package/dist/components/truncated-text.js.map +1 -1
  53. package/dist/editing-actions.d.ts +17 -0
  54. package/dist/editing-actions.d.ts.map +1 -0
  55. package/dist/editing-actions.js +34 -0
  56. package/dist/editing-actions.js.map +1 -0
  57. package/dist/editor-component.d.ts +9 -0
  58. package/dist/editor-component.d.ts.map +1 -1
  59. package/dist/editor-component.js +4 -1
  60. package/dist/editor-component.js.map +1 -1
  61. package/dist/fuzzy.d.ts.map +1 -1
  62. package/dist/fuzzy.js.map +1 -1
  63. package/dist/index.d.ts +1 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +2 -0
  66. package/dist/index.js.map +1 -1
  67. package/dist/keybindings.d.ts +2 -2
  68. package/dist/keybindings.d.ts.map +1 -1
  69. package/dist/keybindings.js +4 -6
  70. package/dist/keybindings.js.map +1 -1
  71. package/dist/keys.d.ts.map +1 -1
  72. package/dist/keys.js.map +1 -1
  73. package/dist/kill-ring.d.ts.map +1 -1
  74. package/dist/kill-ring.js +2 -4
  75. package/dist/kill-ring.js.map +1 -1
  76. package/dist/native-loader.d.ts +3 -0
  77. package/dist/native-loader.d.ts.map +1 -0
  78. package/dist/native-loader.js +25 -0
  79. package/dist/native-loader.js.map +1 -0
  80. package/dist/native-modifiers.d.ts.map +1 -1
  81. package/dist/native-modifiers.js +5 -20
  82. package/dist/native-modifiers.js.map +1 -1
  83. package/dist/stdin-buffer.d.ts +8 -2
  84. package/dist/stdin-buffer.d.ts.map +1 -1
  85. package/dist/stdin-buffer.js +38 -32
  86. package/dist/stdin-buffer.js.map +1 -1
  87. package/dist/terminal-image.d.ts.map +1 -1
  88. package/dist/terminal-image.js.map +1 -1
  89. package/dist/terminal.d.ts +1 -0
  90. package/dist/terminal.d.ts.map +1 -1
  91. package/dist/terminal.js +47 -72
  92. package/dist/terminal.js.map +1 -1
  93. package/dist/tui.d.ts.map +1 -1
  94. package/dist/tui.js +26 -31
  95. package/dist/tui.js.map +1 -1
  96. package/dist/undo-stack.d.ts.map +1 -1
  97. package/dist/undo-stack.js +2 -4
  98. package/dist/undo-stack.js.map +1 -1
  99. package/dist/utils.d.ts.map +1 -1
  100. package/dist/utils.js +197 -275
  101. package/dist/utils.js.map +1 -1
  102. package/dist/word-navigation.d.ts.map +1 -1
  103. package/dist/word-navigation.js.map +1 -1
  104. package/package.json +3 -3
@@ -1,4 +1,5 @@
1
1
  import type { Component } from "../tui.ts";
2
+ import { CachedTextComponent } from "./text-layout.ts";
2
3
  /**
3
4
  * Default text styling for markdown content.
4
5
  * Applied to all text unless overridden by markdown formatting.
@@ -44,22 +45,12 @@ export interface MarkdownOptions {
44
45
  /** Preserve source ordered-list markers instead of normalizing them from the list start. */
45
46
  preserveOrderedListMarkers?: boolean;
46
47
  }
47
- export declare class Markdown implements Component {
48
- private text;
49
- private _renderRevision;
50
- get renderRevision(): number;
51
- private paddingX;
52
- private paddingY;
48
+ export declare class Markdown extends CachedTextComponent implements Component {
53
49
  private defaultTextStyle?;
54
50
  private theme;
55
51
  private options;
56
52
  private defaultStylePrefix?;
57
- private cachedText?;
58
- private cachedWidth?;
59
- private cachedLines?;
60
53
  constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, options?: MarkdownOptions);
61
- setText(text: string): void;
62
- invalidate(): void;
63
54
  render(width: number): string[];
64
55
  /**
65
56
  * Apply default text style to a string.
@@ -68,6 +59,7 @@ export declare class Markdown implements Component {
68
59
  * to ensure it extends to the full line width.
69
60
  */
70
61
  private applyDefaultStyle;
62
+ private applyConfiguredDefaultStyle;
71
63
  private getDefaultStylePrefix;
72
64
  private getStylePrefix;
73
65
  private getDefaultInlineStyleContext;
@@ -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.ts\";\nimport type { Component } from \"../tui.ts\";\nimport { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from \"../utils.ts\";\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\nfunction trimPartialClosingFences(tokens: readonly Token[]): void {\n\tconst token = tokens[tokens.length - 1];\n\tif (token?.type === \"list\") {\n\t\ttrimPartialClosingFences(token.items[token.items.length - 1]?.tokens ?? []);\n\t\treturn;\n\t}\n\tif (token?.type === \"blockquote\") {\n\t\ttrimPartialClosingFences(token.tokens ?? []);\n\t\treturn;\n\t}\n\n\tif (token?.type !== \"code\") {\n\t\treturn;\n\t}\n\n\t// Markdown fenced code blocks can use either backticks or tildes, with length 3 or more.\n\t// Capture the opening fence marker so a streamed partial closing fence shorter than\n\t// the opener can be removed from code content below. This prevents content from\n\t// shrinking when the closing fence completes during streaming.\n\tconst opener = /^(?<marker>`{3,}|~{3,})[^\\n]*(?:\\n|$)/.exec(token.raw);\n\tconst marker = opener?.groups?.marker;\n\tif (!marker) {\n\t\treturn;\n\t}\n\n\tconst fenceChar = marker[0];\n\tconst partialClosingFence = new RegExp(`\\\\n${fenceChar}{1,${marker.length - 1}}$`);\n\tif (partialClosingFence.test(token.raw)) {\n\t\ttoken.text = token.text.replace(partialClosingFence, \"\");\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\nexport interface MarkdownOptions {\n\t/** Preserve source ordered-list markers instead of normalizing them from the list start. */\n\tpreserveOrderedListMarkers?: boolean;\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 _renderRevision = 0;\n\tget renderRevision(): number {\n\t\treturn this._renderRevision;\n\t}\n\tprivate paddingX: number; // Left/right padding\n\tprivate paddingY: number; // Top/bottom padding\n\tprivate defaultTextStyle?: DefaultTextStyle;\n\tprivate theme: MarkdownTheme;\n\tprivate options: MarkdownOptions;\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\toptions?: MarkdownOptions,\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\tthis.options = options ? { ...options } : {};\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\tthis._renderRevision++;\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.slice();\n\t\t}\n\t\tthis._renderRevision++;\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.slice();\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\t\ttrimPartialClosingFences(tokens);\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.slice() : [\"\"];\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\tprivate getOrderedListMarker(item: Tokens.ListItem): string | undefined {\n\t\tconst match = /^(?: {0,3})(\\d{1,9}[.)])[ \\t]+/.exec(item.raw);\n\t\treturn match ? `${match[1]} ` : undefined;\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\n\t\t\t\t? this.options.preserveOrderedListMarkers\n\t\t\t\t\t? (this.getOrderedListMarker(item) ?? `${startNumber + i}. `)\n\t\t\t\t\t: `${startNumber + i}. `\n\t\t\t\t: \"- \";\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"]}
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/components/markdown.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  import { Marked, Tokenizer } from "marked";
2
2
  import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.js";
3
- import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js";
3
+ import { visibleWidth, wrapTextWithAnsi } from "../utils.js";
4
+ import { CachedTextComponent, frameTextLines } from "./text-layout.js";
4
5
  const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
5
6
  class StrictStrikethroughTokenizer extends Tokenizer {
6
7
  del(src) {
@@ -49,68 +50,26 @@ const markdownParser = new Marked();
49
50
  markdownParser.setOptions({
50
51
  tokenizer: new StrictStrikethroughTokenizer(),
51
52
  });
52
- export class Markdown {
53
- text;
54
- _renderRevision = 0;
55
- get renderRevision() {
56
- return this._renderRevision;
57
- }
58
- paddingX; // Left/right padding
59
- paddingY; // Top/bottom padding
60
- defaultTextStyle;
61
- theme;
62
- options;
63
- defaultStylePrefix;
64
- // Cache for rendered output
65
- cachedText;
66
- cachedWidth;
67
- cachedLines;
53
+ export class Markdown extends CachedTextComponent {
68
54
  constructor(text, paddingX, paddingY, theme, defaultTextStyle, options) {
69
- this.text = text;
70
- this.paddingX = paddingX;
71
- this.paddingY = paddingY;
55
+ super(text, paddingX, paddingY);
72
56
  this.theme = theme;
73
57
  this.defaultTextStyle = defaultTextStyle;
74
58
  this.options = options ? { ...options } : {};
75
59
  }
76
- setText(text) {
77
- this.text = text;
78
- this.invalidate();
79
- }
80
- invalidate() {
81
- this.cachedText = undefined;
82
- this.cachedWidth = undefined;
83
- this.cachedLines = undefined;
84
- this._renderRevision++;
85
- }
86
60
  render(width) {
87
- // Check cache
88
- if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) {
89
- return this.cachedLines.slice();
90
- }
91
- this._renderRevision++;
92
- // Calculate available width for content (subtract horizontal padding)
93
- const contentWidth = Math.max(1, width - this.paddingX * 2);
94
- // Don't render anything if there's no actual text
95
- if (!this.text || this.text.trim() === "") {
96
- const result = [];
97
- // Update cache
98
- this.cachedText = this.text;
99
- this.cachedWidth = width;
100
- this.cachedLines = result;
101
- return result.slice();
102
- }
103
- // Replace tabs with 3 spaces for consistent rendering
104
- const normalizedText = this.text.replace(/\t/g, " ");
61
+ const start = this.renderState.begin(this.text, width, this.paddingX);
62
+ if (start.kind === "complete")
63
+ return start.lines;
105
64
  // Parse markdown to HTML-like tokens
106
- const tokens = markdownParser.lexer(normalizedText);
65
+ const tokens = markdownParser.lexer(start.prepared.normalizedText);
107
66
  trimPartialClosingFences(tokens);
108
67
  // Convert tokens to styled terminal output
109
68
  const renderedLines = [];
110
69
  for (let i = 0; i < tokens.length; i++) {
111
70
  const token = tokens[i];
112
71
  const nextToken = tokens[i + 1];
113
- const tokenLines = this.renderToken(token, contentWidth, nextToken?.type);
72
+ const tokenLines = this.renderToken(token, start.prepared.contentWidth, nextToken?.type);
114
73
  for (const tokenLine of tokenLines) {
115
74
  renderedLines.push(tokenLine);
116
75
  }
@@ -122,46 +81,20 @@ export class Markdown {
122
81
  wrappedLines.push(line);
123
82
  }
124
83
  else {
125
- for (const wrappedLine of wrapTextWithAnsi(line, contentWidth)) {
84
+ for (const wrappedLine of wrapTextWithAnsi(line, start.prepared.contentWidth)) {
126
85
  wrappedLines.push(wrappedLine);
127
86
  }
128
87
  }
129
88
  }
130
- // Add margins and background to each wrapped line
131
- const leftMargin = " ".repeat(this.paddingX);
132
- const rightMargin = " ".repeat(this.paddingX);
133
89
  const bgFn = this.defaultTextStyle?.bgColor;
134
- const contentLines = [];
135
- for (const line of wrappedLines) {
136
- if (isImageLine(line)) {
137
- contentLines.push(line);
138
- continue;
139
- }
140
- const lineWithMargins = leftMargin + line + rightMargin;
141
- if (bgFn) {
142
- contentLines.push(applyBackgroundToLine(lineWithMargins, width, bgFn));
143
- }
144
- else {
145
- // No background - just pad to width
146
- const visibleLen = visibleWidth(lineWithMargins);
147
- const paddingNeeded = Math.max(0, width - visibleLen);
148
- contentLines.push(lineWithMargins + " ".repeat(paddingNeeded));
149
- }
150
- }
151
- // Add top/bottom padding (empty lines)
152
- const emptyLine = " ".repeat(width);
153
- const emptyLines = [];
154
- for (let i = 0; i < this.paddingY; i++) {
155
- const line = bgFn ? applyBackgroundToLine(emptyLine, width, bgFn) : emptyLine;
156
- emptyLines.push(line);
157
- }
158
- // Combine top padding, content, and bottom padding
159
- const result = emptyLines.concat(contentLines, emptyLines);
160
- // Update cache
161
- this.cachedText = this.text;
162
- this.cachedWidth = width;
163
- this.cachedLines = result;
164
- return result.length > 0 ? result.slice() : [""];
90
+ const result = frameTextLines(wrappedLines, {
91
+ width,
92
+ paddingX: this.paddingX,
93
+ paddingY: this.paddingY,
94
+ background: bgFn,
95
+ isPassthrough: isImageLine,
96
+ });
97
+ return this.renderState.finish(this.text, width, result);
165
98
  }
166
99
  /**
167
100
  * Apply default text style to a string.
@@ -173,22 +106,28 @@ export class Markdown {
173
106
  if (!this.defaultTextStyle) {
174
107
  return text;
175
108
  }
109
+ return this.applyConfiguredDefaultStyle(text);
110
+ }
111
+ applyConfiguredDefaultStyle(text) {
112
+ const style = this.defaultTextStyle;
113
+ if (!style)
114
+ return text;
176
115
  let styled = text;
177
116
  // Apply foreground color (NOT background - that's applied at padding stage)
178
- if (this.defaultTextStyle.color) {
179
- styled = this.defaultTextStyle.color(styled);
117
+ if (style.color) {
118
+ styled = style.color(styled);
180
119
  }
181
120
  // Apply text decorations using this.theme
182
- if (this.defaultTextStyle.bold) {
121
+ if (style.bold) {
183
122
  styled = this.theme.bold(styled);
184
123
  }
185
- if (this.defaultTextStyle.italic) {
124
+ if (style.italic) {
186
125
  styled = this.theme.italic(styled);
187
126
  }
188
- if (this.defaultTextStyle.strikethrough) {
127
+ if (style.strikethrough) {
189
128
  styled = this.theme.strikethrough(styled);
190
129
  }
191
- if (this.defaultTextStyle.underline) {
130
+ if (style.underline) {
192
131
  styled = this.theme.underline(styled);
193
132
  }
194
133
  return styled;
@@ -201,22 +140,7 @@ export class Markdown {
201
140
  return this.defaultStylePrefix;
202
141
  }
203
142
  const sentinel = "\u0000";
204
- let styled = sentinel;
205
- if (this.defaultTextStyle.color) {
206
- styled = this.defaultTextStyle.color(styled);
207
- }
208
- if (this.defaultTextStyle.bold) {
209
- styled = this.theme.bold(styled);
210
- }
211
- if (this.defaultTextStyle.italic) {
212
- styled = this.theme.italic(styled);
213
- }
214
- if (this.defaultTextStyle.strikethrough) {
215
- styled = this.theme.strikethrough(styled);
216
- }
217
- if (this.defaultTextStyle.underline) {
218
- styled = this.theme.underline(styled);
219
- }
143
+ const styled = this.applyConfiguredDefaultStyle(sentinel);
220
144
  const sentinelIndex = styled.indexOf(sentinel);
221
145
  this.defaultStylePrefix = sentinelIndex >= 0 ? styled.slice(0, sentinelIndex) : "";
222
146
  return this.defaultStylePrefix;
@@ -374,7 +298,7 @@ export class Markdown {
374
298
  return lines;
375
299
  }
376
300
  renderInlineTokens(tokens, styleContext) {
377
- let result = "";
301
+ const parts = [];
378
302
  const resolvedStyleContext = styleContext ?? this.getDefaultInlineStyleContext();
379
303
  const { applyText, stylePrefix } = resolvedStyleContext;
380
304
  const applyTextWithNewlines = (text) => {
@@ -386,28 +310,28 @@ export class Markdown {
386
310
  case "text":
387
311
  // Text tokens in list items can have nested tokens for inline formatting
388
312
  if (token.tokens && token.tokens.length > 0) {
389
- result += this.renderInlineTokens(token.tokens, resolvedStyleContext);
313
+ parts.push(this.renderInlineTokens(token.tokens, resolvedStyleContext));
390
314
  }
391
315
  else {
392
- result += applyTextWithNewlines(token.text);
316
+ parts.push(applyTextWithNewlines(token.text));
393
317
  }
394
318
  break;
395
319
  case "paragraph":
396
320
  // Paragraph tokens contain nested inline tokens
397
- result += this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
321
+ parts.push(this.renderInlineTokens(token.tokens || [], resolvedStyleContext));
398
322
  break;
399
323
  case "strong": {
400
324
  const boldContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
401
- result += this.theme.bold(boldContent) + stylePrefix;
325
+ parts.push(this.theme.bold(boldContent), stylePrefix);
402
326
  break;
403
327
  }
404
328
  case "em": {
405
329
  const italicContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
406
- result += this.theme.italic(italicContent) + stylePrefix;
330
+ parts.push(this.theme.italic(italicContent), stylePrefix);
407
331
  break;
408
332
  }
409
333
  case "codespan":
410
- result += this.theme.code(token.text) + stylePrefix;
334
+ parts.push(this.theme.code(token.text), stylePrefix);
411
335
  break;
412
336
  case "link": {
413
337
  const linkText = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
@@ -415,7 +339,7 @@ export class Markdown {
415
339
  if (getCapabilities().hyperlinks) {
416
340
  // OSC 8: render as a clickable hyperlink. The URL is not printed inline,
417
341
  // so we always show only the link text regardless of whether it matches href.
418
- result += hyperlink(styledLink, token.href) + stylePrefix;
342
+ parts.push(hyperlink(styledLink, token.href), stylePrefix);
419
343
  }
420
344
  else {
421
345
  // Fallback: print URL in parentheses when text differs from href.
@@ -424,39 +348,39 @@ export class Markdown {
424
348
  // but href="mailto:foo@bar.com").
425
349
  const hrefForComparison = token.href.startsWith("mailto:") ? token.href.slice(7) : token.href;
426
350
  if (token.text === token.href || token.text === hrefForComparison) {
427
- result += styledLink + stylePrefix;
351
+ parts.push(styledLink, stylePrefix);
428
352
  }
429
353
  else {
430
- result += styledLink + this.theme.linkUrl(` (${token.href})`) + stylePrefix;
354
+ parts.push(styledLink, this.theme.linkUrl(` (${token.href})`), stylePrefix);
431
355
  }
432
356
  }
433
357
  break;
434
358
  }
435
359
  case "br":
436
- result += "\n";
360
+ parts.push("\n");
437
361
  break;
438
362
  case "del": {
439
363
  const delContent = this.renderInlineTokens(token.tokens || [], resolvedStyleContext);
440
- result += this.theme.strikethrough(delContent) + stylePrefix;
364
+ parts.push(this.theme.strikethrough(delContent), stylePrefix);
441
365
  break;
442
366
  }
443
367
  case "html":
444
368
  // Render inline HTML as plain text
445
369
  if ("raw" in token && typeof token.raw === "string") {
446
- result += applyTextWithNewlines(token.raw);
370
+ parts.push(applyTextWithNewlines(token.raw));
447
371
  }
448
372
  break;
449
373
  default:
450
374
  // Handle any other inline token types as plain text
451
375
  if ("text" in token && typeof token.text === "string") {
452
- result += applyTextWithNewlines(token.text);
376
+ parts.push(applyTextWithNewlines(token.text));
453
377
  }
454
378
  }
455
379
  }
456
- while (stylePrefix && result.endsWith(stylePrefix)) {
457
- result = result.slice(0, -stylePrefix.length);
380
+ while (stylePrefix && parts[parts.length - 1] === stylePrefix) {
381
+ parts.pop();
458
382
  }
459
- return result;
383
+ return parts.join("");
460
384
  }
461
385
  getOrderedListMarker(item) {
462
386
  const match = /^(?: {0,3})(\d{1,9}[.)])[ \t]+/.exec(item.raw);