fix(tui): fall back to raw LaTeX when display math is wider than the content width (#11487)

Display math (`$$…$$`, `\[…\]`) renders as several 2D rows: numerator,
fraction bar and denominator are separate lines. When the layout was
wider than the Markdown content width, `render()` wrapped each row with
`wrapTextWithAnsi` on its own. Only the overflowing row broke, so the
rows below moved down and the equation no longer lined up. For example,
`dx` landed between the fraction bar and the denominators.

The `latexBlock` case in `Markdown.renderToken()` now checks the
rendered layout against the available width. If any row is too wide, it
uses the raw LaTeX source, the same fallback already used when parsing
fails. The raw source then wraps like normal text. Layouts that fit
render exactly as before.

**Testing**
- Added a regression test in `packages/tui/test/markdown.test.ts` using
the equation from the issue. At width 40 it renders the 2D layout
unchanged. At width 30 it falls back to the raw `\[ … \]` source, with
no fraction bars and no line wider than 30. The test fails without the
fix and passes with it.
- `node --test test/markdown.test.ts`: 93 pass. `node --test
test/latex.test.ts`: 111 pass.
- `npm run check` reports no errors in the touched files. The only
errors are 6 type errors in `packages/ai/test`, caused by stale
generated model data, and they are the same on `main` without this
change.
- Reproduction at width 30, before the fix:
  ```
  ∂² u   ∂² u   n   1   1
  ──── + ──── = ∑   ── +∫ f(x)
  dx
  ∂ x²   ∂ y²  i=1  i²  0
  ```
  After the fix, the raw source, wrapped:
  ```
  \[
  \frac{\partial^2 u}{\partial
  x^2} + \frac{\partial^2
  ...
  \]
  ```

Fixes #11483

Requested by @badlogic

Co-authored-by: triagent <triagent@localhost>
This commit is contained in:
robopipi[bot] 2026-10-27 16:09:08 +01:00 • committed by GitHub
parent 807a5c051f
commit cd1ea342ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 31 additions and 3 deletions

View file

@ -11,6 +11,7 @@
- Fixed fullscreen double-click selection splitting paths and tokens at `~`, `.`, `\`, and `+`. Word characters now match iTerm2's defaults ([#10980](https://github.com/earendil-works/pi/issues/10980))
- Fixed `fuzzyMatch()` ranking a query scattered over an earlier part of the text above a contiguous match later in it. It now also scores each contiguous occurrence of the query and keeps the best alignment ([#11220](https://github.com/earendil-works/pi/issues/11220), [#11296](https://github.com/earendil-works/pi/pull/11296))
- Fixed `ProcessTerminal.setTitle()` writing control characters from the title, which could end the title sequence and send the rest to the terminal as escape sequences
- Fixed display math wider than the content width being wrapped row by row, which split fractions and sums apart. It now falls back to the raw LaTeX source ([#11483](https://github.com/earendil-works/pi/issues/11483))
## [1.4.0] - 2026-10-21

View file

@ -536,10 +536,14 @@ export class Markdown implements Component {
case "latexBlock": {
const latexToken = token as LatexToken;
const rendered =
let rendered =
!latexToken.pending && this.options.renderLatex !== false
? (renderLatex(latexToken.text, { display: true }) ?? latexToken.raw.trim())
: latexToken.raw.trim();
? renderLatex(latexToken.text, { display: true })
: undefined;
// A 2D layout cannot be wrapped row by row; fall back to the raw source when it does not fit.
if (rendered === undefined || rendered.split("\n").some((line) => visibleWidth(line) > width)) {
rendered = latexToken.raw.trim();
}
for (const line of rendered.split("\n")) {
lines.push(this.applyDefaultStyle(line));
}

View file

@ -966,6 +966,29 @@ Inline \(A_{sf}\)`,
);
});
// Regression test for https://github.com/earendil-works/pi/issues/11483
it("falls back to raw source when display math is wider than the content width", () => {
const source = [
"\\[",
String.raw`\frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2} = \sum_{i=1}^{n} \frac{1}{i^2} + \int_0^1 f(x)\,dx`,
"\\]",
].join("\n");
const render = (width: number) =>
new Markdown(source, 0, 0, defaultMarkdownTheme).render(width).map((line) => stripAnsi(line).trimEnd());
assert.deepStrictEqual(render(40), [
"∂² u ∂² u n 1 1",
"──── + ──── = ∑ ── +∫ f(x) dx",
"∂ x² ∂ y² i=1 i² 0",
]);
const narrow = render(30);
assert.strictEqual(narrow[0], "\\[");
assert.strictEqual(narrow[narrow.length - 1], "\\]");
assert.ok(!narrow.some((line) => line.includes("─")));
assert.ok(narrow.every((line) => line.length <= 30));
});
// Regression tests for https://github.com/earendil-works/pi/issues/11159
describe("unclosed display math before tables (#11159)", () => {
const table = ["| A | B |", "|---|---|", "| 1 | 2 |"];