fix(tui): stabilize streaming code fence rendering (#5846)
This commit is contained in:
@@ -22,6 +22,31 @@ class StrictStrikethroughTokenizer extends Tokenizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trimPartialClosingFences(tokens: readonly Token[]): void {
|
||||||
|
const token = tokens[tokens.length - 1];
|
||||||
|
if (token?.type === "list") {
|
||||||
|
trimPartialClosingFences(token.items[token.items.length - 1]?.tokens ?? []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (token?.type === "blockquote") {
|
||||||
|
trimPartialClosingFences(token.tokens ?? []);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (token?.type !== "code") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim streamed partial closing fences so code blocks do not shrink/flicker
|
||||||
|
// when the final fence character arrives. See https://github.com/earendil-works/pi/issues/5825.
|
||||||
|
const marker = /^(`{3,}|~{3,})/.exec(token.raw)?.[1];
|
||||||
|
const lastLine = token.raw.split("\n").pop();
|
||||||
|
if (!marker || !lastLine || lastLine.length >= marker.length || lastLine !== marker[0]?.repeat(lastLine.length)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
token.text = token.text.slice(0, -lastLine.length).replace(/\n$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
const markdownParser = new Marked();
|
const markdownParser = new Marked();
|
||||||
markdownParser.setOptions({
|
markdownParser.setOptions({
|
||||||
tokenizer: new StrictStrikethroughTokenizer(),
|
tokenizer: new StrictStrikethroughTokenizer(),
|
||||||
@@ -145,6 +170,7 @@ export class Markdown implements Component {
|
|||||||
|
|
||||||
// Parse markdown to HTML-like tokens
|
// Parse markdown to HTML-like tokens
|
||||||
const tokens = markdownParser.lexer(normalizedText);
|
const tokens = markdownParser.lexer(normalizedText);
|
||||||
|
trimPartialClosingFences(tokens);
|
||||||
|
|
||||||
// Convert tokens to styled terminal output
|
// Convert tokens to styled terminal output
|
||||||
const renderedLines: string[] = [];
|
const renderedLines: string[] = [];
|
||||||
|
|||||||
@@ -1376,4 +1376,47 @@ bar`,
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Streaming code fences", () => {
|
||||||
|
it("stabilizes partial closing fence rendering", () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
input: "```ts\nconst x = 1;\n``",
|
||||||
|
expected: ["```ts", " const x = 1;", "```"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "```md\nnot a closing fence:\n``\n```",
|
||||||
|
expected: ["```md", " not a closing fence:", " ``", "```"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "```ts\n``",
|
||||||
|
expected: ["```ts", "", "```"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "````\n```",
|
||||||
|
expected: ["```", "", "```"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "~~~~~\n~~~~",
|
||||||
|
expected: ["```", "", "```"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "```md\nnot a closing fence:\n``\n```\n\nafter",
|
||||||
|
expected: ["```md", " not a closing fence:", " ``", "```", "", "after"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { input, expected } of cases) {
|
||||||
|
const markdown = new Markdown(input, 0, 0, defaultMarkdownTheme);
|
||||||
|
const lines = markdown.render(80).map((line) => stripAnsi(line).trimEnd());
|
||||||
|
|
||||||
|
assert.deepStrictEqual(lines, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
const partial = new Markdown("```ts\nconst x = 1;\n``", 0, 0, defaultMarkdownTheme);
|
||||||
|
const complete = new Markdown("```ts\nconst x = 1;\n```", 0, 0, defaultMarkdownTheme);
|
||||||
|
|
||||||
|
assert.strictEqual(partial.render(80).length, complete.render(80).length);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user