From 9b14869cb10daa09a65d0f4e321dc63cbd812363 Mon Sep 17 00:00:00 2001 From: Alexander Minges Date: Tue, 10 Feb 2026 15:56:21 +0100 Subject: [PATCH] feat(matrix): support inline markdown html for url and super/subscript --- nanobot/channels/matrix.py | 16 +++++++++++++--- tests/test_matrix_channel.py | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/nanobot/channels/matrix.py b/nanobot/channels/matrix.py index fbe511b..61113ac 100644 --- a/nanobot/channels/matrix.py +++ b/nanobot/channels/matrix.py @@ -24,9 +24,16 @@ LOGGING_STACK_BASE_DEPTH = 2 TYPING_NOTICE_TIMEOUT_MS = 30_000 MATRIX_HTML_FORMAT = "org.matrix.custom.html" +# Keep plugin output aligned with Matrix recommended HTML tags: +# https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes +# - table/strikethrough/task_lists are already used in replies. +# - url, superscript, and subscript map to common tags (, , ) +# that Matrix clients (e.g. Element/FluffyChat) can render consistently. +# We intentionally avoid plugins that emit less-portable tags to keep output +# predictable across clients. MATRIX_MARKDOWN = create_markdown( escape=True, - plugins=["table", "strikethrough", "task_lists"], + plugins=["table", "strikethrough", "task_lists", "url", "superscript", "subscript"], ) @@ -47,8 +54,11 @@ def _render_markdown_html(text: str) -> str | None: # Skip formatted_body for plain output (

...

) to keep payload minimal. stripped = formatted.strip() - if stripped.startswith("

") and stripped.endswith("

") and "

" not in stripped[3:-4]: - return None + if stripped.startswith("

") and stripped.endswith("

"): + paragraph_inner = stripped[3:-4] + # Keep plaintext-only paragraphs minimal, but preserve inline markup/links. + if "<" not in paragraph_inner and ">" not in paragraph_inner: + return None return formatted diff --git a/tests/test_matrix_channel.py b/tests/test_matrix_channel.py index cc834c3..2e3dad2 100644 --- a/tests/test_matrix_channel.py +++ b/tests/test_matrix_channel.py @@ -424,6 +424,26 @@ async def test_send_adds_formatted_body_for_markdown() -> None: assert "task-list-item-checkbox" in str(content["formatted_body"]) +@pytest.mark.asyncio +async def test_send_adds_formatted_body_for_inline_url_superscript_subscript() -> None: + channel = MatrixChannel(_make_config(), MessageBus()) + client = _FakeAsyncClient("", "", "", None) + channel.client = client + + markdown_text = "Visit https://example.com and x^2^ plus H~2~O." + await channel.send( + OutboundMessage(channel="matrix", chat_id="!room:matrix.org", content=markdown_text) + ) + + content = client.room_send_calls[0]["content"] + assert content["msgtype"] == "m.text" + assert content["body"] == markdown_text + assert content["format"] == MATRIX_HTML_FORMAT + assert '
' in str(content["formatted_body"]) + assert "2" in str(content["formatted_body"]) + assert "2" in str(content["formatted_body"]) + + @pytest.mark.asyncio async def test_send_falls_back_to_plaintext_when_markdown_render_fails(monkeypatch) -> None: channel = MatrixChannel(_make_config(), MessageBus())