From 17abd570024f027ae7e04dfc51bbfc5cc0d4826b Mon Sep 17 00:00:00 2001 From: yi111 <153097222+Yi-111-a@users.noreply.github.com> Date: Mon, 14 Sep 2026 02:20:34 +0000 Subject: [PATCH] fix(xlsx/xls): skip completely empty sheets that emit broken tables --- .../markitdown/converters/_xlsx_converter.py | 10 ++++ .../markitdown/tests/test_xlsx_empty_sheet.py | 60 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 packages/markitdown/tests/test_xlsx_empty_sheet.py diff --git a/packages/markitdown/src/markitdown/converters/_xlsx_converter.py b/packages/markitdown/src/markitdown/converters/_xlsx_converter.py index 355dd8f8d7..8636be1eb8 100644 --- a/packages/markitdown/src/markitdown/converters/_xlsx_converter.py +++ b/packages/markitdown/src/markitdown/converters/_xlsx_converter.py @@ -134,6 +134,12 @@ def convert( sheets = _read_xlsx_sheets(file_stream) md_content = "" for s in sheets: + # A completely empty sheet has no columns. pandas then emits a + # column-less HTML table that markdownify turns into broken syntax + # ("|\n| |"). Skip it. A header-only sheet still has columns and + # already renders as a well-formed empty table. + if sheets[s].columns.empty: + continue md_content += f"## {s}\n" html_content = sheets[s].to_html(index=False) md_content += ( @@ -196,6 +202,10 @@ def convert( sheets = pd.read_excel(file_stream, sheet_name=None, engine="xlrd") md_content = "" for s in sheets: + # Same empty-sheet guard as XlsxConverter: no columns means pandas + # would emit a column-less table that is not valid Markdown. + if sheets[s].columns.empty: + continue md_content += f"## {s}\n" html_content = sheets[s].to_html(index=False) md_content += ( diff --git a/packages/markitdown/tests/test_xlsx_empty_sheet.py b/packages/markitdown/tests/test_xlsx_empty_sheet.py new file mode 100644 index 0000000000..c5ef21e237 --- /dev/null +++ b/packages/markitdown/tests/test_xlsx_empty_sheet.py @@ -0,0 +1,60 @@ +import io + +from openpyxl import Workbook + +from markitdown import MarkItDown, StreamInfo + + +def _xlsx_bytes(workbook: Workbook) -> bytes: + buf = io.BytesIO() + workbook.save(buf) + return buf.getvalue() + + +def test_completely_empty_sheet_is_skipped() -> None: + """A sheet with no columns must not emit malformed table syntax.""" + workbook = Workbook() + workbook.active.title = "HasData" + workbook.active["A1"] = "col" + workbook.active["A2"] = "v" + workbook.create_sheet("CompletelyEmpty") + + result = MarkItDown().convert_stream( + io.BytesIO(_xlsx_bytes(workbook)), + stream_info=StreamInfo(extension=".xlsx"), + ) + + assert "## HasData" in result.markdown + assert "| col |" in result.markdown + assert "| v |" in result.markdown + assert "CompletelyEmpty" not in result.markdown + assert "|\n| |" not in result.markdown + + +def test_header_only_sheet_keeps_empty_table() -> None: + """A sheet with a header row but no data already produces a valid table.""" + workbook = Workbook() + workbook.active.title = "HeaderOnly" + workbook.active["A1"] = "col" + + result = MarkItDown().convert_stream( + io.BytesIO(_xlsx_bytes(workbook)), + stream_info=StreamInfo(extension=".xlsx"), + ) + + assert "## HeaderOnly" in result.markdown + assert "| col |" in result.markdown + assert "| --- |" in result.markdown + + +def test_single_completely_empty_workbook_emits_nothing() -> None: + workbook = Workbook() + workbook.active.title = "Empty" + + result = MarkItDown().convert_stream( + io.BytesIO(_xlsx_bytes(workbook)), + stream_info=StreamInfo(extension=".xlsx"), + ) + + assert result.markdown.strip() == "" + assert "|" not in result.markdown