import { EditorSelection } from '@codemirror/state'; import { insertHorizontalRule, insertOrIncreaseIndent, toggleBlockQuote, toggleBolded, toggleCode, toggleHeaderLevel, toggleItalicized, toggleMath, updateLink, } from '../editorCommands/markdownCommands'; import createTestEditor from '../testing/createTestEditor'; import { blockMathTagName } from '../extensions/markdownMathExtension'; describe('markdownCommands', () => { jest.retryTimes(2); it('should toggle only continuously selected lines as blockquotes', async () => { const initialText = 'First line\nSecond line\nThird line\nFourth line'; const editor = await createTestEditor( initialText, EditorSelection.range(0, initialText.indexOf('\nFourth line')), [], ); toggleBlockQuote(editor); expect(editor.state.doc.toString()).toBe('> First line\n> Second line\n> Third line\nFourth line'); toggleBlockQuote(editor); expect(editor.state.doc.toString()).toBe(initialText); }); it('should toggle all selections as blockquotes when selected lines are not continuous', async () => { const initialText = 'First line\nSecond line\nThird line\nFourth line'; const thirdLineStart = initialText.indexOf('Third line'); const fourthLineStart = initialText.indexOf('Fourth line'); const editor = await createTestEditor( initialText, [ EditorSelection.range(0, 'First line'.length), EditorSelection.range(thirdLineStart, thirdLineStart + 'Third line'.length), EditorSelection.range(fourthLineStart, initialText.length), ], [], ); toggleBlockQuote(editor); expect(editor.state.doc.toString()).toBe('> First line\nSecond line\n> Third line\n> Fourth line'); toggleBlockQuote(editor); expect(editor.state.doc.toString()).toBe(initialText); }); it('should bold/italicize everything selected', async () => { const initialDocText = 'Testing...'; const editor = await createTestEditor( initialDocText, EditorSelection.range(0, initialDocText.length), [], ); toggleBolded(editor); let mainSel = editor.state.selection.main; const boldedText = '**Testing...**'; expect(editor.state.doc.toString()).toBe(boldedText); expect(mainSel.from).toBe(0); expect(mainSel.to).toBe(boldedText.length); toggleBolded(editor); mainSel = editor.state.selection.main; expect(editor.state.doc.toString()).toBe(initialDocText); expect(mainSel.from).toBe(0); expect(mainSel.to).toBe(initialDocText.length); toggleItalicized(editor); expect(editor.state.doc.toString()).toBe('*Testing...*'); toggleItalicized(editor); expect(editor.state.doc.toString()).toBe('Testing...'); }); it.each([ { name: 'bolding bullet lists line by line', initialDocText: '- one\n- two', syntaxNodes: ['BulletList'], toggleCommand: toggleBolded, expectedAfterFirstToggle: '- **one**\n- **two**', }, { name: 'bolding bullet lists (alternate format, with indentation) line by line', initialDocText: '+ one\n\t+ two', syntaxNodes: ['BulletList'], toggleCommand: toggleBolded, expectedAfterFirstToggle: '+ **one**\n\t+ **two**', }, { name: 'italicizing ordered lists line by line', initialDocText: '1. one\n2. two', syntaxNodes: ['OrderedList'], toggleCommand: toggleItalicized, expectedAfterFirstToggle: '1. *one*\n2. *two*', }, { name: 'bolding checklist content while preserving markers', initialDocText: '- [ ] one\n- [x] two', syntaxNodes: ['BulletList'], toggleCommand: toggleBolded, expectedAfterFirstToggle: '- [ ] **one**\n- [x] **two**', }, ])('should support $name', async ({ initialDocText, syntaxNodes, toggleCommand, expectedAfterFirstToggle }) => { const editor = await createTestEditor( initialDocText, EditorSelection.range(0, initialDocText.length), syntaxNodes, ); toggleCommand(editor); expect(editor.state.doc.toString()).toBe(expectedAfterFirstToggle); toggleCommand(editor); expect(editor.state.doc.toString()).toBe(initialDocText); }); it.each([ ['trailing', 'ABC ', '**ABC** '], ['leading', ' ABC', ' **ABC**'], ['both leading and trailing', ' ABC ', ' **ABC** '], ])('should place formatting markers inside %s whitespace', async (_label, input, expected) => { const editor = await createTestEditor( input, EditorSelection.range(0, input.length), [], ); toggleBolded(editor); expect(editor.state.doc.toString()).toBe(expected); expect(editor.state.selection.main).toMatchObject({ from: 0, to: expected.length, }); }); it('should wrap fenced code block multiline selections as a whole region', async () => { const initialDocText = '```\none\ntwo\n```'; const editor = await createTestEditor( initialDocText, EditorSelection.range(0, initialDocText.length), ['FencedCode'], ); toggleBolded(editor); expect(editor.state.doc.toString()).toBe('**```\none\ntwo\n```**'); }); it('should apply bold to blockquote list content without wrapping markers', async () => { const initialDocText = '> - one\n> - two'; const editor = await createTestEditor( initialDocText, EditorSelection.range(0, initialDocText.length), ['Blockquote', 'BulletList'], ); toggleBolded(editor); expect(editor.state.doc.toString()).toBe('> - **one**\n> - **two**'); }); it('should preserve blank lines when bolding multiline list selections', async () => { const initialDocText = '- one\n\n- two'; const editor = await createTestEditor( initialDocText, EditorSelection.range(0, initialDocText.length), ['BulletList'], ); toggleBolded(editor); expect(editor.state.doc.toString()).toBe('- **one**\n\n- **two**'); }); it('for a cursor, bolding, then italicizing, should produce a bold-italic region', async () => { const initialDocText = ''; const editor = await createTestEditor( initialDocText, EditorSelection.cursor(0), [], ); toggleBolded(editor); toggleItalicized(editor); expect(editor.state.doc.toString()).toBe('******'); editor.dispatch(editor.state.replaceSelection('Test')); expect(editor.state.doc.toString()).toBe('***Test***'); toggleItalicized(editor); editor.dispatch(editor.state.replaceSelection(' Test')); expect(editor.state.doc.toString()).toBe('***Test*** Test'); }); it('toggling math should both create and navigate out of math regions', async () => { const initialDocText = 'Testing... '; const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []); toggleMath(editor); expect(editor.state.doc.toString()).toBe('Testing... $$'); expect(editor.state.selection.main.empty).toBe(true); editor.dispatch(editor.state.replaceSelection('3 + 3 \\neq 5')); expect(editor.state.doc.toString()).toBe('Testing... $3 + 3 \\neq 5$'); toggleMath(editor); editor.dispatch(editor.state.replaceSelection('...')); expect(editor.state.doc.toString()).toBe('Testing... $3 + 3 \\neq 5$...'); }); it('toggling inline code should both create and navigate out of an inline code region', async () => { const initialDocText = 'Testing...\n\n'; const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []); toggleCode(editor); editor.dispatch(editor.state.replaceSelection('f(x) = ...')); toggleCode(editor); editor.dispatch(editor.state.replaceSelection(' is a function.')); expect(editor.state.doc.toString()).toBe('Testing...\n\n`f(x) = ...` is a function.'); }); it('should set headers to the proper levels (when toggling)', async () => { const initialDocText = 'Testing...\nThis is a test.'; const editor = await createTestEditor(initialDocText, EditorSelection.cursor('Testing...'.length), []); toggleHeaderLevel(1)(editor); let mainSel = editor.state.selection.main; expect(editor.state.doc.toString()).toBe('# Testing...\nThis is a test.'); expect(mainSel.empty).toBe(true); expect(mainSel.from).toBe('# Testing...'.length); toggleHeaderLevel(2)(editor); mainSel = editor.state.selection.main; expect(editor.state.doc.toString()).toBe('## Testing...\nThis is a test.'); expect(mainSel.empty).toBe(true); expect(mainSel.from).toBe('## Testing...'.length); toggleHeaderLevel(2)(editor); mainSel = editor.state.selection.main; expect(editor.state.doc.toString()).toEqual(initialDocText); expect(mainSel.empty).toBe(true); expect(mainSel.from).toBe('Testing...'.length); }); it('headers should toggle properly within block quotes', async () => { const initialDocText = 'Testing...\n\n> This is a test.\n> ...a test'; const editor = await createTestEditor( initialDocText, EditorSelection.cursor('Testing...\n\n> This'.length), ['Blockquote'], ); toggleHeaderLevel(1)(editor); const mainSel = editor.state.selection.main; expect(editor.state.doc.toString()).toBe( 'Testing...\n\n> # This is a test.\n> ...a test', ); expect(mainSel.empty).toBe(true); expect(mainSel.from).toBe('Testing...\n\n> # This'.length); toggleHeaderLevel(3)(editor); expect(editor.state.doc.toString()).toBe( 'Testing...\n\n> ### This is a test.\n> ...a test', ); }); it('block math should be created correctly within block quotes', async () => { const initialDocText = 'Testing...\n\n> This is a test.\n> y = mx + b\n> ...a test'; const editor = await createTestEditor( initialDocText, EditorSelection.range( 'Testing...\n\n> This'.length, 'Testing...\n\n> This is a test.\n> y = mx + b'.length, ), ['Blockquote'], ); toggleMath(editor); // Toggling math should surround the content in '$$'s const mainSel = editor.state.selection.main; expect(editor.state.doc.toString()).toEqual( 'Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$\n> ...a test', ); expect(mainSel.from).toBe('Testing...\n\n'.length); expect(mainSel.to).toBe('Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$'.length); }); it('block math should be correctly removed within block quotes', async () => { const initialDocText = 'Testing...\n\n> $$\n> This is a test.\n> y = mx + b\n> $$\n> ...a test'; const editor = await createTestEditor( initialDocText, EditorSelection.cursor('Testing...\n\n> $$\n> This is'.length), ['Blockquote', blockMathTagName], ); // Toggling math should remove the '$$'s toggleMath(editor); const mainSel = editor.state.selection.main; expect(editor.state.doc.toString()).toEqual('Testing...\n\n> This is a test.\n> y = mx + b\n> ...a test'); expect(mainSel.from).toBe('Testing...\n\n'.length); expect(mainSel.to).toBe('Testing...\n\n> This is a test.\n> y = mx + b'.length); }); it('updateLink should replace link titles and isolate URLs if no title is given', async () => { const initialDocText = '[foo](http://example.com/)'; const editor = await createTestEditor(initialDocText, EditorSelection.cursor('[f'.length), ['Link']); updateLink('bar', 'https://example.com/')(editor); expect(editor.state.doc.toString()).toBe( '[bar](https://example.com/)', ); updateLink('', 'https://example.com/')(editor); expect(editor.state.doc.toString()).toBe( 'https://example.com/', ); }); it('toggling math twice, starting on a line with content, should a math block', async () => { const initialDocText = 'Testing... '; const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []); toggleMath(editor); toggleMath(editor); editor.dispatch(editor.state.replaceSelection('f(x) = ...')); expect(editor.state.doc.toString()).toBe('Testing... \n$$\nf(x) = ...\n$$'); }); it('toggling math twice on an empty line should create an empty math block', async () => { const initialDocText = 'Testing...\n\n'; const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []); toggleMath(editor); toggleMath(editor); editor.dispatch(editor.state.replaceSelection('f(x) = ...')); expect(editor.state.doc.toString()).toBe('Testing...\n\n$$\nf(x) = ...\n$$'); }); it('toggling code twice on an empty line should create an empty code block', async () => { const initialDocText = 'Testing...\n\n'; const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), []); // Toggling code twice should create a block code region toggleCode(editor); toggleCode(editor); editor.dispatch(editor.state.replaceSelection('f(x) = ...')); expect(editor.state.doc.toString()).toBe('Testing...\n\n```\nf(x) = ...\n```'); toggleCode(editor); expect(editor.state.doc.toString()).toBe('Testing...\n\nf(x) = ...\n'); }); it('toggling math twice inside a block quote should produce an empty math block', async () => { const initialDocText = '> Testing...> \n> '; const editor = await createTestEditor(initialDocText, EditorSelection.cursor(initialDocText.length), ['Blockquote']); toggleMath(editor); toggleMath(editor); editor.dispatch(editor.state.replaceSelection('f(x) = ...')); expect(editor.state.doc.toString()).toBe( '> Testing...> \n> \n> $$\n> f(x) = ...\n> $$', ); // If we toggle math again, everything from the start of the line with the first // $$ to the end of the document should be selected. toggleMath(editor); const sel = editor.state.selection.main; expect(sel.from).toBe('> Testing...> \n> \n'.length); expect(sel.to).toBe(editor.state.doc.length); }); it('insertOrIncreaseIndent should indent when text is selected', async () => { const initialText = '> Testing...\n> Test.'; const editor = await createTestEditor( initialText, EditorSelection.range(0, initialText.length), ['Blockquote'], ); insertOrIncreaseIndent(editor); expect(editor.state.doc.toString()).toBe('> \tTesting...\n> \tTest.'); }); it('insertOrIncreaseIndent should insert tabs when selection is empty, in a paragraph', async () => { const initialText = 'This is a test\nof indentation.'; const editor = await createTestEditor( initialText, EditorSelection.cursor(initialText.length), [], ); insertOrIncreaseIndent(editor); const finalText = editor.state.doc.toString(); // Should add tab character at the cursor expect(finalText).toBe('This is a test\nof indentation.\t'); // Should move the selection after the tab expect(editor.state.selection.ranges).toHaveLength(1); expect(editor.state.selection.main).toMatchObject({ from: finalText.length, to: finalText.length, }); }); it('insertOrIncreaseIndent should preserve the cursor location when in a list', async () => { const initialText = '- a\n- b\n- c'; const editor = await createTestEditor( initialText, EditorSelection.cursor(5), // In the 2nd list item ['BulletList'], ); insertOrIncreaseIndent(editor); expect(editor.state.doc.toString()).toBe('- a\n\t- b\n- c'); expect(editor.state.selection.main).toMatchObject({ // The indent unit is a single tab, which has length 1. from: 6, to: 6, }); }); it('insertHorizontalRule should insert a horizontal rule after the current line', async () => { const initialText = 'testing\n\n> this is a test\n> '; const editor = await createTestEditor( initialText, EditorSelection.cursor(0), [], ); // Add a second selection editor.dispatch({ selection: editor.state.selection.addRange(EditorSelection.cursor(initialText.length)), }); expect(editor.state.selection.ranges).toHaveLength(2); insertHorizontalRule(editor); expect(editor.state.doc.toString()).toBe('testing\n* * *\n\n> this is a test\n> * * *'); expect(editor.state.selection.ranges).toMatchObject([{ from: 'testing\n* * *'.length, empty: true, }, { from: editor.state.doc.length, empty: true, }]); insertHorizontalRule(editor); expect(editor.state.doc.toString()).toBe('testing\n* * *\n* * *\n\n> this is a test\n> * * *\n> * * *'); }); });