From 6edc2b16367e4f412458642abb9720a669d29a02 Mon Sep 17 00:00:00 2001 From: Hashim1999164 <64767361+Hashim1999164@users.noreply.github.com> Date: Sat, 8 Aug 2026 01:35:06 +0500 Subject: [PATCH] fix: place task checkboxes after list loose is finalized Loose nested task lists could leave checkboxes outside

because checkbox placement used list.loose before spacer detection finished. Compute the final loose state first, then place checkboxes. Fixes #4045 --- src/Tokenizer.ts | 21 +++++++++++++-------- test/specs/new/list_loose_nested_tasks.html | 20 ++++++++++++++++++++ test/specs/new/list_loose_nested_tasks.md | 11 +++++++++++ 3 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 test/specs/new/list_loose_nested_tasks.html create mode 100644 test/specs/new/list_loose_nested_tasks.md diff --git a/src/Tokenizer.ts b/src/Tokenizer.ts index d4176e0650..b651849810 100644 --- a/src/Tokenizer.ts +++ b/src/Tokenizer.ts @@ -416,9 +416,22 @@ export class _Tokenizer { list.raw = list.raw.trimEnd(); // Item child tokens handled here at end because we needed to have the final item to trim it first + // First pass: tokenize items and finalize list.loose from spacers before placing checkboxes for (const item of list.items) { this.lexer.state.top = false; item.tokens = this.lexer.blockTokens(item.text, []); + + if (!list.loose) { + // Check if list should be loose + const spacers = item.tokens.filter(t => t.type === 'space'); + const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => this.rules.other.anyLine.test(t.raw)); + + list.loose = hasMultipleLineBreaks; + } + } + + // Second pass: place task checkboxes using the final list.loose + for (const item of list.items) { const itemToken = item.tokens[0]; if (item.task && (itemToken?.type === 'text' || itemToken?.type === 'paragraph')) { // Remove checkbox markdown from item tokens @@ -460,14 +473,6 @@ export class _Tokenizer { } else if (item.task) { item.task = false; } - - if (!list.loose) { - // Check if list should be loose - const spacers = item.tokens.filter(t => t.type === 'space'); - const hasMultipleLineBreaks = spacers.length > 0 && spacers.some(t => this.rules.other.anyLine.test(t.raw)); - - list.loose = hasMultipleLineBreaks; - } } // Set all items to loose if list is loose diff --git a/test/specs/new/list_loose_nested_tasks.html b/test/specs/new/list_loose_nested_tasks.html new file mode 100644 index 0000000000..5a5ee8c5f1 --- /dev/null +++ b/test/specs/new/list_loose_nested_tasks.html @@ -0,0 +1,20 @@ +

diff --git a/test/specs/new/list_loose_nested_tasks.md b/test/specs/new/list_loose_nested_tasks.md new file mode 100644 index 0000000000..aef580af2d --- /dev/null +++ b/test/specs/new/list_loose_nested_tasks.md @@ -0,0 +1,11 @@ +* [ ] Prepare the project + + * [ ] Install dependencies + * [x] Update Marked + * [ ] Check the rendering +* [x] Run the tests + + * [x] Test lists + * [ ] Test tables + + * [ ] Test nested cases