Skip to content

Escape row index labels in Markdown tables - #94

Merged
timvink merged 2 commits into
masterfrom
escape-index-labels
Oct 11, 2026
Merged

timvink merged 2 commits into
masterfrom
escape-index-labels

Conversation

@timvink

@timvink timvink commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Update: Thanks @bensynapse for reporting in #93

Problem

With index=True, row labels and the index name were passed to tabulate without escaping. A pipe in a label (a|b) added an extra cell, and a newline in a label split the row, so the Markdown table broke:

df = pd.DataFrame({"value": [1, 2]}, index=pd.Index(["a|b", "two\nlines"], name="row|name"))
convert_to_md_table(df, index=True)

Cells and column headers were already escaped; the index was not.

Fix

When index=True, apply the existing cell escaping (| → \|, and newlines → <br> for pipe/github) to the row labels and index name:

if markdown_kwargs["index"] and not isinstance(df.index, pd.MultiIndex):
    df = df.rename(index=escape).rename_axis(index=escape)
  • Non-string labels (ints, dates, tuples) are left unchanged.
  • The default index=False path skips this step entirely.
  • A MultiIndex is left as is: tabulate renders its labels as tuple reprs ((1, 'a\\|b')), which escaping cannot fix, and does not show its names.
  • Escaping works on a copy, so a DataFrame passed in by a macros user is not modified.

Benchmark

convert_to_md_table on a 5-column string table, master versus this branch, median of 15 runs (macOS, Apple Silicon):

Rows Index index master branch diff
100 RangeIndex False 2.3 ms 2.3 ms -1.0%
100 RangeIndex True 2.4 ms 2.5 ms +4.4%
100 string labels True 2.6 ms 2.8 ms +6.7%
1,000 RangeIndex False 19.6 ms 19.6 ms -0.2%
1,000 RangeIndex True 20.8 ms 21.0 ms +1.1%
1,000 string labels True 23.0 ms 24.4 ms +6.2%
10,000 RangeIndex False 193.5 ms 192.6 ms -0.4%
10,000 RangeIndex True 204.9 ms 207.3 ms +1.2%
10,000 string labels True 226.4 ms 235.8 ms +4.1%
100,000 RangeIndex False 1951.8 ms 1970.3 ms +0.9%
100,000 RangeIndex True 2074.6 ms 2105.4 ms +1.5%
100,000 string labels True 2283.9 ms 2404.8 ms +5.3%
  • index=False (the default): no measurable change.
  • index=True with a RangeIndex (what read_csv produces): about 1–1.5%.
  • index=True with string labels: about 4–7%, one extra pass over the row labels.

Most of the time is spent in DataFrame.to_markdown itself in all cases.

Benchmark script
import sys, timeit, statistics
import pandas as pd
sys.path.insert(0, sys.argv[1])  # folder holding master's markdown.py as md_master.py
from md_master import convert_to_md_table as old
from mkdocs_table_reader_plugin.markdown import convert_to_md_table as new

def t(f, df, **kw):
    return statistics.median(timeit.repeat(lambda: f(df, **kw), number=1, repeat=15))

for n in [100, 1_000, 10_000, 100_000]:
    data = {c: [f"val {i}" for i in range(n)] for c in "abcde"}
    for label, df, index in [
        ("range", pd.DataFrame(data), False),
        ("range", pd.DataFrame(data), True),
        ("str", pd.DataFrame(data, index=[f"row {i}" for i in range(n)]), True),
    ]:
        a, b = t(old, df, index=index), t(new, df, index=index)
        print(f"{n:>7} {label:>5} {str(index):>6} {a*1000:8.1f}ms {b*1000:8.1f}ms {(b/a-1)*100:+6.1f}%")

Tests

New tests cover pipes and newlines in labels and the index name, the input DataFrame staying unchanged, and unchanged output for integer, date, tuple and MultiIndex indexes. make test passes (64 tests, 100% coverage).

🤖 Generated with Claude Code

timvink and others added 2 commits October 11, 2026 18:29
With index=True, a pipe in a row label or index name added an extra cell,
and a newline split the row. Apply the existing cell escaping to the index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@timvink
timvink merged commit 13fa217 into master Oct 11, 2026
31 checks passed
@timvink
timvink deleted the escape-index-labels branch October 11, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant