use std::collections::HashMap; use crate::escape::html_escape; /// Quote author info for attribution rendering. pub struct QuoteAuthor { pub username: String, pub display_name: String, pub is_removed: bool, } /// Post-process rendered HTML to replace `[quote:POST_ID:HASH]` markers with /// clickable author attribution. pub fn post_process_quotes( html: &str, quote_authors: &HashMap, ) -> String { static QUOTE_RE: std::sync::LazyLock = std::sync::LazyLock::new(|| { regex_lite::Regex::new(r"\[quote:([0-9a-f\-]{36}):([0-9a-f]{8})\]").unwrap() }); QUOTE_RE .replace_all(html, |caps: ®ex_lite::Captures| { let post_id_str = &caps[1]; let resolved = uuid::Uuid::parse_str(post_id_str) .ok() .and_then(|post_id| quote_authors.get(&post_id)); if let Some(author) = resolved { if author.is_removed { format!( "(original post removed)", post_id_str ) } else { format!( "— {} (@{})", post_id_str, html_escape(&author.display_name), html_escape(&author.username), ) } } else { caps[0].to_string() } }) .to_string() } #[cfg(test)] mod tests { use super::*; #[test] fn replaces_quote_marker_with_attribution() { let post_id = uuid::Uuid::new_v4(); let mut authors = HashMap::new(); authors.insert( post_id, QuoteAuthor { username: "alice".to_string(), display_name: "Alice Smith".to_string(), is_removed: false, }, ); let input = format!("[quote:{}:abcd1234]", post_id); let result = post_process_quotes(&input, &authors); assert!(result.contains("Alice Smith")); assert!(result.contains("@alice")); assert!(result.contains("quote-attribution")); } #[test] fn removed_post_shows_removed_text() { let post_id = uuid::Uuid::new_v4(); let mut authors = HashMap::new(); authors.insert( post_id, QuoteAuthor { username: "bob".to_string(), display_name: "Bob".to_string(), is_removed: true, }, ); let input = format!("[quote:{}:abcd1234]", post_id); let result = post_process_quotes(&input, &authors); assert!(result.contains("original post removed")); assert!(!result.contains("Bob")); } #[test] fn unknown_post_id_left_unchanged() { let authors = HashMap::new(); let input = "[quote:00000000-0000-0000-0000-000000000000:abcd1234]"; let result = post_process_quotes(input, &authors); assert_eq!(result, input); } #[test] fn non_quote_text_unchanged() { let authors = HashMap::new(); let input = "

Hello world

"; let result = post_process_quotes(input, &authors); assert_eq!(result, input); } #[test] fn html_escapes_display_name() { let post_id = uuid::Uuid::new_v4(); let mut authors = HashMap::new(); authors.insert( post_id, QuoteAuthor { username: "user".to_string(), display_name: "A & C".to_string(), is_removed: false, }, ); let input = format!("[quote:{}:abcd1234]", post_id); let result = post_process_quotes(&input, &authors); assert!(result.contains("A <B> & C")); assert!(!result.contains("")); } }