Files
cinny/src/app/utils/mathParse.test.ts
T
jaredandClaude Opus 4.8 ed51c39fe7 feat(messages): KaTeX math rendering (P4-4)
Renders LaTeX via spec data-mx-maths spans/divs (KaTeX render of the attr,
children as fallback) and conservative $…$ / $$…$$ text detection (escape-aware,
currency-guarded, never inside code/pre). KaTeX + CSS load lazily on first math
(ReactPrism pattern) — verified absent from the eager bundle. Sanitizer
unchanged by design (we render post-sanitize from attr/text; no incoming MathML
accepted). +14 unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 21:19:02 -04:00

84 lines
2.9 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { splitMathSegments } from './mathParse';
test('plain text with no dollars is a single text segment', () => {
assert.deepEqual(splitMathSegments('hello world'), [{ type: 'text', value: 'hello world' }]);
});
test('empty string yields no segments', () => {
assert.deepEqual(splitMathSegments(''), []);
});
test('inline $…$ is extracted between surrounding text', () => {
assert.deepEqual(splitMathSegments('a $x^2$ b'), [
{ type: 'text', value: 'a ' },
{ type: 'inline', value: 'x^2' },
{ type: 'text', value: ' b' },
]);
});
test('block $$…$$ is extracted', () => {
assert.deepEqual(splitMathSegments('$$block$$'), [{ type: 'block', value: 'block' }]);
});
test('block math may span newlines', () => {
assert.deepEqual(splitMathSegments('$$\na=b\n$$'), [{ type: 'block', value: '\na=b\n' }]);
});
test('currency "$5 and $10" is NOT treated as math', () => {
assert.deepEqual(splitMathSegments('$5 and $10'), [{ type: 'text', value: '$5 and $10' }]);
});
test('escaped \\$ never opens or closes math', () => {
assert.deepEqual(splitMathSegments('cost \\$5 today'), [
{ type: 'text', value: 'cost $5 today' },
]);
assert.deepEqual(splitMathSegments('\\$x\\$'), [{ type: 'text', value: '$x$' }]);
});
test('unbalanced single $ stays as text', () => {
assert.deepEqual(splitMathSegments('price is $ here'), [
{ type: 'text', value: 'price is $ here' },
]);
});
test('unbalanced $$ stays as text', () => {
assert.deepEqual(splitMathSegments('$$x'), [{ type: 'text', value: '$$x' }]);
});
test('inline requires non-space adjacency on both delimiters', () => {
// Space right after opening $ -> not math.
assert.deepEqual(splitMathSegments('$ x$'), [{ type: 'text', value: '$ x$' }]);
// Space right before closing $ -> not math.
assert.deepEqual(splitMathSegments('$x $'), [{ type: 'text', value: '$x $' }]);
});
test('multiple inline spans on one line', () => {
assert.deepEqual(splitMathSegments('$a$ and $b$'), [
{ type: 'inline', value: 'a' },
{ type: 'text', value: ' and ' },
{ type: 'inline', value: 'b' },
]);
});
test('escaped dollar inside inline math is preserved in LaTeX', () => {
assert.deepEqual(splitMathSegments('$a\\$b$'), [{ type: 'inline', value: 'a\\$b' }]);
});
test('closing $ followed by a digit is skipped (currency guard) then recovers', () => {
// The first candidate closer is followed by `2` so it is skipped; the later
// `$` closes the span.
assert.deepEqual(splitMathSegments('$x$2 + y$'), [{ type: 'inline', value: 'x$2 + y' }]);
});
test('block and inline mixed with text', () => {
assert.deepEqual(splitMathSegments('see $$E=mc^2$$ and $a$ ok'), [
{ type: 'text', value: 'see ' },
{ type: 'block', value: 'E=mc^2' },
{ type: 'text', value: ' and ' },
{ type: 'inline', value: 'a' },
{ type: 'text', value: ' ok' },
]);
});