Skip to main content

mermaid_cli/utils/
text.rs

1use crate::constants::WEB_CONTENT_MAX_CHARS;
2
3/// Truncate content to a maximum character count (char-boundary safe)
4pub fn truncate_content(content: &str, max_chars: usize) -> String {
5    // Fast path: if byte length fits, char count definitely fits too
6    // (every char is at least 1 byte, so len <= max_chars implies char_count <= max_chars)
7    if content.len() <= max_chars {
8        return content.to_string();
9    }
10
11    // Slow path: multi-byte content might have fewer chars than bytes
12    // Find the byte position of the max_chars-th character
13    if let Some((byte_end, _)) = content.char_indices().nth(max_chars) {
14        format!("{}...[truncated]", &content[..byte_end])
15    } else {
16        // Fewer than max_chars characters total — no truncation needed
17        content.to_string()
18    }
19}
20
21/// Truncate web content using the default limit
22pub fn truncate_web_content(content: &str) -> String {
23    truncate_content(content, WEB_CONTENT_MAX_CHARS)
24}
25
26/// Format a duration in seconds as a human-readable string.
27///
28/// Uses decimal precision for sub-minute durations (e.g., "12.3s"),
29/// and integer components for longer durations (e.g., "1m 47s", "2h 5m 0s").
30pub fn format_duration(total_secs: f64) -> String {
31    let secs = total_secs as u64;
32    if secs < 60 {
33        return format!("{:.1}s", total_secs);
34    }
35    let days = secs / 86400;
36    let hours = (secs % 86400) / 3600;
37    let mins = (secs % 3600) / 60;
38    let remainder = secs % 60;
39    if days > 0 {
40        format!("{}d {}h {}m {}s", days, hours, mins, remainder)
41    } else if hours > 0 {
42        format!("{}h {}m {}s", hours, mins, remainder)
43    } else {
44        format!("{}m {}s", mins, remainder)
45    }
46}
47
48/// Format token count for display: "X.Xk" for >= 1000, raw number otherwise.
49pub fn format_tokens(tokens: usize) -> String {
50    if tokens >= 1000 {
51        format!("{:.1}k tokens", tokens as f64 / 1000.0)
52    } else {
53        format!("{} tokens", tokens)
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn test_format_duration_sub_minute() {
63        assert_eq!(format_duration(0.0), "0.0s");
64        assert_eq!(format_duration(12.3), "12.3s");
65        assert_eq!(format_duration(59.9), "59.9s");
66    }
67
68    #[test]
69    fn test_format_duration_minutes_and_above() {
70        assert_eq!(format_duration(60.0), "1m 0s");
71        assert_eq!(format_duration(107.0), "1m 47s");
72        assert_eq!(format_duration(3600.0), "1h 0m 0s");
73        assert_eq!(format_duration(86400.0), "1d 0h 0m 0s");
74        assert_eq!(format_duration(90061.0), "1d 1h 1m 1s");
75    }
76
77    #[test]
78    fn test_truncate_content() {
79        let short = "hello";
80        assert_eq!(truncate_content(short, 100), "hello");
81
82        let long = "a".repeat(200);
83        let truncated = truncate_content(&long, 50);
84        assert!(truncated.ends_with("...[truncated]"));
85        assert!(truncated.len() < 200);
86    }
87}