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}