diff --git a/docling/backend/md_backend.py b/docling/backend/md_backend.py index 99cbcaf23a..eec4aac3e4 100644 --- a/docling/backend/md_backend.py +++ b/docling/backend/md_backend.py @@ -63,7 +63,13 @@ from marko.ext.gfm import elements as _gfm_el from marko.helpers import MarkoExtension - class _GfmParagraph(_gfm_el.Paragraph): + class _GfmParagraph(marko.block.Paragraph): + # Based on marko's plain Paragraph rather than the GFM one: GFM's strips a + # leading task-list marker ("[x] " / "[ ] ") and keeps it only in + # `checked`, which a DoclingDocument list item cannot carry, so the marker + # was lost. Here it stays part of the text, in list items and elsewhere. + override = True + @classmethod def break_paragraph( cls, source: marko.source.Source, lazy: bool = False diff --git a/tests/test_backend_markdown.py b/tests/test_backend_markdown.py index 95bbaecc55..29d5558fe0 100644 --- a/tests/test_backend_markdown.py +++ b/tests/test_backend_markdown.py @@ -1306,3 +1306,60 @@ def test_rich_table_cell_no_stray_body_items(): # The only direct body child must be the table itself. body_crefs = {ref.cref for ref in doc.body.children} assert body_crefs == {"#/tables/0"} + + +def _md_convert(markdown: str): + conv_result = get_converter().convert_string(markdown, format=InputFormat.MD) + assert conv_result.status == ConversionStatus.SUCCESS + return conv_result.document + + +def test_task_list_items_keep_their_markers(): + """GFM task-list markers stay in the list item text instead of being dropped (#4930).""" + doc = _md_convert( + "- [x] done item\n- [ ] open item\n- [X] upper case\n- plain item\n" + ) + + assert [(t.label, t.text) for t in doc.texts] == [ + (DocItemLabel.LIST_ITEM, "[x] done item"), + (DocItemLabel.LIST_ITEM, "[ ] open item"), + (DocItemLabel.LIST_ITEM, "[X] upper case"), + (DocItemLabel.LIST_ITEM, "plain item"), + ] + assert doc.export_to_markdown() == ( + "- [x] done item\n- [ ] open item\n- [X] upper case\n- plain item" + ) + html = doc.export_to_html() + assert "