Skip to content
Merged
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
12 changes: 12 additions & 0 deletions docs/readers.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ Example:

{{ read_csv('assets/tables/basic_table.csv') | add_indentation(spaces=4) }}

!!! info "Multiline cells"

A markdown table row has to fit on a single line, so newlines inside a cell are
replaced with `<br>`. Note that such a cell needs to be quoted to be valid CSV:

```csv
id,description
23456,"Some description.

With a line break."
```


### `read_fwf`

Expand Down
52 changes: 40 additions & 12 deletions src/mkdocs_table_reader_plugin/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@

import pandas as pd

# Table formats that render each row on a single line, and therefore cannot
# contain literal newlines. See https://github.com/timvink/mkdocs-table-reader-plugin/issues/83
SINGLE_LINE_TABLE_FORMATS = ("pipe", "github")


def replace_unescaped_pipes(text: str) -> str:
"""
Expand All @@ -19,29 +23,53 @@ def replace_unescaped_pipes(text: str) -> str:
return re.sub(r"(?<!\\)\|", "\\|", text)


def replace_newlines(text: str) -> str:
"""
Replace newlines with <br>.

A markdown table row must fit on a single line, so a cell that contains a
newline (as multiline CSV cells do) would otherwise break up the table.

Args:
text (str): input string

Returns:
str: output string
"""
return re.sub(r"\r\n|\r|\n", "<br>", text)


def convert_to_md_table(df: pd.DataFrame, **markdown_kwargs: dict) -> str:
"""
Convert dataframe to markdown table using tabulate.
"""
if "index" not in markdown_kwargs:
markdown_kwargs["index"] = False
if "tablefmt" not in markdown_kwargs:
markdown_kwargs["tablefmt"] = "pipe"

# Escape any pipe characters, | to \|
# See https://github.com/astanin/python-tabulate/issues/241
df.columns = [
replace_unescaped_pipes(c) if isinstance(c, str) else c for c in df.columns
]
# And replace newlines with <br>, but only for table formats that need it:
# formats like 'grid' display multiline cells just fine.
escape_newlines = markdown_kwargs["tablefmt"] in SINGLE_LINE_TABLE_FORMATS
Comment thread
timvink marked this conversation as resolved.

def escape(value):
if not isinstance(value, str):
return value
value = replace_unescaped_pipes(value)
if escape_newlines:
value = replace_newlines(value)
return value

df.columns = [escape(c) for c in df.columns]

# Avoid deprecated applymap warning on pandas>=2.0
# See https://github.com/timvink/mkdocs-table-reader-plugin/issues/55
if pd.__version__ >= "2.1.0":
df = df.map(lambda s: replace_unescaped_pipes(s) if isinstance(s, str) else s)
df = df.map(escape)
else:
df = df.applymap(
lambda s: replace_unescaped_pipes(s) if isinstance(s, str) else s
)

if "index" not in markdown_kwargs:
markdown_kwargs["index"] = False
if "tablefmt" not in markdown_kwargs:
markdown_kwargs["tablefmt"] = "pipe"
df = df.applymap(escape)

return df.to_markdown(**markdown_kwargs)

Expand Down
5 changes: 5 additions & 0 deletions tests/fixtures/csv_multiline/docs/example_multiline.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
id,severity,description
23456,low,"Sometimes the cell text is quoted.

But not always"
34567,high,Single line description.
9 changes: 9 additions & 0 deletions tests/fixtures/csv_multiline/docs/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Test page

This test is related to this issue: https://github.com/timvink/mkdocs-table-reader-plugin/issues/83

## Table with a multiline cell

A cell containing newlines, which are rendered as `<br>`.

{{ read_csv("example_multiline.csv") }}
7 changes: 7 additions & 0 deletions tests/fixtures/csv_multiline/mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
site_name: test git_table_reader site
use_directory_urls: true

plugins:
- search
- table-reader:
data_path: "docs"
35 changes: 33 additions & 2 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,12 @@ def test_wrong_path(tmp_path):

result = build_docs_setup(tmp_proj)
assert result.exit_code == 1, "'mkdocs build' command succeeded but should have failed"
assert "[table-reader-plugin]: Cannot find table file" in result.output
assert "non_existing_table.csv" in result.output
# Assert on the raised exception rather than on the captured log output.
# mkdocs logs the error and then re-raises it, and whether that log record
# makes it into result.output turns out to be flaky on windows.
assert isinstance(result.exception, FileNotFoundError)
assert "[table-reader-plugin]: Cannot find table file" in str(result.exception)
assert "non_existing_table.csv" in str(result.exception)


def test_mixed_quotation_marks(tmp_path):
Expand Down Expand Up @@ -408,3 +412,30 @@ def test_non_utf8_encoding(tmp_path):
# read_raw() inserted the cp1251 encoded markdown file
assert re.search(r"539956", contents)
assert re.search(r"Сыр", contents)


def test_csv_with_multiline_cells(tmp_path):
"""
A CSV with a quoted, multiline cell should render as a single table row.

See https://github.com/timvink/mkdocs-table-reader-plugin/issues/83
"""

tmp_proj = setup_clean_mkdocs_folder(
"tests/fixtures/csv_multiline/mkdocs.yml", tmp_path
)

result = build_docs_setup(tmp_proj)
assert result.exit_code == 0, "'mkdocs build' command failed"

page_with_tag = tmp_proj / "site/index.html"
contents = page_with_tag.read_text()

# The multiline cell is kept together on one row, separated by <br>
assert re.search(
r"Sometimes the cell text is quoted\.<br><br>But not always", contents
)
# Both CSV records became a table row, and no newline split them up
table = re.search(r"<table>.*?</table>", contents, flags=re.DOTALL)
assert table is not None, "no table was inserted"
assert len(re.findall(r"<tr>", table.group())) == 3
44 changes: 42 additions & 2 deletions tests/test_markdown.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import pandas as pd
from mkdocs_table_reader_plugin.markdown import convert_to_md_table, replace_unescaped_pipes

from mkdocs_table_reader_plugin.markdown import (
convert_to_md_table,
replace_newlines,
replace_unescaped_pipes,
)


def test_unescaped_pipes():
assert replace_unescaped_pipes("hi|there\\|you|there") == "hi\\|there\\|you\\|there"


def test_replace_newlines():
assert replace_newlines("one\ntwo") == "one<br>two"
assert replace_newlines("one\r\ntwo") == "one<br>two"
assert replace_newlines("one\rtwo") == "one<br>two"
assert replace_newlines("one\n\ntwo") == "one<br><br>two"
assert replace_newlines("no newlines here") == "no newlines here"


def test_convert_to_md_table():

df_bad = pd.read_csv("tests/fixtures/csv_with_pipes/docs/example_unescaped.csv")
Expand All @@ -16,4 +29,31 @@ def test_convert_to_md_table():
# Because we escape pipes, the 'bad' df
md_bad = convert_to_md_table(df_bad, **{})
md_good = convert_to_md_table(df_good, **{})
assert md_bad == md_good
assert md_bad == md_good


def test_convert_to_md_table_multiline():
"""
A multiline cell should not break up a markdown table.

See https://github.com/timvink/mkdocs-table-reader-plugin/issues/83
"""
df = pd.read_csv("tests/fixtures/csv_multiline/docs/example_multiline.csv")
assert df.shape == (2, 3)

md = convert_to_md_table(df, **{})
assert "Sometimes the cell text is quoted.<br><br>But not always" in md
# A header, a separator and one line per record
assert len(md.split("\n")) == 4


def test_convert_to_md_table_multiline_other_tablefmt():
"""
Table formats that display multiline cells themselves are left alone.
"""
df = pd.read_csv("tests/fixtures/csv_multiline/docs/example_multiline.csv")

md = convert_to_md_table(df, tablefmt="grid")
assert "<br>" not in md
assert "Sometimes the cell text is quoted." in md
assert "But not always" in md
Loading