diff --git a/resources/js/lib/markdown-format.ts b/resources/js/lib/markdown-format.ts index b193d03df..fb7bfa962 100644 --- a/resources/js/lib/markdown-format.ts +++ b/resources/js/lib/markdown-format.ts @@ -72,8 +72,10 @@ export function formatMarkdown( } const prefix = PREFIXES[format] ?? (() => ''); - const from = value.lastIndexOf('\n', start - 1) + 1; - const lineEnd = value.indexOf('\n', end); + const from = start === 0 ? 0 : value.lastIndexOf('\n', start - 1) + 1; + // A selection ending just after a newline does not include the next line. + const lastSelected = end > start && value[end - 1] === '\n' ? end - 1 : end; + const lineEnd = value.indexOf('\n', lastSelected); const to = lineEnd === -1 ? value.length : lineEnd; const insert = value .slice(from, to) diff --git a/tests/Unit/MarkdownFormat.test.ts b/tests/Unit/MarkdownFormat.test.ts index b6e0fb6bf..77e3cbffb 100644 --- a/tests/Unit/MarkdownFormat.test.ts +++ b/tests/Unit/MarkdownFormat.test.ts @@ -35,3 +35,21 @@ await test('prefixes every line the selection touches', () => { assert.deepEqual(apply('What shipped', 3, 3, 'heading'), ['## What shipped', '']); assert.deepEqual(apply('a\nquote', 2, 2, 'quote'), ['a\n> quote', '']); }); + +await test('does not prefix the next line when selection ends after a newline', () => { + assert.deepEqual(apply('first\nsecond', 0, 6, 'bulleted'), [ + '- first\nsecond', + '', + ]); + assert.deepEqual(apply('first\nsecond\nthird', 0, 13, 'numbered'), [ + '1. first\n2. second\nthird', + '', + ]); +}); + +void test('formats a selected first-line newline without an invalid range', () => { + assert.deepEqual(apply('\nsecond', 0, 1, 'bulleted'), [ + '- \nsecond', + '', + ]); +});