wrap_ansi/lib.rs
1//! # π¨ wrap-ansi
2//!
3//! [](https://crates.io/crates/wrap-ansi)
4//! [](https://docs.rs/wrap-ansi)
5//! [](LICENSE)
6//! [](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(¤t_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;