#![deny(missing_docs)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![warn(clippy::nursery)]
use std::fmt;
use std::ops::{Add, Div, Mul, Sub};
use string_width::string_width;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Alignment {
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Width(usize);
impl Width {
#[must_use]
pub const fn new(value: usize) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> usize {
self.0
}
#[must_use]
pub const fn saturating_sub(self, other: Self) -> Self {
Self(self.0.saturating_sub(other.0))
}
#[must_use]
pub const fn is_zero(self) -> bool {
self.0 == 0
}
}
impl From<usize> for Width {
fn from(value: usize) -> Self {
Self(value)
}
}
impl Add for Width {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub for Width {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl Mul for Width {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
Self(self.0 * rhs.0)
}
}
impl Div for Width {
type Output = Self;
fn div(self, rhs: Self) -> Self::Output {
Self(self.0 / rhs.0)
}
}
impl fmt::Display for Width {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone)]
pub struct AlignOptions {
pub align: Alignment,
pub split: String,
pub pad: char,
}
impl Default for AlignOptions {
fn default() -> Self {
Self {
align: Alignment::Center,
split: "\n".to_string(),
pad: ' ',
}
}
}
impl AlignOptions {
#[must_use]
pub fn new(align: Alignment) -> Self {
Self {
align,
..Default::default()
}
}
#[must_use]
pub fn with_split<S: Into<String>>(mut self, split: S) -> Self {
self.split = split.into();
self
}
#[must_use]
pub const fn with_pad(mut self, pad: char) -> Self {
self.pad = pad;
self
}
}
fn create_padding(pad_char: char, count: usize) -> String {
match count {
0 => String::new(),
1 => pad_char.to_string(),
2..=16 => pad_char.to_string().repeat(count),
_ => {
let mut padding = String::with_capacity(count * pad_char.len_utf8());
for _ in 0..count {
padding.push(pad_char);
}
padding
}
}
}
#[must_use]
pub fn ansi_align(text: &str) -> String {
ansi_align_with_options(text, &AlignOptions::default())
}
fn calculate_line_widths<'a>(text: &'a str, split: &str) -> Vec<(&'a str, Width)> {
text.split(split)
.map(|line| (line, Width::from(string_width(line))))
.collect()
}
fn find_max_width(line_data: &[(&str, Width)]) -> Width {
line_data
.iter()
.map(|(_, width)| *width)
.max()
.unwrap_or(Width::new(0))
}
fn align_line(
line: &str,
line_width: Width,
max_width: Width,
alignment: Alignment,
pad_char: char,
) -> String {
let padding_needed = match alignment {
Alignment::Left => 0,
Alignment::Center => (max_width.get() - line_width.get()) / 2,
Alignment::Right => max_width.get() - line_width.get(),
};
if padding_needed == 0 {
line.to_string()
} else {
let mut result = create_padding(pad_char, padding_needed);
result.push_str(line);
result
}
}
#[must_use]
pub fn ansi_align_with_options(text: &str, opts: &AlignOptions) -> String {
if text.is_empty() || opts.align == Alignment::Left {
return text.to_string();
}
let line_data = calculate_line_widths(text, &opts.split);
let max_width = find_max_width(&line_data);
let aligned_lines: Vec<String> = line_data
.into_iter()
.map(|(line, width)| align_line(line, width, max_width, opts.align, opts.pad))
.collect();
aligned_lines.join(&opts.split)
}
#[must_use]
pub fn left(text: &str) -> String {
ansi_align_with_options(text, &AlignOptions::new(Alignment::Left))
}
#[must_use]
pub fn center(text: &str) -> String {
ansi_align_with_options(text, &AlignOptions::new(Alignment::Center))
}
#[must_use]
pub fn right(text: &str) -> String {
ansi_align_with_options(text, &AlignOptions::new(Alignment::Right))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_left_alignment() {
let text = "hello\nworld";
let result = left(text);
assert_eq!(result, text); }
#[test]
fn test_center_alignment() {
let text = "hi\nhello";
let result = center(text);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], " hi"); assert_eq!(lines[1], "hello"); }
#[test]
fn test_right_alignment() {
let text = "hi\nhello";
let result = right(text);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], " hi"); assert_eq!(lines[1], "hello"); }
#[test]
fn test_unicode_characters() {
let text = "ε€\nε€ε€ε€";
let result = center(text);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], " ε€"); assert_eq!(lines[1], "ε€ε€ε€"); }
#[test]
fn test_ansi_escape_sequences() {
let text = "hello\n\u{001B}[1mworld\u{001B}[0m";
let result = center(text);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], "hello");
assert_eq!(lines[1], "\u{001B}[1mworld\u{001B}[0m"); }
#[test]
fn test_complex_ansi_sequences() {
let text = "\x1b[31m\x1b[1mred\x1b[0m\n\x1b[32mgreen text\x1b[0m";
let result = right(text);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], " \x1b[31m\x1b[1mred\x1b[0m"); assert_eq!(lines[1], "\x1b[32mgreen text\x1b[0m"); }
#[test]
fn test_empty_string() {
assert_eq!(ansi_align_with_options("", &AlignOptions::default()), "");
assert_eq!(left(""), "");
assert_eq!(center(""), "");
assert_eq!(right(""), "");
}
#[test]
fn test_single_line() {
let text = "hello";
assert_eq!(left(text), "hello");
assert_eq!(center(text), "hello");
assert_eq!(right(text), "hello");
}
#[test]
fn test_single_character() {
let text = "a\nb";
let result = center(text);
assert_eq!(result, "a\nb"); }
#[test]
fn test_whitespace_only() {
let text = " \n ";
let result = center(text);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], " "); assert_eq!(lines[1], " "); }
#[test]
fn test_custom_split_and_pad() {
let text = "a|bb";
let opts = AlignOptions::new(Alignment::Right)
.with_split("|")
.with_pad('.');
let result = ansi_align_with_options(text, &opts);
assert_eq!(result, ".a|bb");
}
#[test]
fn test_custom_split_multichar() {
let text = "short<->very long line";
let opts = AlignOptions::new(Alignment::Center).with_split("<->");
let result = ansi_align_with_options(text, &opts);
assert_eq!(result, " short<->very long line");
}
#[test]
fn test_different_padding_chars() {
let text = "hi\nhello";
let opts = AlignOptions::new(Alignment::Right).with_pad('.');
let result = ansi_align_with_options(text, &opts);
assert_eq!(result, "...hi\nhello");
let opts = AlignOptions::new(Alignment::Center).with_pad('_');
let result = ansi_align_with_options(text, &opts);
assert_eq!(result, "_hi\nhello");
let opts = AlignOptions::new(Alignment::Right).with_pad('0');
let result = ansi_align_with_options(text, &opts);
assert_eq!(result, "000hi\nhello");
}
#[test]
fn test_large_padding() {
let text = format!("a\n{}", "b".repeat(100));
let result = right(&text);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0].len(), 100); assert!(lines[0].starts_with(&" ".repeat(99)));
assert!(lines[0].ends_with('a'));
assert_eq!(lines[1], "b".repeat(100));
}
#[test]
fn test_no_padding_optimization() {
let text = "same\nsame\nsame";
let result = center(text);
assert_eq!(result, text); }
#[test]
fn test_width_type() {
let width = Width::new(42);
assert_eq!(width.get(), 42);
let width_from_usize: Width = 24.into();
assert_eq!(width_from_usize.get(), 24);
assert!(Width::new(10) < Width::new(20));
assert_eq!(Width::new(15), Width::new(15));
}
#[test]
fn test_width_arithmetic() {
let w1 = Width::new(10);
let w2 = Width::new(5);
assert_eq!((w1 + w2).get(), 15);
assert_eq!((w1 - w2).get(), 5);
assert_eq!((w1 * w2).get(), 50);
assert_eq!((w1 / w2).get(), 2);
assert_eq!(w2.saturating_sub(w1).get(), 0); assert_eq!(w1.saturating_sub(w2).get(), 5);
assert!(Width::new(0).is_zero());
assert!(!w1.is_zero());
}
#[test]
fn test_width_display() {
let width = Width::new(42);
assert_eq!(format!("{width}"), "42");
assert_eq!(format!("{}", Width::new(0)), "0");
assert_eq!(format!("{}", Width::new(999)), "999");
}
#[test]
fn test_mixed_width_lines() {
let text = "a\nbb\nccc\ndddd\neeeee";
let result = center(text);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], " a"); assert_eq!(lines[1], " bb"); assert_eq!(lines[2], " ccc"); assert_eq!(lines[3], "dddd"); assert_eq!(lines[4], "eeeee");
let result = right(text);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], " a"); assert_eq!(lines[1], " bb"); assert_eq!(lines[2], " ccc"); assert_eq!(lines[3], " dddd"); assert_eq!(lines[4], "eeeee"); }
#[test]
fn test_center_odd_padding() {
let text = "a\nbbbb";
let result = center(text);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], " a"); assert_eq!(lines[1], "bbbb"); }
#[test]
fn test_multiline_with_empty_lines() {
let text = "hello\n\nworld";
let result = center(text);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], "hello");
assert_eq!(lines[1], " "); assert_eq!(lines[2], "world");
}
#[test]
fn test_no_unnecessary_allocations() {
let text = "line1\nline2\nline3";
let result = left(text);
assert_eq!(result, text);
}
#[test]
fn test_padding_efficiency() {
let text = format!("a\n{}", "b".repeat(20));
let opts = AlignOptions::new(Alignment::Right);
let result = ansi_align_with_options("a\nbb", &opts);
assert_eq!(result, " a\nbb");
let result = ansi_align_with_options(&text, &opts);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0].len(), 20); assert!(lines[0].ends_with('a'));
}
#[test]
fn test_create_padding_unicode() {
let text = "a\nbb";
let opts = AlignOptions::new(Alignment::Right).with_pad('β’');
let result = ansi_align_with_options(text, &opts);
assert_eq!(result, "β’a\nbb");
let opts = AlignOptions::new(Alignment::Right).with_pad('π―');
let result = ansi_align_with_options(text, &opts);
assert_eq!(result, "π―a\nbb");
}
#[test]
fn test_real_world_scenario() {
let menu = "Home\nAbout Us\nContact\nServices";
let result = center(menu);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], " Home"); assert_eq!(lines[1], "About Us"); assert_eq!(lines[2], "Contact"); assert_eq!(lines[3], "Services"); }
#[test]
fn test_code_alignment() {
let code = "if x:\n return y\nelse:\n return z";
let result = right(code);
let lines: Vec<&str> = result.split('\n').collect();
assert_eq!(lines[0], " if x:"); assert_eq!(lines[1], " return y"); assert_eq!(lines[2], " else:"); assert_eq!(lines[3], " return z"); }
}