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}