use crate::error::BoxenError;
use unicode_width::UnicodeWidthStr;
#[must_use]
pub fn text_width(text: &str) -> usize {
if !text.contains('\x1b') {
return UnicodeWidthStr::width(text);
}
let clean_text = strip_ansi_codes(text);
UnicodeWidthStr::width(clean_text.as_str())
}
#[must_use]
pub fn strip_ansi_codes(text: &str) -> String {
if !text.contains('\x1b') {
return text.to_string();
}
let mut result = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next();
for escape_char in chars.by_ref() {
if escape_char.is_ascii_alphabetic() {
break;
}
}
} else {
result.push(ch);
}
} else {
result.push(ch);
}
}
result
}
#[must_use]
pub fn max_line_width(lines: &[&str]) -> usize {
lines.iter().map(|line| text_width(line)).max().unwrap_or(0)
}
pub fn line_widths(text: &str) -> Vec<usize> {
text.lines().map(text_width).collect()
}
pub fn validate_text_measurement(text: &str) -> Result<usize, BoxenError> {
let width = text_width(text);
if width > 10000 {
return Err(BoxenError::text_processing_error(
format!("Text width {width} seems unreasonably large"),
vec![
crate::error::ErrorRecommendation::suggestion_only(
"Excessive text width".to_string(),
"This may indicate an issue with text measurement or very wide content"
.to_string(),
),
crate::error::ErrorRecommendation::with_auto_fix(
"Use width constraint".to_string(),
"Limit the box width to prevent issues".to_string(),
".width(80)".to_string(),
),
],
));
}
Ok(width)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_ascii_width() {
assert_eq!(text_width("hello"), 5);
assert_eq!(text_width(""), 0);
assert_eq!(text_width("a"), 1);
}
#[test]
fn test_unicode_width() {
assert_eq!(text_width("你好"), 4); assert_eq!(text_width("こんにちは"), 10);
assert_eq!(text_width("é"), 1);
assert_eq!(text_width("a\u{200B}b"), 2); }
#[test]
fn test_ansi_escape_sequences() {
assert_eq!(text_width("\x1b[31mred\x1b[0m"), 3);
assert_eq!(text_width("\x1b[1;32mbold green\x1b[0m"), 10);
assert_eq!(text_width("\x1b[38;5;196mhello\x1b[0m"), 5);
assert_eq!(text_width("\x1b[48;2;255;0;0mworld\x1b[0m"), 5);
}
#[test]
fn test_mixed_content() {
assert_eq!(text_width("\x1b[31m你好\x1b[0m"), 4);
let lines = vec!["hello", "\x1b[31mred\x1b[0m", "你好"];
assert_eq!(max_line_width(&lines), 5);
}
#[test]
fn test_strip_ansi_codes() {
assert_eq!(strip_ansi_codes("hello"), "hello");
assert_eq!(strip_ansi_codes("\x1b[31mred\x1b[0m"), "red");
assert_eq!(
strip_ansi_codes("\x1b[1;32mbold green\x1b[0m"),
"bold green"
);
assert_eq!(
strip_ansi_codes("no\x1b[31mcolor\x1b[0mhere"),
"nocolorhere"
);
}
#[test]
fn test_line_widths() {
let text = "hello\nworld\n你好";
let widths = line_widths(text);
assert_eq!(widths, vec![5, 5, 4]);
}
#[test]
fn test_validate_text_measurement() {
assert!(validate_text_measurement("hello").is_ok());
assert!(validate_text_measurement("你好").is_ok());
assert!(validate_text_measurement("\x1b[31mred\x1b[0m").is_ok());
let long_text = "a".repeat(1000);
assert!(validate_text_measurement(&long_text).is_ok());
}
#[test]
fn test_empty_and_whitespace() {
assert_eq!(text_width(""), 0);
assert_eq!(text_width(" "), 1);
assert_eq!(text_width(" "), 3);
assert_eq!(text_width("\t"), 1); assert_eq!(text_width("\n"), 1); }
}