youfoundjk_TeXcore/test_helpers/utils.test.ts
JK ec2e6f7dda fix(equations): resolve display math parsing failure with adjacent inline math
- Replace naive $$ regexes and dollar counters with a state-machine parser `findDisplayMathBlocks`.
- Correctly handle adjacent single-dollar inline math delimiters (e.g. `$[\text{charge}]$$^2$`) without misinterpreting them as opening display math blocks.
- Update `findMathBlocks` in live preview, callout block equation extraction in `provider-equation`, and `checkAndFixCalloutMath` in fixer utils.
- Add unit test coverage for adjacent inline math blocks and display math boundary detection.
2026-07-20 18:54:31 +02:00

159 lines
5.4 KiB
TypeScript

import {
toRomanUpper,
toRomanLower,
toAlphUpper,
toAlphLower,
getEqNumberPrefix,
CONVERTER
} from '../src/utils/format';
import {
trimMathText,
parseMarkdownComment,
parseYamlLike,
getCalloutPrefix,
isStructuralCalloutLine,
findDisplayMathBlocks
} from '../src/utils/parse';
import { splitIntoLines, insertAt } from '../src/utils/general';
import { App, TFile } from 'obsidian';
describe('format.ts tests', () => {
it('Roman numerals conversion', () => {
expect(toRomanUpper(1)).toBe('I');
expect(toRomanUpper(3)).toBe('III');
expect(toRomanUpper(4)).toBe('IV');
expect(toRomanUpper(9)).toBe('IX');
expect(toRomanUpper(10)).toBe('X');
expect(toRomanUpper(39)).toBe('XXXIX');
expect(toRomanUpper(40)).toBe('XL');
expect(toRomanUpper(50)).toBe('L');
expect(toRomanUpper(90)).toBe('XC');
expect(toRomanUpper(100)).toBe('C');
expect(toRomanUpper(400)).toBe('CD');
expect(toRomanUpper(500)).toBe('D');
expect(toRomanUpper(900)).toBe('CM');
expect(toRomanUpper(1000)).toBe('M');
expect(toRomanUpper(1994)).toBe('MCMXCIV');
expect(toRomanLower(1994)).toBe('mcmxciv');
});
it('Alphabetic numerals conversion', () => {
expect(toAlphUpper(1)).toBe('A');
expect(toAlphUpper(26)).toBe('Z');
expect(toAlphUpper(27)).toBe('BA');
expect(toAlphUpper(28)).toBe('BB');
expect(toAlphLower(1)).toBe('a');
expect(toAlphLower(27)).toBe('ba');
});
it('CONVERTER mapping', () => {
expect(CONVERTER.arabic(123)).toBe('123');
expect(CONVERTER.alph(1)).toBe('a');
expect(CONVERTER.Alph(1)).toBe('A');
expect(CONVERTER.roman(10)).toBe('x');
expect(CONVERTER.Roman(10)).toBe('X');
});
it('getEqNumberPrefix', () => {
const dummyApp = {} as App;
type TempTFile = TFile;
const dummyFile = {} as unknown as TempTFile;
const settings = {
eqNumberPrefix: 'Prefix-'
} as unknown;
expect(getEqNumberPrefix(dummyApp, dummyFile, settings)).toBe('Prefix-');
});
});
describe('parse.ts tests', () => {
it('trimMathText', () => {
expect(trimMathText('$$ E = mc^2 $$')).toBe('E = mc^2');
expect(trimMathText('$$\nE = mc^2\n$$')).toBe('E = mc^2');
expect(trimMathText('no math block')).toBe('');
});
it('findDisplayMathBlocks handles adjacent inline math like $[text]$$^2$', () => {
const content = `$[\\text{charge}]$$^2$ Refer Eq. [[#^eq-robp7m93]]
$$
\\mathbf{D}_0=\\begin{pmatrix}\\alpha_0^{\\,2}\\,Z_i Z_j & \\alpha_0\\alpha_2\\,Z_i\\\\-\\alpha_0\\alpha_2\\,Z_j & 0\\end{pmatrix}
% id: eq-robp7m93
$$`;
const blocks = findDisplayMathBlocks(content);
expect(blocks.length).toBe(1);
const matchedText = content.substring(blocks[0].from, blocks[0].to);
expect(matchedText).toContain('% id: eq-robp7m93');
expect(matchedText.startsWith('$$')).toBe(true);
expect(matchedText.endsWith('$$')).toBe(true);
});
it('findDisplayMathBlocks ignores math in code blocks', () => {
const content = `\`\`\`
$$ fake math $$
\`\`\`
$$ real math $$`;
const blocks = findDisplayMathBlocks(content);
expect(blocks.length).toBe(1);
expect(content.substring(blocks[0].from, blocks[0].to)).toBe('$$ real math $$');
});
it('parseMarkdownComment', () => {
const text = 'Some text\n%% comment line 1\ncomment line 2 %%\nOther text %% single comment %%';
const comments = parseMarkdownComment(text);
expect(comments).toEqual(['comment line 1', 'comment line 2', 'single comment']);
});
it('parseYamlLike', () => {
expect(parseYamlLike('key: value')).toEqual({ key: 'value' });
expect(parseYamlLike(' key : value ')).toEqual({ key: 'value' });
expect(parseYamlLike('no colon')).toBeNull();
expect(parseYamlLike('key: value: extra')).toEqual({ key: 'value: extra' });
});
it('getCalloutPrefix', () => {
expect(getCalloutPrefix('> hello')).toBe('> ');
expect(getCalloutPrefix('>> hello')).toBe('>> ');
expect(getCalloutPrefix(' > > hello')).toBe(' > > ');
expect(getCalloutPrefix('no callout')).toBe('');
});
it('isStructuralCalloutLine', () => {
expect(isStructuralCalloutLine('>')).toBe(true);
expect(isStructuralCalloutLine('> > ')).toBe(true);
expect(isStructuralCalloutLine(' > >')).toBe(true);
expect(isStructuralCalloutLine('> text')).toBe(false);
expect(isStructuralCalloutLine('text')).toBe(false);
});
});
describe('general.ts tests', () => {
it('splitIntoLines', () => {
expect(splitIntoLines('line1\nline2\r\nline3')).toEqual(['line1', 'line2', 'line3']);
});
it('insertAt', () => {
const arr = [1, 2, 3];
insertAt(arr, 99, 1);
expect(arr).toEqual([1, 99, 2, 3]);
});
});
describe('Sub-equation splitting regex tests', () => {
it('should split rows while keeping \\[dimen] intact', () => {
const regex = /(\\\\(?:\s*\[[^\]]*\])?)/;
const input = `\\beta \\frac{\\partial P}{\\partial \\rho} & = \\frac{\\partial}{\\partial \\eta} \\left[ \\eta \\frac{1 + \\eta + \\eta^2}{(1 - \\eta)^3} \\right] \\\\[0.6em]\n\\implies q(\\eta) & = \\frac{(1 + 2\\eta)^2}{(1 - \\eta)^4} \\end{align}`;
const parts = input.split(regex);
expect(parts.length).toBe(3);
expect(parts[0]).toContain('\\beta');
expect(parts[1]).toBe('\\\\[0.6em]');
expect(parts[2]).toContain('\\implies');
});
it('should split rows when there is no dimension parameter', () => {
const regex = /(\\\\(?:\s*\[[^\]]*\])?)/;
const input = `row1 \\\\ row2`;
const parts = input.split(regex);
expect(parts).toEqual(['row1 ', '\\\\', ' row2']);
});
});