use std::collections::HashMap;
pub fn compact_post_id(handle: &str, rkey: &str, seen_posts: &HashMap<String, String>) -> String {
let full_id = format!("{}/{}", handle, rkey);
if seen_posts.contains_key(&full_id) {
ultra_compact_id(handle, rkey)
} else {
format!("@{}/{}", handle, rkey)
}
}
pub fn ultra_compact_id(handle: &str, rkey: &str) -> String {
let first_letter = handle.chars().next().unwrap_or('?');
let last_four = if rkey.len() > 4 {
&rkey[rkey.len()-4..]
} else {
rkey
};
format!("@{}/…{}", first_letter, last_four)
}
pub fn blockquote_content(text: &str) -> String {
if text.is_empty() {
return "> \n".to_string();
}
text.lines()
.map(|line| format!("> {}", line))
.collect::<Vec<_>>()
.join("\n")
}
pub fn format_stats(likes: i32, reposts: i32, quotes: i32, replies: i32) -> String {
let mut parts = Vec::new();
if likes > 0 {
parts.push(format!("👍 {}", likes));
}
let reshares = reposts + quotes;
if reshares > 0 {
parts.push(format!("♻️ {}", reshares));
}
if replies > 0 {
parts.push(format!("💬 {}", replies));
}
parts.join(" ")
}
pub fn format_timestamp(timestamp: &str) -> String {
if let Some(dot_pos) = timestamp.find('.') {
let before_dot = ×tamp[..dot_pos];
format!("{}Z", before_dot)
} else if timestamp.ends_with('Z') {
timestamp.to_string()
} else {
format!("{}Z", timestamp.trim_end_matches('+').split('+').next().unwrap_or(timestamp))
}
}
pub fn extract_rkey(uri: &str) -> &str {
if uri.is_empty() {
return "unknown";
}
uri.split('/').last().unwrap_or("unknown")
}
pub fn threading_indicator(depth: usize, reply_to_compact: &str, author_id: &str) -> String {
if depth == 0 {
author_id.to_string()
} else {
let indent = " ".repeat(depth - 1);
format!("{}└─{} → {}", indent, reply_to_compact, author_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compact_post_id_first_mention() {
let seen = HashMap::new();
let result = compact_post_id("alice.bsky.social", "3kq8a3f1", &seen);
assert_eq!(result, "@alice.bsky.social/3kq8a3f1");
}
#[test]
fn test_compact_post_id_subsequent() {
let mut seen = HashMap::new();
seen.insert("alice.bsky.social/3kq8a3f1".to_string(), "1".to_string());
let result = compact_post_id("alice.bsky.social", "3kq8a3f1", &seen);
assert_eq!(result, "@a/…a3f1");
}
#[test]
fn test_ultra_compact_id() {
assert_eq!(ultra_compact_id("alice.bsky.social", "3kq8a3f1"), "@a/…a3f1");
assert_eq!(ultra_compact_id("bob", "3kq8b2e4"), "@b/…b2e4");
assert_eq!(ultra_compact_id("carol-long-handle", "abc"), "@c/…abc");
}
#[test]
fn test_blockquote_content_simple() {
assert_eq!(blockquote_content("Hello world"), "> Hello world");
}
#[test]
fn test_blockquote_content_multiline() {
let input = "Line 1\nLine 2\nLine 3";
let expected = "> Line 1\n> Line 2\n> Line 3";
assert_eq!(blockquote_content(input), expected);
}
#[test]
fn test_blockquote_content_with_markdown() {
let input = "# Header\n## Subheader\n- Item 1\n- Item 2";
let expected = "> # Header\n> ## Subheader\n> - Item 1\n> - Item 2";
assert_eq!(blockquote_content(input), expected);
}
#[test]
fn test_blockquote_content_empty() {
assert_eq!(blockquote_content(""), "> \n");
}
#[test]
fn test_format_stats_all() {
assert_eq!(format_stats(234, 50, 39, 45), "👍 234 ♻️ 89 💬 45");
}
#[test]
fn test_format_stats_only_likes() {
assert_eq!(format_stats(12, 0, 0, 0), "👍 12");
}
#[test]
fn test_format_stats_no_reposts() {
assert_eq!(format_stats(33, 0, 1, 1), "👍 33 ♻️ 1 💬 1");
}
#[test]
fn test_format_stats_all_zero() {
assert_eq!(format_stats(0, 0, 0, 0), "");
}
#[test]
fn test_format_stats_reshares_combined() {
assert_eq!(format_stats(10, 5, 3, 2), "👍 10 ♻️ 8 💬 2");
}
#[test]
fn test_format_timestamp_with_millis() {
assert_eq!(
format_timestamp("2025-10-31T23:38:49.569Z"),
"2025-10-31T23:38:49Z"
);
}
#[test]
fn test_format_timestamp_without_millis() {
assert_eq!(
format_timestamp("2024-10-06T10:15:33Z"),
"2024-10-06T10:15:33Z"
);
}
#[test]
fn test_format_timestamp_no_z_suffix() {
assert_eq!(
format_timestamp("2024-10-06T10:15:33"),
"2024-10-06T10:15:33Z"
);
}
#[test]
fn test_extract_rkey() {
assert_eq!(
extract_rkey("at://did:plc:abc/app.bsky.feed.post/3m4jnj3efp22t"),
"3m4jnj3efp22t"
);
assert_eq!(extract_rkey("3m4jnj3efp22t"), "3m4jnj3efp22t");
assert_eq!(extract_rkey(""), "unknown");
}
#[test]
fn test_threading_indicator_root() {
assert_eq!(
threading_indicator(0, "", "@alice/3kq8a3f1"),
"@alice/3kq8a3f1"
);
}
#[test]
fn test_threading_indicator_depth_1() {
assert_eq!(
threading_indicator(1, "@a/…a3f1", "@bob/3kq8b2e4"),
"└─@a/…a3f1 → @bob/3kq8b2e4"
);
}
#[test]
fn test_threading_indicator_depth_2() {
assert_eq!(
threading_indicator(2, "@b/…b2e4", "@carol/3kq8c3f5"),
" └─@b/…b2e4 → @carol/3kq8c3f5"
);
}
#[test]
fn test_threading_indicator_depth_3() {
assert_eq!(
threading_indicator(3, "@c/…c3f5", "@dave/3kq8d4f6"),
" └─@c/…c3f5 → @dave/3kq8d4f6"
);
}
}