2026-03-28 08:26:50 +00:00
|
|
|
use pulldown_cmark::{Options, Parser, html};
|
|
|
|
|
|
|
|
|
|
/// Format the startup greeting the bot sends to each room when it comes online.
|
|
|
|
|
///
|
|
|
|
|
/// Uses the bot's configured display name so the message reads naturally
|
2026-04-10 10:12:54 +00:00
|
|
|
/// (e.g. "Timmy is online."). When `drift_count` is `Some(n)` and `n > 0`,
|
|
|
|
|
/// appends a drift warning so operators know to check the server logs.
|
|
|
|
|
pub fn format_startup_announcement(bot_name: &str, drift_count: Option<usize>) -> String {
|
|
|
|
|
match drift_count {
|
|
|
|
|
Some(n) if n > 0 => format!(
|
|
|
|
|
"{bot_name} is online. \u{26a0}\u{fe0f} {n} item(s) have CRDT/DB drift \u{2014} check server logs."
|
|
|
|
|
),
|
|
|
|
|
_ => format!("{bot_name} is online."),
|
|
|
|
|
}
|
2026-03-28 08:26:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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 {
|
2026-03-28 10:39:13 +00:00
|
|
|
let normalized = crate::chat::util::normalize_line_breaks(markdown);
|
2026-03-28 08:26:50 +00:00
|
|
|
let options = Options::ENABLE_TABLES
|
|
|
|
|
| Options::ENABLE_FOOTNOTES
|
|
|
|
|
| Options::ENABLE_STRIKETHROUGH
|
|
|
|
|
| Options::ENABLE_TASKLISTS;
|
2026-03-28 10:39:13 +00:00
|
|
|
let parser = Parser::new_ext(&normalized, options);
|
2026-03-28 08:26:50 +00:00
|
|
|
let mut html_output = String::new();
|
|
|
|
|
html::push_html(&mut html_output, parser);
|
|
|
|
|
html_output
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Tests
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[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}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 10:39:13 +00:00
|
|
|
#[test]
|
|
|
|
|
fn markdown_to_html_single_newline_prose_becomes_paragraphs() {
|
|
|
|
|
// Single newlines between prose sentences should produce separate paragraphs.
|
|
|
|
|
let html = markdown_to_html("Sentence one.\nSentence two.");
|
|
|
|
|
assert!(
|
|
|
|
|
html.contains("<p>Sentence one.</p>"),
|
|
|
|
|
"expected separate paragraph for first sentence: {html}"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
html.contains("<p>Sentence two.</p>"),
|
|
|
|
|
"expected separate paragraph for second sentence: {html}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 08:26:50 +00:00
|
|
|
#[test]
|
|
|
|
|
fn startup_announcement_uses_bot_name() {
|
2026-04-10 10:12:54 +00:00
|
|
|
assert_eq!(format_startup_announcement("Timmy", None), "Timmy is online.");
|
2026-03-28 08:26:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn startup_announcement_uses_configured_display_name_not_hardcoded() {
|
2026-04-10 10:12:54 +00:00
|
|
|
assert_eq!(format_startup_announcement("HAL", None), "HAL is online.");
|
|
|
|
|
assert_eq!(format_startup_announcement("Assistant", None), "Assistant is online.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn startup_announcement_with_zero_drift_omits_warning() {
|
|
|
|
|
assert_eq!(format_startup_announcement("Timmy", Some(0)), "Timmy is online.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn startup_announcement_with_drift_includes_count_and_warning() {
|
|
|
|
|
let msg = format_startup_announcement("Timmy", Some(3));
|
|
|
|
|
assert!(msg.starts_with("Timmy is online."), "should start with online msg: {msg}");
|
|
|
|
|
assert!(msg.contains('3'), "should include drift count: {msg}");
|
|
|
|
|
assert!(msg.contains("drift"), "should mention drift: {msg}");
|
|
|
|
|
assert!(msg.contains("server logs"), "should mention server logs: {msg}");
|
2026-03-28 08:26:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn bot_name_defaults_to_assistant_when_display_name_absent() {
|
|
|
|
|
// When display_name is not set in bot.toml, bot_name should be "Assistant".
|
|
|
|
|
// This mirrors the logic in run_bot: config.display_name.clone().unwrap_or_else(...)
|
|
|
|
|
fn resolve_bot_name(display_name: Option<String>) -> String {
|
|
|
|
|
display_name.unwrap_or_else(|| "Assistant".to_string())
|
|
|
|
|
}
|
|
|
|
|
assert_eq!(resolve_bot_name(None), "Assistant");
|
|
|
|
|
assert_eq!(resolve_bot_name(Some("Timmy".to_string())), "Timmy");
|
|
|
|
|
}
|
|
|
|
|
}
|