2025-06-18 00:58:51 -04:00
|
|
|
import { preprocessLaTeX } from './latex';
|
2025-02-09 18:05:31 +01:00
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
describe('preprocessLaTeX', () => {
|
2024-01-18 14:44:10 -05:00
|
|
|
test('returns the same string if no LaTeX patterns are found', () => {
|
2025-06-18 00:58:51 -04:00
|
|
|
const content = 'This is a test string without LaTeX or dollar signs';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(content);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('returns the same string if no dollar signs are present', () => {
|
|
|
|
|
const content = 'This has LaTeX \\(x^2\\) and \\[y^2\\] but no dollars';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(content);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('preserves valid inline LaTeX delimiters \\(...\\)', () => {
|
|
|
|
|
const content = 'This is inline LaTeX: \\(x^2 + y^2 = z^2\\)';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(content);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('preserves valid block LaTeX delimiters \\[...\\]', () => {
|
|
|
|
|
const content = 'This is block LaTeX: \\[E = mc^2\\]';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(content);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('preserves valid double dollar delimiters', () => {
|
|
|
|
|
const content = 'This is valid: $$x^2 + y^2 = z^2$$';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(content);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('converts single dollar delimiters to double dollars', () => {
|
|
|
|
|
const content = 'Inline math: $x^2 + y^2 = z^2$';
|
|
|
|
|
const expected = 'Inline math: $$x^2 + y^2 = z^2$$';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('converts multiple single dollar expressions', () => {
|
|
|
|
|
const content = 'First $a + b = c$ and second $x^2 + y^2 = z^2$';
|
|
|
|
|
const expected = 'First $$a + b = c$$ and second $$x^2 + y^2 = z^2$$';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('escapes currency dollar signs', () => {
|
|
|
|
|
const content = 'Price is $50 and $100';
|
|
|
|
|
const expected = 'Price is \\$50 and \\$100';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('escapes currency with spaces', () => {
|
|
|
|
|
const content = '$50 is $20 + $30';
|
|
|
|
|
const expected = '\\$50 is \\$20 + \\$30';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('escapes currency with commas', () => {
|
|
|
|
|
const content = 'The price is $1,000,000 for this item.';
|
|
|
|
|
const expected = 'The price is \\$1,000,000 for this item.';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('escapes currency with decimals', () => {
|
|
|
|
|
const content = 'Total: $29.50 plus tax';
|
|
|
|
|
const expected = 'Total: \\$29.50 plus tax';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('converts LaTeX expressions while escaping currency', () => {
|
|
|
|
|
const content = 'LaTeX $x^2$ and price $50';
|
|
|
|
|
const expected = 'LaTeX $$x^2$$ and price \\$50';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
2024-01-18 14:44:10 -05:00
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles Goldbach Conjecture example', () => {
|
|
|
|
|
const content = '- **Goldbach Conjecture**: $2n = p + q$ (every even integer > 2)';
|
|
|
|
|
const expected = '- **Goldbach Conjecture**: $$2n = p + q$$ (every even integer > 2)';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
2024-01-18 14:44:10 -05:00
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('does not escape already escaped dollar signs', () => {
|
|
|
|
|
const content = 'Already escaped \\$50 and \\$100';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(content);
|
2024-01-18 14:44:10 -05:00
|
|
|
});
|
2024-01-22 10:02:36 -05:00
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('does not convert already escaped single dollars', () => {
|
|
|
|
|
const content = 'Escaped \\$x^2\\$ should not change';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(content);
|
|
|
|
|
});
|
2024-01-22 10:02:36 -05:00
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('escapes mhchem commands', () => {
|
|
|
|
|
const content = '$\\ce{H2O}$ and $\\pu{123 J}$';
|
|
|
|
|
const expected = '$$\\\\ce{H2O}$$ and $$\\\\pu{123 J}$$';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
2024-01-22 10:02:36 -05:00
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles empty string', () => {
|
|
|
|
|
expect(preprocessLaTeX('')).toBe('');
|
2024-01-22 10:02:36 -05:00
|
|
|
});
|
2024-08-23 13:45:27 -04:00
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles complex mixed content', () => {
|
|
|
|
|
const content = `Valid double $$y^2$$
|
|
|
|
|
Currency $100 and $200
|
|
|
|
|
Single dollar math $x^2 + y^2$
|
|
|
|
|
Chemical $\\ce{H2O}$
|
|
|
|
|
Valid brackets \\[z^2\\]`;
|
|
|
|
|
const expected = `Valid double $$y^2$$
|
|
|
|
|
Currency \\$100 and \\$200
|
|
|
|
|
Single dollar math $$x^2 + y^2$$
|
|
|
|
|
Chemical $$\\\\ce{H2O}$$
|
|
|
|
|
Valid brackets \\[z^2\\]`;
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-08-23 13:45:27 -04:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles multiple equations with currency', () => {
|
|
|
|
|
const content = `- **Euler's Totient Function**: $\\phi(n) = n \\prod_{p|n} \\left(1 - \\frac{1}{p}\\right)$
|
|
|
|
|
- **Total Savings**: $500 + $200 + $150 = $850`;
|
|
|
|
|
const expected = `- **Euler's Totient Function**: $$\\phi(n) = n \\prod_{p|n} \\left(1 - \\frac{1}{p}\\right)$$
|
|
|
|
|
- **Total Savings**: \\$500 + \\$200 + \\$150 = \\$850`;
|
2024-08-23 13:45:27 -04:00
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles inline code blocks', () => {
|
|
|
|
|
const content = 'Outside $x^2$ and inside code: `$100`';
|
|
|
|
|
const expected = 'Outside $$x^2$$ and inside code: `$100`';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-08-23 13:45:27 -04:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles multiline code blocks', () => {
|
|
|
|
|
const content = '```\n$100\n$variable\n```\nOutside $x^2$';
|
|
|
|
|
const expected = '```\n$100\n$variable\n```\nOutside $$x^2$$';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-08-23 13:45:27 -04:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('preserves LaTeX expressions with special characters', () => {
|
|
|
|
|
const content = 'The set is defined as $\\{x | x > 0\\}$.';
|
|
|
|
|
const expected = 'The set is defined as $$\\{x | x > 0\\}$$.';
|
2024-08-23 13:45:27 -04:00
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles complex physics equations', () => {
|
|
|
|
|
const content = `- **Schrödinger Equation**: $i\\hbar\\frac{\\partial}{\\partial t}|\\psi\\rangle = \\hat{H}|\\psi\\rangle$
|
|
|
|
|
- **Einstein Field Equations**: $G_{\\mu\\nu} = \\frac{8\\pi G}{c^4} T_{\\mu\\nu}$`;
|
|
|
|
|
const expected = `- **Schrödinger Equation**: $$i\\hbar\\frac{\\partial}{\\partial t}|\\psi\\rangle = \\hat{H}|\\psi\\rangle$$
|
|
|
|
|
- **Einstein Field Equations**: $$G_{\\mu\\nu} = \\frac{8\\pi G}{c^4} T_{\\mu\\nu}$$`;
|
2024-08-23 13:45:27 -04:00
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles financial calculations with currency', () => {
|
|
|
|
|
const content = `- **Simple Interest**: $A = P + Prt = $1,000 + ($1,000)(0.05)(2) = $1,100$
|
|
|
|
|
- **ROI**: $\\text{ROI} = \\frac{$1,200 - $1,000}{$1,000} \\times 100\\% = 20\\%$`;
|
|
|
|
|
const expected = `- **Simple Interest**: $$A = P + Prt = \\$1,000 + (\\$1,000)(0.05)(2) = \\$1,100$$
|
|
|
|
|
- **ROI**: $$\\text{ROI} = \\frac{\\$1,200 - \\$1,000}{\\$1,000} \\times 100\\% = 20\\%$$`;
|
2024-08-23 13:45:27 -04:00
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('does not convert partial or malformed expressions', () => {
|
|
|
|
|
const content = 'A single $ sign should not be converted';
|
|
|
|
|
const expected = 'A single $ sign should not be converted';
|
2024-08-23 13:45:27 -04:00
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles nested parentheses in LaTeX', () => {
|
|
|
|
|
const content =
|
|
|
|
|
'Matrix determinant: $\\det(A) = \\sum_{\\sigma \\in S_n} \\text{sgn}(\\sigma) \\prod_{i=1}^n a_{i,\\sigma(i)}$';
|
|
|
|
|
const expected =
|
|
|
|
|
'Matrix determinant: $$\\det(A) = \\sum_{\\sigma \\in S_n} \\text{sgn}(\\sigma) \\prod_{i=1}^n a_{i,\\sigma(i)}$$';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-08-23 13:45:27 -04:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('preserves spacing in equations', () => {
|
|
|
|
|
const content = 'Equation: $f(x) = 2x + 3$ where x is a variable.';
|
|
|
|
|
const expected = 'Equation: $$f(x) = 2x + 3$$ where x is a variable.';
|
2024-08-23 13:45:27 -04:00
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles LaTeX with newlines inside should not be converted', () => {
|
|
|
|
|
const content = `This has $x
|
|
|
|
|
y$ which spans lines`;
|
|
|
|
|
const expected = `This has $x
|
|
|
|
|
y$ which spans lines`;
|
2024-08-23 13:45:27 -04:00
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles multiple dollar signs in text', () => {
|
|
|
|
|
const content = 'Price $100 then equation $x + y = z$ then another price $50';
|
|
|
|
|
const expected = 'Price \\$100 then equation $$x + y = z$$ then another price \\$50';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-08-23 13:45:27 -04:00
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('handles complex LaTeX with currency in same expression', () => {
|
|
|
|
|
const content = 'Calculate $\\text{Total} = \\$500 + \\$200$';
|
|
|
|
|
const expected = 'Calculate $$\\text{Total} = \\$500 + \\$200$$';
|
2024-08-23 13:45:27 -04:00
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-18 00:58:51 -04:00
|
|
|
test('preserves already escaped dollars in LaTeX', () => {
|
|
|
|
|
const content = 'The formula $f(x) = \\$2x$ represents cost';
|
|
|
|
|
const expected = 'The formula $$f(x) = \\$2x$$ represents cost';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('handles adjacent LaTeX and currency', () => {
|
|
|
|
|
const content = 'Formula $x^2$ costs $25';
|
|
|
|
|
const expected = 'Formula $$x^2$$ costs \\$25';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('handles LaTeX with special characters and currency', () => {
|
|
|
|
|
const content = 'Set $\\{x | x > \\$0\\}$ for positive prices';
|
|
|
|
|
const expected = 'Set $$\\{x | x > \\$0\\}$$ for positive prices';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
2024-08-23 13:45:27 -04:00
|
|
|
});
|
🧮 fix: Properly Escape Currency and Prevent Code Block LaTeX Bugs (#9248)
* fix(latex): prevent LaTeX conversion when closing $ is preceded by backtick
When text contained patterns like "$lookup namespace" followed by "`$lookup`",
the regex would match from the first $ to the backtick's $, treating the entire
span as a LaTeX expression. This caused programming constructs to be incorrectly
converted to double dollars.
- Added negative lookbehind (?<!`) to single dollar regex
- Prevents matching when closing $ immediately follows a backtick
- Fixes issues with inline code blocks containing $ symbols
* fix(latex): detect currency amounts with 4+ digits without commas
The currency regex pattern \d{1,3} only matched amounts with 1-3 initial digits,
causing amounts like $1157.90 to be interpreted as LaTeX instead of currency.
This resulted in text like "$1157.90 (text) + $500 (text) = $1657.90" being
incorrectly converted to a single LaTeX expression.
- Changed pattern from \d{1,3} to \d+ to match any number of initial digits
- Now properly escapes $1000, $10000, $123456, etc. without requiring commas
- Maintains support for comma-formatted amounts like $1,234.56
* fix(latex): support currency with unlimited decimal places
The currency regex limited decimal places to 1-2 digits (\.\d{1,2}), which
failed to properly escape amounts with more precision like cryptocurrency
values ($0.00001234), gas prices ($3.999), or exchange rates ($1.23456).
- Changed decimal pattern from \.\d{1,2} to \.\d+
- Now supports any number of decimal places
- Handles edge cases like scientific calculations and high-precision values
2025-08-25 02:44:13 -04:00
|
|
|
|
|
|
|
|
test('does not convert when closing dollar is preceded by backtick', () => {
|
|
|
|
|
const content = 'The error "invalid $lookup namespace" occurs when using `$lookup` operator';
|
|
|
|
|
const expected = 'The error "invalid $lookup namespace" occurs when using `$lookup` operator';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('handles mixed backtick and non-backtick cases', () => {
|
|
|
|
|
const content = 'Use $x + y$ in math but `$lookup` in code';
|
|
|
|
|
const expected = 'Use $$x + y$$ in math but `$lookup` in code';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('escapes currency amounts without commas', () => {
|
|
|
|
|
const content =
|
|
|
|
|
'The total amount invested is $1157.90 (existing amount) + $500 (new investment) = $1657.90.';
|
|
|
|
|
const expected =
|
|
|
|
|
'The total amount invested is \\$1157.90 (existing amount) + \\$500 (new investment) = \\$1657.90.';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('handles large currency amounts', () => {
|
|
|
|
|
const content = 'You can win $1000000 or even $9999999.99!';
|
|
|
|
|
const expected = 'You can win \\$1000000 or even \\$9999999.99!';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('escapes currency with many decimal places', () => {
|
|
|
|
|
const content = 'Bitcoin: $0.00001234, Gas: $3.999, Rate: $1.234567890';
|
|
|
|
|
const expected = 'Bitcoin: \\$0.00001234, Gas: \\$3.999, Rate: \\$1.234567890';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
2025-08-26 23:33:56 -04:00
|
|
|
|
|
|
|
|
test('escapes abbreviated currency notation', () => {
|
|
|
|
|
const content = '$250k is 25% of $1M';
|
|
|
|
|
const expected = '\\$250k is 25% of \\$1M';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('handles various abbreviated currency formats', () => {
|
|
|
|
|
const content = 'Revenue: $5M to $10M, funding: $1.5B, price: $5K';
|
|
|
|
|
const expected = 'Revenue: \\$5M to \\$10M, funding: \\$1.5B, price: \\$5K';
|
|
|
|
|
expect(preprocessLaTeX(content)).toBe(expected);
|
|
|
|
|
});
|
2024-08-23 13:45:27 -04:00
|
|
|
});
|