mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(tui): stop unclosed display math from swallowing Markdown tables
An unclosed $$ opener could pair with a later $$ across a GFM table, or be treated as pending streaming math, hiding the table. Block-math tokens no longer span a table start, and the pending-math fallback skips content containing a table. fixes #11159
This commit is contained in:
parent
396ecd7b8f
commit
2ed3118b00
3 changed files with 92 additions and 4 deletions
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- Fixed an unclosed `$$` display-math block swallowing or corrupting every Markdown table after it. A block-math token no longer spans a GFM table, and the streaming pending-math fallback no longer treats a following table as pending math ([#11159](https://github.com/earendil-works/pi/issues/11159))
|
||||
- Fixed Markdown bold and italic not rendering next to CJK punctuation ([#10730](https://github.com/earendil-works/pi/pull/10730) by [@theBucky](https://github.com/theBucky))
|
||||
|
||||
## [1.3.0] - 2026-10-15
|
||||
|
|
|
|||
|
|
@ -99,23 +99,41 @@ function tokenizeInlineLatex(source: string): LatexToken | undefined {
|
|||
return { type: "latex", raw, text };
|
||||
}
|
||||
|
||||
const TABLE_DELIMITER_ROW_REGEX = /^ {0,3}\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/;
|
||||
|
||||
// A GFM table starts with a header row followed by a delimiter row such as "|---|---|" or "| --- |".
|
||||
// The header needs a "|" only when the delimiter has none: a pipe-free delimiter without a pipe in the
|
||||
// header is a thematic break, not a table.
|
||||
function containsTableStart(source: string): boolean {
|
||||
const lines = source.split("\n");
|
||||
for (let index = 0; index < lines.length - 1; index++) {
|
||||
const delimiterRow = lines[index + 1];
|
||||
if ((delimiterRow.includes("|") || lines[index].includes("|")) && TABLE_DELIMITER_ROW_REGEX.test(delimiterRow)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function tokenizeBlockLatex(source: string): LatexToken | undefined {
|
||||
// Display math never spans a table. Without this check, an unclosed "$$" or "\[" pairs with a later
|
||||
// opener or stays pending, swallowing the table. See https://github.com/earendil-works/pi/issues/11159.
|
||||
const dollarMatch = /^ {0,3}\$\$[ \t]*(?:\n)?([\s\S]*?)\$\$[ \t]*(?:\n|$)/.exec(source);
|
||||
if (dollarMatch?.[1]) {
|
||||
if (dollarMatch?.[1] && !containsTableStart(dollarMatch[1])) {
|
||||
return { type: "latexBlock", raw: dollarMatch[0], text: dollarMatch[1].trim() };
|
||||
}
|
||||
|
||||
const bracketMatch = /^ {0,3}\\\[[ \t]*(?:\n)?([\s\S]*?)\\\][ \t]*(?:\n|$)/.exec(source);
|
||||
if (bracketMatch?.[1]) {
|
||||
if (bracketMatch?.[1] && !containsTableStart(bracketMatch[1])) {
|
||||
return { type: "latexBlock", raw: bracketMatch[0], text: bracketMatch[1].trim() };
|
||||
}
|
||||
|
||||
const pendingBracket = /^ {0,3}\\\[[ \t]*(?:\n)?([\s\S]*)$/.exec(source);
|
||||
if (pendingBracket) {
|
||||
if (pendingBracket?.[1] && !containsTableStart(pendingBracket[1])) {
|
||||
return { type: "latexBlock", raw: pendingBracket[0], text: pendingBracket[1], pending: true };
|
||||
}
|
||||
const pendingDollar = /^ {0,3}\$\$[ \t]*(?:\n)?([\s\S]*)$/.exec(source);
|
||||
if (pendingDollar?.[1] && looksLikePendingDollarMath(pendingDollar[1])) {
|
||||
if (pendingDollar?.[1] && looksLikePendingDollarMath(pendingDollar[1]) && !containsTableStart(pendingDollar[1])) {
|
||||
return { type: "latexBlock", raw: pendingDollar[0], text: pendingDollar[1], pending: true };
|
||||
}
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -966,6 +966,75 @@ Inline \(A_{sf}\)`,
|
|||
);
|
||||
});
|
||||
|
||||
// Regression tests for https://github.com/earendil-works/pi/issues/11159
|
||||
describe("unclosed display math before tables (#11159)", () => {
|
||||
const table = ["| A | B |", "|---|---|", "| 1 | 2 |"];
|
||||
const renderedTable = ["┌───┬───┐", "│ A │ B │", "├───┼───┤", "│ 1 │ 2 │", "└───┴───┘"];
|
||||
const render = (source: string) =>
|
||||
new Markdown(source, 0, 0, defaultMarkdownTheme).render(80).map((line) => stripAnsi(line).trimEnd());
|
||||
|
||||
it("renders a table after an orphan $$ opener", () => {
|
||||
const source = ["Intro", "", "$$", "", ...table, "", "After"].join("\n");
|
||||
assert.deepStrictEqual(render(source), ["Intro", "", "$$", "", ...renderedTable, "", "After"]);
|
||||
});
|
||||
|
||||
it("renders a table after an orphan $$ opener with math-like content", () => {
|
||||
const source = ["$$", "x = 1", "", ...table].join("\n");
|
||||
assert.deepStrictEqual(render(source), ["$$", "x = 1", "", ...renderedTable]);
|
||||
});
|
||||
|
||||
it("does not pair an unclosed $$ with a later $$ across a table", () => {
|
||||
const source = ["Intro", "", "$$", "x = 1", "", ...table, "", "$$", "y^2", "$$"].join("\n");
|
||||
assert.deepStrictEqual(render(source), ["Intro", "", "$$", "x = 1", "", ...renderedTable, "", "y²"]);
|
||||
});
|
||||
|
||||
it("renders a single-column table with a pipe-free aligned delimiter after an orphan $$", () => {
|
||||
const source = ["$$", "", "| A |", ":---:", "| 1 |"].join("\n");
|
||||
assert.deepStrictEqual(render(source), ["$$", "", "┌───┐", "│ A │", "├───┤", "│ 1 │", "└───┘"]);
|
||||
});
|
||||
|
||||
it("renders a table after an orphan \\[ opener", () => {
|
||||
const source = ["\\\\[", "x^2", "", ...table].join("\n");
|
||||
assert.deepStrictEqual(render(source), ["", "\\[", "x^2", "", ...renderedTable]);
|
||||
});
|
||||
|
||||
it("does not pair an unclosed \\[ with a later \\[ across a table", () => {
|
||||
const source = ["\\\\[", "x = 1", "", ...table, "", "\\\\[", "y^2", "\\\\]"].join("\n");
|
||||
assert.deepStrictEqual(render(source), [
|
||||
"",
|
||||
"\\[",
|
||||
"x = 1",
|
||||
"",
|
||||
...renderedTable,
|
||||
"",
|
||||
"\\",
|
||||
"",
|
||||
"\\[",
|
||||
"y^2",
|
||||
"\\\\]",
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders a table with a pipe-free header after an orphan $$", () => {
|
||||
const source = ["$$", "x^2", "", "Value", "| --- |", "1"].join("\n");
|
||||
assert.deepStrictEqual(render(source), [
|
||||
"$$",
|
||||
"x^2",
|
||||
"",
|
||||
"┌───────┐",
|
||||
"│ Value │",
|
||||
"├───────┤",
|
||||
"│ 1 │",
|
||||
"└───────┘",
|
||||
]);
|
||||
});
|
||||
|
||||
it("still renders closed multi-line display math followed by a table", () => {
|
||||
const source = ["$$", "a", "+ b", "$$", "", ...table].join("\n");
|
||||
assert.deepStrictEqual(render(source), ["a + b", "", ...renderedTable]);
|
||||
});
|
||||
});
|
||||
|
||||
it("switches from raw to rendered math when a streamed delimiter closes", () => {
|
||||
const markdown = new Markdown(String.raw`Map $\mathbb{C}^3`, 0, 0, defaultMarkdownTheme);
|
||||
assert.deepStrictEqual(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue