From 7d952d74cafaa0f1e0442263b873ce3f1d24d27e Mon Sep 17 00:00:00 2001 From: AryanRG Date: Sat, 17 Oct 2026 09:34:39 +0530 Subject: [PATCH] Preserve task markers in indented list code --- CHANGELOG.md | 1 + markdown_it/rules_block/list.py | 27 +++++++++++-------- tests/test_port/test_misc.py | 46 +++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 040ff138..35ebc00e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* šŸ› Preserve literal task-list markers in indented code blocks inside list items. * ✨ Add `--enable-tables` to the CLI for file, standard input and interactive parsing in [#422](https://github.com/executablebooks/markdown-it-py/pull/422) * šŸ› Fix CLI interactive mode joining input lines with an extra newline, which split every line into its own paragraph and broke hard line breaks, in [#172](https://github.com/executablebooks/markdown-it-py/issues/172) * šŸ› Fix trimming and splitting with the Python whitespace set instead of the CommonMark one, which dropped U+001C–U+001F and U+0085 from paragraphs, headings, table cells and fence info strings and let distinct reference labels resolve each other, in [#418](https://github.com/executablebooks/markdown-it-py/pull/418), thanks to [@Nexory](https://github.com/Nexory) diff --git a/markdown_it/rules_block/list.py b/markdown_it/rules_block/list.py index c8fe7af5..27c16870 100644 --- a/markdown_it/rules_block/list.py +++ b/markdown_it/rules_block/list.py @@ -235,17 +235,6 @@ def list_block(state: StateBlock, startLine: int, endLine: int, silent: bool) -> if isOrdered: token.info = state.src[start : posAfterMarker - 1] - # Detect GFM task checkbox: `[ ] ` or `[x] `/`[X] ` at content start - checkboxLen = 0 - if state.md.options.get("tasklists", False) and contentStart < maximum: - checked = _detect_task_checkbox(state.src, contentStart, maximum) - if checked is not None: - token.meta = {"checked": checked} - # Advance content past the checkbox: `[x]` (3 chars) + whitespace. - # `_detect_task_checkbox` already guarantees a whitespace char at - # pos+3, so we always consume 4 characters. - checkboxLen = 4 - # change current state, then restore it after parser subcall oldTight = state.tight oldBMark = state.bMarks[startLine] @@ -264,6 +253,22 @@ def list_block(state: StateBlock, startLine: int, endLine: int, silent: bool) -> state.tShift[startLine] = contentStart - state.bMarks[startLine] state.sCount[startLine] = offset + # Detect GFM task checkboxes only after setting the item's indentation, + # so literal markers in indented code blocks are left untouched. + checkboxLen = 0 + if ( + state.md.options.get("tasklists", False) + and contentStart < maximum + and not state.is_code_block(startLine) + ): + checked = _detect_task_checkbox(state.src, contentStart, maximum) + if checked is not None: + token.meta = {"checked": checked} + # Advance content past the checkbox: `[x]` (3 chars) + whitespace. + # `_detect_task_checkbox` already guarantees a whitespace char at + # pos+3, so we always consume 4 characters. + checkboxLen = 4 + # If we detected a checkbox, advance bMarks past it so that # getLines() doesn't include the checkbox text in the content. if checkboxLen: diff --git a/tests/test_port/test_misc.py b/tests/test_port/test_misc.py index 62b5bf85..f2482e76 100644 --- a/tests/test_port/test_misc.py +++ b/tests/test_port/test_misc.py @@ -1,3 +1,5 @@ +import pytest + from markdown_it import MarkdownIt, presets @@ -42,3 +44,47 @@ def type_filter(tokens, type_): assert tokens[2].markup == "." assert tokens[3].info == "199" assert tokens[3].markup == "." + + +@pytest.mark.parametrize("editable", [False, True]) +@pytest.mark.parametrize( + "source,code", + [ + ("- [x] literal\n", "[x] literal\n"), + ("- [ ] literal\n", " [ ] literal\n"), + ("1. [X] literal\n", "[X] literal\n"), + ("-\t\t[x] literal\n", " [x] literal\n"), + ("> - [X] literal\n", "[X] literal\n"), + ("- outer\n - [ ] literal\n", "[ ] literal\n"), + ], +) +def test_task_marker_in_indented_code(source, code, editable): + """Task detection must preserve literal markers and indentation in code.""" + md = MarkdownIt("gfm-like2", {"tasklists_editable": editable}) + tokens = md.parse(source) + assert [token.content for token in tokens if token.type == "code_block"] == [code] + assert all("checked" not in token.meta for token in tokens) + assert md.render(source) == MarkdownIt("gfm-like2", {"tasklists": False}).render( + source + ) + + +@pytest.mark.parametrize("spacing", [" ", " ", "\t"]) +@pytest.mark.parametrize("code_enabled", [False, True]) +def test_task_marker_in_paragraph(spacing, code_enabled): + """Keep recognizing tasks up to the paragraph indentation boundary.""" + md = MarkdownIt("gfm-like2") + if not code_enabled: + md.disable("code") + tokens = md.parse(f"-{spacing}[x] task\n") + item = next(token for token in tokens if token.type == "list_item_open") + assert item.meta["checked"] is True + assert not any(token.type == "code_block" for token in tokens) + + +def test_indented_task_marker_with_code_disabled(): + """Without the code rule, the indented text is still eligible for a task.""" + md = MarkdownIt("gfm-like2").disable("code") + tokens = md.parse("- [x] task\n") + item = next(token for token in tokens if token.type == "list_item_open") + assert item.meta["checked"] is True