ansi_align/lib.rs
1//! # ansi-align
2//!
3//! [](https://crates.io/crates/ansi-align)
4//! [](https://docs.rs/ansi-align)
5//! [](https://github.com/sabry-awad97/ansi-align#license)
6//!
7//! A high-performance Rust library for aligning text with comprehensive support for ANSI escape sequences,
8//! Unicode characters, and customizable formatting options.
9//!
10//! This crate provides robust text alignment capabilities that correctly handle:
11//! - ANSI escape sequences (colors, formatting codes)
12//! - Unicode characters with varying display widths (including CJK characters)
13//! - Custom line separators and padding characters
14//! - Performance-optimized algorithms for large text processing
15//!
16//! ## π Features
17//!
18//! - **π¨ ANSI-aware alignment**: Correctly handles text containing ANSI escape sequences
19//! - **π Unicode support**: Properly calculates display width for all Unicode characters
20//! - **β‘ High performance**: Single-pass processing with optimized memory usage
21//! - **π§ Highly customizable**: Configure alignment, separators, and padding
22//! - **π‘οΈ Type-safe**: Uses a [`Width`] type to prevent display width confusion
23//! - **π¦ Zero-copy optimizations**: Left alignment is implemented as a no-op
24//!
25//! ## π Quick Start
26//!
27//! Add this to your `Cargo.toml`:
28//!
29//! ```toml
30//! [dependencies]
31//! ansi-align = "0.1.0"
32//! ```
33//!
34//! ### Basic Usage
35//!
36//! ```rust
37//! use ansi_align::{ansi_align, center, left, right};
38//!
39//! // Basic alignment (defaults to center)
40//! let text = "hello\nworld";
41//! let centered = ansi_align(text);
42//! println!("{}", centered);
43//! // Output:
44//! // hello
45//! // world
46//!
47//! // Specific alignment functions
48//! let left_aligned = left("short\nlonger line");
49//! let centered = center("short\nlonger line");
50//! let right_aligned = right("short\nlonger line");
51//! ```
52//!
53//! ### ANSI Color Support
54//!
55//! ```rust
56//! use ansi_align::center;
57//!
58//! // Colors and formatting are preserved but don't affect alignment
59//! let colored_text = "\x1b[31mRed\x1b[0m\n\x1b[32mGreen Text\x1b[0m";
60//! let aligned = center(colored_text);
61//! println!("{}", aligned);
62//! // ANSI codes are preserved in output but ignored for width calculation
63//! ```
64//!
65//! ### Unicode Character Support
66//!
67//! ```rust
68//! use ansi_align::right;
69//!
70//! // Correctly handles wide Unicode characters (CJK, emojis, etc.)
71//! let unicode_text = "ε€\nε€ε€ε€\nHello δΈη";
72//! let aligned = right(unicode_text);
73//! println!("{}", aligned);
74//! // Properly accounts for double-width characters
75//! ```
76//!
77//! ## π§ Advanced Usage
78//!
79//! ### Custom Options
80//!
81//! ```rust
82//! use ansi_align::{ansi_align_with_options, Alignment, AlignOptions};
83//!
84//! // Custom separator and padding
85//! let data = "Name|Age|City";
86//! let options = AlignOptions::new(Alignment::Center)
87//! .with_split("|")
88//! .with_pad('_');
89//!
90//! let result = ansi_align_with_options(data, &options);
91//! println!("{}", result);
92//! // Output: __Name|Age|City
93//! ```
94//!
95//! ### Builder Pattern
96//!
97//! ```rust
98//! use ansi_align::{ansi_align_with_options, Alignment, AlignOptions};
99//!
100//! let options = AlignOptions::new(Alignment::Right)
101//! .with_split("<->")
102//! .with_pad('.');
103//!
104//! let text = "short<->very long line";
105//! let result = ansi_align_with_options(text, &options);
106//! ```
107//!
108//! ## π Performance Characteristics
109//!
110//! - **Left alignment**: O(1) - implemented as a no-op for maximum performance
111//! - **Center/Right alignment**: O(n) - single pass through input text
112//! - **Memory usage**: Minimal allocations with pre-calculated capacity
113//! - **Large text handling**: Optimized padding creation for different sizes
114//!
115//! ## π― Use Cases
116//!
117//! - **CLI applications**: Align help text, tables, and menus
118//! - **Terminal UIs**: Create visually appealing layouts
119//! - **Log formatting**: Align log entries and structured output
120//! - **Code generation**: Format generated code with proper indentation
121//! - **Documentation**: Align text blocks in generated docs
122//!
123//! ## ποΈ Architecture
124//!
125//! The library is built around three core concepts:
126//!
127//! 1. **[`Alignment`]** - Defines the alignment direction (Left, Center, Right)
128//! 2. **[`AlignOptions`]** - Configuration for alignment behavior
129//! 3. **[`Width`]** - Type-safe wrapper for display width calculations
130//!
131//! The main entry point is [`ansi_align_with_options`], which provides full customization,
132//! while convenience functions like [`left`], [`center`], and [`right`] offer simplified APIs.
133//!
134//! ## π Examples
135//!
136//! ### Menu Alignment
137//!
138//! ```rust
139//! use ansi_align::center;
140//!
141//! let menu = "π Home\nπ About\nπ Contact\nβοΈ Settings";
142//! let aligned_menu = center(menu);
143//! println!("{}", aligned_menu);
144//! ```
145//!
146//! ### Table-like Data
147//!
148//! ```rust
149//! use ansi_align::{ansi_align_with_options, Alignment, AlignOptions};
150//!
151//! let data = "ID|Name|Status";
152//! let options = AlignOptions::new(Alignment::Center).with_split("|");
153//! let result = ansi_align_with_options(data, &options);
154//! ```
155//!
156//! ### Code Block Alignment
157//!
158//! ```rust
159//! use ansi_align::right;
160//!
161//! let code = "if condition:\n execute()\nelse:\n handle_error()";
162//! let aligned = right(code);
163//! println!("{}", aligned);
164//! ```
165
166#![deny(missing_docs)]
167#![warn(clippy::all)]
168#![warn(clippy::pedantic)]
169#![warn(clippy::nursery)]
170
171use std::fmt;
172use std::ops::{Add, Div, Mul, Sub};
173use string_width::string_width;
174
175/// Specifies the alignment direction for text.
176///
177/// This enum defines the three fundamental alignment options available in the library.
178/// Each variant represents a different strategy for positioning text within the available width.
179///
180/// # Alignment Behavior
181///
182/// - **[`Left`](Alignment::Left)**: Text is positioned at the leftmost edge (no padding on left)
183/// - **[`Center`](Alignment::Center)**: Text is centered with equal padding on both sides
184/// - **[`Right`](Alignment::Right)**: Text is positioned at the rightmost edge (padding on left)
185///
186/// # Performance Notes
187///
188/// - `Left` alignment is optimized as a no-op operation
189/// - `Center` and `Right` alignments require width calculation and padding
190///
191/// # Examples
192///
193/// ## Basic Usage
194///
195/// ```rust
196/// use ansi_align::{Alignment, AlignOptions, ansi_align_with_options};
197///
198/// let text = "hello\nworld";
199///
200/// // Center alignment - distributes padding evenly
201/// let opts = AlignOptions::new(Alignment::Center);
202/// let result = ansi_align_with_options(text, &opts);
203/// // Result: " hello\nworld" (hello gets 1 space padding)
204///
205/// // Right alignment - adds all padding to the left
206/// let opts = AlignOptions::new(Alignment::Right);
207/// let result = ansi_align_with_options(text, &opts);
208/// // Result: " hello\n world" (both lines right-aligned)
209///
210/// // Left alignment - no changes to input
211/// let opts = AlignOptions::new(Alignment::Left);
212/// let result = ansi_align_with_options(text, &opts);
213/// // Result: "hello\nworld" (unchanged)
214/// ```
215///
216/// ## With Custom Padding
217///
218/// ```rust
219/// use ansi_align::{Alignment, AlignOptions, ansi_align_with_options};
220///
221/// let text = "short\nlonger";
222/// let opts = AlignOptions::new(Alignment::Right).with_pad('.');
223/// let result = ansi_align_with_options(text, &opts);
224/// // Result: ".short\nlonger"
225/// ```
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub enum Alignment {
228 /// Align text to the left (no padding added to the left side)
229 Left,
230 /// Align text to the center (padding distributed evenly on both sides)
231 Center,
232 /// Align text to the right (padding added to the left side)
233 Right,
234}
235
236/// A type-safe wrapper for display width values.
237///
238/// This type represents the visual width of text as it would appear on a terminal or console,
239/// correctly accounting for:
240/// - ANSI escape sequences (which have zero display width)
241/// - Unicode characters with varying display widths
242/// - Multi-byte characters that occupy single or double terminal columns
243///
244/// The `Width` type prevents common bugs by distinguishing between:
245/// - **Byte length**: The number of bytes in a string (`str.len()`)
246/// - **Character count**: The number of Unicode scalar values (`str.chars().count()`)
247/// - **Display width**: The number of terminal columns occupied when rendered
248///
249/// # Why Display Width Matters
250///
251/// Consider these examples:
252/// - `"hello"` has 5 bytes, 5 characters, and 5 display width
253/// - `"ε€ε€ε€"` has 9 bytes, 3 characters, and 6 display width (CJK characters are wide)
254/// - `"\x1b[31mred\x1b[0m"` has 11 bytes, 7 characters, and 3 display width (ANSI codes are invisible)
255///
256/// # Examples
257///
258/// ## Basic Usage
259///
260/// ```rust
261/// use ansi_align::Width;
262///
263/// let width = Width::new(42);
264/// assert_eq!(width.get(), 42);
265///
266/// // Convert from usize
267/// let width: Width = 24.into();
268/// assert_eq!(width.get(), 24);
269/// ```
270///
271/// ## Comparison and Ordering
272///
273/// ```rust
274/// use ansi_align::Width;
275///
276/// let small = Width::new(10);
277/// let large = Width::new(20);
278///
279/// assert!(small < large);
280/// assert_eq!(Width::new(15), Width::new(15));
281///
282/// // Can be used in collections
283/// let mut widths = vec![Width::new(30), Width::new(10), Width::new(20)];
284/// widths.sort();
285/// assert_eq!(widths, vec![Width::new(10), Width::new(20), Width::new(30)]);
286/// ```
287///
288/// ## Integration with String Width Calculation
289///
290/// ```rust
291/// use ansi_align::Width;
292/// // Note: This example shows the concept, actual width calculation
293/// // is done internally by the library
294///
295/// let display_width = Width::new(5); // Represents 5 terminal columns
296/// let padding_needed = 10 - display_width.get(); // 5 columns of padding
297/// ```
298#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
299pub struct Width(usize);
300
301impl Width {
302 /// Creates a new `Width` value from a `usize`.
303 ///
304 /// # Arguments
305 ///
306 /// * `value` - The display width value
307 ///
308 /// # Examples
309 ///
310 /// ```rust
311 /// use ansi_align::Width;
312 ///
313 /// let width = Width::new(10);
314 /// assert_eq!(width.get(), 10);
315 /// ```
316 #[must_use]
317 pub const fn new(value: usize) -> Self {
318 Self(value)
319 }
320
321 /// Returns the underlying `usize` value.
322 ///
323 /// # Examples
324 ///
325 /// ```rust
326 /// use ansi_align::Width;
327 ///
328 /// let width = Width::new(42);
329 /// assert_eq!(width.get(), 42);
330 /// ```
331 #[must_use]
332 pub const fn get(self) -> usize {
333 self.0
334 }
335
336 /// Performs saturating subtraction, returning 0 if the result would be negative.
337 ///
338 /// # Arguments
339 ///
340 /// * `other` - The `Width` value to subtract
341 ///
342 /// # Examples
343 ///
344 /// ```rust
345 /// use ansi_align::Width;
346 ///
347 /// let w1 = Width::new(10);
348 /// let w2 = Width::new(15);
349 /// assert_eq!(w1.saturating_sub(w2).get(), 0); // 10 - 15 = 0 (saturated)
350 ///
351 /// let w3 = Width::new(20);
352 /// assert_eq!(w3.saturating_sub(w1).get(), 10); // 20 - 10 = 10
353 /// ```
354 #[must_use]
355 pub const fn saturating_sub(self, other: Self) -> Self {
356 Self(self.0.saturating_sub(other.0))
357 }
358
359 /// Returns `true` if the width is zero.
360 ///
361 /// # Examples
362 ///
363 /// ```rust
364 /// use ansi_align::Width;
365 ///
366 /// assert!(Width::new(0).is_zero());
367 /// assert!(!Width::new(5).is_zero());
368 /// ```
369 #[must_use]
370 pub const fn is_zero(self) -> bool {
371 self.0 == 0
372 }
373}
374
375impl From<usize> for Width {
376 fn from(value: usize) -> Self {
377 Self(value)
378 }
379}
380
381// Arithmetic operations for Width
382impl Add for Width {
383 type Output = Self;
384
385 /// Adds two `Width` values.
386 ///
387 /// # Examples
388 ///
389 /// ```rust
390 /// use ansi_align::Width;
391 ///
392 /// let w1 = Width::new(10);
393 /// let w2 = Width::new(5);
394 /// assert_eq!((w1 + w2).get(), 15);
395 /// ```
396 fn add(self, rhs: Self) -> Self::Output {
397 Self(self.0 + rhs.0)
398 }
399}
400
401impl Sub for Width {
402 type Output = Self;
403
404 /// Subtracts one `Width` value from another.
405 ///
406 /// # Examples
407 ///
408 /// ```rust
409 /// use ansi_align::Width;
410 ///
411 /// let w1 = Width::new(10);
412 /// let w2 = Width::new(5);
413 /// assert_eq!((w1 - w2).get(), 5);
414 /// ```
415 fn sub(self, rhs: Self) -> Self::Output {
416 Self(self.0 - rhs.0)
417 }
418}
419
420impl Mul for Width {
421 type Output = Self;
422
423 /// Multiplies two `Width` values.
424 ///
425 /// # Examples
426 ///
427 /// ```rust
428 /// use ansi_align::Width;
429 ///
430 /// let w1 = Width::new(10);
431 /// let w2 = Width::new(5);
432 /// assert_eq!((w1 * w2).get(), 50);
433 /// ```
434 fn mul(self, rhs: Self) -> Self::Output {
435 Self(self.0 * rhs.0)
436 }
437}
438
439impl Div for Width {
440 type Output = Self;
441
442 /// Divides one `Width` value by another.
443 ///
444 /// # Examples
445 ///
446 /// ```rust
447 /// use ansi_align::Width;
448 ///
449 /// let w1 = Width::new(10);
450 /// let w2 = Width::new(5);
451 /// assert_eq!((w1 / w2).get(), 2);
452 /// ```
453 fn div(self, rhs: Self) -> Self::Output {
454 Self(self.0 / rhs.0)
455 }
456}
457
458// Display implementation for Width
459impl fmt::Display for Width {
460 /// Formats the `Width` value for display.
461 ///
462 /// # Examples
463 ///
464 /// ```rust
465 /// use ansi_align::Width;
466 ///
467 /// let width = Width::new(42);
468 /// assert_eq!(format!("{}", width), "42");
469 /// ```
470 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471 write!(f, "{}", self.0)
472 }
473}
474
475/// Configuration options for text alignment operations.
476///
477/// This struct provides comprehensive control over text alignment behavior through a fluent
478/// builder API. It allows customization of alignment direction, line separators, and padding
479/// characters to handle diverse text formatting requirements.
480///
481/// # Fields
482///
483/// - **`align`**: The alignment direction ([`Alignment`])
484/// - **`split`**: String used to separate lines (default: `"\n"`)
485/// - **`pad`**: Character used for padding (default: `' '`)
486///
487/// # Design Philosophy
488///
489/// The options struct follows the builder pattern, allowing for method chaining and
490/// readable configuration. All methods return `Self`, enabling fluent composition:
491///
492/// ```rust
493/// use ansi_align::{AlignOptions, Alignment};
494///
495/// let opts = AlignOptions::new(Alignment::Center)
496/// .with_split("|")
497/// .with_pad('_');
498/// ```
499///
500/// # Common Use Cases
501///
502/// ## Standard Text Alignment
503///
504/// ```rust
505/// use ansi_align::{AlignOptions, Alignment, ansi_align_with_options};
506///
507/// let text = "Line 1\nLonger Line 2\nShort";
508/// let opts = AlignOptions::new(Alignment::Center);
509/// let result = ansi_align_with_options(text, &opts);
510/// ```
511///
512/// ## Custom Line Separators
513///
514/// ```rust
515/// use ansi_align::{AlignOptions, Alignment, ansi_align_with_options};
516///
517/// // Pipe-separated data
518/// let data = "Name|Age|Location";
519/// let opts = AlignOptions::new(Alignment::Center).with_split("|");
520/// let result = ansi_align_with_options(data, &opts);
521///
522/// // Multi-character separators
523/// let data = "Part1<->Part2<->Part3";
524/// let opts = AlignOptions::new(Alignment::Right).with_split("<->");
525/// let result = ansi_align_with_options(data, &opts);
526/// ```
527///
528/// ## Custom Padding Characters
529///
530/// ```rust
531/// use ansi_align::{AlignOptions, Alignment, ansi_align_with_options};
532///
533/// // Dot padding for visual emphasis
534/// let text = "Title\nSubtitle";
535/// let opts = AlignOptions::new(Alignment::Right).with_pad('.');
536/// let result = ansi_align_with_options(text, &opts);
537/// // Result: "..Title\nSubtitle"
538///
539/// // Zero padding for numeric alignment
540/// let numbers = "1\n42\n123";
541/// let opts = AlignOptions::new(Alignment::Right).with_pad('0');
542/// let result = ansi_align_with_options(numbers, &opts);
543/// // Result: "001\n042\n123"
544/// ```
545///
546/// ## Complex Configurations
547///
548/// ```rust
549/// use ansi_align::{AlignOptions, Alignment, ansi_align_with_options};
550///
551/// // Table-like data with custom formatting
552/// let table_row = "ID|Name|Status";
553/// let opts = AlignOptions::new(Alignment::Center)
554/// .with_split("|")
555/// .with_pad('_');
556/// let result = ansi_align_with_options(table_row, &opts);
557/// ```
558///
559/// # Performance Considerations
560///
561/// - Options are designed to be lightweight and cheap to clone
562/// - The builder pattern methods are `const fn` where possible for compile-time optimization
563/// - String splitting is performed only once during alignment processing
564#[derive(Debug, Clone)]
565pub struct AlignOptions {
566 /// The alignment type (left, center, right)
567 pub align: Alignment,
568 /// The string to split lines on (default: "\n")
569 pub split: String,
570 /// The padding character to use (default: " ")
571 pub pad: char,
572}
573
574impl Default for AlignOptions {
575 fn default() -> Self {
576 Self {
577 align: Alignment::Center,
578 split: "\n".to_string(),
579 pad: ' ',
580 }
581 }
582}
583
584impl AlignOptions {
585 /// Creates new alignment options with the specified alignment direction.
586 ///
587 /// Uses default values for split string (`"\n"`) and padding character (`' '`).
588 ///
589 /// # Arguments
590 ///
591 /// * `align` - The alignment direction to use
592 ///
593 /// # Examples
594 ///
595 /// ```rust
596 /// use ansi_align::{AlignOptions, Alignment};
597 ///
598 /// let opts = AlignOptions::new(Alignment::Center);
599 /// ```
600 #[must_use]
601 pub fn new(align: Alignment) -> Self {
602 Self {
603 align,
604 ..Default::default()
605 }
606 }
607
608 /// Sets the string used to split lines using the builder pattern.
609 ///
610 /// By default, lines are split on `"\n"`, but you can specify any string
611 /// as a line separator.
612 ///
613 /// # Arguments
614 ///
615 /// * `split` - The string to use as a line separator
616 ///
617 /// # Examples
618 ///
619 /// ```rust
620 /// use ansi_align::{AlignOptions, Alignment};
621 ///
622 /// let opts = AlignOptions::new(Alignment::Center)
623 /// .with_split("|")
624 /// .with_split("<->"); // Multi-character separators work too
625 /// ```
626 #[must_use]
627 pub fn with_split<S: Into<String>>(mut self, split: S) -> Self {
628 self.split = split.into();
629 self
630 }
631
632 /// Sets the character used for padding using the builder pattern.
633 ///
634 /// By default, spaces (`' '`) are used for padding, but you can specify
635 /// any character.
636 ///
637 /// # Arguments
638 ///
639 /// * `pad` - The character to use for padding
640 ///
641 /// # Examples
642 ///
643 /// ```rust
644 /// use ansi_align::{AlignOptions, Alignment};
645 ///
646 /// let opts = AlignOptions::new(Alignment::Right)
647 /// .with_pad('.');
648 /// ```
649 #[must_use]
650 pub const fn with_pad(mut self, pad: char) -> Self {
651 self.pad = pad;
652 self
653 }
654}
655
656/// Efficiently create padding string for alignment
657fn create_padding(pad_char: char, count: usize) -> String {
658 match count {
659 0 => String::new(),
660 1 => pad_char.to_string(),
661 // Use repeat for small counts (more efficient)
662 2..=16 => pad_char.to_string().repeat(count),
663 // Use with_capacity + push for larger counts to avoid reallocations
664 _ => {
665 let mut padding = String::with_capacity(count * pad_char.len_utf8());
666 for _ in 0..count {
667 padding.push(pad_char);
668 }
669 padding
670 }
671 }
672}
673
674/// Align text with center alignment (default behavior)
675#[must_use]
676pub fn ansi_align(text: &str) -> String {
677 ansi_align_with_options(text, &AlignOptions::default())
678}
679
680/// Calculate display widths for lines
681fn calculate_line_widths<'a>(text: &'a str, split: &str) -> Vec<(&'a str, Width)> {
682 text.split(split)
683 .map(|line| (line, Width::from(string_width(line))))
684 .collect()
685}
686
687/// Find the maximum width among lines
688fn find_max_width(line_data: &[(&str, Width)]) -> Width {
689 line_data
690 .iter()
691 .map(|(_, width)| *width)
692 .max()
693 .unwrap_or(Width::new(0))
694}
695
696/// Apply alignment to a single line
697fn align_line(
698 line: &str,
699 line_width: Width,
700 max_width: Width,
701 alignment: Alignment,
702 pad_char: char,
703) -> String {
704 let padding_needed = match alignment {
705 Alignment::Left => 0,
706 Alignment::Center => (max_width.get() - line_width.get()) / 2,
707 Alignment::Right => max_width.get() - line_width.get(),
708 };
709
710 if padding_needed == 0 {
711 line.to_string()
712 } else {
713 let mut result = create_padding(pad_char, padding_needed);
714 result.push_str(line);
715 result
716 }
717}
718
719/// Aligns text with comprehensive support for ANSI escape sequences and Unicode characters.
720///
721/// This is the core alignment function that provides full customization through [`AlignOptions`].
722/// It correctly handles complex text containing ANSI escape sequences, Unicode characters,
723/// and custom formatting requirements while maintaining optimal performance.
724///
725/// # Algorithm Overview
726///
727/// 1. **Input validation**: Handle empty strings and edge cases
728/// 2. **Left alignment optimization**: Return input unchanged for left alignment
729/// 3. **Width calculation**: Single-pass analysis to determine display widths
730/// 4. **Alignment processing**: Apply padding based on alignment strategy
731/// 5. **Output reconstruction**: Join aligned lines with original separator
732///
733/// # Supported Text Features
734///
735/// - **ANSI escape sequences**: Colors, formatting, cursor control
736/// - **Unicode characters**: Including CJK, emojis, and combining characters
737/// - **Mixed content**: Text with both ANSI codes and Unicode
738/// - **Custom separators**: Any string can be used as a line delimiter
739/// - **Flexible padding**: Any character for padding alignment gaps
740///
741/// # Examples
742///
743/// ```
744/// use ansi_align::{ansi_align, ansi_align_with_options, Alignment, AlignOptions};
745///
746/// // Basic center alignment
747/// let result = ansi_align("hello\nworld");
748///
749/// // Right alignment with custom padding
750/// let opts = AlignOptions::new(Alignment::Right).with_pad('.');
751/// let result = ansi_align_with_options("hi\nhello", &opts);
752///
753/// // Works with ANSI escape sequences
754/// let colored = "\x1b[31mred\x1b[0m\n\x1b[32mgreen\x1b[0m";
755/// let result = ansi_align_with_options(colored, &AlignOptions::new(Alignment::Center));
756/// ```
757///
758/// # Performance
759///
760/// This function makes a single pass through the input text for optimal performance.
761/// For left alignment, it returns the input unchanged as an optimization.
762#[must_use]
763pub fn ansi_align_with_options(text: &str, opts: &AlignOptions) -> String {
764 if text.is_empty() || opts.align == Alignment::Left {
765 return text.to_string();
766 }
767
768 let line_data = calculate_line_widths(text, &opts.split);
769 let max_width = find_max_width(&line_data);
770
771 let aligned_lines: Vec<String> = line_data
772 .into_iter()
773 .map(|(line, width)| align_line(line, width, max_width, opts.align, opts.pad))
774 .collect();
775
776 aligned_lines.join(&opts.split)
777}
778
779/// Align text to the left (no-op, returns original text)
780#[must_use]
781pub fn left(text: &str) -> String {
782 ansi_align_with_options(text, &AlignOptions::new(Alignment::Left))
783}
784
785/// Align text to the center
786#[must_use]
787pub fn center(text: &str) -> String {
788 ansi_align_with_options(text, &AlignOptions::new(Alignment::Center))
789}
790
791/// Align text to the right
792#[must_use]
793pub fn right(text: &str) -> String {
794 ansi_align_with_options(text, &AlignOptions::new(Alignment::Right))
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800
801 // Basic alignment tests
802 #[test]
803 fn test_left_alignment() {
804 let text = "hello\nworld";
805 let result = left(text);
806 assert_eq!(result, text); // Left alignment should be no-op
807 }
808
809 #[test]
810 fn test_center_alignment() {
811 let text = "hi\nhello";
812 let result = center(text);
813 let lines: Vec<&str> = result.split('\n').collect();
814 assert_eq!(lines[0], " hi"); // 1 space padding for "hi"
815 assert_eq!(lines[1], "hello"); // No padding for "hello"
816 }
817
818 #[test]
819 fn test_right_alignment() {
820 let text = "hi\nhello";
821 let result = right(text);
822 let lines: Vec<&str> = result.split('\n').collect();
823 assert_eq!(lines[0], " hi"); // 3 spaces padding for "hi"
824 assert_eq!(lines[1], "hello"); // No padding for "hello"
825 }
826
827 // Unicode and ANSI tests
828 #[test]
829 fn test_unicode_characters() {
830 let text = "ε€\nε€ε€ε€";
831 let result = center(text);
832 let lines: Vec<&str> = result.split('\n').collect();
833 assert_eq!(lines[0], " ε€"); // 2 spaces padding (CJK char is width 2)
834 assert_eq!(lines[1], "ε€ε€ε€"); // No padding
835 }
836
837 #[test]
838 fn test_ansi_escape_sequences() {
839 let text = "hello\n\u{001B}[1mworld\u{001B}[0m";
840 let result = center(text);
841 let lines: Vec<&str> = result.split('\n').collect();
842 assert_eq!(lines[0], "hello");
843 assert_eq!(lines[1], "\u{001B}[1mworld\u{001B}[0m"); // ANSI codes preserved
844 }
845
846 #[test]
847 fn test_complex_ansi_sequences() {
848 // Test with multiple ANSI codes and colors
849 let text = "\x1b[31m\x1b[1mred\x1b[0m\n\x1b[32mgreen text\x1b[0m";
850 let result = right(text);
851 let lines: Vec<&str> = result.split('\n').collect();
852 // "red" has display width 3, "green text" has width 10
853 assert_eq!(lines[0], " \x1b[31m\x1b[1mred\x1b[0m"); // 7 spaces padding
854 assert_eq!(lines[1], "\x1b[32mgreen text\x1b[0m"); // No padding
855 }
856
857 // Edge cases
858 #[test]
859 fn test_empty_string() {
860 assert_eq!(ansi_align_with_options("", &AlignOptions::default()), "");
861 assert_eq!(left(""), "");
862 assert_eq!(center(""), "");
863 assert_eq!(right(""), "");
864 }
865
866 #[test]
867 fn test_single_line() {
868 let text = "hello";
869 assert_eq!(left(text), "hello");
870 assert_eq!(center(text), "hello");
871 assert_eq!(right(text), "hello");
872 }
873
874 #[test]
875 fn test_single_character() {
876 let text = "a\nb";
877 let result = center(text);
878 assert_eq!(result, "a\nb"); // Both lines same width, no padding needed
879 }
880
881 #[test]
882 fn test_whitespace_only() {
883 let text = " \n ";
884 let result = center(text);
885 let lines: Vec<&str> = result.split('\n').collect();
886 assert_eq!(lines[0], " "); // 3 spaces, no padding needed
887 assert_eq!(lines[1], " "); // 1 space + 1 padding space
888 }
889
890 // Custom options tests
891 #[test]
892 fn test_custom_split_and_pad() {
893 let text = "a|bb";
894 let opts = AlignOptions::new(Alignment::Right)
895 .with_split("|")
896 .with_pad('.');
897 let result = ansi_align_with_options(text, &opts);
898 assert_eq!(result, ".a|bb");
899 }
900
901 #[test]
902 fn test_custom_split_multichar() {
903 let text = "short<->very long line";
904 let opts = AlignOptions::new(Alignment::Center).with_split("<->");
905 let result = ansi_align_with_options(text, &opts);
906 assert_eq!(result, " short<->very long line");
907 }
908
909 #[test]
910 fn test_different_padding_chars() {
911 let text = "hi\nhello";
912
913 // Test dot padding
914 let opts = AlignOptions::new(Alignment::Right).with_pad('.');
915 let result = ansi_align_with_options(text, &opts);
916 assert_eq!(result, "...hi\nhello");
917
918 // Test underscore padding
919 let opts = AlignOptions::new(Alignment::Center).with_pad('_');
920 let result = ansi_align_with_options(text, &opts);
921 assert_eq!(result, "_hi\nhello");
922
923 // Test zero padding
924 let opts = AlignOptions::new(Alignment::Right).with_pad('0');
925 let result = ansi_align_with_options(text, &opts);
926 assert_eq!(result, "000hi\nhello");
927 }
928
929 // Performance and memory optimization tests
930 #[test]
931 fn test_large_padding() {
932 let text = format!("a\n{}", "b".repeat(100));
933 let result = right(&text);
934 let lines: Vec<&str> = result.split('\n').collect();
935 assert_eq!(lines[0].len(), 100); // 99 spaces + "a"
936 assert!(lines[0].starts_with(&" ".repeat(99)));
937 assert!(lines[0].ends_with('a'));
938 assert_eq!(lines[1], "b".repeat(100));
939 }
940
941 #[test]
942 fn test_no_padding_optimization() {
943 // Test that lines requiring no padding are handled efficiently
944 let text = "same\nsame\nsame";
945 let result = center(text);
946 assert_eq!(result, text); // Should be unchanged
947 }
948
949 // Width type tests
950 #[test]
951 fn test_width_type() {
952 let width = Width::new(42);
953 assert_eq!(width.get(), 42);
954
955 let width_from_usize: Width = 24.into();
956 assert_eq!(width_from_usize.get(), 24);
957
958 // Test ordering
959 assert!(Width::new(10) < Width::new(20));
960 assert_eq!(Width::new(15), Width::new(15));
961 }
962
963 // Width type tests
964 #[test]
965 fn test_width_arithmetic() {
966 let w1 = Width::new(10);
967 let w2 = Width::new(5);
968
969 // Test addition
970 assert_eq!((w1 + w2).get(), 15);
971
972 // Test subtraction
973 assert_eq!((w1 - w2).get(), 5);
974
975 // Test multiplication
976 assert_eq!((w1 * w2).get(), 50);
977
978 // Test division
979 assert_eq!((w1 / w2).get(), 2);
980
981 // Test saturating subtraction
982 assert_eq!(w2.saturating_sub(w1).get(), 0); // 5 - 10 = 0 (saturated)
983 assert_eq!(w1.saturating_sub(w2).get(), 5); // 10 - 5 = 5
984
985 // Test is_zero
986 assert!(Width::new(0).is_zero());
987 assert!(!w1.is_zero());
988 }
989
990 #[test]
991 fn test_width_display() {
992 let width = Width::new(42);
993 assert_eq!(format!("{width}"), "42");
994 assert_eq!(format!("{}", Width::new(0)), "0");
995 assert_eq!(format!("{}", Width::new(999)), "999");
996 }
997
998 // Comprehensive alignment scenarios
999 #[test]
1000 fn test_mixed_width_lines() {
1001 let text = "a\nbb\nccc\ndddd\neeeee";
1002
1003 // Center alignment
1004 let result = center(text);
1005 let lines: Vec<&str> = result.split('\n').collect();
1006
1007 // The longest line is "eeeee" with 5 chars
1008 // So padding for center should be: (5-1)/2=2, (5-2)/2=1, (5-3)/2=1, (5-4)/2=0, (5-5)/2=0
1009 assert_eq!(lines[0], " a"); // 2 spaces + "a"
1010 assert_eq!(lines[1], " bb"); // 1 space + "bb"
1011 assert_eq!(lines[2], " ccc"); // 1 space + "ccc" (corrected)
1012 assert_eq!(lines[3], "dddd"); // no padding (corrected)
1013 assert_eq!(lines[4], "eeeee"); // no padding
1014
1015 // Right alignment
1016 let result = right(text);
1017 let lines: Vec<&str> = result.split('\n').collect();
1018 assert_eq!(lines[0], " a"); // 4 spaces + "a"
1019 assert_eq!(lines[1], " bb"); // 3 spaces + "bb"
1020 assert_eq!(lines[2], " ccc"); // 2 spaces + "ccc"
1021 assert_eq!(lines[3], " dddd"); // 1 space + "dddd"
1022 assert_eq!(lines[4], "eeeee"); // no padding
1023 }
1024
1025 #[test]
1026 fn test_center_odd_padding() {
1027 // Test center alignment with odd padding amounts
1028 let text = "a\nbbbb";
1029 let result = center(text);
1030 let lines: Vec<&str> = result.split('\n').collect();
1031 assert_eq!(lines[0], " a"); // (4-1)/2 = 1 space
1032 assert_eq!(lines[1], "bbbb"); // no padding
1033 }
1034
1035 #[test]
1036 fn test_multiline_with_empty_lines() {
1037 let text = "hello\n\nworld";
1038 let result = center(text);
1039 let lines: Vec<&str> = result.split('\n').collect();
1040 assert_eq!(lines[0], "hello");
1041 assert_eq!(lines[1], " "); // 2 spaces for empty line (center of 5-char width)
1042 assert_eq!(lines[2], "world");
1043 }
1044
1045 // Regression tests for performance improvements
1046 #[test]
1047 fn test_no_unnecessary_allocations() {
1048 // This test ensures we don't regress on the performance improvements
1049 let text = "line1\nline2\nline3";
1050 let result = left(text);
1051 // Left alignment should return original string (no allocations for processing)
1052 assert_eq!(result, text);
1053 }
1054
1055 #[test]
1056 fn test_padding_efficiency() {
1057 // Test the efficient padding creation for different sizes
1058 let text = format!("a\n{}", "b".repeat(20));
1059
1060 // Small padding (should use repeat)
1061 let opts = AlignOptions::new(Alignment::Right);
1062 let result = ansi_align_with_options("a\nbb", &opts);
1063 assert_eq!(result, " a\nbb");
1064
1065 // Large padding (should use with_capacity)
1066 let result = ansi_align_with_options(&text, &opts);
1067 let lines: Vec<&str> = result.split('\n').collect();
1068 assert_eq!(lines[0].len(), 20); // 19 spaces + "a"
1069 assert!(lines[0].ends_with('a'));
1070 }
1071
1072 #[test]
1073 fn test_create_padding_unicode() {
1074 // Test padding with Unicode characters
1075 let text = "a\nbb";
1076 let opts = AlignOptions::new(Alignment::Right).with_pad('β’');
1077 let result = ansi_align_with_options(text, &opts);
1078 assert_eq!(result, "β’a\nbb");
1079
1080 // Test with multi-byte Unicode character
1081 let opts = AlignOptions::new(Alignment::Right).with_pad('π―');
1082 let result = ansi_align_with_options(text, &opts);
1083 assert_eq!(result, "π―a\nbb");
1084 }
1085
1086 // Integration tests
1087 #[test]
1088 fn test_real_world_scenario() {
1089 // Simulate aligning a simple table or menu
1090 let menu = "Home\nAbout Us\nContact\nServices";
1091 let result = center(menu);
1092 let lines: Vec<&str> = result.split('\n').collect();
1093
1094 // "About Us" and "Services" are both 8 chars (longest)
1095 assert_eq!(lines[0], " Home"); // 2 spaces (8-4)/2 = 2
1096 assert_eq!(lines[1], "About Us"); // no padding (8 chars)
1097 assert_eq!(lines[2], "Contact"); // no padding - "Contact" is 7 chars, (8-7)/2 = 0
1098 assert_eq!(lines[3], "Services"); // no padding (8 chars)
1099 }
1100
1101 #[test]
1102 fn test_code_alignment() {
1103 // Test aligning code-like content
1104 let code = "if x:\n return y\nelse:\n return z";
1105 let result = right(code);
1106 let lines: Vec<&str> = result.split('\n').collect();
1107
1108 // " return y" and " return z" are longest at 12 chars
1109 assert_eq!(lines[0], " if x:"); // 7 spaces + "if x:" (12-5=7)
1110 assert_eq!(lines[1], " return y"); // no padding (12 chars)
1111 assert_eq!(lines[2], " else:"); // 7 spaces + "else:" (12-5=7)
1112 assert_eq!(lines[3], " return z"); // no padding (12 chars)
1113 }
1114}