fix(tui): render blockquote list content correctly

closes #1787
This commit is contained in:
Mario Zechner 2026-03-04 20:25:42 +01:00
parent e0754fdbb3
commit 9673f52919
3 changed files with 49 additions and 8 deletions

View file

@ -8,6 +8,7 @@
- Fixed Kitty CSI-u handling to ignore unsupported modifiers so modifier-only events do not insert stray printable characters ([#1807](https://github.com/badlogic/pi-mono/issues/1807))
- Fixed single-line paste performance by inserting pasted text atomically instead of character-by-character, preventing repeated `@` autocomplete scans during paste ([#1812](https://github.com/badlogic/pi-mono/issues/1812))
- Fixed `visibleWidth()` to ignore generic OSC escape sequences (including OSC 133 semantic prompt markers), preventing width drift when terminals emit semantic zone markers ([#1805](https://github.com/badlogic/pi-mono/issues/1805))
- Fixed markdown blockquotes dropping nested list content by rendering blockquote children as block-level tokens ([#1787](https://github.com/badlogic/pi-mono/issues/1787))
## [0.55.4] - 2026-03-02

View file

@ -331,19 +331,36 @@ export class Markdown implements Component {
case "blockquote": {
const quoteStyle = (text: string) => this.theme.quote(this.theme.italic(text));
const quoteStyleContext: InlineStyleContext = {
applyText: quoteStyle,
stylePrefix: this.getStylePrefix(quoteStyle),
const quoteStylePrefix = this.getStylePrefix(quoteStyle);
const applyQuoteStyle = (line: string): string => {
if (!quoteStylePrefix) {
return quoteStyle(line);
}
const lineWithReappliedStyle = line.replace(/\x1b\[0m/g, `\x1b[0m${quoteStylePrefix}`);
return quoteStyle(lineWithReappliedStyle);
};
const quoteText = this.renderInlineTokens(token.tokens || [], quoteStyleContext);
const quoteLines = quoteText.split("\n");
// Calculate available width for quote content (subtract border "│ " = 2 chars)
const quoteContentWidth = Math.max(1, width - 2);
for (const quoteLine of quoteLines) {
// Wrap the styled line, then add border to each wrapped line
const wrappedLines = wrapTextWithAnsi(quoteLine, quoteContentWidth);
// Blockquotes contain block-level tokens (paragraph, list, code, etc.), so render
// children with renderToken() instead of renderInlineTokens().
const quoteTokens = token.tokens || [];
const renderedQuoteLines: string[] = [];
for (let i = 0; i < quoteTokens.length; i++) {
const quoteToken = quoteTokens[i];
const nextQuoteToken = quoteTokens[i + 1];
renderedQuoteLines.push(...this.renderToken(quoteToken, quoteContentWidth, nextQuoteToken?.type));
}
// Avoid rendering an extra empty quote line before the outer blockquote spacing.
while (renderedQuoteLines.length > 0 && renderedQuoteLines[renderedQuoteLines.length - 1] === "") {
renderedQuoteLines.pop();
}
for (const quoteLine of renderedQuoteLines) {
const styledLine = applyQuoteStyle(quoteLine);
const wrappedLines = wrapTextWithAnsi(styledLine, quoteContentWidth);
for (const wrappedLine of wrappedLines) {
lines.push(this.theme.quoteBorder("│ ") + wrappedLine);
}

View file

@ -757,6 +757,29 @@ bar`,
assert.ok(!barLine?.includes("\x1b[36m"), `bar line should NOT have cyan color: ${barLine}`);
});
it("should render list content inside blockquotes", () => {
const markdown = new Markdown(
`> 1. bla bla
> - nested bullet`,
0,
0,
defaultMarkdownTheme,
);
const lines = markdown.render(80);
const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, ""));
const quotedLines = plainLines.filter((line) => line.startsWith("│ "));
assert.ok(
quotedLines.some((line) => line.includes("1. bla bla")),
`Missing ordered list item: ${JSON.stringify(quotedLines)}`,
);
assert.ok(
quotedLines.some((line) => line.includes("- nested bullet")),
`Missing unordered list item: ${JSON.stringify(quotedLines)}`,
);
});
it("should wrap long blockquote lines and add border to each wrapped line", () => {
const longText = "This is a very long blockquote line that should wrap to multiple lines when rendered";
const markdown = new Markdown(`> ${longText}`, 0, 0, defaultMarkdownTheme);