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('('); // A plain bracket must not: the dictionary would stretch it to the row's tallest. expect(astToMathml(parseLatex('C(b) = \\frac{1}{2}'))).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( '' + 'ϕref' + '=1π', ); 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'); }); });