use crate::errors::Result;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Measurement {
pub minimum: usize,
pub maximum: usize,
}
impl Measurement {
#[inline]
#[must_use]
pub const fn new(minimum: usize, maximum: usize) -> Self {
Self { minimum, maximum }
}
#[inline]
#[must_use]
pub const fn fixed(width: usize) -> Self {
Self {
minimum: width,
maximum: width,
}
}
#[inline]
#[must_use]
pub const fn span(&self) -> usize {
self.maximum.saturating_sub(self.minimum)
}
#[inline]
#[must_use]
pub const fn union(self, other: Self) -> Self {
let min = if self.minimum > other.minimum {
self.minimum
} else {
other.minimum
};
let max = if self.maximum > other.maximum {
self.maximum
} else {
other.maximum
};
Self {
minimum: min,
maximum: max,
}
}
#[inline]
#[must_use]
pub const fn clamp_max(self, max_width: usize) -> Self {
let max = if self.maximum > max_width {
max_width
} else {
self.maximum
};
let min = if self.minimum > max {
max
} else {
self.minimum
};
Self {
minimum: min,
maximum: max,
}
}
#[inline]
#[must_use]
pub const fn expand_min(self, min_width: usize) -> Self {
let min = if self.minimum < min_width {
min_width
} else {
self.minimum
};
let max = if self.maximum < min {
min
} else {
self.maximum
};
Self {
minimum: min,
maximum: max,
}
}
}
#[inline]
#[must_use]
pub fn cell_len(s: &str) -> usize {
UnicodeWidthStr::width(s)
}
#[must_use]
pub fn max_line_width(s: &str) -> usize {
s.lines().map(cell_len).max().unwrap_or(0)
}
#[must_use]
pub fn min_width(s: &str) -> usize {
s.split_whitespace().map(cell_len).max().unwrap_or(0)
}
#[must_use]
pub fn measure_text(s: &str) -> Measurement {
Measurement {
minimum: min_width(s),
maximum: max_line_width(s),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct MeasureOptions {
pub max_width: usize,
pub strip_ansi: bool,
}
impl MeasureOptions {
#[inline]
#[must_use]
pub const fn new(max_width: usize) -> Self {
Self {
max_width,
strip_ansi: true,
}
}
}
impl Default for MeasureOptions {
#[inline]
fn default() -> Self {
Self {
max_width: 80,
strip_ansi: true,
}
}
}
pub trait Measurable {
fn measure(&self, options: &MeasureOptions) -> Result<Measurement>;
}
#[must_use]
pub fn strip_ansi(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut in_escape = false;
for ch in s.chars() {
if in_escape {
if ch.is_ascii_alphabetic() {
in_escape = false;
}
} else if ch == '\x1b' {
in_escape = true;
} else {
result.push(ch);
}
}
result
}
#[must_use]
pub fn cell_len_with_options(s: &str, strip: bool) -> usize {
if strip {
cell_len(&strip_ansi(s))
} else {
cell_len(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_measurement_new() {
let m = Measurement::new(5, 10);
assert_eq!(m.minimum, 5);
assert_eq!(m.maximum, 10);
}
#[test]
fn test_measurement_fixed() {
let m = Measurement::fixed(7);
assert_eq!(m.minimum, 7);
assert_eq!(m.maximum, 7);
}
#[test]
fn test_measurement_span() {
let m = Measurement::new(5, 10);
assert_eq!(m.span(), 5);
}
#[test]
fn test_measurement_union() {
let m1 = Measurement::new(5, 10);
let m2 = Measurement::new(3, 15);
let union = m1.union(m2);
assert_eq!(union.minimum, 5);
assert_eq!(union.maximum, 15);
}
#[test]
fn test_measurement_clamp_max() {
let m = Measurement::new(5, 20);
let clamped = m.clamp_max(10);
assert_eq!(clamped.minimum, 5);
assert_eq!(clamped.maximum, 10);
let m = Measurement::new(15, 20);
let clamped = m.clamp_max(10);
assert_eq!(clamped.minimum, 10);
assert_eq!(clamped.maximum, 10);
}
#[test]
fn test_measurement_expand_min() {
let m = Measurement::new(5, 20);
let expanded = m.expand_min(10);
assert_eq!(expanded.minimum, 10);
assert_eq!(expanded.maximum, 20);
let m = Measurement::new(5, 8);
let expanded = m.expand_min(10);
assert_eq!(expanded.minimum, 10);
assert_eq!(expanded.maximum, 10);
}
#[test]
fn test_cell_len() {
assert_eq!(cell_len("hello"), 5);
assert_eq!(cell_len(""), 0);
assert_eq!(cell_len("日本語"), 6);
assert_eq!(cell_len("hello日本"), 9);
}
#[test]
fn test_max_line_width() {
assert_eq!(max_line_width("hello\nworld"), 5);
assert_eq!(max_line_width("short\nlonger line"), 11);
assert_eq!(max_line_width("single line"), 11);
assert_eq!(max_line_width(""), 0);
}
#[test]
fn test_min_width() {
assert_eq!(min_width("hello world"), 5);
assert_eq!(min_width("supercalifragilisticexpialidocious"), 34);
assert_eq!(min_width("a b c"), 1);
assert_eq!(min_width(""), 0);
}
#[test]
fn test_measure_text() {
let m = measure_text("hello world");
assert_eq!(m.minimum, 5); assert_eq!(m.maximum, 11);
let m = measure_text("short\nmuch longer line");
assert_eq!(m.minimum, 6); assert_eq!(m.maximum, 16); }
#[test]
fn test_strip_ansi() {
assert_eq!(strip_ansi("hello"), "hello");
assert_eq!(strip_ansi("\x1b[31mhello\x1b[0m"), "hello");
assert_eq!(
strip_ansi("\x1b[1;31;40mcolored text\x1b[0m"),
"colored text"
);
}
#[test]
fn test_cell_len_with_options() {
assert_eq!(cell_len_with_options("hello", false), 5);
assert_eq!(cell_len_with_options("hello", true), 5);
assert_eq!(cell_len_with_options("\x1b[31mhello\x1b[0m", true), 5);
assert!(cell_len_with_options("\x1b[31mhello\x1b[0m", false) > 5);
}
#[test]
fn test_measure_options_default() {
let opts = MeasureOptions::default();
assert_eq!(opts.max_width, 80);
assert!(opts.strip_ansi);
}
#[test]
fn test_measure_options_new() {
let opts = MeasureOptions::new(120);
assert_eq!(opts.max_width, 120);
assert!(opts.strip_ansi);
}
}