Skip to content

Commit 4413d19

Browse files
Preserve task-list checkbox in markdown and rst renderers
The task_lists plugin rewrites a list_item into a task_list_item and moves the [ ]/[x] marker into attrs, registering a renderer method only for the html renderer. The markdown and rst renderers render list items through the shared _render_list_item helper rather than a per-token method, so the checkbox had nowhere to be emitted and was silently dropped: reformatting '- [x] done' produced '- done', losing information on round-trip. Re-emit the marker for task_list_item tokens in _render_list_item so the checkbox survives. Adds round-trip tests covering ordered, nested, multiline and mixed lists, plus the rst renderer. Fixes #441 Fixes #401
1 parent 74608f5 commit 4413d19

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

src/mistune/renderers/_list.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ def _render_list_item(
3030
) -> str:
3131
leading = cast(str, parent["leading"])
3232
text = ""
33+
# The task_lists plugin rewrites a list_item into a task_list_item and
34+
# moves the "[ ] "/"[x] " marker into attrs. Unlike the html renderer, the
35+
# markdown and rst renderers render list items here rather than through a
36+
# per-token method, so re-emit the checkbox to keep it from being silently
37+
# dropped on round-trip.
38+
if item["type"] == "task_list_item":
39+
text = "[x] " if item["attrs"]["checked"] else "[ ] "
3340
for tok in item["children"]:
3441
if tok["type"] == "list":
3542
tok["parent"] = parent

tests/test_renderers.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,41 @@ def test_prose_not_over_escaped(self):
6565
"ratio a/b is fine\n",
6666
):
6767
self.assert_round_trip(text)
68+
69+
70+
class TestTaskListRoundTrip(TestCase):
71+
"""The task_lists plugin rewrites a list_item into a task_list_item and
72+
moves the checkbox marker into attrs. The markdown and rst renderers must
73+
re-emit it instead of silently dropping it (issues #401 and #441)."""
74+
75+
to_html = create_markdown(escape=False, plugins=["task_lists"])
76+
reformat = create_markdown(renderer=MarkdownRenderer(), plugins=["task_lists"])
77+
to_rst = create_markdown(renderer=RSTRenderer(), plugins=["task_lists"])
78+
79+
def assert_round_trip(self, text):
80+
self.assertEqual(
81+
self.to_html(self.reformat(text)),
82+
self.to_html(text),
83+
)
84+
85+
def test_checkbox_preserved(self):
86+
# the exact issue example: the checkbox used to disappear entirely
87+
self.assertEqual(
88+
self.reformat("- [x] done\n- [ ] todo\n"),
89+
"- [x] done\n- [ ] todo\n",
90+
)
91+
92+
def test_round_trip_variants(self):
93+
for text in (
94+
"- [x] done\n- [ ] todo\n",
95+
"- [X] upper-x is checked\n",
96+
"1. [ ] a\n2. [x] b\n",
97+
"- [ ] outer\n - [x] inner\n",
98+
"- [ ] line one\n line two\n",
99+
"- plain item\n- [ ] task item\n",
100+
):
101+
self.assert_round_trip(text)
102+
103+
def test_rst_preserves_checkbox(self):
104+
self.assertIn("[x] done", self.to_rst("- [x] done\n"))
105+
self.assertIn("[ ] todo", self.to_rst("- [ ] todo\n"))

0 commit comments

Comments
 (0)