pub(crate) const SUB_ITEM_INDENT: &str = " ";
pub(crate) const SECTION_HEADER_INDENT: &str = " ";
pub(crate) fn truncate_str(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
return s.to_string();
}
let mut out: String = s.chars().take(max_chars.saturating_sub(1)).collect();
out.push('…');
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_string_stays_empty() {
assert_eq!(truncate_str("", 0), "");
assert_eq!(truncate_str("", 5), "");
}
#[test]
fn string_shorter_than_cap_is_unchanged() {
assert_eq!(truncate_str("abc", 10), "abc");
assert_eq!(truncate_str("abc", 4), "abc");
}
#[test]
fn string_equal_to_cap_is_unchanged() {
assert_eq!(truncate_str("abcde", 5), "abcde");
assert_eq!(truncate_str("abcdefghij", 10), "abcdefghij");
}
#[test]
fn string_longer_than_cap_is_truncated_with_ellipsis() {
let out = truncate_str("abcdefghij", 5);
assert_eq!(out.chars().count(), 5);
assert!(out.ends_with('…'), "expected ellipsis, got: {out:?}");
assert!(out.starts_with("abcd"), "unexpected prefix: {out:?}");
}
#[test]
fn truncation_at_cap_one_keeps_only_ellipsis() {
let out = truncate_str("hello", 1);
assert_eq!(out, "…");
}
#[test]
fn multibyte_cjk_characters_counted_by_char_not_byte() {
let input = "你好世界ABC"; assert_eq!(input.chars().count(), 7);
assert_eq!(truncate_str(input, 7), input);
assert_eq!(truncate_str(input, 10), input);
let out = truncate_str(input, 4);
assert_eq!(out.chars().count(), 4);
assert!(out.ends_with('…'), "expected ellipsis, got: {out:?}");
assert!(out.starts_with("你好世"), "unexpected prefix: {out:?}");
}
#[test]
fn multibyte_emoji_counted_by_scalar_value() {
let input = "🦀🎉🚀AB"; assert_eq!(input.chars().count(), 5);
assert_eq!(truncate_str(input, 5), input);
let out = truncate_str(input, 4);
assert_eq!(out.chars().count(), 4);
assert!(out.ends_with('…'));
}
#[test]
fn max_chars_zero_returns_empty() {
assert_eq!(truncate_str("", 0), "");
let out = truncate_str("x", 0);
assert_eq!(out, "…");
}
}