feat(matrix): render bot messages with HTML formatting

Add HTML formatted_body to Matrix bot messages so that markdown-style
formatting (code blocks, bold, italic, lists) renders properly in Matrix
clients. Uses the pulldown-cmark crate to convert markdown to HTML and
sets the message format to org.matrix.custom.html.

Story: 188_story_render_matrix_bot_messages_with_html_formatting
This commit is contained in:
Dave
2026-02-25 16:08:57 +00:00
parent cda7915e12
commit d4f23051aa
4 changed files with 81 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
use crate::llm::providers::claude_code::{ClaudeCodeProvider, ClaudeCodeResult};
use crate::slog;
use pulldown_cmark::{Options, Parser, html};
use matrix_sdk::{
Client,
config::SyncSettings,
@@ -293,8 +294,9 @@ async fn handle_message(
let post_room = room.clone();
let post_task = tokio::spawn(async move {
while let Some(chunk) = msg_rx.recv().await {
let html = markdown_to_html(&chunk);
let _ = post_room
.send(RoomMessageEventContent::text_plain(chunk))
.send(RoomMessageEventContent::text_html(chunk, html))
.await;
}
});
@@ -388,6 +390,26 @@ async fn handle_message(
}
}
// ---------------------------------------------------------------------------
// Markdown rendering helper
// ---------------------------------------------------------------------------
/// Convert a Markdown string to an HTML string using pulldown-cmark.
///
/// Enables the standard extension set (tables, footnotes, strikethrough,
/// tasklists) so that common Markdown constructs render correctly in Matrix
/// clients such as Element.
pub fn markdown_to_html(markdown: &str) -> String {
let options = Options::ENABLE_TABLES
| Options::ENABLE_FOOTNOTES
| Options::ENABLE_STRIKETHROUGH
| Options::ENABLE_TASKLISTS;
let parser = Parser::new_ext(markdown, options);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
html_output
}
// ---------------------------------------------------------------------------
// Paragraph buffering helper
// ---------------------------------------------------------------------------
@@ -417,6 +439,43 @@ pub fn drain_complete_paragraphs(buffer: &mut String) -> Vec<String> {
mod tests {
use super::*;
// -- markdown_to_html ---------------------------------------------------
#[test]
fn markdown_to_html_bold() {
let html = markdown_to_html("**bold**");
assert!(html.contains("<strong>bold</strong>"), "expected <strong>: {html}");
}
#[test]
fn markdown_to_html_unordered_list() {
let html = markdown_to_html("- item one\n- item two");
assert!(html.contains("<ul>"), "expected <ul>: {html}");
assert!(html.contains("<li>item one</li>"), "expected list item: {html}");
}
#[test]
fn markdown_to_html_inline_code() {
let html = markdown_to_html("`inline_code()`");
assert!(html.contains("<code>inline_code()</code>"), "expected <code>: {html}");
}
#[test]
fn markdown_to_html_code_block() {
let html = markdown_to_html("```rust\nfn main() {}\n```");
assert!(html.contains("<pre>"), "expected <pre>: {html}");
assert!(html.contains("<code"), "expected <code> inside pre: {html}");
assert!(html.contains("fn main() {}"), "expected code content: {html}");
}
#[test]
fn markdown_to_html_plain_text_passthrough() {
let html = markdown_to_html("Hello, world!");
assert!(html.contains("Hello, world!"), "expected plain text passthrough: {html}");
}
// -- bot_context_is_clone -----------------------------------------------
#[test]
fn bot_context_is_clone() {
// BotContext must be Clone for the Matrix event handler injection.