mermaid_cli/utils/
text.rs1use crate::constants::WEB_CONTENT_MAX_CHARS;
2
3pub fn truncate_content(content: &str, max_chars: usize) -> String {
7 if content.len() <= max_chars {
8 return content.to_string();
9 }
10 if let Some((byte_end, _)) = content.char_indices().nth(max_chars) {
11 format!("{}...[truncated]", &content[..byte_end])
12 } else {
13 content.to_string()
14 }
15}
16
17pub fn truncate_middle(content: &str, max_chars: usize) -> String {
23 if content.len() <= max_chars {
25 return content.to_string();
26 }
27 let total_chars = content.chars().count();
28 if total_chars <= max_chars {
29 return content.to_string();
30 }
31 let head_chars = max_chars / 2;
32 let tail_chars = max_chars - head_chars;
33 let elided = total_chars - head_chars - tail_chars;
34 let head_end = content
35 .char_indices()
36 .nth(head_chars)
37 .map(|(i, _)| i)
38 .unwrap_or(content.len());
39 let tail_start = content
40 .char_indices()
41 .nth(total_chars - tail_chars)
42 .map(|(i, _)| i)
43 .unwrap_or(content.len());
44 format!(
45 "{}\n…[{elided} chars elided]…\n{}",
46 &content[..head_end],
47 &content[tail_start..]
48 )
49}
50
51pub fn truncate_web_content(content: &str) -> String {
53 truncate_middle(content, WEB_CONTENT_MAX_CHARS)
54}
55
56const CONTINUATION_OVERLAP_WINDOW_BYTES: usize = 400;
60const CONTINUATION_OVERLAP_MIN_BYTES: usize = 16;
65
66pub fn continuation_overlap(prev: &str, continuation: &str) -> usize {
76 let mut window_start = prev.len().saturating_sub(CONTINUATION_OVERLAP_WINDOW_BYTES);
78 while window_start < prev.len() && !prev.is_char_boundary(window_start) {
79 window_start += 1;
80 }
81 let tail = &prev[window_start..];
82 let max_len = tail.len().min(continuation.len());
83 if max_len < CONTINUATION_OVERLAP_MIN_BYTES {
84 return 0;
85 }
86 for len in (CONTINUATION_OVERLAP_MIN_BYTES..=max_len).rev() {
88 if !continuation.is_char_boundary(len) {
89 continue;
90 }
91 let head = &continuation[..len];
92 if tail.ends_with(head) {
93 return len;
94 }
95 }
96 0
97}
98
99pub fn format_duration(total_secs: f64) -> String {
104 let secs = total_secs as u64;
105 if secs < 60 {
106 return format!("{:.1}s", total_secs);
107 }
108 let days = secs / 86400;
109 let hours = (secs % 86400) / 3600;
110 let mins = (secs % 3600) / 60;
111 let remainder = secs % 60;
112 if days > 0 {
113 format!("{}d {}h {}m {}s", days, hours, mins, remainder)
114 } else if hours > 0 {
115 format!("{}h {}m {}s", hours, mins, remainder)
116 } else {
117 format!("{}m {}s", mins, remainder)
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn test_format_duration_sub_minute() {
127 assert_eq!(format_duration(0.0), "0.0s");
128 assert_eq!(format_duration(12.3), "12.3s");
129 assert_eq!(format_duration(59.9), "59.9s");
130 }
131
132 #[test]
133 fn test_format_duration_minutes_and_above() {
134 assert_eq!(format_duration(60.0), "1m 0s");
135 assert_eq!(format_duration(107.0), "1m 47s");
136 assert_eq!(format_duration(3600.0), "1h 0m 0s");
137 assert_eq!(format_duration(86400.0), "1d 0h 0m 0s");
138 assert_eq!(format_duration(90061.0), "1d 1h 1m 1s");
139 }
140
141 #[test]
142 fn continuation_overlap_trims_a_resume_echo() {
143 let prev = "The resolver clamps the budget to the window room";
145 let cont = "to the window room, then omits the field entirely.";
146 assert_eq!(continuation_overlap(prev, cont), "to the window room".len());
147 }
148
149 #[test]
150 fn continuation_overlap_keeps_short_ambiguous_matches() {
151 assert_eq!(
154 continuation_overlap("…and then the ", "the answer is 42"),
155 0
156 );
157 assert_eq!(continuation_overlap("first half", "second half"), 0);
159 assert_eq!(continuation_overlap("", "anything"), 0);
161 assert_eq!(continuation_overlap("anything", ""), 0);
162 }
163
164 #[test]
165 fn continuation_overlap_prefers_the_longest_echo() {
166 let prev = "It hit the cap. It hit the cap. ";
168 let cont = "It hit the cap. Continuing now.";
169 assert_eq!(continuation_overlap(prev, cont), "It hit the cap. ".len());
170 }
171
172 #[test]
173 fn continuation_overlap_is_window_bounded() {
174 let echo = "a distinctive sentence that repeats";
177 let prev = format!("{echo}{}", "x".repeat(500));
178 assert_eq!(continuation_overlap(&prev, echo), 0);
179 }
180
181 #[test]
182 fn continuation_overlap_respects_char_boundaries() {
183 let prev = "código con acentuación específica";
185 let cont = "acentuación específica y más contenido";
186 let n = continuation_overlap(prev, cont);
187 assert_eq!(&cont[..n], "acentuación específica");
188 let _ = &cont[n..]; }
190
191 #[test]
192 fn truncate_middle_keeps_head_and_tail() {
193 let short = "hello";
194 assert_eq!(truncate_middle(short, 100), "hello");
195
196 let long = format!("{}TAIL_ERROR", "H".repeat(200));
198 let truncated = truncate_middle(&long, 50);
199 assert!(
200 truncated.starts_with("HHHH"),
201 "head must survive: {truncated}"
202 );
203 assert!(
204 truncated.ends_with("TAIL_ERROR"),
205 "tail must survive: {truncated}"
206 );
207 assert!(
208 truncated.contains("elided"),
209 "must mark elision: {truncated}"
210 );
211 assert!(truncated.chars().count() < long.chars().count());
212 }
213}