flashmind_types/utils.rs
1//! Shared utility functions for safe string manipulation.
2//!
3//! All functions are UTF-8-safe — they never split multi-byte characters, and the
4//! newline variant stops at line boundaries for log-preview use cases.
5
6/// Truncate `s` to at most `max` bytes, landing on a valid UTF-8 character boundary.
7///
8/// Unlike naive `&s[..max]` this avoids splitting multi-byte characters (emoji, CJK).
9///
10/// # Arguments
11///
12/// * `s` - The string slice to truncate.
13/// * `max` - Maximum byte length. If `max` exceeds `s.len()`, the entire string is returned unchanged.
14///
15/// # Returns
16///
17/// A subslice of `s` that is at most `max` bytes long and ends on a valid UTF-8 character boundary.
18///
19/// # Panics
20///
21/// Does not panic — uses [`str::floor_char_boundary`] internally which clamps safely.
22pub fn truncate_utf8(s: &str, max: usize) -> &str {
23 &s[..s.floor_char_boundary(max)]
24}
25
26/// Like [`truncate_utf8`] but also stops at the first newline if it appears before `max`.
27/// Useful for log previews where long multi-line output should show only the first line.
28///
29/// # Arguments
30///
31/// * `s` - The string slice to truncate.
32/// * `max` - Maximum byte length of the returned slice (before newline clipping).
33///
34/// # Returns
35///
36/// A subslice of `s` that is at most `max` bytes, ends on a valid UTF-8 boundary, and does not
37/// cross a newline (`\n` or `\r`). If no newline exists within `max` bytes, behaves identically
38/// to [`truncate_utf8`].
39pub fn truncate_utf8_line(s: &str, max: usize) -> &str {
40 let max = s
41 .bytes()
42 .position(|b| b == b'\r' || b == b'\n')
43 .map_or(max, |nl| nl.min(max));
44
45 truncate_utf8(s, max)
46}