gitdataai/lib/channel/richtext.rs

151 lines
4.8 KiB
Rust

use serde::{Deserialize, Serialize};
/// Parsed mention from `@[type:id:label]` IR format.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Mention {
pub mention_type: String,
pub target_id: String,
pub label: String,
}
/// Parse all `@[type:id:label]` mentions from content.
/// Returns deduplicated mentions in order of first appearance.
pub fn parse_mentions(content: &str) -> Vec<Mention> {
let mut mentions = Vec::new();
let mut seen = std::collections::HashSet::new();
// Simple manual parser for @[type:id:label]
let bytes = content.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
// Look for "@["
if i + 2 < len && bytes[i] == b'@' && bytes[i + 1] == b'[' {
let start = i + 2; // after "@["
// Find first ':'' after start
if let Some(type_end) = content[start..].find(':') {
let mention_type = &content[start..start + type_end];
let after_type = start + type_end + 1; // after first ':'
// Find second ':' (between id and label)
if let Some(id_end) = content[after_type..].find(':') {
let target_id = &content[after_type..after_type + id_end];
let after_id = after_type + id_end + 1; // after second ':'
// Find closing ']'
if let Some(close) = content[after_id..].find(']') {
let label = &content[after_id..after_id + close];
if !mention_type.is_empty() && !target_id.is_empty() {
let key = format!("{}:{}", mention_type, target_id);
if seen.insert(key) {
mentions.push(Mention {
mention_type: mention_type.to_string(),
target_id: target_id.to_string(),
label: label.to_string(),
});
}
}
i = after_id + close + 1; // skip past ']'
continue;
}
}
}
}
i += 1;
}
mentions
}
/// Extract unique target IDs of a specific mention type.
pub fn extract_mention_ids(
mentions: &[Mention],
mention_type: &str,
) -> Vec<String> {
mentions
.iter()
.filter(|m| m.mention_type == mention_type)
.map(|m| m.target_id.clone())
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_single_room_mention() {
let input = "hey check out @[room:abc123:general]";
let mentions = parse_mentions(input);
assert_eq!(mentions.len(), 1);
assert_eq!(mentions[0].mention_type, "room");
assert_eq!(mentions[0].target_id, "abc123");
assert_eq!(mentions[0].label, "general");
}
#[test]
fn test_parse_single_repo_mention() {
let input = "look at @[repo:my-repo:my-repo]";
let mentions = parse_mentions(input);
assert_eq!(mentions.len(), 1);
assert_eq!(mentions[0].mention_type, "repo");
assert_eq!(mentions[0].target_id, "my-repo");
}
#[test]
fn test_parse_multiple_mentions() {
let input =
"compare @[repo:backend:backend] with @[repo:frontend:frontend]";
let mentions = parse_mentions(input);
assert_eq!(mentions.len(), 2);
assert_eq!(mentions[0].target_id, "backend");
assert_eq!(mentions[1].target_id, "frontend");
}
#[test]
fn test_deduplicate() {
let input = "look at @[repo:a:a] and also @[repo:a:a] please";
let mentions = parse_mentions(input);
assert_eq!(mentions.len(), 1);
}
#[test]
fn test_no_mentions() {
let input = "hello world no mentions here";
let mentions = parse_mentions(input);
assert_eq!(mentions.len(), 0);
}
#[test]
fn test_mixed_mentions() {
let input = "@[user:abc:John] and @[room:xyz:general] and @[repo:r:r]";
let mentions = parse_mentions(input);
assert_eq!(mentions.len(), 3);
}
#[test]
fn test_incomplete_mention_ignored() {
let input = "this @[incomplete is just text";
let mentions = parse_mentions(input);
assert_eq!(mentions.len(), 0);
}
#[test]
fn test_empty_input() {
let mentions = parse_mentions("");
assert_eq!(mentions.len(), 0);
}
#[test]
fn test_extract_mention_ids() {
let input = "@[repo:a:a] and @[room:b:general] and @[repo:c:c]";
let mentions = parse_mentions(input);
let repo_ids = extract_mention_ids(&mentions, "repo");
assert_eq!(repo_ids, vec!["a", "c"]);
}
}