mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(tui): stop nested blockquote rendering from growing exponentially (#11321)
**Problem.** A line of many `>` characters makes `marked` produce one nested blockquote token per `>`. The blockquote branch of `Markdown.renderToken` renders its children at `Math.max(1, width - 2)` columns, then wraps each child line again at that width and adds a `│ ` prefix. Once the nesting uses up the width (about 39 levels at 80 columns), the width stays clamped at 1. From then on, each child line (`│ x`, 3 columns wide) is wrapped into about 3 lines at every further level, so the output roughly triples per level. At 80 columns: - 50 levels: 4096 lines, about 30 s to render; - 55 levels: `push(...lines)` exceeds the JavaScript argument limit and throws `RangeError: Maximum call stack size exceeded`, which crashes pi. Reopening the session crashes it again. **Fix.** - When there is no room left for another border (`width < 3`), the blockquote branch now renders its child tokens at the same width without adding a border and without wrapping them again. - Child lines are appended with a loop instead of a spread `push`. Quotes that fit in the width render exactly as before. **Testing.** - Added a regression test in `packages/tui/test/markdown.test.ts`: 60 nested `>` rendered at width 80 give one line that keeps the content. Without the fix the test fails with the RangeError; with it, all 92 tests in that file pass (`node --test test/markdown.test.ts`). - `npm run check` passes. - Reproduction script rendering `">".repeat(n) + "x"` at width 80: - before: 50 levels gave 4096 lines in 29.5 s, and 55 or 60 levels threw a RangeError; - after: 40, 50, 60 and 200 levels each give 1 line in about 10 ms. **Not covered.** A run of about 2000 `>` still overflows the stack inside `marked`'s own lexer, before pi's renderer runs. That is a separate limit and is not changed here. Fixes #11272 Requested by @davidbrai Co-authored-by: triagent <triagent@localhost>
This commit is contained in:
parent
6dfb546785
commit
8a2ebd3854
2 changed files with 34 additions and 4 deletions
|
|
@ -596,8 +596,21 @@ export class Markdown implements Component {
|
|||
return quoteStyle(lineWithReappliedStyle);
|
||||
};
|
||||
|
||||
// With no room for another border ("│ " plus at least one content column), render the
|
||||
// nested content without one. Otherwise each extra level re-wraps every child line at
|
||||
// width 1 and the output grows exponentially with nesting depth.
|
||||
if (width < 3) {
|
||||
const quoteTokens = token.tokens || [];
|
||||
for (let i = 0; i < quoteTokens.length; i++) {
|
||||
for (const line of this.renderToken(quoteTokens[i], width, quoteTokens[i + 1]?.type, styleContext)) {
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate available width for quote content (subtract border "│ " = 2 chars)
|
||||
const quoteContentWidth = Math.max(1, width - 2);
|
||||
const quoteContentWidth = width - 2;
|
||||
|
||||
// Blockquotes contain block-level tokens (paragraph, list, code, etc.), so render
|
||||
// children with renderToken() instead of renderInlineTokens().
|
||||
|
|
@ -611,9 +624,14 @@ export class Markdown implements Component {
|
|||
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, quoteInlineStyleContext),
|
||||
);
|
||||
for (const line of this.renderToken(
|
||||
quoteToken,
|
||||
quoteContentWidth,
|
||||
nextQuoteToken?.type,
|
||||
quoteInlineStyleContext,
|
||||
)) {
|
||||
renderedQuoteLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid rendering an extra empty quote line before the outer blockquote spacing.
|
||||
|
|
|
|||
|
|
@ -1367,6 +1367,18 @@ again, hello world`,
|
|||
});
|
||||
});
|
||||
|
||||
describe("Deeply nested blockquotes", () => {
|
||||
// Regression test for https://github.com/earendil-works/pi/issues/11272
|
||||
it("should render quotes nested deeper than the width allows without exploding", () => {
|
||||
const markdown = new Markdown(`${">".repeat(60)}x`, 1, 0, defaultMarkdownTheme);
|
||||
const lines = markdown.render(80);
|
||||
const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd());
|
||||
|
||||
assert.strictEqual(plainLines.length, 1, `Expected a single line: ${JSON.stringify(plainLines)}`);
|
||||
assert.ok(plainLines[0].endsWith("x"), `Expected content to be kept: ${JSON.stringify(plainLines)}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Blockquotes with multiline content", () => {
|
||||
it("should apply consistent styling to all lines in lazy continuation blockquote", () => {
|
||||
// Markdown "lazy continuation" - second line without > is still part of the quote
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue