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
18 changes: 18 additions & 0 deletions packages/markitdown/src/markitdown/converters/_markdownify.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@

_PERCENT_ENCODED_OCTET = re.compile(r"%[0-9A-Fa-f]{2}")

# Matches a pipe together with the (possibly empty) run of backslashes in front
# of it, so that run can be doubled before the pipe is escaped. Mirrors the
# escaping the CSV converter applies to its table cells.
_TABLE_PIPE_RE = re.compile(r"(?<!\\)(\\*)\|")


def _escape_table_pipes(text: str) -> str:
"""Escape pipes so cell text cannot split a Markdown table row."""
return _TABLE_PIPE_RE.sub(lambda m: m.group(1) * 2 + r"\|", text)


def _quote_path_preserving_percent_encoded_octets(path: str) -> str:
"""Quote a URL path while preserving existing %HH byte encodings."""
Expand Down Expand Up @@ -174,5 +184,13 @@ def convert_strike(self, el: Any, text: str, *args, **kwargs) -> str:
"""Obsolete <strike> is still in the wild; treat it like <s>/<del>."""
return self.convert_s(el, text, *args, **kwargs) # type: ignore

def convert_td(self, el: Any, text: str, *args, **kwargs) -> str:
"""Same as usual, but escape pipes so they do not add columns."""
return super().convert_td(el, _escape_table_pipes(text), *args, **kwargs) # type: ignore

def convert_th(self, el: Any, text: str, *args, **kwargs) -> str:
"""Same as usual, but escape pipes so they do not add columns."""
return super().convert_th(el, _escape_table_pipes(text), *args, **kwargs) # type: ignore

def convert_soup(self, soup: Any) -> str:
return super().convert_soup(soup) # type: ignore
20 changes: 20 additions & 0 deletions packages/markitdown/tests/test_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,3 +346,23 @@ def test_deeply_nested_html_fallback() -> None:

if __name__ == "__main__":
raise SystemExit(pytest.main([__file__]))


@pytest.mark.parametrize(
"cell, expected",
[
("a|b", r"a\|b"),
(r"a\|b", r"a\\\|b"),
("|", r"\|"),
("no pipe", "no pipe"),
],
)
def test_table_cell_pipes_are_escaped(cell: str, expected: str) -> None:
markdown = _convert_html(
f"<table><tr><th>{cell}</th><th>h2</th></tr>"
f"<tr><td>{cell}</td><td>x</td></tr></table>"
)

rows = markdown.strip().splitlines()
assert rows[0] == f"| {expected} | h2 |"
assert rows[2] == f"| {expected} | x |"
14 changes: 14 additions & 0 deletions packages/markitdown/tests/test_xlsx.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,3 +542,17 @@ def test_xlsx_show_zeroes_rename_is_scoped_to_sheet_view_tags() -> None:

if __name__ == "__main__":
raise SystemExit(pytest.main([__file__]))


def test_xlsx_cell_pipes_do_not_add_columns() -> None:
workbook = openpyxl.Workbook()
sheet = workbook.active
sheet.append(["name", "value"])
sheet.append(["a|b", "c"])
stream = io.BytesIO()
workbook.save(stream)
stream.seek(0)

markdown = MarkItDown().convert_stream(stream, file_extension=".xlsx").markdown

assert r"| a\|b | c |" in markdown