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
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def convert(

style_map = kwargs.get("style_map", None)
pre_process_stream = pre_process_docx(file_stream)
kwargs.setdefault("preserve_complex_tables", True)
return self._html_converter.convert_string(
mammoth.convert_to_html(pre_process_stream, style_map=style_map).value,
**kwargs,
Expand Down
12 changes: 12 additions & 0 deletions packages/markitdown/src/markitdown/converters/_markdownify.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,23 @@ class _CustomMarkdownify(markdownify.MarkdownConverter):
"""

def __init__(self, **options: Any):
self._preserve_complex_tables = options.pop("preserve_complex_tables", False)
options["heading_style"] = options.get("heading_style", markdownify.ATX)
options["keep_data_uris"] = options.get("keep_data_uris", False)
# Explicitly cast options to the expected type if necessary
super().__init__(**options)

def convert_table(self, el: Any, text: str, parent_tags: set[str]) -> str:
"""Preserve tables with merged cells when Markdown cannot represent them."""
if self._preserve_complex_tables:
for cell in el.find_all(["td", "th"]):
for attribute in ("rowspan", "colspan"):
span = cell.get(attribute)
if span is not None and span.isdigit() and int(span) > 1:
return f"\n\n{el}\n\n"

return super().convert_table(el, text, parent_tags)

def convert_hn(
self,
n: int,
Expand Down
85 changes: 85 additions & 0 deletions packages/markitdown/tests/test_docx_merged_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import io
from types import SimpleNamespace

import pytest

import markitdown.converters._docx_converter as docx_converter_module
from markitdown import StreamInfo
from markitdown.converters import DocxConverter, HtmlConverter


@pytest.fixture
def mock_docx_html(monkeypatch: pytest.MonkeyPatch):
def _convert(html: str):
monkeypatch.setattr(
docx_converter_module,
"pre_process_docx",
lambda file_stream: file_stream,
)
monkeypatch.setattr(
docx_converter_module.mammoth,
"convert_to_html",
lambda *args, **kwargs: SimpleNamespace(value=html),
)
return (
DocxConverter()
.convert(
io.BytesIO(b"mock docx"),
StreamInfo(extension=".docx"),
)
.markdown
)

return _convert


@pytest.mark.parametrize("attribute", ["rowspan", "colspan"])
def test_docx_preserves_tables_with_merged_cells_as_html(
mock_docx_html,
attribute: str,
) -> None:
html = f"""
<p>Before table</p>
<table>
<tr><th {attribute}="2">Merged heading</th><th>Second heading</th></tr>
<tr><td>First value</td><td>Second value</td></tr>
</table>
<p>After table</p>
"""

result = mock_docx_html(html)

assert "Before table" in result
assert "After table" in result
assert "<table>" in result
assert f'{attribute}="2"' in result
assert "Merged heading" in result


def test_docx_keeps_simple_tables_as_markdown(mock_docx_html) -> None:
html = """
<table>
<tr><th>First heading</th><th>Second heading</th></tr>
<tr><td>First value</td><td>Second value</td></tr>
</table>
"""

result = mock_docx_html(html)

assert "<table>" not in result
assert "| First heading | Second heading |" in result
assert "| First value | Second value |" in result


def test_html_converter_does_not_preserve_complex_tables_by_default() -> None:
html = """
<table>
<tr><th rowspan="2">Merged heading</th><th>Second heading</th></tr>
<tr><td>First value</td></tr>
</table>
"""

result = HtmlConverter().convert_string(html).markdown

assert "<table>" not in result
assert "Merged heading" in result