Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions studio/frontend/src/lib/latex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,14 +173,21 @@ 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++) {
const c = content[i];
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep scanning past generated math delimiters

When a legitimate dollar-delimited math span starts with a number and contains a converted \(...\) span before its closing $, this early return makes the currency pass treat the opener as a price and escape it. For example, preprocessLaTeX('$2 + 2 \\(x\\)$') now returns \$2 + 2 $x$ $, breaking mixed-delimiter math emitted by models rather than only protecting cases like $5 + x \(y\). Continuing the scan past generated regions, instead of immediately returning false, would avoid corrupting the outer math span.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mixes $...$ and (...) delimiters in a single span ($2 + 2 (x)$), which is malformed input models do not emit; they use one delimiter style. The guard protects the realistic case (a price followed by a separate (...) span). Leaving as-is.

if (content[i + 1] === "$") {
i++;
continue;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading