import { describe, it, expect } from 'vitest';
import { JSDOM } from 'jsdom';
import { parseLatex, astToLatex } from '../../src/lib/math/latex';
import { astToMathml, mathmlDocument, parseMathml } from '../../src/lib/math/mathml';
import { astToOmml, ommlDocument, parseOmml } from '../../src/lib/math/omml';
const parseXml = (xml: string): Element => {
const doc = new JSDOM(xml, { contentType: 'text/xml' }).window.document;
return doc.documentElement;
};
// LaTeX is the formula node's only stored form, so every other direction has to land
// back on it unchanged — otherwise editing an imported formula rewrites it.
const roundTrip = (src: string) => astToLatex(parseLatex(src));
describe('LaTeX parse ↔ serialize', () => {
it('is idempotent over the constructs the editor offers', () => {
for (const src of [
'a+b',
'\\frac{1}{2}',
'\\frac{a+1}{2\\pi }',
'x^{2}',
'x_{i}^{2}',
'\\phi _{ref}',
'\\sqrt{x+y}',
'\\sqrt[3]{x}',
'\\left(\\frac{1}{2}\\right)',
'\\left|\\gamma \\right|',
'\\sum_{i=1}^{n} x_{i}',
'\\int_{0}^{\\infty } f',
'\\bar{x}',
'\\vec{v}',
'\\sin \\left(x\\right)',
'\\begin{pmatrix}a & b \\\\ c & d\\end{pmatrix}',
]) {
expect(roundTrip(src), src).toBe(src);
// A second pass must not drift either.
expect(roundTrip(roundTrip(src)), src).toBe(src);
}
});
it('keeps an unknown macro as readable text instead of dropping it', () => {
expect(roundTrip('\\wibble')).toBe('\\text{wibble}');
});
});
describe('MathML', () => {
it('emits the elements the browser needs to typeset', () => {
expect(astToMathml(parseLatex('\\frac{1}{2}'))).toBe('12');
expect(astToMathml(parseLatex('x^2'))).toBe('x2');
expect(astToMathml(parseLatex('\\sqrt{x}'))).toBe('x');
// A sum's limits sit under/over the sign, an integral's beside it.
expect(astToMathml(parseLatex('\\sum_{i}^{n} x'))).toContain('');
expect(astToMathml(parseLatex('\\int_{0}^{1} x'))).toContain('');
// Brackets must be stretchy or they stay glyph-height next to a fraction.
expect(astToMathml(parseLatex('\\left(x\\right)'))).toContain('(');
});
it('round-trips through the document element', () => {
const src = '\\frac{a+1}{2\\pi }';
const el = parseXml(mathmlDocument(parseLatex(src), false, src));
const got = parseMathml(el);
expect(got.latex).toBe(src); // our own annotation is preferred verbatim
expect(astToLatex(got.ast)).toBe(src); // and the presentation markup agrees
expect(got.display).toBe(false);
});
it('reads foreign MathML that carries no annotation', () => {
const el = parseXml(
'',
);
const got = parseMathml(el);
expect(got.latex).toBeNull();
expect(astToLatex(got.ast)).toBe('\\phi _{\\text{ref}}=\\frac{1}{\\pi }');
expect(got.display).toBe(true);
});
});
describe('OMML', () => {
it('emits Word\'s elements', () => {
expect(astToOmml(parseLatex('\\frac{1}{2}'))).toContain('');
expect(astToOmml(parseLatex('x_i'))).toContain('');
expect(astToOmml(parseLatex('\\sqrt{x}'))).toContain('');
expect(astToOmml(parseLatex('\\left[x\\right]'))).toContain('');
expect(astToOmml(parseLatex('\\sum_{i}^{n} x'))).toContain('');
// A function name has to be marked upright or Word renders "sin" italic.
expect(astToOmml(parseLatex('\\sin x'))).toContain('');
});
it('round-trips our own output', () => {
for (const src of ['\\frac{a}{b}', 'x_{i}^{2}', '\\sqrt[3]{x}', '\\sum_{i=1}^{n} x', '\\left(a+b\\right)']) {
const el = parseXml(ommlDocument(parseLatex(src)));
expect(astToLatex(parseOmml(el).ast), src).toBe(src);
}
});
// Lifted verbatim from a real Word document; markup of this shape was dropped on
// import before this feature existed.
it('reads Word\'s own markup', () => {
const NS = ' xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"' +
' xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"';
const sub = parseXml(
`` +
'\u03D5ref',
);
expect(astToLatex(parseOmml(sub).ast)).toBe('\\phi _{ref}');
const frac = parseXml(
`` +
'' +
'\u03B32' +
'2\u03C0' +
'arcsin' +
'\u03B2',
);
const got = parseOmml(frac);
expect(got.display).toBe(true);
expect(astToLatex(got.ast)).toBe(
'\\frac{\\left|\\gamma \\right|^{2}}{2\\pi }\\arcsin \\left(\\beta \\right)',
);
});
it('splits a run that holds a whole expression into typed tokens', () => {
const NS = ' xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"';
const el = parseXml(`2*L-1`);
expect(astToMathml(parseOmml(el).ast))
.toBe('2*L-1');
});
});