string_overlap/
lib.rs

1//! Overlap text.
2//!
3//! For overlapping purposes, whitespace characters in the foreground are
4//! treated as "invisible," and the character from the background will be
5//! used instead.
6//!
7//! *__NOTE__ Final newlines are inserted.*
8//!
9//! # Example
10//!
11//! ```rust
12//! use string_overlap::overlap;
13//!
14//! let background = "\
15//! ...
16//! ...
17//! ...";
18//! let foreground = "\
19//! foo
20//!   o
21//!   f";
22//!
23//! assert_eq!(overlap(background, foreground), "\
24//! foo
25//! ..o
26//! ..f\n");
27//! ```
28//!
29//! # Features
30//!
31//! ## Default
32//!
33//! ### `colored`
34//!
35//! Allows overlapping of `ColoredString`s from the [colored] crate.
36//!
37//! [colored]: https://crates.io/crates/colored
38#[cfg(feature = "colored")]
39use colored_crate::ColoredString;
40
41use itertools::EitherOrBoth::{Both, Left, Right};
42use itertools::Itertools;
43use std::fmt::Display;
44
45/// Places `foreground` "on top of" `background`.
46pub fn overlap<B, F>(background: B, foreground: F) -> String
47where
48    B: Display,
49    F: Display,
50{
51    let background = background.to_string();
52    let foreground = foreground.to_string();
53    let background = background.lines();
54    let foreground = foreground.lines();
55
56    background
57        .zip_longest(foreground)
58        .map(|lines| {
59            let combined_line: String = match lines {
60                Both(b_line, f_line) => {
61                    let b_chars = b_line.chars();
62                    let f_chars = f_line.chars();
63
64                    let combined_line: String = b_chars
65                        .zip_longest(f_chars)
66                        .map(|chars| match chars {
67                            Both(b_char, f_char) if f_char.is_whitespace() => b_char,
68                            Both(_, f_char) => f_char,
69                            Left(b_char) => b_char,
70                            Right(f_char) => f_char,
71                        })
72                        .collect();
73                    combined_line
74                }
75                Left(b_line) => b_line.to_string(),
76                Right(f_line) => f_line.to_string(),
77            };
78            format!("{}\n", combined_line)
79        })
80        .collect()
81}
82
83/// Overlap `ColoredString`s.
84///
85/// # Example
86///
87/// ```rust
88/// # extern crate colored_crate as colored;
89/// use colored::Colorize;
90/// use string_overlap::overlap_colored;
91///
92/// let background = "\
93/// ...
94/// ...
95/// ...".red();
96/// let foreground = "\
97/// foo
98///   o
99///   f".blue();
100///
101/// assert_eq!(
102///     overlap_colored(background, foreground),
103///     format!(
104///         "{line1}\n{line2}\n{line3}\n",
105///         line1="foo".blue(),
106///         line2=format!("{}{}", "..".red(), "o".blue()),
107///         line3=format!("{}{}", "..".red(), "f".blue()),
108///     ),
109/// );
110/// ```
111#[cfg(feature = "colored")]
112pub fn overlap_colored(background: ColoredString, foreground: ColoredString) -> String {
113    use colored_crate::Colorize;
114    use lazy_static::lazy_static;
115    use regex::Regex;
116
117    lazy_static! {
118        static ref NOT_WHITESPACE: Regex = Regex::new(r"[^\s]+").unwrap();
119    }
120
121    let background_bg = background.bgcolor();
122    let background_fg = background.fgcolor();
123    let _background_style = background.style();
124    let foreground_bg = foreground.bgcolor();
125    let foreground_fg = foreground.fgcolor();
126    let _foreground_style = foreground.style();
127
128    let background = background.lines();
129    let foreground = foreground.lines();
130
131    background
132        .zip_longest(foreground)
133        .map(|lines| {
134            let combined_line: String = match lines {
135                Both(b_line, f_line) => {
136                    let fg_matches = NOT_WHITESPACE.find_iter(f_line);
137
138                    let mut background_left_boundary = 0;
139                    let mut line = String::new();
140
141                    for m in fg_matches {
142                        let b_left = b_line.get(background_left_boundary..m.start());
143                        background_left_boundary = m.end();
144                        match b_left {
145                            Some(s) => {
146                                if s.len() > 0 {
147                                    let s = match background_fg {
148                                        Some(color) => s.color(color),
149                                        None => s.into(),
150                                    };
151                                    let s = match background_bg {
152                                        Some(color) => s.on_color(color),
153                                        None => s,
154                                    };
155                                    line.push_str(&s.to_string());
156                                }
157                            }
158                            None => {}
159                        }
160                        let fg = m.as_str();
161                        let fg = match foreground_fg {
162                            Some(color) => fg.color(color),
163                            None => fg.into(),
164                        };
165                        let fg = match foreground_bg {
166                            Some(color) => fg.on_color(color),
167                            None => fg,
168                        };
169                        line.push_str(&fg.to_string());
170                    }
171
172                    let final_part = b_line.get(background_left_boundary..);
173                    match final_part {
174                        Some(s) => {
175                            if s.len() > 0 {
176                                let s = match background_fg {
177                                    Some(color) => s.color(color),
178                                    None => s.into(),
179                                };
180                                let s = match background_bg {
181                                    Some(color) => s.on_color(color),
182                                    None => s,
183                                };
184                                line.push_str(&s.to_string());
185                            }
186                        }
187                        None => {}
188                    }
189                    line.push('\n');
190                    line
191                }
192                Left(b_line) => {
193                    let b_line = match background_fg {
194                        Some(color) => b_line.color(color),
195                        None => b_line.into(),
196                    };
197                    let b_line = match background_bg {
198                        Some(color) => b_line.on_color(color),
199                        None => b_line,
200                    };
201                    b_line.to_string()
202                }
203                Right(f_line) => {
204                    let f_line = match foreground_fg {
205                        Some(color) => f_line.color(color),
206                        None => f_line.into(),
207                    };
208                    let f_line = match foreground_bg {
209                        Some(color) => f_line.on_color(color),
210                        None => f_line,
211                    };
212                    f_line.to_string()
213                }
214            };
215            combined_line
216        })
217        .collect()
218}