Spaces:
Running
Running
| /** | |
| * Normalise LaTeX delimiters so `remark-math` can see them. | |
| * | |
| * `remark-math` only understands `$…$` and `$$…$$`, but LLMs routinely emit the | |
| * other standard LaTeX forms — `\(…\)` for inline and `\[…\]` for display. | |
| * Without this pass those answers render as literal backslash noise. | |
| */ | |
| /** | |
| * Code that must never be rewritten: a closed fenced block, an *unclosed* | |
| * fenced block (mid-stream, the closing fence has not arrived yet), or an | |
| * inline code span. Split with a capturing group so the segments come back | |
| * interleaved with the prose. | |
| * | |
| * Both fence alternatives are anchored to the start of a line (`m` flag, up to | |
| * the three leading spaces CommonMark allows). Without that anchor a stray | |
| * ``` written mid-sentence — an answer explaining markdown, for instance — | |
| * looks like an unterminated block, and every formula after it silently stops | |
| * rendering for the rest of the message. | |
| */ | |
| const CODE_SEGMENT = /(^ {0,3}```[\s\S]*?^ {0,3}```[^\n]*|^ {0,3}```[\s\S]*|`[^`\n]*`)/gm; | |
| /** | |
| * Both patterns are non-greedy and require a closing delimiter, which is what | |
| * makes them safe during streaming: a half-arrived `\(E = mc` simply does not | |
| * match and is left alone until its `\)` shows up. | |
| * | |
| * No lookbehind is used to exclude an escaped `\\(`. Lookbehind is unsupported | |
| * on Safari < 16.4, and the tradeoff is not worth it: a `\(…\)` pair in prose | |
| * is math essentially every time. | |
| */ | |
| const DISPLAY_MATH = /\\\[([\s\S]+?)\\\]/g; | |
| const INLINE_MATH = /\\\(([\s\S]+?)\\\)/g; | |
| /** | |
| * Characters that only appear in a formula, never in a price. Their presence | |
| * between two dollars is what distinguishes `$5^2 = 25$` from `$5 and $`. | |
| */ | |
| const LATEX_HINT = /[\\^_{}]/; | |
| /** | |
| * Escape dollar signs that introduce a monetary amount rather than a formula. | |
| * | |
| * `remark-math` treats `$` as a math delimiter, so "costs $5 and $10 total" | |
| * silently renders "5 and " as a formula. The rule applied here: a `$` | |
| * followed by a digit is currency *unless* the text up to the next `$` on the | |
| * same line contains a LaTeX hint — which keeps genuinely digit-initial math | |
| * such as `$2\pi r$` or `$5^2$` working. | |
| * | |
| * Must run before the bracket-delimiter rewrite below, so the dollars this | |
| * module generates itself are never mistaken for currency. | |
| */ | |
| function escapeCurrency(prose: string): string { | |
| let out = ""; | |
| let i = 0; | |
| while (i < prose.length) { | |
| const ch = prose[i]; | |
| // Copy any escape pair verbatim — notably an already-escaped `\$`, and the | |
| // `\(` / `\[` openers that the next pass depends on. | |
| if (ch === "\\") { | |
| out += prose.slice(i, i + 2); | |
| i += 2; | |
| continue; | |
| } | |
| // `$$` is a display delimiter, never currency. | |
| if (ch === "$" && prose[i + 1] === "$") { | |
| out += "$$"; | |
| i += 2; | |
| continue; | |
| } | |
| if (ch === "$" && /\d/.test(prose[i + 1] ?? "")) { | |
| const rest = prose.slice(i + 1); | |
| const newline = rest.indexOf("\n"); | |
| const line = newline === -1 ? rest : rest.slice(0, newline); | |
| const close = line.indexOf("$"); | |
| const inner = close === -1 ? null : line.slice(0, close); | |
| // No closing dollar on this line, or nothing formula-like between them. | |
| if (inner === null || !LATEX_HINT.test(inner)) { | |
| out += "\\$"; | |
| i += 1; | |
| continue; | |
| } | |
| } | |
| out += ch; | |
| i += 1; | |
| } | |
| return out; | |
| } | |
| /** | |
| * Rewrite the LaTeX delimiters in one run of non-code markdown. | |
| * | |
| * Display math must end up with its `$$` fences alone on their own lines. | |
| * `remark-math` only produces a block formula when `$$` opens a line — written | |
| * inline as `$$ x $$` it degrades to *inline* math, silently losing the | |
| * centering and the larger operators. A single newline is enough to interrupt | |
| * the surrounding paragraph, so no blank line is inserted. | |
| */ | |
| function normalizeProse(prose: string): string { | |
| return escapeCurrency(prose) | |
| .replace(DISPLAY_MATH, (_match, formula: string) => `\n$$\n${formula.trim()}\n$$\n`) | |
| .replace(INLINE_MATH, (_match, formula: string) => `$${formula}$`); | |
| } | |
| /** | |
| * Convert `\(…\)` to `$…$` and `\[…\]` to `$$…$$`, leaving code untouched. | |
| * | |
| * Text already using `$` delimiters passes through unchanged, so this is safe | |
| * to run over every message regardless of which form the model chose. | |
| * | |
| * @param markdown Raw markdown, possibly a partial chunk from a stream. | |
| * @returns The same markdown with math delimiters normalised to dollars. | |
| */ | |
| export function normalizeMath(markdown: string): string { | |
| if (!markdown) return markdown; | |
| // split() with a capturing group yields [prose, code, prose, code, …], so | |
| // the odd indices are the code segments that must be passed through as-is. | |
| return markdown | |
| .split(CODE_SEGMENT) | |
| .map((segment, index) => (index % 2 === 1 ? segment : normalizeProse(segment))) | |
| .join(""); | |
| } | |