Studio: fix currency and indentation edge cases in LaTeX rendering (#6957)

* Studio: fix link, currency and indentation edge cases in LaTeX rendering

Follow-up to #6914. Three fixes to studio/frontend/src/lib/latex.ts:

- Skip reference-link definition URLs ([id]: url) during delimiter
  conversion, so escaped parens in such URLs are not rewritten as math.
- Preserve the opener line's indentation when emitting a display $$ block,
  so a \[...\] inside a list item stays part of the list.
- Stop a currency amount from pairing with a converted span's opening $,
  which swallowed the price into math (for example $5 + x \(y\)).

* Exclude GFM footnote definitions from the reference-URL skip

A footnote definition like [^1]: \(x\) had its body treated as a link
destination, so leading math was left literal. Skip [^...] labels.

* Merge overlapping link destination regions

A reference-def token can nest inline-link spans (for example
[1]: http://h/[a](b)/foo\(x\)), so the combined spans could overlap and
isInRegion's binary search missed the outer one, rewriting the URL. Merge
overlapping spans before the search.

* Guard lineStart when the display opener is at index 0

Behavior is unchanged (lastIndexOf clamps a negative fromIndex to 0), but
the explicit guard avoids relying on that implicit clamp.

* Scope to indentation and currency fixes

Drop the reference-link URL protection added earlier. It guards a case
models effectively never emit (escaped parens in a reference-style URL),
and approximating CommonMark reference definitions with a regex needs
open-ended special-casing. Keep the two high-value fixes: preserve display
math indentation (including multi-line bodies) inside a list item, and stop
a currency amount from pairing with a converted span's opening dollar sign.
This commit is contained in:
Daniel Han 2026-07-08 03:13:32 -07:00 committed by GitHub
parent f1a2621631
commit de60a3a994
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -173,7 +173,11 @@ function looksLikeMathBody(body: string): boolean {
* (`**$X$**`, `__$X$__`) are always math: LLMs use that for "bold math"
* and the heuristic would otherwise reject prose-shaped bodies like "90 - x".
*/
function hasInlineMathCloser(content: string, offset: number): boolean {
function hasInlineMathCloser(
content: string,
offset: number,
mathRegions: Array<[number, number]>,
): boolean {
const MAX_SPAN = 200;
const limit = Math.min(content.length, offset + 1 + MAX_SPAN);
for (let i = offset + 1; i < limit; i++) {
@ -181,6 +185,9 @@ function hasInlineMathCloser(content: string, offset: number): boolean {
if (c === "\n") return false;
if (c !== "$") continue;
if (content[i - 1] === "\\") continue;
// A `$` opening a generated span (from `\(...\)`) is not a currency closer;
// pairing with it would swallow the price into math (`$5 + x \(y\)`).
if (isInRegion(i, mathRegions)) return false;
if (content[i + 1] === "$") {
i++;
continue;
@ -294,7 +301,22 @@ function convertLatexDelimiters(content: string): {
continue;
}
append(content.slice(last, match.index));
const wrapped = isDisplay ? `\n$$\n${body}\n$$\n` : `$${body}$`;
let wrapped: string;
if (isDisplay) {
// Keep the opener's leading indentation so a `$$` block inside a list item
// stays in the container instead of breaking out at column 0. Only when the
// opener is whitespace-prefixed, so inline `text \[x\]` keeps column 0.
const lineStart =
match.index > 0 ? content.lastIndexOf("\n", match.index - 1) + 1 : 0;
const prefix = content.slice(lineStart, match.index);
const indent = /^\s*$/.test(prefix) ? prefix : "";
// Indent every body line, not just the first, so multi-line display math
// (`\[a\nb\]`) stays wholly inside the container.
const inner = indent ? body.replace(/\n/g, `\n${indent}`) : body;
wrapped = `\n${indent}$$\n${indent}${inner}\n${indent}$$\n`;
} else {
wrapped = `$${body}$`;
}
const start = append(wrapped);
mathRegions.push([start, offset]);
last = matchEnd;
@ -334,7 +356,7 @@ export function preprocessLaTeX(content: string): string {
if (isInRegion(offset, mathRegions)) {
return match;
}
if (hasInlineMathCloser(text, offset)) {
if (hasInlineMathCloser(text, offset, mathRegions)) {
return match;
}
return "\\" + match;