import { syntaxTree } from '@codemirror/language'; import { SyntaxNode } from '@lezer/common'; import { EditorSelection, EditorState } from '@codemirror/state'; import { blockMathTagName, inlineMathContentTagName, inlineMathTagName } from './markdownMathParser'; import createTestEditor from '../testUtil/createTestEditor'; // Creates an EditorState with math and markdown extensions const createEditorState = async (initialText: string, expectedTags: string[]): Promise => { return (await createTestEditor(initialText, EditorSelection.cursor(0), expectedTags)).state; }; // Returns a list of all nodes with the given name in the given editor's syntax tree. // Attempts to create the syntax tree if it doesn't exist. const findNodesWithName = (editor: EditorState, nodeName: string) => { const result: SyntaxNode[] = []; syntaxTree(editor).iterate({ enter: (node) => { if (node.name === nodeName) { result.push(node.node); } }, }); return result; }; describe('markdownMathParser', () => { jest.retryTimes(2); it('should parse inline math that contains space characters, numbers, and symbols', async () => { const documentText = '$3 + 3$'; const editor = await createEditorState(documentText, [inlineMathTagName, 'number']); const inlineMathNodes = findNodesWithName(editor, inlineMathTagName); const inlineMathContentNodes = findNodesWithName(editor, inlineMathContentTagName); // There should only be one inline node expect(inlineMathNodes.length).toBe(1); expect(inlineMathNodes[0].from).toBe(0); expect(inlineMathNodes[0].to).toBe(documentText.length); // The content tag should be replaced by the internal sTeX parser expect(inlineMathContentNodes.length).toBe(0); }); it('should parse comment within multi-word inline math', async () => { const beforeMath = '# Testing!\n\nThis is a test of '; const mathRegion = '$\\TeX % TeX Comment!$'; const afterMath = ' formatting.'; const documentText = `${beforeMath}${mathRegion}${afterMath}`; const editor = await createEditorState(documentText, [inlineMathTagName, 'comment']); const inlineMathNodes = findNodesWithName(editor, inlineMathTagName); const blockMathNodes = findNodesWithName(editor, blockMathTagName); const commentNodes = findNodesWithName(editor, 'comment'); expect(inlineMathNodes.length).toBe(1); expect(blockMathNodes.length).toBe(0); expect(commentNodes.length).toBe(1); expect(inlineMathNodes[0].from).toBe(beforeMath.length); expect(inlineMathNodes[0].to).toBe(beforeMath.length + mathRegion.length); }); it('shouldn\'t start inline math if there is no ending $', async () => { const documentText = '*This* is a $test\n\nof inline math$...'; const editor = await createEditorState(documentText, ['Emphasis']); const inlineMathNodes = findNodesWithName(editor, inlineMathTagName); // Math should end if there is no matching '$'. expect(inlineMathNodes.length).toBe(0); }); it('shouldn\'t start if math would have a space just after the $', async () => { const documentText = 'This *is* a $ test of inline math$...\n\n$Testing... $...'; const editor = await createEditorState(documentText, ['Emphasis']); expect(findNodesWithName(editor, inlineMathTagName).length).toBe(0); }); it('shouldn\'t start inline math if $ is escaped', async () => { const documentText = 'This is a \\$test of inline math$... **Testing...**'; const editor = await createEditorState(documentText, ['StrongEmphasis']); expect(findNodesWithName(editor, inlineMathTagName).length).toBe(0); }); it('should correctly parse document containing just block math', async () => { const documentText = '$$\n\t\\{ 1, 1, 2, 3, 5, ... \\} % Comment\n$$'; const editor = await createEditorState(documentText, [blockMathTagName, 'comment']); const inlineMathNodes = findNodesWithName(editor, inlineMathTagName); const blockMathNodes = findNodesWithName(editor, blockMathTagName); expect(inlineMathNodes.length).toBe(0); expect(blockMathNodes.length).toBe(1); expect(blockMathNodes[0].from).toBe(0); expect(blockMathNodes[0].to).toBe(documentText.length); }); it('should correctly parse comment in block math', async () => { const startingText = '$$ % Testing...\n\t\\text{Test.}\n$$'; const afterMath = '\nTest.'; const editor = await createEditorState(startingText + afterMath, ['comment', blockMathTagName]); const inlineMathNodes = findNodesWithName(editor, inlineMathTagName); const blockMathNodes = findNodesWithName(editor, blockMathTagName); const texParserComments = findNodesWithName(editor, 'comment'); expect(inlineMathNodes.length).toBe(0); expect(blockMathNodes.length).toBe(1); expect(texParserComments.length).toBe(1); expect(blockMathNodes[0].from).toBe(0); expect(blockMathNodes[0].to).toBe(startingText.length); expect(texParserComments[0]).toMatchObject({ from: '$$ '.length, to: '$$ % Testing...'.length, }); }); it('should extend block math without ending tag to end of document', async () => { const beforeMath = '# Testing...\n\n'; const documentText = `${beforeMath}$$\n\t\\text{Testing...}\n\n\t3 + 3 = 6 % Comment`; const editor = await createEditorState(documentText, ['ATXHeading1', blockMathTagName, 'comment']); const blockMathNodes = findNodesWithName(editor, blockMathTagName); expect(blockMathNodes.length).toBe(1); expect(blockMathNodes[0].from).toBe(beforeMath.length); expect(blockMathNodes[0].to).toBe(documentText.length); }); it('should parse block math declared on a single line', async () => { const documentText = '$$ Test. $$'; const editor = await createEditorState(documentText, [blockMathTagName]); const blockMathNodes = findNodesWithName(editor, blockMathTagName); expect(blockMathNodes.length).toBe(1); expect(blockMathNodes[0].from).toBe(0); expect(blockMathNodes[0].to).toBe(documentText.length); }); });