Skip to content

Commit 39f8c1c

Browse files
jay20012024claude
andcommitted
feat(preview): canonicalize single-line $$x$$ alone-on-line to block math
User-observed bug: pasting \`$$ E = mc^2 $$\` (compact, single line) rendered as inline display math — no \`.katex-display\` node, so the Math-align toggle had nothing to act on. The user reasonably thought the toggle was broken when actually the input never crossed the block-math threshold. Fix --- Extend canonicalizeBlockMath's rewriteMath to detect "alone on its own line" — preceded by start-of-input / \`\n\` (allowing trailing spaces) AND followed by \`\n\` / end-of-input. When that holds, the single-line \`$$x$$\` is rewritten to canonical block form (\`\n\n$$\nx\n$$\n\n\`) just like the existing multi-line and \`\\begin{env}\` cases. Mid-line \`text $$x$$ text\` is left alone — that's legitimate inline display usage. The new condition only kicks in when the user clearly meant "this fragment is a math block." Two new unit tests pin the new behavior (alone-on-line + start-of-input) and the existing mid-line test was clarified. 130 / 130 tests pass. Combined with the existing \`\\[ \\]\` → \`$$ $$\` translation in normalize-latex-delimiters, compact LLM shapes like \`\\[E = mc^2\\]\` on a single line now also become block math. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d7991b0 commit 39f8c1c

2 files changed

Lines changed: 43 additions & 7 deletions

File tree

src/lib/__tests__/canonicalize-block-math.test.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,34 @@ $$`;
5656
expect(math[0].value).toBe('a + b\n= c');
5757
});
5858

59-
it('leaves single-line $$x$$ without an environment alone', () => {
60-
// This is "inline display" math; remark-math doesn't parse it as a
61-
// math node anyway, but we should not corrupt the source either.
59+
it('leaves single-line $$x$$ mid-line alone (inline display)', () => {
60+
// Mid-line $$x$$ is "inline display" math. remark-math doesn't
61+
// parse it as a block math node, but we shouldn't corrupt the
62+
// source either — the user might want literal $$ in prose.
6263
const src = `before $$E = mc^2$$ after`;
6364
expect(canonicalizeBlockMath(src)).toBe(src);
6465
});
6566

67+
it('rewrites single-line $$x$$ alone on a line to block form', () => {
68+
// The common LLM / user-paste shape. remark-math would otherwise
69+
// miss it (single-line $$x$$ stays inline), so .katex-display
70+
// never gets emitted and math-align has nothing to act on.
71+
const src = `text before\n$$ E = mc^2 $$\ntext after`;
72+
const out = canonicalizeBlockMath(src);
73+
expect(out).toMatch(/\n\$\$\nE = mc\^2\n\$\$\n/);
74+
const math = parseToMath(out);
75+
expect(math).toHaveLength(1);
76+
expect(math[0].value).toBe('E = mc^2');
77+
});
78+
79+
it('rewrites single-line $$x$$ at start of input', () => {
80+
const src = `$$E = mc^2$$\nafter`;
81+
const out = canonicalizeBlockMath(src);
82+
const math = parseToMath(out);
83+
expect(math).toHaveLength(1);
84+
expect(math[0].value).toBe('E = mc^2');
85+
});
86+
6687
it('leaves inline $x$ math untouched', () => {
6788
const src = `inline $E = mc^2$ here`;
6889
expect(canonicalizeBlockMath(src)).toBe(src);

src/lib/canonicalize-block-math.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,27 @@ const BLOCK_MATH_PAIR = /\$\$([\s\S]+?)\$\$/g;
104104
const HAS_ENV = /\\(?:begin|end)\{/;
105105

106106
function rewriteMath(segment: string): string {
107-
return segment.replace(BLOCK_MATH_PAIR, (full, inner: string) => {
107+
return segment.replace(BLOCK_MATH_PAIR, (full, inner: string, offset: number) => {
108108
const hasNewline = inner.includes('\n');
109109
const hasEnv = HAS_ENV.test(inner);
110-
if (!hasNewline && !hasEnv) {
111-
// Single-line, no env — leave as-is. May be inline display, which
112-
// is its own quirk but not what this preprocessor is for.
110+
111+
// "Alone on its own line" — preceded by start-of-segment or `\n`
112+
// (allowing trailing spaces in between), and followed by `\n` or
113+
// end-of-segment. This is what the user intends when they paste
114+
// `$$ E = mc^2 $$` on a line by itself — remark-math otherwise
115+
// treats compact `$$x$$` as inline display, no `.katex-display`
116+
// gets emitted, and our math-align toggle has nothing to act on.
117+
//
118+
// We only normalize the alone-on-line case so legitimate inline
119+
// uses like `prefix text $$x$$ suffix text` stay unchanged.
120+
const beforeText = segment.slice(0, offset);
121+
const afterText = segment.slice(offset + full.length);
122+
const aloneOnLine = /(^|\n)[ \t]*$/.test(beforeText) && /^[ \t]*(\n|$)/.test(afterText);
123+
124+
if (!hasNewline && !hasEnv && !aloneOnLine) {
125+
// Mid-line, single-line `$$x$$` — leave as-is. Treated as inline
126+
// display by remark-math (which is its own quirk, but not the
127+
// preprocessor's job).
113128
return full;
114129
}
115130
// Strip whitespace at the boundaries; remark-math is whitespace-strict

0 commit comments

Comments
 (0)