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
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ v9.9.9 (unreleased)
- chore: bump actions/setup-python version (#1773, #1775)
- refactor: switch to pyproject.toml (#1753)
- chore: bump dependencies (#1774, #1777)
- feat: add md_table helper to render Markdown tables in templates (#1780)


v6.2.1 (2026-06-06)
Expand Down
9 changes: 9 additions & 0 deletions docs/user_guide/plugin_development/messaging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,15 @@ using `self.send_templated()`:
response = tenv().get_template('Hello/hello.md').render(name=args)
self.send(msg.frm, response)

Building tables by hand in a Jinja2 template is fiddly, so a ``md_table``
helper is available in every template. Give it the rows (an iterable of
iterables) and, optionally, a list of headers, and it returns a Markdown table.
Pipe and newline characters in the cells are escaped for you so the table stays
valid:

.. code-block:: jinja

{{ md_table(rows, headers=['Name', 'Score']) }}

Cards
-----
Expand Down
35 changes: 35 additions & 0 deletions errbot/templating.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pathlib import Path

from jinja2 import ChoiceLoader, Environment, FileSystemLoader, PrefixLoader
from markupsafe import Markup

from errbot.plugin_info import PluginInfo

Expand All @@ -12,6 +13,39 @@ def make_templates_path(root: Path) -> Path:
return root / "templates"


def _md_table_cell(value) -> str:
text = str(value)
# Newlines and pipes would break a Markdown table, so neutralize them.
text = text.replace("\r\n", " ").replace("\n", " ").replace("\r", " ")
return text.replace("|", "\\|")


def md_table(rows, headers=None) -> Markup:
"""Render an iterable of rows as a GitHub flavored Markdown table.

``rows`` is an iterable of iterables, one per table row. ``headers`` is an
optional list used for the header row. Cells are turned into strings and any
pipe or newline characters in them are escaped so the table stays valid.

The result is a :class:`~markupsafe.Markup` string so it can be dropped
straight into a Markdown template with ``{{ md_table(rows, headers=[...]) }}``.
"""
rows = [list(row) for row in rows]
header_cells = list(headers) if headers is not None else []
ncols = max([len(header_cells)] + [len(row) for row in rows])
if ncols == 0:
return Markup("")

def line(cells):
cells = [_md_table_cell(c) for c in cells]
cells += [""] * (ncols - len(cells))
return "| " + " | ".join(cells) + " |"

lines = [line(header_cells), "| " + " | ".join(["---"] * ncols) + " |"]
lines += [line(row) for row in rows]
return Markup("\n".join(lines))


system_templates_path = str(make_templates_path(Path(__file__).parent))
template_path = [system_templates_path]
plugin_templates = {} # plugin_name -> FileSystemLoader
Expand All @@ -30,6 +64,7 @@ def _recreate_env():
keep_trailing_newline=False,
autoescape=True,
)
env.globals["md_table"] = md_table


_recreate_env()
Expand Down
43 changes: 43 additions & 0 deletions tests/templating_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from markupsafe import Markup

from errbot.templating import md_table, tenv


def test_md_table_with_headers():
result = md_table([[1, 2], [3, 4]], headers=["a", "b"])
assert result == "| a | b |\n| --- | --- |\n| 1 | 2 |\n| 3 | 4 |"


def test_md_table_without_headers_uses_blank_header():
lines = str(md_table([["x", "y"]])).splitlines()
assert lines[0] == "| | |"
assert lines[1] == "| --- | --- |"
assert lines[2] == "| x | y |"


def test_md_table_pads_short_rows():
lines = str(md_table([["only"]], headers=["a", "b"])).splitlines()
assert lines[2] == "| only | |"


def test_md_table_escapes_pipes_and_newlines():
result = md_table([["a|b", "c\nd"]], headers=["h1", "h2"])
assert "| a\\|b | c d |" in result


def test_md_table_empty():
assert md_table([]) == ""


def test_md_table_returns_markup():
assert isinstance(md_table([["x"]]), Markup)


def test_md_table_available_in_template_env():
assert "md_table" in tenv().globals
rendered = (
tenv()
.from_string("{{ md_table(rows, headers=cols) }}")
.render(rows=[["a", "b"]], cols=["c1", "c2"])
)
assert rendered == "| c1 | c2 |\n| --- | --- |\n| a | b |"