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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user