webex_message_handler/
mention_parser.rs1use once_cell::sync::Lazy;
4use regex::Regex;
5use std::collections::HashSet;
6
7#[derive(Debug, Clone, Default)]
9pub struct ParsedMentions {
10 pub mentioned_people: Vec<String>,
12 pub mentioned_groups: Vec<String>,
14}
15
16static MENTION_RE: Lazy<Regex> = Lazy::new(|| {
17 Regex::new(r#"(?i)<spark-mention[^>]*data-object-type="([^"]*)"[^>]*>"#).unwrap()
18});
19
20static PERSON_ID_RE: Lazy<Regex> = Lazy::new(|| {
21 Regex::new(r#"(?i)data-object-id="([^"]*)""#).unwrap()
22});
23
24static GROUP_TYPE_RE: Lazy<Regex> = Lazy::new(|| {
25 Regex::new(r#"(?i)data-group-type="([^"]*)""#).unwrap()
26});
27
28pub fn parse_mentions(html: Option<&str>) -> ParsedMentions {
33 let mut result = ParsedMentions::default();
34 let html = match html {
35 Some(h) if !h.is_empty() => h,
36 _ => return result,
37 };
38
39 let mut seen = HashSet::new();
40
41 for cap in MENTION_RE.captures_iter(html) {
42 let tag = cap.get(0).unwrap().as_str();
43 let object_type = &cap[1];
44
45 if object_type == "person" {
46 if let Some(id_cap) = PERSON_ID_RE.captures(tag) {
47 let id = &id_cap[1];
48 if !id.is_empty() && seen.insert(id.to_string()) {
49 result.mentioned_people.push(id.to_string());
50 }
51 }
52 } else if object_type == "groupMention" {
53 if let Some(group_cap) = GROUP_TYPE_RE.captures(tag) {
54 let group_type = &group_cap[1];
55 if !group_type.is_empty() && seen.insert(format!("group:{group_type}")) {
56 result.mentioned_groups.push(group_type.to_string());
57 }
58 }
59 }
60 }
61
62 result
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn test_single_person_mention() {
71 let html = r#"<p><spark-mention data-object-type="person" data-object-id="abc-123">Alice</spark-mention> hello</p>"#;
72 let m = parse_mentions(Some(html));
73 assert_eq!(m.mentioned_people, vec!["abc-123"]);
74 assert!(m.mentioned_groups.is_empty());
75 }
76
77 #[test]
78 fn test_group_mention() {
79 let html = r#"<p><spark-mention data-object-type="groupMention" data-group-type="all">All</spark-mention> hello</p>"#;
80 let m = parse_mentions(Some(html));
81 assert!(m.mentioned_people.is_empty());
82 assert_eq!(m.mentioned_groups, vec!["all"]);
83 }
84
85 #[test]
86 fn test_mixed_mentions() {
87 let html = r#"<p><spark-mention data-object-type="person" data-object-id="p1">Alice</spark-mention> and <spark-mention data-object-type="groupMention" data-group-type="all">All</spark-mention></p>"#;
88 let m = parse_mentions(Some(html));
89 assert_eq!(m.mentioned_people, vec!["p1"]);
90 assert_eq!(m.mentioned_groups, vec!["all"]);
91 }
92
93 #[test]
94 fn test_duplicate_dedup() {
95 let html = r#"<spark-mention data-object-type="person" data-object-id="p1">A</spark-mention> <spark-mention data-object-type="person" data-object-id="p1">A</spark-mention>"#;
96 let m = parse_mentions(Some(html));
97 assert_eq!(m.mentioned_people, vec!["p1"]);
98 }
99
100 #[test]
101 fn test_empty_html() {
102 let m = parse_mentions(None);
103 assert!(m.mentioned_people.is_empty());
104 assert!(m.mentioned_groups.is_empty());
105 }
106
107 #[test]
108 fn test_no_mentions() {
109 let m = parse_mentions(Some("<p>Hello world</p>"));
110 assert!(m.mentioned_people.is_empty());
111 assert!(m.mentioned_groups.is_empty());
112 }
113}