Skip to main content

skilllite_agent/types/
string_utils.rs

1//! UTF-8 safe string helpers.
2
3/// Truncate a string at a safe UTF-8 char boundary (from the start).
4/// Returns a &str of at most `max_bytes` bytes, never splitting a multi-byte character.
5pub fn safe_truncate(s: &str, max_bytes: usize) -> &str {
6    if s.len() <= max_bytes {
7        return s;
8    }
9    let mut end = max_bytes;
10    while end > 0 && !s.is_char_boundary(end) {
11        end -= 1;
12    }
13    &s[..end]
14}
15
16/// Get a &str starting from approximately `start_pos`, adjusted forward to a safe UTF-8 boundary.
17pub fn safe_slice_from(s: &str, start_pos: usize) -> &str {
18    if start_pos >= s.len() {
19        return "";
20    }
21    let mut start = start_pos;
22    while start < s.len() && !s.is_char_boundary(start) {
23        start += 1;
24    }
25    &s[start..]
26}
27
28/// Split a string into chunks of approximately `chunk_size` bytes,
29/// ensuring each split occurs at a valid UTF-8 char boundary.
30pub fn chunk_str(s: &str, chunk_size: usize) -> Vec<&str> {
31    let mut chunks = Vec::new();
32    let mut start = 0;
33    while start < s.len() {
34        let target_end = (start + chunk_size).min(s.len());
35        let mut safe_end = target_end;
36        while safe_end > start && !s.is_char_boundary(safe_end) {
37            safe_end -= 1;
38        }
39        if safe_end == start && start < s.len() {
40            safe_end = start + 1;
41            while safe_end < s.len() && !s.is_char_boundary(safe_end) {
42                safe_end += 1;
43            }
44        }
45        chunks.push(&s[start..safe_end]);
46        start = safe_end;
47    }
48    chunks
49}