diff --git a/tests/e2e/markdown-editor.test.e2e.ts b/tests/e2e/markdown-editor.test.e2e.ts index 1e30b8d3b9..35e9de2ea6 100644 --- a/tests/e2e/markdown-editor.test.e2e.ts +++ b/tests/e2e/markdown-editor.test.e2e.ts @@ -109,7 +109,7 @@ test('markdown indentation', async ({page}) => { }); test('markdown list continuation', async ({page}) => { - const initText = `* first\n* second\n* third\n* last`; + const initText = `* first\n* second`; const response = await page.goto('/user2/repo1/issues/new'); expect(response?.status()).toBe(200); @@ -119,25 +119,20 @@ test('markdown list continuation', async ({page}) => { const indent = page.locator('button[data-md-action="indent"]'); await textarea.fill(initText); - // Test continuation of '* ' prefix - await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond'))); + // Test continuation of ' * ' prefix + await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('rst'), it.value.indexOf('rst'))); + await indent.click(); await textarea.press('End'); await textarea.press('Enter'); - await textarea.pressSequentially('middle'); - await expect(textarea).toHaveValue(`* first\n* second\n* middle\n* third\n* last`); - - // Test continuation of ' * ' prefix - await indent.click(); - await textarea.press('Enter'); await textarea.pressSequentially('muddle'); - await expect(textarea).toHaveValue(`* first\n* second\n${tab}* middle\n${tab}* muddle\n* third\n* last`); + await expect(textarea).toHaveValue(`${tab}* first\n${tab}* muddle\n* second`); // Test breaking in the middle of a line await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.lastIndexOf('ddle'), it.value.lastIndexOf('ddle'))); await textarea.pressSequentially('tate'); await textarea.press('Enter'); await textarea.pressSequentially('me'); - await expect(textarea).toHaveValue(`* first\n* second\n${tab}* middle\n${tab}* mutate\n${tab}* meddle\n* third\n* last`); + await expect(textarea).toHaveValue(`${tab}* first\n${tab}* mutate\n${tab}* meddle\n* second`); // Test not triggering when Shift held await textarea.fill(initText); @@ -145,35 +140,36 @@ test('markdown list continuation', async ({page}) => { await textarea.press('Shift+Enter'); await textarea.press('Enter'); await textarea.pressSequentially('...but not least'); - await expect(textarea).toHaveValue(`* first\n* second\n* third\n* last\n\n...but not least`); + await expect(textarea).toHaveValue(`* first\n* second\n\n...but not least`); // Test continuation of ordered list - await textarea.fill(`1. one\n2. two`); + await textarea.fill(`1. one`); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.press('Enter'); + await textarea.pressSequentially(' '); + await textarea.press('Enter'); await textarea.pressSequentially('three'); - await expect(textarea).toHaveValue(`1. one\n2. two\n3. three`); + await textarea.press('Enter'); + await textarea.press('Enter'); + await expect(textarea).toHaveValue(`1. one\n2. \n3. three\n\n`); // Test continuation of alternative ordered list syntax - await textarea.fill(`1) one\n2) two`); + await textarea.fill(`1) one`); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.press('Enter'); + await textarea.pressSequentially(' '); + await textarea.press('Enter'); await textarea.pressSequentially('three'); - await expect(textarea).toHaveValue(`1) one\n2) two\n3) three`); - - // Test continuation of blockquote - await textarea.fill(`> knowledge is power`); - await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.press('Enter'); - await textarea.pressSequentially('france is bacon'); - await expect(textarea).toHaveValue(`> knowledge is power\n> france is bacon`); + await textarea.press('Enter'); + await expect(textarea).toHaveValue(`1) one\n2) \n3) three\n\n`); // Test continuation of checklists - await textarea.fill(`- [ ] have a problem\n- [x] create a solution`); + await textarea.fill(`- [ ]have a problem\n- [x]create a solution`); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.press('Enter'); await textarea.pressSequentially('write a test'); - await expect(textarea).toHaveValue(`- [ ] have a problem\n- [x] create a solution\n- [ ] write a test`); + await expect(textarea).toHaveValue(`- [ ]have a problem\n- [x]create a solution\n- [ ]write a test`); // Test all conceivable syntax (except ordered lists) const prefixes = [ @@ -189,7 +185,6 @@ test('markdown list continuation', async ({page}) => { '> ', '> > ', '- [ ] ', - '- [ ]', // This does seem to render, so allow. '* [ ] ', '+ [ ] ', ]; @@ -197,8 +192,12 @@ test('markdown list continuation', async ({page}) => { await textarea.fill(`${prefix}one`); await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length)); await textarea.press('Enter'); + await textarea.pressSequentially(' '); + await textarea.press('Enter'); await textarea.pressSequentially('two'); - await expect(textarea).toHaveValue(`${prefix}one\n${prefix}two`); + await textarea.press('Enter'); + await textarea.press('Enter'); + await expect(textarea).toHaveValue(`${prefix}one\n${prefix} \n${prefix}two\n\n`); } }); diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js index 707101190c..89a252f6f3 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.js +++ b/web_src/js/features/comp/ComboMarkdownEditor.js @@ -409,13 +409,27 @@ class ComboMarkdownEditor { // Find the beginning of the current line. const lineStart = Math.max(0, value.lastIndexOf('\n', start - 1) + 1); // Find the end and extract the line. - const lineEnd = value.indexOf('\n', start); - const line = value.slice(lineStart, lineEnd === -1 ? value.length : lineEnd); + const nextLF = value.indexOf('\n', start); + const lineEnd = nextLF === -1 ? value.length : nextLF; + const line = value.slice(lineStart, lineEnd); // Match any whitespace at the start + any repeatable prefix + exactly one space after. - const prefix = line.match(/^\s*((\d+)[.)]\s|[-*+]\s+(\[[ x]\]\s?)?|(>\s+)+)?/); + const prefix = line.match(/^\s*((\d+)[.)]\s|[-*+]\s{1,4}\[[ x]\]\s?|[-*+]\s|(>\s?)+)?/); // Defer to browser if we can't do anything more useful, or if the cursor is inside the prefix. - if (!prefix || !prefix[0].length || lineStart + prefix[0].length > start) return false; + if (!prefix) return false; + const prefixLength = prefix[0].length; + if (!prefixLength || lineStart + prefixLength > start) return false; + // If the prefix is just indentation (which should always be an even number of spaces or tabs), check if a single whitespace is added to the end of the line. + // If this is the case do not leave the indentation and continue with the prefix. + if ((prefixLength % 2 === 1 && /^ +$/.test(prefix[0])) || /^\t+ $/.test(prefix[0])) { + prefix[0] = prefix[0].slice(0, prefixLength - 1); + } else if (prefixLength === lineEnd - lineStart) { + this.textarea.setSelectionRange(lineStart, lineEnd); + if (!document.execCommand('insertText', false, '\n')) { + this.textarea.setRangeText('\n'); + } + return true; + } // Insert newline + prefix. let text = `\n${prefix[0]}`;