Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 16 additions & 11 deletions markdown_it/rules_block/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:
Expand Down
46 changes: 46 additions & 0 deletions tests/test_port/test_misc.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from markdown_it import MarkdownIt, presets


Expand Down Expand Up @@ -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