From eb4b88a0287134f8937ca452f522b9405eec1940 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sun, 20 Sep 2026 15:35:54 -0400 Subject: [PATCH] =?UTF-8?q?fix(math):=20$=E2=80=A6$=20inside=20a=20backtic?= =?UTF-8?q?k=20code=20span=20stays=20literal=20(#194=20P4-4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verifying the KaTeX checklist: `$x^2$` in inline code was turned into math and the backticks were left as literal text, because the math split runs before markdown. The splitter now skips backtick code spans (N ticks close with N), so markdown's inline code wins: wire is $y^2$. Fenced blocks were already fine. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/utils/mathParse.test.ts | 15 +++++++++++++++ src/app/utils/mathParse.ts | 17 +++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/app/utils/mathParse.test.ts b/src/app/utils/mathParse.test.ts index 4824fe09a..3952a33b9 100644 --- a/src/app/utils/mathParse.test.ts +++ b/src/app/utils/mathParse.test.ts @@ -81,3 +81,18 @@ test('block and inline mixed with text', () => { { type: 'text', value: ' ok' }, ]); }); + +test('math inside a backtick code span stays literal text (#194 P4-4)', () => { + assert.deepEqual(splitMathSegments('inline `$y^2$` here'), [ + { type: 'text', value: 'inline `$y^2$` here' }, + ]); + assert.deepEqual(splitMathSegments('``$a$`` and $b$'), [ + { type: 'text', value: '``$a$`` and ' }, + { type: 'inline', value: 'b' }, + ]); + // An unclosed backtick is just a character. + assert.deepEqual(splitMathSegments('tick ` then $c$'), [ + { type: 'text', value: 'tick ` then ' }, + { type: 'inline', value: 'c' }, + ]); +}); diff --git a/src/app/utils/mathParse.ts b/src/app/utils/mathParse.ts index c228b3748..2b5bc8c51 100644 --- a/src/app/utils/mathParse.ts +++ b/src/app/utils/mathParse.ts @@ -88,6 +88,23 @@ export const splitMathSegments = (text: string): MathSegment[] => { }; while (i < text.length) { + // Backtick code spans are literal: `$x$` inside them must stay text so + // markdown's inline code wins (a run of N backticks closes with N). + if (text[i] === '`') { + let ticks = 0; + while (text[i + ticks] === '`') ticks += 1; + const fence = '`'.repeat(ticks); + const close = text.indexOf(fence, i + ticks); + if (close !== -1) { + buffer += text.slice(i, close + ticks); + i = close + ticks; + continue; + } + buffer += fence; + i += ticks; + continue; + } + // Escaped dollar: consume `\$` and emit a literal `$` as text. if (text[i] === '\\' && text[i + 1] === '$') { buffer += '$';