1use string_width::string_width;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Alignment {
6    Left,
7    Center,
8    Right,
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
13pub struct Width(usize);
14
15impl Width {
16    pub fn new(value: usize) -> Self {
18        Self(value)
19    }
20
21    pub fn get(self) -> usize {
23        self.0
24    }
25}
26
27impl From<usize> for Width {
28    fn from(value: usize) -> Self {
29        Self(value)
30    }
31}
32
33#[derive(Debug, Clone)]
35pub struct AlignOptions {
36    pub align: Alignment,
38    pub split: String,
40    pub pad: char,
42}
43
44impl Default for AlignOptions {
45    fn default() -> Self {
46        Self {
47            align: Alignment::Center,
48            split: "\n".to_string(),
49            pad: ' ',
50        }
51    }
52}
53
54impl AlignOptions {
55    pub fn new(align: Alignment) -> Self {
57        Self {
58            align,
59            ..Default::default()
60        }
61    }
62
63    pub fn with_split<S: Into<String>>(mut self, split: S) -> Self {
65        self.split = split.into();
66        self
67    }
68
69    pub fn with_pad(mut self, pad: char) -> Self {
71        self.pad = pad;
72        self
73    }
74}
75
76fn create_padding(pad_char: char, count: usize) -> String {
78    match count {
79        0 => String::new(),
80        1 => pad_char.to_string(),
81        2..=8 => pad_char.to_string().repeat(count),
82        _ => {
83            let mut padding = String::with_capacity(count);
84            for _ in 0..count {
85                padding.push(pad_char);
86            }
87            padding
88        }
89    }
90}
91
92#[must_use]
94pub fn ansi_align(text: &str) -> String {
95    ansi_align_with_options(text, AlignOptions::default())
96}
97
98#[must_use]
127pub fn ansi_align_with_options(text: &str, opts: AlignOptions) -> String {
128    if text.is_empty() {
129        return text.to_string();
130    }
131
132    if opts.align == Alignment::Left {
134        return text.to_string();
135    }
136
137    let line_data: Vec<(&str, Width)> = text
139        .split(&opts.split)
140        .map(|line| (line, Width::from(string_width(line))))
141        .collect();
142
143    let max_width = line_data
144        .iter()
145        .map(|(_, width)| width.get())
146        .max()
147        .unwrap_or(0);
148
149    let aligned_lines: Vec<String> = line_data
150        .into_iter()
151        .map(|(line, width)| {
152            let padding_needed = match opts.align {
153                Alignment::Left => 0, Alignment::Center => (max_width - width.get()) / 2,
155                Alignment::Right => max_width - width.get(),
156            };
157
158            if padding_needed == 0 {
159                line.to_string()
160            } else {
161                let mut result = create_padding(opts.pad, padding_needed);
162                result.push_str(line);
163                result
164            }
165        })
166        .collect();
167
168    aligned_lines.join(&opts.split)
169}
170
171#[must_use]
173pub fn left(text: &str) -> String {
174    ansi_align_with_options(text, AlignOptions::new(Alignment::Left))
175}
176
177#[must_use]
179pub fn center(text: &str) -> String {
180    ansi_align_with_options(text, AlignOptions::new(Alignment::Center))
181}
182
183#[must_use]
185pub fn right(text: &str) -> String {
186    ansi_align_with_options(text, AlignOptions::new(Alignment::Right))
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
195    fn test_left_alignment() {
196        let text = "hello\nworld";
197        let result = left(text);
198        assert_eq!(result, text); }
200
201    #[test]
202    fn test_center_alignment() {
203        let text = "hi\nhello";
204        let result = center(text);
205        let lines: Vec<&str> = result.split('\n').collect();
206        assert_eq!(lines[0], " hi"); assert_eq!(lines[1], "hello"); }
209
210    #[test]
211    fn test_right_alignment() {
212        let text = "hi\nhello";
213        let result = right(text);
214        let lines: Vec<&str> = result.split('\n').collect();
215        assert_eq!(lines[0], "   hi"); assert_eq!(lines[1], "hello"); }
218
219    #[test]
221    fn test_unicode_characters() {
222        let text = "古\n古古古";
223        let result = center(text);
224        let lines: Vec<&str> = result.split('\n').collect();
225        assert_eq!(lines[0], "  古"); assert_eq!(lines[1], "古古古"); }
228
229    #[test]
230    fn test_ansi_escape_sequences() {
231        let text = "hello\n\u{001B}[1mworld\u{001B}[0m";
232        let result = center(text);
233        let lines: Vec<&str> = result.split('\n').collect();
234        assert_eq!(lines[0], "hello");
235        assert_eq!(lines[1], "\u{001B}[1mworld\u{001B}[0m"); }
237
238    #[test]
239    fn test_complex_ansi_sequences() {
240        let text = "\x1b[31m\x1b[1mred\x1b[0m\n\x1b[32mgreen text\x1b[0m";
242        let result = right(text);
243        let lines: Vec<&str> = result.split('\n').collect();
244        assert_eq!(lines[0], "       \x1b[31m\x1b[1mred\x1b[0m"); assert_eq!(lines[1], "\x1b[32mgreen text\x1b[0m"); }
248
249    #[test]
251    fn test_empty_string() {
252        assert_eq!(ansi_align_with_options("", AlignOptions::default()), "");
253        assert_eq!(left(""), "");
254        assert_eq!(center(""), "");
255        assert_eq!(right(""), "");
256    }
257
258    #[test]
259    fn test_single_line() {
260        let text = "hello";
261        assert_eq!(left(text), "hello");
262        assert_eq!(center(text), "hello");
263        assert_eq!(right(text), "hello");
264    }
265
266    #[test]
267    fn test_single_character() {
268        let text = "a\nb";
269        let result = center(text);
270        assert_eq!(result, "a\nb"); }
272
273    #[test]
274    fn test_whitespace_only() {
275        let text = "   \n ";
276        let result = center(text);
277        let lines: Vec<&str> = result.split('\n').collect();
278        assert_eq!(lines[0], "   "); assert_eq!(lines[1], "  "); }
281
282    #[test]
284    fn test_custom_split_and_pad() {
285        let text = "a|bb";
286        let opts = AlignOptions::new(Alignment::Right)
287            .with_split("|")
288            .with_pad('.');
289        let result = ansi_align_with_options(text, opts);
290        assert_eq!(result, ".a|bb");
291    }
292
293    #[test]
294    fn test_custom_split_multichar() {
295        let text = "short<->very long line";
296        let opts = AlignOptions::new(Alignment::Center).with_split("<->");
297        let result = ansi_align_with_options(text, opts);
298        assert_eq!(result, "    short<->very long line");
299    }
300
301    #[test]
302    fn test_different_padding_chars() {
303        let text = "hi\nhello";
304
305        let opts = AlignOptions::new(Alignment::Right).with_pad('.');
307        let result = ansi_align_with_options(text, opts);
308        assert_eq!(result, "...hi\nhello");
309
310        let opts = AlignOptions::new(Alignment::Center).with_pad('_');
312        let result = ansi_align_with_options(text, opts);
313        assert_eq!(result, "_hi\nhello");
314
315        let opts = AlignOptions::new(Alignment::Right).with_pad('0');
317        let result = ansi_align_with_options(text, opts);
318        assert_eq!(result, "000hi\nhello");
319    }
320
321    #[test]
323    fn test_large_padding() {
324        let text = format!("a\n{}", "b".repeat(100));
325        let result = right(&text);
326        let lines: Vec<&str> = result.split('\n').collect();
327        assert_eq!(lines[0].len(), 100); assert!(lines[0].starts_with(&" ".repeat(99)));
329        assert!(lines[0].ends_with("a"));
330        assert_eq!(lines[1], "b".repeat(100));
331    }
332
333    #[test]
334    fn test_no_padding_optimization() {
335        let text = "same\nsame\nsame";
337        let result = center(text);
338        assert_eq!(result, text); }
340
341    #[test]
343    fn test_width_type() {
344        let width = Width::new(42);
345        assert_eq!(width.get(), 42);
346
347        let width_from_usize: Width = 24.into();
348        assert_eq!(width_from_usize.get(), 24);
349
350        assert!(Width::new(10) < Width::new(20));
352        assert_eq!(Width::new(15), Width::new(15));
353    }
354
355    #[test]
357    fn test_mixed_width_lines() {
358        let text = "a\nbb\nccc\ndddd\neeeee";
359
360        let result = center(text);
362        let lines: Vec<&str> = result.split('\n').collect();
363
364        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);
374        let lines: Vec<&str> = result.split('\n').collect();
375        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"); }
381
382    #[test]
383    fn test_center_odd_padding() {
384        let text = "a\nbbbb";
386        let result = center(text);
387        let lines: Vec<&str> = result.split('\n').collect();
388        assert_eq!(lines[0], " a"); assert_eq!(lines[1], "bbbb"); }
391
392    #[test]
393    fn test_multiline_with_empty_lines() {
394        let text = "hello\n\nworld";
395        let result = center(text);
396        let lines: Vec<&str> = result.split('\n').collect();
397        assert_eq!(lines[0], "hello");
398        assert_eq!(lines[1], "  "); assert_eq!(lines[2], "world");
400    }
401
402    #[test]
404    fn test_no_unnecessary_allocations() {
405        let text = "line1\nline2\nline3";
407        let result = left(text);
408        assert_eq!(result, text);
410    }
411
412    #[test]
413    fn test_padding_efficiency() {
414        let text = format!("a\n{}", "b".repeat(20));
416
417        let opts = AlignOptions::new(Alignment::Right);
419        let result = ansi_align_with_options("a\nbb", opts.clone());
420        assert_eq!(result, " a\nbb");
421
422        let result = ansi_align_with_options(&text, opts);
424        let lines: Vec<&str> = result.split('\n').collect();
425        assert_eq!(lines[0].len(), 20); assert!(lines[0].ends_with("a"));
427    }
428
429    #[test]
431    fn test_real_world_scenario() {
432        let menu = "Home\nAbout Us\nContact\nServices";
434        let result = center(menu);
435        let lines: Vec<&str> = result.split('\n').collect();
436
437        assert_eq!(lines[0], "  Home"); assert_eq!(lines[1], "About Us"); assert_eq!(lines[2], "Contact"); assert_eq!(lines[3], "Services"); }
443
444    #[test]
445    fn test_code_alignment() {
446        let code = "if x:\n    return y\nelse:\n    return z";
448        let result = right(code);
449        let lines: Vec<&str> = result.split('\n').collect();
450
451        assert_eq!(lines[0], "       if x:"); assert_eq!(lines[1], "    return y"); assert_eq!(lines[2], "       else:"); assert_eq!(lines[3], "    return z"); }
457}