ansi_align/
lib.rs

1//! # ansi-align
2//!
3//! [![Crates.io](https://img.shields.io/crates/v/ansi-align.svg)](https://crates.io/crates/ansi-align)
4//! [![Documentation](https://docs.rs/ansi-align/badge.svg)](https://docs.rs/ansi-align)
5//! [![License](https://img.shields.io/crates/l/ansi-align.svg)](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}