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>
84 lines
2.9 KiB
TypeScript
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' },
|
|
]);
|
|
});
|