From 1142dbb2e4422fc7064b8517773edfea75624a30 Mon Sep 17 00:00:00 2001 From: mck09 Date: Sun, 30 Aug 2026 15:30:57 +0200 Subject: [PATCH 1/3] fix: drop the leading whitespace after a hard line break CommonMark ignores leading spaces at the start of the line following a hard line break. The br rule consumed the break but not that indentation, so it stayed on the following text token. Fixes CommonMark examples 636 and 637. Neither marked's differ nor an exact comparison can see this: the differ ignores whitespace, and exact still differs on
versus
. Under the spec's own normalize.py the count goes from 28 to 26. --- src/rules.ts | 2 +- test/unit/marked.test.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/rules.ts b/src/rules.ts index aeb880c597..715e94871c 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -282,7 +282,7 @@ const blockPedantic: Record = { const escape = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/; const inlineCode = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/; -const br = /^( {2,}|\\)\n(?!\s*$)/; +const br = /^( {2,}|\\)\n(?!\s*$) */; const inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ { }); }); + describe('hard line break', () => { + it('should drop the leading whitespace of the continuation line', () => { + assert.strictEqual(marked.parse('foo \n bar\n'), '

foo
bar

\n'); + }); + + it('should do the same for a backslash break', () => { + assert.strictEqual(marked.parse('foo\\\n bar\n'), '

foo
bar

\n'); + }); + + it('should keep a soft break unchanged', () => { + assert.strictEqual(marked.parse('foo\nbar\n'), '

foo\nbar

\n'); + }); + }); + describe('changeDefaults', () => { it('should change global defaults', async() => { const { defaults, setOptions } = await import('../../lib/marked.esm.js'); From 23425868c7dfd5df328bbca1b59a2f213f5f950c Mon Sep 17 00:00:00 2001 From: mck09 Date: Tue, 1 Sep 2026 19:46:19 +0200 Subject: [PATCH 2/3] fix: also drop a leading tab after a hard line break The rule only consumed spaces, so a continuation line indented with a tab kept it. Both the reference implementation and markdown-it drop tabs there as well. --- src/rules.ts | 2 +- test/unit/marked.test.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/rules.ts b/src/rules.ts index 715e94871c..568fca4e52 100644 --- a/src/rules.ts +++ b/src/rules.ts @@ -282,7 +282,7 @@ const blockPedantic: Record = { const escape = /^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/; const inlineCode = /^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/; -const br = /^( {2,}|\\)\n(?!\s*$) */; +const br = /^( {2,}|\\)\n(?!\s*$)[ \t]*/; const inlineText = /^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\ { assert.strictEqual(marked.parse('foo\\\n bar\n'), '

foo
bar

\n'); }); + it('should drop a leading tab as well', () => { + assert.strictEqual(marked.parse('foo \n\tbar\n'), '

foo
bar

\n'); + }); + + it('should drop a mix of spaces and tabs', () => { + assert.strictEqual(marked.parse('foo \n \t bar\n'), '

foo
bar

\n'); + }); + + it('should keep the leading tab of a soft break', () => { + assert.strictEqual(marked.parse('foo\n\tbar\n'), '

foo\n\tbar

\n'); + }); + it('should keep a soft break unchanged', () => { assert.strictEqual(marked.parse('foo\nbar\n'), '

foo\nbar

\n'); }); From 382f84413fc846f9b7247711b02822bfac8ac8b5 Mon Sep 17 00:00:00 2001 From: mck09 Date: Tue, 1 Sep 2026 20:40:16 +0200 Subject: [PATCH 3/3] test: move the hard line break tests to test/specs/new Uses renderExact so the fixture compares exactly, which htmlIsEqual does not, and the assertions keep their teeth outside marked.test.js. --- .../new/hardbreak_leading_whitespace.html | 8 ++++++ .../specs/new/hardbreak_leading_whitespace.md | 20 ++++++++++++++ test/unit/marked.test.js | 26 ------------------- 3 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 test/specs/new/hardbreak_leading_whitespace.html create mode 100644 test/specs/new/hardbreak_leading_whitespace.md diff --git a/test/specs/new/hardbreak_leading_whitespace.html b/test/specs/new/hardbreak_leading_whitespace.html new file mode 100644 index 0000000000..69bbbf9f38 --- /dev/null +++ b/test/specs/new/hardbreak_leading_whitespace.html @@ -0,0 +1,8 @@ +

foo
bar

+

foo
bar

+

foo
bar

+

foo
bar

+

foo + bar

+

foo +bar

diff --git a/test/specs/new/hardbreak_leading_whitespace.md b/test/specs/new/hardbreak_leading_whitespace.md new file mode 100644 index 0000000000..14ec0ca7a6 --- /dev/null +++ b/test/specs/new/hardbreak_leading_whitespace.md @@ -0,0 +1,20 @@ +--- +renderExact: true +--- +foo + bar + +foo\ + bar + +foo + bar + +foo + bar + +foo + bar + +foo +bar diff --git a/test/unit/marked.test.js b/test/unit/marked.test.js index 182817eaed..5d1449b4aa 100644 --- a/test/unit/marked.test.js +++ b/test/unit/marked.test.js @@ -22,32 +22,6 @@ describe('marked unit', () => { }); }); - describe('hard line break', () => { - it('should drop the leading whitespace of the continuation line', () => { - assert.strictEqual(marked.parse('foo \n bar\n'), '

foo
bar

\n'); - }); - - it('should do the same for a backslash break', () => { - assert.strictEqual(marked.parse('foo\\\n bar\n'), '

foo
bar

\n'); - }); - - it('should drop a leading tab as well', () => { - assert.strictEqual(marked.parse('foo \n\tbar\n'), '

foo
bar

\n'); - }); - - it('should drop a mix of spaces and tabs', () => { - assert.strictEqual(marked.parse('foo \n \t bar\n'), '

foo
bar

\n'); - }); - - it('should keep the leading tab of a soft break', () => { - assert.strictEqual(marked.parse('foo\n\tbar\n'), '

foo\n\tbar

\n'); - }); - - it('should keep a soft break unchanged', () => { - assert.strictEqual(marked.parse('foo\nbar\n'), '

foo\nbar

\n'); - }); - }); - describe('changeDefaults', () => { it('should change global defaults', async() => { const { defaults, setOptions } = await import('../../lib/marked.esm.js');