Skip to content

Commit ff7644e

Browse files
committed
Narrow the find/replace of showZeros
1 parent 1bd206a commit ff7644e

2 files changed

Lines changed: 64 additions & 5 deletions

File tree

packages/markitdown/src/markitdown/converters/_xlsx_converter.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import io
2+
import re
23
import sys
34
import zipfile
45
from typing import BinaryIO, Any
@@ -35,19 +36,36 @@
3536
ACCEPTED_XLS_FILE_EXTENSIONS = [".xls"]
3637

3738

39+
# Some producers write the legacy attribute "showZeroes" on <sheetView>, where the
40+
# schema calls it "showZeros". openpyxl rejects the unknown attribute outright, so the
41+
# workbook is repaired by renaming it. The rename is confined to <sheetView> start tags.
42+
_SHEET_VIEW_START_TAG = re.compile(rb"<sheetView(?=[\s/>])[^>]*>")
43+
_SHOW_ZEROES_ATTRIBUTE = re.compile(rb"(?<=[\s])showZeroes(\s*=)")
44+
45+
3846
def _read_xlsx_sheets(file_stream: BinaryIO) -> dict[str, Any]:
47+
start_pos = file_stream.tell()
3948
try:
4049
return pd.read_excel(file_stream, sheet_name=None, engine="openpyxl")
4150
except TypeError as exc:
4251
if "showZeroes" not in str(exc):
4352
raise
4453

45-
repaired_stream = _repair_sheetview_show_zeroes(file_stream)
54+
repaired_stream = _repair_sheetview_show_zeroes(file_stream, start_pos)
4655
return pd.read_excel(repaired_stream, sheet_name=None, engine="openpyxl")
4756

4857

49-
def _repair_sheetview_show_zeroes(file_stream: BinaryIO) -> io.BytesIO:
50-
file_stream.seek(0)
58+
def _rename_show_zeroes_attribute(data: bytes) -> bytes:
59+
return _SHEET_VIEW_START_TAG.sub(
60+
lambda match: _SHOW_ZEROES_ATTRIBUTE.sub(rb"showZeros\1", match.group(0)),
61+
data,
62+
)
63+
64+
65+
def _repair_sheetview_show_zeroes(
66+
file_stream: BinaryIO, start_pos: int = 0
67+
) -> io.BytesIO:
68+
file_stream.seek(start_pos)
5169
repaired_stream = io.BytesIO()
5270

5371
with zipfile.ZipFile(file_stream) as source:
@@ -57,8 +75,9 @@ def _repair_sheetview_show_zeroes(file_stream: BinaryIO) -> io.BytesIO:
5775
if (
5876
item.filename.startswith("xl/worksheets/")
5977
and item.filename.endswith(".xml")
78+
and b"showZeroes" in data
6079
):
61-
data = data.replace(b" showZeroes=", b" showZeros=")
80+
data = _rename_show_zeroes_attribute(data)
6281
target.writestr(item, data)
6382

6483
repaired_stream.seek(0)

packages/markitdown/tests/test_module_misc.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,9 @@ def test_xlsx_legacy_show_zeroes_sheetview(tmp_path) -> None:
385385
sheet.title = "Data"
386386
sheet["A1"] = "hello"
387387
sheet["B1"] = "world"
388+
# A cell whose text is byte-identical to the malformed attribute. openpyxl stores it
389+
# as an inline string, so it lands in the very worksheet part that gets repaired.
390+
sheet["A2"] = ' showZeroes="0" '
388391
workbook.save(base_path)
389392

390393
with zipfile.ZipFile(base_path) as source:
@@ -395,7 +398,9 @@ def test_xlsx_legacy_show_zeroes_sheetview(tmp_path) -> None:
395398
data = data.replace(
396399
b"<sheetView ", b'<sheetView showZeroes="0" ', 1
397400
)
398-
assert b"showZeroes" in data
401+
# Guard the fixture: the sheet view attribute and the cell text must
402+
# both live here, or this test stops exercising the repair.
403+
assert data.count(b'showZeroes="0"') == 2
399404
target.writestr(item, data)
400405

401406
result = MarkItDown().convert(str(xlsx_path))
@@ -404,6 +409,41 @@ def test_xlsx_legacy_show_zeroes_sheetview(tmp_path) -> None:
404409
assert "hello" in result.markdown
405410
assert "world" in result.markdown
406411

412+
# The repair must not reach into the worksheet's data: the cell keeps its text ...
413+
assert 'showZeroes="0"' in result.markdown
414+
# ... and no part of the document was silently renamed on the way through.
415+
assert "showZeros" not in result.markdown
416+
417+
418+
def test_xlsx_show_zeroes_rename_is_scoped_to_sheet_view_tags() -> None:
419+
from markitdown.converters._xlsx_converter import _rename_show_zeroes_attribute
420+
421+
worksheet_xml = (
422+
b"<worksheet><sheetViews>"
423+
b'<sheetView showZeroes="0" workbookViewId="0"/>'
424+
b'<sheetView\n\tshowZeroes="1"\ttabSelected="1"/>'
425+
b"</sheetViews>"
426+
# openpyxl ignores custom sheet views entirely, so they need no repair
427+
b'<customSheetViews><customSheetView showZeroes="0"/></customSheetViews>'
428+
b'<sheetData><row r="1">'
429+
b'<c r="A1" t="inlineStr"><is><t xml:space="preserve"> showZeroes="0" '
430+
b"</t></is></c>"
431+
b'<c r="B1"><f>IF(A1=" showZeroes=","x","y")</f><v>y</v></c>'
432+
b"</row></sheetData></worksheet>"
433+
)
434+
435+
repaired = _rename_show_zeroes_attribute(worksheet_xml)
436+
437+
# Every <sheetView> start tag is repaired, whatever separates its attributes ...
438+
assert repaired.count(b"showZeros=") == 2
439+
assert b'<sheetView showZeros="0" workbookViewId="0"/>' in repaired
440+
assert b'<sheetView\n\tshowZeros="1"\ttabSelected="1"/>' in repaired
441+
442+
# ... and nothing outside those start tags is rewritten.
443+
assert b'<t xml:space="preserve"> showZeroes="0" </t>' in repaired
444+
assert b'<f>IF(A1=" showZeroes=","x","y")</f>' in repaired
445+
assert b'<customSheetView showZeroes="0"/>' in repaired
446+
407447

408448
def test_input_as_strings() -> None:
409449
markitdown = MarkItDown()

0 commit comments

Comments
 (0)