pub(crate) fn head(s: &str, max_bytes: usize) -> &str {
let mut end = max_bytes.min(s.len());
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
pub(crate) fn tail(s: &str, max_bytes: usize) -> &str {
let mut start = s.len().saturating_sub(max_bytes);
while start < s.len() && !s.is_char_boundary(start) {
start += 1;
}
&s[start..]
}
pub(crate) fn char_boundary_at_or_before(s: &str, n: usize) -> usize {
if n >= s.len() {
return s.len();
}
let mut i = n;
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
i
}
pub(crate) fn char_boundary_at_or_after(s: &str, n: usize) -> usize {
if n >= s.len() {
return s.len();
}
let mut i = n;
while i < s.len() && !s.is_char_boundary(i) {
i += 1;
}
i
}
pub(crate) fn ellipsize(s: &str, max_bytes: usize) -> String {
if s.len() <= max_bytes {
return s.to_string();
}
format!("{}…", head(s, max_bytes.saturating_sub('…'.len_utf8())))
}
pub(crate) fn first_line_preview(content: &str) -> String {
let first = content.lines().next().unwrap_or("").trim();
if first.chars().count() <= 80 {
first.to_string()
} else {
let cut: String = first.chars().take(77).collect();
format!("{cut}...")
}
}
pub(crate) fn short_id(id: &str) -> String {
id.chars().take(8).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn head_ascii_within_and_over_budget() {
assert_eq!(head("hello", 10), "hello");
assert_eq!(head("hello", 3), "hel");
assert_eq!(head("hello", 0), "");
}
#[test]
fn head_never_splits_a_multibyte_char() {
let s = "café"; assert_eq!(head(s, 4), "caf"); assert_eq!(head(s, 5), "café");
let cjk = "日本語"; assert_eq!(head(cjk, 4), "日"); assert_eq!(head(cjk, 3), "日");
let emoji = "a😀b"; assert_eq!(head(emoji, 3), "a"); assert_eq!(head(emoji, 5), "a😀");
}
#[test]
fn head_handles_multibyte_straddling_a_large_cut() {
let mut s = "a".repeat(199);
s.push('世'); s.push_str(&"b".repeat(50));
let cut = head(&s, 200); assert!(s.starts_with(cut));
assert_eq!(cut.len(), 199, "floored below the multibyte char");
}
#[test]
fn tail_never_splits_a_multibyte_char() {
let s = "café"; assert_eq!(tail(s, 2), "é"); assert_eq!(tail(s, 3), "fé"); assert_eq!(tail(s, 10), "café");
let cjk = "日本語";
assert_eq!(tail(cjk, 4), "語"); assert_eq!(tail(cjk, 0), "");
}
#[test]
fn ellipsize_caps_and_appends_marker_utf8_safe() {
assert_eq!(ellipsize("short", 100), "short"); assert_eq!(ellipsize("abcdefgh", 6), "abc…");
assert_eq!(ellipsize("日本語", 7), "日…");
}
#[test]
fn char_boundary_helpers_floor_and_ceil() {
let cjk = "日本語"; assert_eq!(char_boundary_at_or_before(cjk, 4), 3); assert_eq!(char_boundary_at_or_after(cjk, 4), 6); assert_eq!(char_boundary_at_or_before(cjk, 99), cjk.len());
}
}