wrap_ansi/
lib.rs

1//! # 🎨 wrap-ansi
2//!
3//! [![Crates.io](https://img.shields.io/crates/v/wrap-ansi.svg)](https://crates.io/crates/wrap-ansi)
4//! [![Documentation](https://docs.rs/wrap-ansi/badge.svg)](https://docs.rs/wrap-ansi)
5//! [![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE)
6//! [![Build Status](https://github.com/sabry-awad97/wrap-ansi/workflows/CI/badge.svg)](https://github.com/sabry-awad97/wrap-ansi/actions)
7//!
8//! A **high-performance**, **Unicode-aware** Rust library for intelligently wrapping text while
9//! preserving ANSI escape sequences, colors, styles, and hyperlinks.
10//!
11//! This library is a faithful Rust port of the popular JavaScript
12//! [wrap-ansi](https://github.com/chalk/wrap-ansi) library, designed for terminal applications,
13//! CLI tools, and any software that needs to display formatted text in constrained widths.
14//!
15//! ## ✨ Key Features
16//!
17//! | Feature | Description |
18//! |---------|-------------|
19//! | 🎨 **ANSI-Aware Wrapping** | Preserves colors, styles, and formatting across line breaks |
20//! | πŸ”— **Hyperlink Support** | Maintains clickable OSC 8 hyperlinks when wrapping |
21//! | 🌍 **Unicode Ready** | Correctly handles CJK characters, emojis, and combining marks |
22//! | ⚑ **High Performance** | Optimized algorithms with pre-compiled regex patterns |
23//! | πŸ› οΈ **Flexible Options** | Hard/soft wrapping, trimming, word boundaries control |
24//! | πŸ”’ **Memory Safe** | Built-in protection against `DoS` attacks with input size limits |
25//! | πŸ“š **Rich API** | Fluent builder pattern and comprehensive error handling |
26//!
27//! ## πŸš€ Quick Start
28//!
29//! Add this to your `Cargo.toml`:
30//!
31//! ```toml
32//! [dependencies]
33//! wrap-ansi = "0.1.0"
34//! ```
35//!
36//! ### Basic Text Wrapping
37//!
38//! ```rust
39//! use wrap_ansi::wrap_ansi;
40//!
41//! let text = "The quick brown fox jumps over the lazy dog";
42//! let wrapped = wrap_ansi(text, 20, None);
43//! println!("{}", wrapped);
44//! // Output:
45//! // The quick brown fox
46//! // jumps over the lazy
47//! // dog
48//! ```
49//!
50//! ### ANSI Colors & Styles
51//!
52//! ```rust
53//! use wrap_ansi::wrap_ansi;
54//!
55//! // Colors are preserved across line breaks!
56//! let colored = "\u{001B}[31mThis is red text that will be wrapped properly\u{001B}[39m";
57//! let wrapped = wrap_ansi(colored, 15, None);
58//! println!("{}", wrapped);
59//! // Each line will have proper color codes: \u{001B}[31m...\u{001B}[39m
60//! ```
61//!
62//! ### Advanced Configuration
63//!
64//! ```rust
65//! use wrap_ansi::{wrap_ansi, WrapOptions};
66//!
67//! // Using the builder pattern for clean configuration
68//! let options = WrapOptions::builder()
69//!     .hard_wrap(true)           // Break long words
70//!     .trim_whitespace(false)    // Preserve whitespace
71//!     .word_wrap(true)           // Respect word boundaries
72//!     .build();
73//!
74//! let text = "supercalifragilisticexpialidocious word";
75//! let wrapped = wrap_ansi(text, 10, Some(options));
76//! println!("{}", wrapped);
77//! // Output:
78//! // supercalif
79//! // ragilistic
80//! // expialidoc
81//! // ious word
82//! ```
83//!
84//! ## 🎯 Use Cases
85//!
86//! Perfect for:
87//!
88//! - **CLI Applications**: Format help text, error messages, and output
89//! - **Terminal UIs**: Create responsive layouts that adapt to terminal width
90//! - **Documentation Tools**: Generate formatted text with preserved styling
91//! - **Log Processing**: Wrap log entries while maintaining color coding
92//! - **Code Formatters**: Handle syntax-highlighted code with proper wrapping
93//!
94//! ## 🌟 Advanced Features
95//!
96//! ### Wrapping Modes
97//!
98//! | Mode | Behavior | Use Case |
99//! |------|----------|----------|
100//! | **Soft Wrap** (default) | Long words move to next line intact | Natural text flow |
101//! | **Hard Wrap** | Long words break at column boundary | Strict width constraints |
102//! | **Character Wrap** | Break anywhere, ignore word boundaries | Monospace layouts |
103//!
104//! ### ANSI Sequence Support
105//!
106//! βœ… **Foreground Colors**: Standard (30-37) and bright (90-97)
107//! βœ… **Background Colors**: Standard (40-47) and bright (100-107)
108//! βœ… **Text Styles**: Bold, italic, underline, strikethrough
109//! βœ… **Color Resets**: Proper handling of reset sequences (39, 49)
110//! βœ… **Hyperlinks**: OSC 8 sequences for clickable terminal links
111//! βœ… **Custom SGR**: Support for any Select Graphic Rendition codes  
112//!
113//! ### Unicode & Internationalization
114//!
115//! ```rust
116//! use wrap_ansi::{wrap_ansi, WrapOptions};
117//!
118//! // CJK characters are properly counted as 2 columns
119//! let chinese = "δ½ ε₯½δΈ–η•ŒοΌŒθΏ™ζ˜―δΈ€δΈͺζ΅‹θ―•";
120//! let wrapped = wrap_ansi(chinese, 8, None);
121//!
122//! // Emojis and combining characters work correctly
123//! let emoji = "Hello πŸ‘‹ World 🌍 with emojis";
124//! let options = WrapOptions::builder().hard_wrap(true).build();
125//! let wrapped = wrap_ansi(emoji, 10, Some(options));
126//! ```
127//!
128//! ## πŸ”§ Error Handling
129//!
130//! For applications requiring robust error handling:
131//!
132//! ```rust
133//! use wrap_ansi::{wrap_ansi_checked, WrapError};
134//!
135//! let text = "Hello, world!";
136//! match wrap_ansi_checked(text, 0, None) {
137//!     Ok(wrapped) => println!("Wrapped: {}", wrapped),
138//!     Err(WrapError::InvalidColumnWidth(width)) => {
139//!         eprintln!("Invalid width: {}", width);
140//!     }
141//!     Err(WrapError::InputTooLarge(size, max)) => {
142//!         eprintln!("Input too large: {} bytes (max: {})", size, max);
143//!     }
144//!     _ => {}
145//! }
146//! ```
147//!
148//! ## πŸ“Š Performance
149//!
150//! - **Zero-copy operations** where possible
151//! - **Pre-compiled regex patterns** for ANSI sequence parsing
152//! - **Efficient string operations** with capacity pre-allocation
153//! - **`DoS` protection** with configurable input size limits
154//! - **Minimal allocations** through careful memory management
155//!
156//! ## 🀝 Compatibility
157//!
158//! - **Rust Edition**: 2021+
159//! - **MSRV**: 1.70.0
160//! - **Platforms**: All platforms supported by Rust
161//! - **Terminal Compatibility**: Works with all ANSI-compatible terminals
162//!
163//! ## πŸ“š Examples
164//!
165//! Check out the [examples directory](https://github.com/sabry-awad97/wrap-ansi/tree/main/examples)
166//! for more comprehensive usage examples, including:
167//!
168//! - Terminal UI layouts
169//! - Progress bars with colors
170//! - Syntax highlighting preservation
171//! - Interactive CLI applications
172
173#![cfg_attr(docsrs, feature(doc_cfg))]
174#![deny(missing_docs)]
175#![warn(clippy::all)]
176#![warn(clippy::pedantic)]
177#![warn(clippy::nursery)]
178#![warn(clippy::cargo)]
179
180use ansi_escape_sequences::strip_ansi;
181use regex::Regex;
182use string_width::string_width;
183
184/// ANSI SGR (Select Graphic Rendition) codes and escape sequences
185mod ansi_codes {
186    /// ANSI escape characters
187    pub const ESCAPE_ESC: char = '\u{001B}';
188    pub const ESCAPE_CSI: char = '\u{009B}';
189    pub const ANSI_ESCAPE_BELL: char = '\u{0007}';
190    pub const ANSI_CSI: &str = "[";
191    pub const ANSI_SGR_TERMINATOR: char = 'm';
192    pub const ANSI_ESCAPE_LINK: &str = "]8;;";
193
194    // Foreground colors (standard)
195    pub const FG_BLACK: u8 = 30;
196    // pub const FG_RED: u8 = 31;
197    // pub const FG_GREEN: u8 = 32;
198    // pub const FG_YELLOW: u8 = 33;
199    // pub const FG_BLUE: u8 = 34;
200    // pub const FG_MAGENTA: u8 = 35;
201    // pub const FG_CYAN: u8 = 36;
202    pub const FG_WHITE: u8 = 37;
203    pub const FG_RESET: u8 = 39;
204
205    // Background colors (standard)
206    pub const BG_BLACK: u8 = 40;
207    // pub const BG_RED: u8 = 41;
208    // pub const BG_GREEN: u8 = 42;
209    // pub const BG_YELLOW: u8 = 43;
210    // pub const BG_BLUE: u8 = 44;
211    // pub const BG_MAGENTA: u8 = 45;
212    // pub const BG_CYAN: u8 = 46;
213    pub const BG_WHITE: u8 = 47;
214    pub const BG_RESET: u8 = 49;
215
216    // Bright foreground colors
217    pub const FG_BRIGHT_BLACK: u8 = 90;
218    // pub const FG_BRIGHT_RED: u8 = 91;
219    // pub const FG_BRIGHT_GREEN: u8 = 92;
220    // pub const FG_BRIGHT_YELLOW: u8 = 93;
221    // pub const FG_BRIGHT_BLUE: u8 = 94;
222    // pub const FG_BRIGHT_MAGENTA: u8 = 95;
223    // pub const FG_BRIGHT_CYAN: u8 = 96;
224    pub const FG_BRIGHT_WHITE: u8 = 97;
225
226    // Bright background colors
227    pub const BG_BRIGHT_BLACK: u8 = 100;
228    // pub const BG_BRIGHT_RED: u8 = 101;
229    // pub const BG_BRIGHT_GREEN: u8 = 102;
230    // pub const BG_BRIGHT_YELLOW: u8 = 103;
231    // pub const BG_BRIGHT_BLUE: u8 = 104;
232    // pub const BG_BRIGHT_MAGENTA: u8 = 105;
233    // pub const BG_BRIGHT_CYAN: u8 = 106;
234    pub const BG_BRIGHT_WHITE: u8 = 107;
235
236    // Ranges for easier checking
237    pub const FG_STANDARD_RANGE: std::ops::RangeInclusive<u8> = FG_BLACK..=FG_WHITE;
238    pub const BG_STANDARD_RANGE: std::ops::RangeInclusive<u8> = BG_BLACK..=BG_WHITE;
239    pub const FG_BRIGHT_RANGE: std::ops::RangeInclusive<u8> = FG_BRIGHT_BLACK..=FG_BRIGHT_WHITE;
240    pub const BG_BRIGHT_RANGE: std::ops::RangeInclusive<u8> = BG_BRIGHT_BLACK..=BG_BRIGHT_WHITE;
241}
242
243/// Comprehensive error types for wrap-ansi operations.
244///
245/// This enum provides detailed error information for various failure modes
246/// that can occur during text wrapping operations.
247///
248/// # Examples
249///
250/// ```rust
251/// use wrap_ansi::{wrap_ansi_checked, WrapError};
252///
253/// // Handle invalid column width
254/// match wrap_ansi_checked("test", 0, None) {
255///     Err(WrapError::InvalidColumnWidth(width)) => {
256///         println!("Column width {} is invalid, must be > 0", width);
257///     }
258///     Ok(result) => println!("Wrapped: {}", result),
259///     _ => {}
260/// }
261///
262/// // Handle input size limits
263/// let huge_input = "x".repeat(20_000_000);
264/// match wrap_ansi_checked(&huge_input, 80, None) {
265///     Err(WrapError::InputTooLarge(size, max_size)) => {
266///         println!("Input {} bytes exceeds limit of {} bytes", size, max_size);
267///     }
268///     Ok(result) => println!("Wrapped successfully"),
269///     _ => {}
270/// }
271/// ```
272#[derive(Debug, thiserror::Error, PartialEq, Eq)]
273pub enum WrapError {
274    /// Column width must be greater than 0.
275    ///
276    /// This error occurs when attempting to wrap text with a column width of 0,
277    /// which would be impossible to satisfy.
278    #[error("Column width must be greater than 0, got {0}")]
279    InvalidColumnWidth(usize),
280
281    /// Input string is too large to process safely.
282    ///
283    /// This error provides protection against potential `DoS` attacks by limiting
284    /// the maximum input size that can be processed.
285    #[error("Input string is too large: {0} characters (max: {1})")]
286    InputTooLarge(usize, usize),
287}
288
289/// Classification of ANSI SGR codes for proper handling
290#[derive(Debug, Clone, PartialEq)]
291enum AnsiCodeType {
292    /// Foreground color code
293    Foreground(u8),
294    /// Background color code
295    Background(u8),
296    /// Foreground color reset
297    ForegroundReset,
298    /// Background color reset
299    BackgroundReset,
300    /// Other SGR code (bold, italic, etc.)
301    Other(u8),
302}
303
304/// Represents the current ANSI state while processing text
305#[derive(Debug, Default, Clone)]
306struct AnsiState {
307    /// Current foreground color code
308    foreground_code: Option<u8>,
309    /// Current background color code
310    background_code: Option<u8>,
311    /// Current hyperlink URL
312    hyperlink_url: Option<String>,
313}
314
315impl AnsiState {
316    /// Reset the foreground color
317    const fn reset_foreground(&mut self) {
318        self.foreground_code = None;
319    }
320
321    /// Reset the background color
322    const fn reset_background(&mut self) {
323        self.background_code = None;
324    }
325
326    /// Close the current hyperlink
327    #[allow(unused)]
328    fn close_hyperlink(&mut self) {
329        self.hyperlink_url = None;
330    }
331
332    /// Generate ANSI codes to apply before a newline (closing codes)
333    fn apply_before_newline(&self) -> String {
334        let mut result = String::new();
335
336        if self.hyperlink_url.is_some() {
337            result.push_str(&wrap_ansi_hyperlink(""));
338        }
339        if self.foreground_code.is_some() {
340            result.push_str(&wrap_ansi_code(ansi_codes::FG_RESET));
341        }
342        if self.background_code.is_some() {
343            result.push_str(&wrap_ansi_code(ansi_codes::BG_RESET));
344        }
345
346        result
347    }
348
349    /// Generate ANSI codes to apply after a newline (opening codes)
350    fn apply_after_newline(&self) -> String {
351        let mut result = String::new();
352
353        if let Some(code) = self.foreground_code {
354            result.push_str(&wrap_ansi_code(code));
355        }
356        if let Some(code) = self.background_code {
357            result.push_str(&wrap_ansi_code(code));
358        }
359        if let Some(ref url) = self.hyperlink_url {
360            result.push_str(&wrap_ansi_hyperlink(url));
361        }
362
363        result
364    }
365
366    /// Update state based on an ANSI code
367    fn update_with_code(&mut self, code: u8) {
368        match classify_ansi_code(code) {
369            AnsiCodeType::Foreground(c) => self.foreground_code = Some(c),
370            AnsiCodeType::Background(c) => self.background_code = Some(c),
371            AnsiCodeType::ForegroundReset => self.reset_foreground(),
372            AnsiCodeType::BackgroundReset => self.reset_background(),
373            AnsiCodeType::Other(_) => {
374                // Handle other codes as needed
375                // For now, treat unknown codes as foreground for backward compatibility
376                self.foreground_code = Some(code);
377            }
378        }
379    }
380
381    /// Update state with a hyperlink URL
382    fn update_with_hyperlink(&mut self, url: &str) {
383        self.hyperlink_url = if url.is_empty() {
384            None
385        } else {
386            Some(url.to_string())
387        };
388    }
389}
390
391/// State machine for parsing ANSI escape sequences
392#[derive(Debug, PartialEq)]
393enum ParseState {
394    /// Normal text processing
395    Normal,
396    /// Inside an escape sequence (after ESC)
397    InEscape,
398    /// Inside a CSI sequence (after ESC[)
399    InCsi,
400    /// Inside an OSC sequence (after ESC])
401    InOsc,
402}
403
404/// ANSI escape sequence parser
405struct AnsiParser {
406    state: ParseState,
407}
408
409impl AnsiParser {
410    /// Create a new ANSI parser
411    const fn new() -> Self {
412        Self {
413            state: ParseState::Normal,
414        }
415    }
416
417    /// Check if currently inside an ANSI sequence
418    fn is_in_sequence(&self) -> bool {
419        self.state != ParseState::Normal
420    }
421
422    /// Process a character and update parser state
423    const fn process_char(&mut self, ch: char) {
424        use ansi_codes::{ANSI_ESCAPE_BELL, ANSI_SGR_TERMINATOR, ESCAPE_ESC};
425
426        match (&self.state, ch) {
427            (ParseState::Normal, ESCAPE_ESC) => {
428                self.state = ParseState::InEscape;
429            }
430            (ParseState::InEscape, '[') => {
431                self.state = ParseState::InCsi;
432            }
433            (ParseState::InEscape, ']') => {
434                self.state = ParseState::InOsc;
435            }
436            (ParseState::InCsi, ANSI_SGR_TERMINATOR) | (ParseState::InOsc, ANSI_ESCAPE_BELL) => {
437                self.state = ParseState::Normal;
438            }
439            _ => {}
440        }
441    }
442}
443
444/// Classify an ANSI SGR code for proper handling
445fn classify_ansi_code(code: u8) -> AnsiCodeType {
446    use ansi_codes::{
447        BG_BRIGHT_RANGE, BG_RESET, BG_STANDARD_RANGE, FG_BRIGHT_RANGE, FG_RESET, FG_STANDARD_RANGE,
448    };
449
450    match code {
451        FG_RESET => AnsiCodeType::ForegroundReset,
452        BG_RESET => AnsiCodeType::BackgroundReset,
453        code if FG_STANDARD_RANGE.contains(&code) || FG_BRIGHT_RANGE.contains(&code) => {
454            AnsiCodeType::Foreground(code)
455        }
456        code if BG_STANDARD_RANGE.contains(&code) || BG_BRIGHT_RANGE.contains(&code) => {
457            AnsiCodeType::Background(code)
458        }
459        code => AnsiCodeType::Other(code),
460    }
461}
462
463/// πŸŽ›οΈ Configuration options for advanced text wrapping behavior.
464///
465/// This struct provides fine-grained control over how text is wrapped, including
466/// whitespace handling, word breaking strategies, and wrapping modes. All options
467/// work seamlessly with ANSI escape sequences and Unicode text.
468///
469/// # Quick Reference
470///
471/// | Option | Default | Description |
472/// |--------|---------|-------------|
473/// | `trim` | `true` | Remove leading/trailing whitespace from wrapped lines |
474/// | `hard` | `false` | Break long words at column boundary (vs. moving to next line) |
475/// | `word_wrap` | `true` | Respect word boundaries when wrapping |
476///
477/// # Examples
478///
479/// ## Default Configuration
480/// ```rust
481/// use wrap_ansi::{wrap_ansi, WrapOptions};
482///
483/// // These are equivalent
484/// let result1 = wrap_ansi("Hello world", 10, None);
485/// let result2 = wrap_ansi("Hello world", 10, Some(WrapOptions::default()));
486/// assert_eq!(result1, result2);
487/// ```
488///
489/// ## Builder Pattern (Recommended)
490/// ```rust
491/// use wrap_ansi::{wrap_ansi, WrapOptions};
492///
493/// let options = WrapOptions::builder()
494///     .hard_wrap(true)           // Break long words
495///     .trim_whitespace(false)    // Preserve all whitespace
496///     .word_wrap(true)           // Still respect word boundaries where possible
497///     .build();
498///
499/// let text = "supercalifragilisticexpialidocious";
500/// let wrapped = wrap_ansi(text, 10, Some(options));
501/// // Result: "supercalif\nragilistic\nexpialidoc\nious"
502/// ```
503///
504/// ## Struct Initialization
505/// ```rust
506/// use wrap_ansi::{wrap_ansi, WrapOptions};
507///
508/// // Character-level wrapping for monospace layouts
509/// let char_wrap = WrapOptions {
510///     hard: true,
511///     trim: true,
512///     word_wrap: false,  // Break anywhere, ignore word boundaries
513/// };
514///
515/// let text = "hello world";
516/// let wrapped = wrap_ansi(text, 7, Some(char_wrap));
517/// // Result: "hello w\norld"
518/// ```
519///
520/// ## Common Configurations
521///
522/// ### Terminal Output (Default)
523/// ```rust
524/// # use wrap_ansi::WrapOptions;
525/// let terminal = WrapOptions::default(); // Soft wrap, trim spaces, respect words
526/// ```
527///
528/// ### Code/Monospace Text
529/// ```rust
530/// # use wrap_ansi::WrapOptions;
531/// let code = WrapOptions {
532///     hard: true,        // Break long identifiers
533///     trim: false,       // Preserve indentation
534///     word_wrap: false,  // Break anywhere for consistent columns
535/// };
536/// ```
537///
538/// ### Natural Language
539/// ```rust
540/// # use wrap_ansi::WrapOptions;
541/// let prose = WrapOptions {
542///     hard: false,       // Keep words intact
543///     trim: true,        // Clean up spacing
544///     word_wrap: true,   // Break at word boundaries
545/// };
546/// ```
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
548pub struct WrapOptions {
549    /// 🧹 **Whitespace trimming control**
550    ///
551    /// When `true` (default), leading and trailing spaces are automatically removed
552    /// from lines that result from wrapping, creating clean, left-aligned text.
553    /// When `false`, all whitespace is preserved exactly as in the input.
554    ///
555    /// # Use Cases
556    /// - `true`: Clean terminal output, formatted text display
557    /// - `false`: Code formatting, preserving indentation, ASCII art
558    ///
559    /// # Examples
560    /// ```rust
561    /// use wrap_ansi::{wrap_ansi, WrapOptions};
562    ///
563    /// let text = "   hello    world   ";
564    ///
565    /// // With trimming (default)
566    /// let trimmed = wrap_ansi(text, 8, None);
567    /// assert_eq!(trimmed, "hello\nworld");
568    ///
569    /// // Without trimming - preserves all spaces
570    /// let options = WrapOptions { trim: false, ..Default::default() };
571    /// let untrimmed = wrap_ansi(text, 8, Some(options));
572    /// assert_eq!(untrimmed, "   hello\n    \nworld   ");
573    /// ```
574    pub trim: bool,
575
576    /// ⚑ **Word breaking strategy**
577    ///
578    /// Controls how long words that exceed the column limit are handled:
579    /// - `false` (default, **soft wrapping**): Long words move to the next line intact
580    /// - `true` (**hard wrapping**): Long words are broken at the column boundary
581    ///
582    /// # Use Cases
583    /// - Soft wrap: Natural text flow, readability, terminal output
584    /// - Hard wrap: Strict width constraints, tabular data, code formatting
585    ///
586    /// # Examples
587    /// ```rust
588    /// use wrap_ansi::{wrap_ansi, WrapOptions};
589    ///
590    /// let long_word = "supercalifragilisticexpialidocious";
591    ///
592    /// // Soft wrapping - word stays intact
593    /// let soft = wrap_ansi(long_word, 10, None);
594    /// assert_eq!(soft, "supercalifragilisticexpialidocious"); // No wrapping!
595    ///
596    /// // Hard wrapping - word is broken
597    /// let options = WrapOptions { hard: true, ..Default::default() };
598    /// let hard = wrap_ansi(long_word, 10, Some(options));
599    /// assert_eq!(hard, "supercalif\nragilistic\nexpialidoc\nious");
600    /// ```
601    pub hard: bool,
602
603    /// πŸ”€ **Word boundary respect**
604    ///
605    /// Determines whether wrapping should occur at word boundaries (spaces) or
606    /// can break anywhere within the text:
607    /// - `true` (default): Text wraps at word boundaries, preserving word integrity
608    /// - `false`: Text can wrap at any character position (character-level wrapping)
609    ///
610    /// # Use Cases
611    /// - Word boundaries: Natural language, readable text, most terminal output
612    /// - Character boundaries: Monospace layouts, code, precise column control
613    ///
614    /// # Examples
615    /// ```rust
616    /// use wrap_ansi::{wrap_ansi, WrapOptions};
617    ///
618    /// let text = "hello world";
619    ///
620    /// // Word wrapping (default) - breaks at spaces
621    /// let word_wrap = wrap_ansi(text, 7, None);
622    /// assert_eq!(word_wrap, "hello\nworld");
623    ///
624    /// // Character wrapping - breaks anywhere
625    /// let options = WrapOptions { word_wrap: false, ..Default::default() };
626    /// let char_wrap = wrap_ansi(text, 7, Some(options));
627    /// assert_eq!(char_wrap, "hello w\norld");
628    /// ```
629    pub word_wrap: bool,
630}
631
632impl Default for WrapOptions {
633    fn default() -> Self {
634        Self {
635            trim: true,
636            hard: false,
637            word_wrap: true,
638        }
639    }
640}
641
642/// πŸ—οΈ Fluent builder for creating `WrapOptions` with a clean, readable interface.
643///
644/// This builder provides a more ergonomic way to configure wrapping options
645/// compared to struct initialization, especially when you only need to change
646/// a few settings from the defaults.
647///
648/// # Examples
649///
650/// ## Basic Usage
651/// ```rust
652/// use wrap_ansi::WrapOptions;
653///
654/// let options = WrapOptions::builder()
655///     .hard_wrap(true)
656///     .build();
657/// ```
658///
659/// ## Method Chaining
660/// ```rust
661/// use wrap_ansi::{wrap_ansi, WrapOptions};
662///
663/// let result = wrap_ansi(
664///     "Some long text that needs wrapping",
665///     15,
666///     Some(WrapOptions::builder()
667///         .hard_wrap(true)
668///         .trim_whitespace(false)
669///         .word_wrap(true)
670///         .build())
671/// );
672/// ```
673///
674/// ## Conditional Configuration
675/// ```rust
676/// use wrap_ansi::WrapOptions;
677///
678/// let preserve_formatting = true;
679/// let strict_width = false;
680///
681/// let options = WrapOptions::builder()
682///     .trim_whitespace(!preserve_formatting)
683///     .hard_wrap(strict_width)
684///     .build();
685/// ```
686#[derive(Debug, Default, Clone)]
687pub struct WrapOptionsBuilder {
688    options: WrapOptions,
689}
690
691impl WrapOptionsBuilder {
692    /// ⚑ Configure hard wrapping behavior.
693    ///
694    /// When enabled, long words that exceed the column limit will be broken
695    /// at the boundary. When disabled (default), long words move to the next line.
696    ///
697    /// # Examples
698    /// ```rust
699    /// use wrap_ansi::WrapOptions;
700    ///
701    /// let strict = WrapOptions::builder().hard_wrap(true).build();
702    /// let flexible = WrapOptions::builder().hard_wrap(false).build();
703    /// ```
704    #[must_use]
705    pub const fn hard_wrap(mut self, enabled: bool) -> Self {
706        self.options.hard = enabled;
707        self
708    }
709
710    /// 🧹 Configure whitespace trimming behavior.
711    ///
712    /// When enabled (default), leading and trailing whitespace is removed from
713    /// wrapped lines. When disabled, all whitespace is preserved.
714    ///
715    /// # Examples
716    /// ```rust
717    /// use wrap_ansi::WrapOptions;
718    ///
719    /// let clean = WrapOptions::builder().trim_whitespace(true).build();
720    /// let preserve = WrapOptions::builder().trim_whitespace(false).build();
721    /// ```
722    #[must_use]
723    pub const fn trim_whitespace(mut self, enabled: bool) -> Self {
724        self.options.trim = enabled;
725        self
726    }
727
728    /// πŸ”€ Configure word boundary respect.
729    ///
730    /// When enabled (default), text wraps at word boundaries (spaces).
731    /// When disabled, text can wrap at any character position.
732    ///
733    /// # Examples
734    /// ```rust
735    /// use wrap_ansi::WrapOptions;
736    ///
737    /// let word_aware = WrapOptions::builder().word_wrap(true).build();
738    /// let char_level = WrapOptions::builder().word_wrap(false).build();
739    /// ```
740    #[must_use]
741    pub const fn word_wrap(mut self, enabled: bool) -> Self {
742        self.options.word_wrap = enabled;
743        self
744    }
745
746    /// 🏁 Build the final `WrapOptions` configuration.
747    ///
748    /// Consumes the builder and returns the configured `WrapOptions` struct.
749    ///
750    /// # Examples
751    /// ```rust
752    /// use wrap_ansi::WrapOptions;
753    ///
754    /// let options = WrapOptions::builder()
755    ///     .hard_wrap(true)
756    ///     .trim_whitespace(false)
757    ///     .build();
758    /// ```
759    #[must_use]
760    pub const fn build(self) -> WrapOptions {
761        self.options
762    }
763}
764
765impl WrapOptions {
766    /// πŸ—οΈ Create a new fluent builder for `WrapOptions`.
767    ///
768    /// This is the recommended way to create custom `WrapOptions` configurations
769    /// as it provides a clean, readable interface with method chaining.
770    ///
771    /// # Examples
772    /// ```rust
773    /// use wrap_ansi::WrapOptions;
774    ///
775    /// // Simple configuration
776    /// let options = WrapOptions::builder()
777    ///     .hard_wrap(true)
778    ///     .build();
779    ///
780    /// // Complex configuration
781    /// let advanced = WrapOptions::builder()
782    ///     .hard_wrap(false)        // Soft wrapping
783    ///     .trim_whitespace(true)   // Clean output
784    ///     .word_wrap(true)         // Respect word boundaries
785    ///     .build();
786    /// ```
787    #[must_use]
788    pub fn builder() -> WrapOptionsBuilder {
789        WrapOptionsBuilder::default()
790    }
791}
792
793/// Check if character is an ANSI escape character
794const fn is_escape_char(ch: char) -> bool {
795    ch == ansi_codes::ESCAPE_ESC || ch == ansi_codes::ESCAPE_CSI
796}
797
798/// Wrap ANSI code with escape sequence
799///
800/// Creates a complete ANSI escape sequence for the given SGR (Select Graphic Rendition) code.
801fn wrap_ansi_code(code: u8) -> String {
802    format!(
803        "{}{}{}{}",
804        ansi_codes::ESCAPE_ESC,
805        ansi_codes::ANSI_CSI,
806        code,
807        ansi_codes::ANSI_SGR_TERMINATOR
808    )
809}
810
811/// Wrap ANSI hyperlink with escape sequence
812///
813/// Creates an OSC 8 hyperlink escape sequence for the given URL.
814fn wrap_ansi_hyperlink(url: &str) -> String {
815    format!(
816        "{}{}{}{}",
817        ansi_codes::ESCAPE_ESC,
818        ansi_codes::ANSI_ESCAPE_LINK,
819        url,
820        ansi_codes::ANSI_ESCAPE_BELL
821    )
822}
823
824/// Calculate the visual width of each word in the input string.
825///
826/// Words are split on whitespace, and their visual width is calculated
827/// taking into account ANSI escape sequences and Unicode characters.
828fn calculate_word_visual_widths(text: &str) -> Vec<usize> {
829    text.split(' ').map(string_width).collect()
830}
831
832/// πŸ”’ Maximum input size to prevent potential `DoS` attacks.
833///
834/// This limit protects against malicious inputs that could cause excessive
835/// memory usage or processing time. The limit is set to 10MB, which should
836/// be sufficient for most legitimate use cases while preventing abuse.
837const MAX_INPUT_SIZE: usize = 10_000_000; // 10MB
838
839/// πŸ›‘οΈ **Wrap text with comprehensive error handling and input validation.**
840///
841/// This is the safe, production-ready version of [`wrap_ansi`] that provides robust
842/// error handling for edge cases and validates input parameters to prevent potential
843/// security issues or unexpected behavior.
844///
845/// # πŸ”’ Safety Features
846///
847/// - **Input size validation**: Prevents `DoS` attacks from extremely large inputs
848/// - **Parameter validation**: Ensures column width is valid (> 0)
849/// - **Memory protection**: Built-in limits to prevent excessive memory usage
850///
851/// # Parameters
852///
853/// - `string`: The input text to wrap (may contain ANSI escape sequences)
854/// - `columns`: Target column width (must be > 0)
855/// - `options`: Optional wrapping configuration (uses defaults if `None`)
856///
857/// # Returns
858///
859/// - `Ok(String)`: Successfully wrapped text with preserved ANSI sequences
860/// - `Err(WrapError)`: Detailed error information for invalid inputs
861///
862/// # Errors
863///
864/// | Error | Condition | Solution |
865/// |-------|-----------|----------|
866/// | `InvalidColumnWidth` | `columns == 0` | Use a positive column width |
867/// | `InputTooLarge` | Input exceeds 10MB | Split input or increase limit |
868///
869/// # Examples
870///
871/// ## Basic Usage with Error Handling
872/// ```rust
873/// use wrap_ansi::{wrap_ansi_checked, WrapError};
874///
875/// let text = "Hello, world! This is a test of error handling.";
876/// match wrap_ansi_checked(text, 20, None) {
877///     Ok(wrapped) => println!("Success: {}", wrapped),
878///     Err(e) => eprintln!("Error: {}", e),
879/// }
880/// ```
881///
882/// ## Handling Invalid Column Width
883/// ```rust
884/// use wrap_ansi::{wrap_ansi_checked, WrapError};
885///
886/// match wrap_ansi_checked("test", 0, None) {
887///     Err(WrapError::InvalidColumnWidth(width)) => {
888///         println!("Invalid width: {}. Must be greater than 0.", width);
889///     }
890///     Ok(_) => unreachable!(),
891///     _ => {}
892/// }
893/// ```
894///
895/// ## Handling Large Input Protection
896/// ```rust
897/// use wrap_ansi::{wrap_ansi_checked, WrapError};
898///
899/// let huge_input = "x".repeat(15_000_000); // 15MB
900/// match wrap_ansi_checked(&huge_input, 80, None) {
901///     Err(WrapError::InputTooLarge(size, max)) => {
902///         println!("Input {} bytes exceeds limit of {} bytes", size, max);
903///         // Consider splitting the input or processing in chunks
904///     }
905///     Ok(result) => println!("Wrapped {} characters", result.len()),
906///     _ => {}
907/// }
908/// ```
909///
910/// ## Production Error Handling Pattern
911/// ```rust
912/// use wrap_ansi::{wrap_ansi_checked, WrapOptions, WrapError};
913///
914/// fn safe_wrap_text(text: &str, width: usize) -> Result<String, String> {
915///     let options = WrapOptions::builder()
916///         .hard_wrap(true)
917///         .trim_whitespace(true)
918///         .build();
919///
920///     wrap_ansi_checked(text, width, Some(options))
921///         .map_err(|e| match e {
922///             WrapError::InvalidColumnWidth(w) => {
923///                 format!("Invalid column width: {}. Must be > 0.", w)
924///             }
925///             WrapError::InputTooLarge(size, max) => {
926///                 format!("Input too large: {} bytes (max: {} bytes)", size, max)
927///             }
928///         })
929/// }
930/// ```
931///
932/// # Performance Notes
933///
934/// - Input validation adds minimal overhead (O(1) for most checks)
935/// - Large input detection is O(1) as it checks string length
936/// - Consider using [`wrap_ansi`] directly if you can guarantee valid inputs
937///
938/// # See Also
939///
940/// - [`wrap_ansi`]: The unchecked version for maximum performance
941/// - [`WrapError`]: Detailed error type documentation
942/// - [`WrapOptions`]: Configuration options
943pub fn wrap_ansi_checked(
944    string: &str,
945    columns: usize,
946    options: Option<WrapOptions>,
947) -> Result<String, WrapError> {
948    if columns == 0 {
949        return Err(WrapError::InvalidColumnWidth(columns));
950    }
951
952    if string.len() > MAX_INPUT_SIZE {
953        return Err(WrapError::InputTooLarge(string.len(), MAX_INPUT_SIZE));
954    }
955
956    Ok(wrap_ansi(string, columns, options))
957}
958
959/// Wrap a long word across multiple rows with ANSI-aware processing
960///
961/// This function handles wrapping of individual words that exceed the column limit,
962/// taking into account ANSI escape sequences that don't contribute to visual width.
963fn wrap_word_with_ansi_awareness(rows: &mut Vec<String>, word: &str, columns: usize) {
964    if word.is_empty() {
965        return;
966    }
967
968    // Pre-allocate capacity to reduce reallocations
969    let estimated_lines = (string_width(word) / columns.max(1)) + 1;
970    if rows.capacity() < rows.len() + estimated_lines {
971        rows.reserve(estimated_lines);
972    }
973
974    let mut current_line = rows.last().cloned().unwrap_or_default();
975    let mut visible_width = string_width(&strip_ansi(&current_line));
976    let chars = word.chars();
977    let mut ansi_parser = AnsiParser::new();
978
979    for ch in chars {
980        let char_width = if ansi_parser.is_in_sequence() {
981            ansi_parser.process_char(ch);
982            0 // ANSI characters don't contribute to visible width
983        } else {
984            ansi_parser.process_char(ch);
985            if ansi_parser.is_in_sequence() {
986                0 // Just entered an ANSI sequence
987            } else {
988                string_width(&ch.to_string())
989            }
990        };
991
992        if visible_width + char_width > columns && visible_width > 0 {
993            // Start new line
994            if let Some(last_row) = rows.last_mut() {
995                *last_row = current_line;
996            }
997            rows.push(String::new());
998            current_line = String::new();
999            visible_width = 0;
1000        }
1001
1002        current_line.push(ch);
1003        visible_width += char_width;
1004    }
1005
1006    // Update the last row
1007    if let Some(last_row) = rows.last_mut() {
1008        *last_row = current_line;
1009    } else {
1010        rows.push(current_line);
1011    }
1012}
1013
1014/// Remove trailing spaces while preserving ANSI escape sequences.
1015///
1016/// This function is ANSI-aware, meaning it won't trim spaces that are
1017/// part of ANSI escape sequences or that come after invisible sequences.
1018fn trim_trailing_spaces_ansi_aware(text: &str) -> String {
1019    let words: Vec<&str> = text.split(' ').collect();
1020    let mut last_visible = words.len();
1021
1022    // Find the last word with visible content
1023    while last_visible > 0 {
1024        if string_width(words[last_visible - 1]) > 0 {
1025            break;
1026        }
1027        last_visible -= 1;
1028    }
1029
1030    if last_visible == words.len() {
1031        return text.to_string();
1032    }
1033
1034    // Reconstruct string: visible words joined with spaces + invisible words concatenated
1035    let mut result = words[..last_visible].join(" ");
1036    result.push_str(&words[last_visible..].join(""));
1037    result
1038}
1039
1040/// Core text wrapping implementation that handles both text layout and ANSI sequence preservation.
1041///
1042/// This function processes a single line of text and wraps it according to the specified
1043/// options while maintaining ANSI escape sequences across line boundaries.
1044fn wrap_text_preserving_ansi_sequences(
1045    text: &str,
1046    max_columns: usize,
1047    wrap_options: WrapOptions,
1048) -> String {
1049    if wrap_options.trim && text.trim().is_empty() {
1050        return String::new();
1051    }
1052
1053    let word_widths = calculate_word_visual_widths(text);
1054    let mut rows = vec![String::new()];
1055
1056    // Process each word
1057    for (word_index, word) in text.split(' ').enumerate() {
1058        process_word_for_wrapping(
1059            &mut rows,
1060            word,
1061            word_index,
1062            &word_widths,
1063            max_columns,
1064            wrap_options,
1065        );
1066    }
1067
1068    // Apply trimming if requested
1069    if wrap_options.trim {
1070        rows = rows
1071            .into_iter()
1072            .map(|row| trim_trailing_spaces_ansi_aware(&row))
1073            .collect();
1074    }
1075
1076    let wrapped_text = rows.join("\n");
1077    apply_ansi_preservation_across_lines(&wrapped_text)
1078}
1079
1080/// Process a single word for wrapping, handling spacing and line breaks
1081fn process_word_for_wrapping(
1082    rows: &mut Vec<String>,
1083    word: &str,
1084    word_index: usize,
1085    word_widths: &[usize],
1086    max_columns: usize,
1087    options: WrapOptions,
1088) {
1089    // Handle trimming for the current row
1090    if options.trim {
1091        if let Some(last_row) = rows.last_mut() {
1092            *last_row = last_row.trim_start().to_string();
1093        }
1094    }
1095
1096    let mut current_row_width = string_width(rows.last().unwrap_or(&String::new()));
1097
1098    // Add space between words (except for the first word)
1099    if word_index != 0 {
1100        if current_row_width >= max_columns && (!options.word_wrap || !options.trim) {
1101            rows.push(String::new());
1102            current_row_width = 0;
1103        }
1104
1105        if current_row_width > 0 || !options.trim {
1106            if let Some(last_row) = rows.last_mut() {
1107                last_row.push(' ');
1108                current_row_width += 1;
1109            }
1110        }
1111    }
1112
1113    let word_width = word_widths[word_index];
1114
1115    // Handle hard wrapping for long words
1116    if options.hard && word_width > max_columns {
1117        let remaining_columns = max_columns - current_row_width;
1118        let breaks_starting_this_line = 1 + (word_width - remaining_columns - 1) / max_columns;
1119        let breaks_starting_next_line = (word_width - 1) / max_columns;
1120
1121        if breaks_starting_next_line < breaks_starting_this_line {
1122            rows.push(String::new());
1123        }
1124
1125        wrap_word_with_ansi_awareness(rows, word, max_columns);
1126        return;
1127    }
1128
1129    // Check if word fits on current line
1130    if current_row_width + word_width > max_columns && current_row_width > 0 && word_width > 0 {
1131        if !options.word_wrap && current_row_width < max_columns {
1132            wrap_word_with_ansi_awareness(rows, word, max_columns);
1133            return;
1134        }
1135        rows.push(String::new());
1136    }
1137
1138    // Handle character-level wrapping
1139    if current_row_width + word_width > max_columns && !options.word_wrap {
1140        wrap_word_with_ansi_awareness(rows, word, max_columns);
1141        return;
1142    }
1143
1144    // Add word to current row
1145    if let Some(last_row) = rows.last_mut() {
1146        last_row.push_str(word);
1147    }
1148}
1149
1150// Pre-compiled regex patterns for better performance
1151static CSI_REGEX: std::sync::LazyLock<Regex> =
1152    std::sync::LazyLock::new(|| Regex::new(r"^\u{001B}\[(\d+)m").unwrap());
1153static OSC_REGEX: std::sync::LazyLock<Regex> =
1154    std::sync::LazyLock::new(|| Regex::new(r"^\u{001B}\]8;;([^\u{0007}]*)\u{0007}").unwrap());
1155
1156/// Apply ANSI sequence preservation across line boundaries
1157///
1158/// This function processes the wrapped text and ensures that ANSI escape sequences
1159/// are properly closed before newlines and reopened after newlines.
1160fn apply_ansi_preservation_across_lines(text: &str) -> String {
1161    let mut result = String::with_capacity(text.len() * 2); // Pre-allocate for efficiency
1162    let mut ansi_state = AnsiState::default();
1163    let chars: Vec<char> = text.chars().collect();
1164    let mut char_index = 0;
1165
1166    for (index, &character) in chars.iter().enumerate() {
1167        result.push(character);
1168
1169        if is_escape_char(character) {
1170            let remaining = &text[char_index..];
1171
1172            // Try to match CSI sequence (e.g., \u001B[31m)
1173            if let Some(caps) = CSI_REGEX.captures(remaining) {
1174                if let Some(code_match) = caps.get(1) {
1175                    if let Ok(code) = code_match.as_str().parse::<u8>() {
1176                        ansi_state.update_with_code(code);
1177                    }
1178                }
1179            }
1180            // Try to match OSC hyperlink sequence
1181            else if let Some(caps) = OSC_REGEX.captures(remaining) {
1182                if let Some(uri_match) = caps.get(1) {
1183                    ansi_state.update_with_hyperlink(uri_match.as_str());
1184                }
1185            }
1186        }
1187
1188        // Handle newlines: close styles before, reopen after
1189        if index + 1 < chars.len() && chars[index + 1] == '\n' {
1190            result.push_str(&ansi_state.apply_before_newline());
1191        } else if character == '\n' {
1192            result.push_str(&ansi_state.apply_after_newline());
1193        }
1194
1195        char_index += character.len_utf8();
1196    }
1197
1198    result
1199}
1200
1201/// 🎨 **The main text wrapping function with full ANSI escape sequence support.**
1202///
1203/// This is the primary function for wrapping text while intelligently preserving
1204/// ANSI escape sequences for colors, styles, and hyperlinks. It handles Unicode
1205/// characters correctly and provides flexible wrapping options for any use case.
1206///
1207/// # ✨ Key Features
1208///
1209/// - **🎨 ANSI Preservation**: Colors and styles are maintained across line breaks
1210/// - **🌍 Unicode Aware**: Proper handling of CJK characters, emojis, and combining marks
1211/// - **πŸ”— Hyperlink Support**: OSC 8 sequences remain clickable after wrapping
1212/// - **⚑ High Performance**: Optimized algorithms with pre-compiled regex patterns
1213/// - **πŸ› οΈ Flexible Options**: Customizable wrapping behavior via [`WrapOptions`]
1214///
1215/// # Parameters
1216///
1217/// | Parameter | Type | Description |
1218/// |-----------|------|-------------|
1219/// | `string` | `&str` | Input text (may contain ANSI sequences) |
1220/// | `columns` | `usize` | Maximum width in columns per line |
1221/// | `options` | `Option<WrapOptions>` | Wrapping configuration (uses defaults if `None`) |
1222///
1223/// # Returns
1224///
1225/// A `String` with text wrapped to the specified width, with all ANSI sequences
1226/// properly preserved and applied to each line as needed.
1227///
1228/// # Examples
1229///
1230/// ## πŸ“ Basic Text Wrapping
1231/// ```rust
1232/// use wrap_ansi::wrap_ansi;
1233///
1234/// let text = "The quick brown fox jumps over the lazy dog";
1235/// let wrapped = wrap_ansi(text, 20, None);
1236/// println!("{}", wrapped);
1237/// // Output:
1238/// // The quick brown fox
1239/// // jumps over the lazy
1240/// // dog
1241/// ```
1242///
1243/// ## 🎨 Colored Text Wrapping
1244/// ```rust
1245/// use wrap_ansi::wrap_ansi;
1246///
1247/// // Red text that maintains color across line breaks
1248/// let colored = "\u{001B}[31mThis is a long red text that will be wrapped properly\u{001B}[39m";
1249/// let wrapped = wrap_ansi(colored, 15, None);
1250/// println!("{}", wrapped);
1251/// // Each line will have: \u{001B}[31m[text]\u{001B}[39m
1252/// // Output (with colors):
1253/// // This is a long
1254/// // red text that
1255/// // will be wrapped
1256/// // properly
1257/// ```
1258///
1259/// ## πŸ”— Hyperlink Preservation
1260/// ```rust
1261/// use wrap_ansi::wrap_ansi;
1262///
1263/// let link = "Visit \u{001B}]8;;https://example.com\u{0007}my website\u{001B}]8;;\u{0007} for more info";
1264/// let wrapped = wrap_ansi(link, 20, None);
1265/// // Hyperlinks remain clickable across line breaks
1266/// ```
1267///
1268/// ## 🌍 Unicode and Emoji Support
1269/// ```rust
1270/// use wrap_ansi::wrap_ansi;
1271///
1272/// // CJK characters are counted as 2 columns each
1273/// let chinese = "δ½ ε₯½δΈ–η•ŒοΌθΏ™ζ˜―δΈ€δΈͺ桋试。";
1274/// let wrapped = wrap_ansi(chinese, 10, None);
1275///
1276/// // Emojis work correctly too
1277/// let emoji = "Hello πŸ‘‹ World 🌍 with emojis πŸŽ‰";
1278/// let wrapped_emoji = wrap_ansi(emoji, 15, None);
1279/// ```
1280///
1281/// ## βš™οΈ Advanced Configuration
1282/// ```rust
1283/// use wrap_ansi::{wrap_ansi, WrapOptions};
1284///
1285/// // Using builder pattern for clean configuration
1286/// let options = WrapOptions::builder()
1287///     .hard_wrap(true)           // Break long words at boundary
1288///     .trim_whitespace(false)    // Preserve all whitespace
1289///     .word_wrap(true)           // Still respect word boundaries where possible
1290///     .build();
1291///
1292/// let text = "supercalifragilisticexpialidocious word";
1293/// let wrapped = wrap_ansi(text, 10, Some(options));
1294/// // Output:
1295/// // supercalif
1296/// // ragilistic
1297/// // expialidoc
1298/// // ious word
1299/// ```
1300///
1301/// ## 🎯 Real-World Use Cases
1302///
1303/// ### Terminal Application Help Text
1304/// ```rust
1305/// use wrap_ansi::{wrap_ansi, WrapOptions};
1306///
1307/// fn format_help_text(text: &str, terminal_width: usize) -> String {
1308///     let options = WrapOptions::builder()
1309///         .hard_wrap(false)  // Keep words intact for readability
1310///         .trim_whitespace(true)  // Clean formatting
1311///         .build();
1312///     
1313///     wrap_ansi(text, terminal_width.saturating_sub(4), Some(options))
1314/// }
1315/// ```
1316///
1317/// ### Code Syntax Highlighting
1318/// ```rust
1319/// use wrap_ansi::wrap_ansi;
1320///
1321/// // Preserve syntax highlighting colors when wrapping code
1322/// let highlighted_code = "\u{001B}[34mfunction\u{001B}[39m \u{001B}[33mhello\u{001B}[39m() { \u{001B}[32m'world'\u{001B}[39m }";
1323/// let wrapped = wrap_ansi(highlighted_code, 25, None);
1324/// // Colors are preserved on each line
1325/// ```
1326///
1327/// # 🎨 ANSI Sequence Support
1328///
1329/// | Type | Codes | Description |
1330/// |------|-------|-------------|
1331/// | **Foreground Colors** | 30-37, 90-97 | Standard and bright text colors |
1332/// | **Background Colors** | 40-47, 100-107 | Standard and bright background colors |
1333/// | **Text Styles** | 1, 3, 4, 9 | Bold, italic, underline, strikethrough |
1334/// | **Color Resets** | 39, 49, 0 | Foreground, background, and full reset |
1335/// | **Hyperlinks** | OSC 8 | Clickable terminal links (OSC 8 sequences) |
1336/// | **Custom SGR** | Any | Support for any Select Graphic Rendition codes |
1337///
1338/// When text with ANSI codes is wrapped across multiple lines, each line gets
1339/// complete escape sequences (opening codes at the start, closing codes at the end).
1340///
1341/// # 🌍 Unicode Support Details
1342///
1343/// | Feature | Support | Description |
1344/// |---------|---------|-------------|
1345/// | **Fullwidth Characters** | βœ… | CJK characters counted as 2 columns |
1346/// | **Surrogate Pairs** | βœ… | Proper handling of 4-byte Unicode sequences |
1347/// | **Combining Characters** | βœ… | Don't add to visual width |
1348/// | **Emoji** | βœ… | Correct width calculation for emoji sequences |
1349/// | **RTL Text** | ⚠️ | Basic support (visual width only) |
1350///
1351/// # ⚑ Performance Characteristics
1352///
1353/// - **Time Complexity**: O(n) where n is input length
1354/// - **Space Complexity**: O(n) for output string
1355/// - **Regex Compilation**: Pre-compiled patterns for optimal performance
1356/// - **Memory Usage**: Efficient string operations with capacity pre-allocation
1357/// - **ANSI Parsing**: Optimized state machine for escape sequence detection
1358///
1359/// # πŸ”§ Performance Tips
1360///
1361/// ```rust
1362/// use wrap_ansi::{wrap_ansi, WrapOptions};
1363///
1364/// // For maximum performance with trusted input:
1365/// let fast_options = WrapOptions {
1366///     trim: false,      // Skip trimming operations
1367///     hard: false,      // Avoid complex word breaking
1368///     word_wrap: true,  // Use efficient word boundary detection
1369/// };
1370///
1371/// // For memory-constrained environments, process in chunks:
1372/// fn wrap_large_text(text: &str, columns: usize) -> String {
1373///     text.lines()
1374///         .map(|line| wrap_ansi(line, columns, None))
1375///         .collect::<Vec<_>>()
1376///         .join("\n")
1377/// }
1378/// ```
1379///
1380/// # 🚨 Important Notes
1381///
1382/// - **Column Width**: Must be > 0 (use [`wrap_ansi_checked`] for validation)
1383/// - **Input Size**: No built-in limits (use [`wrap_ansi_checked`] for protection)
1384/// - **Line Endings**: `\r\n` is normalized to `\n` automatically
1385/// - **ANSI Sequences**: Malformed sequences are passed through unchanged
1386///
1387/// # See Also
1388///
1389/// - [`wrap_ansi_checked`]: Safe version with input validation and error handling
1390/// - [`WrapOptions`]: Detailed configuration options
1391/// - [`WrapOptionsBuilder`]: Fluent interface for creating options
1392/// - [`WrapError`]: Error types for the checked version
1393#[must_use]
1394pub fn wrap_ansi(string: &str, columns: usize, options: Option<WrapOptions>) -> String {
1395    let opts = options.unwrap_or_default();
1396
1397    string
1398        .replace("\r\n", "\n")
1399        .split('\n')
1400        .map(|line| wrap_text_preserving_ansi_sequences(line, columns, opts))
1401        .collect::<Vec<_>>()
1402        .join("\n")
1403}
1404
1405#[cfg(test)]
1406mod tests;