1 | //! This module contains a different functions which are used by the [`Grid`]. |
2 | //! |
3 | //! You should use it if you want to comply with how [`Grid`]. |
4 | //! |
5 | //! [`Grid`]: crate::grid::iterable::Grid |
6 | |
7 | /// Returns string width and count lines of a string. It's a combination of [`string_width_multiline`] and [`count_lines`]. |
8 | #[cfg (feature = "std" )] |
9 | pub fn string_dimension(text: &str) -> (usize, usize) { |
10 | #[cfg (not(feature = "color" ))] |
11 | { |
12 | let (lines: usize, acc: usize, max: usize) = text.chars().fold((1, 0, 0), |(lines: usize, acc: usize, max: usize), c: char| { |
13 | if c == ' \n' { |
14 | (lines + 1, 0, acc.max(max)) |
15 | } else { |
16 | let w = unicode_width::UnicodeWidthChar::width(self:c).unwrap_or(0); |
17 | (lines, acc + w, max) |
18 | } |
19 | }); |
20 | |
21 | (lines, acc.max(max)) |
22 | } |
23 | |
24 | #[cfg (feature = "color" )] |
25 | { |
26 | get_lines(text) |
27 | .map(|line| string_width(&line)) |
28 | .fold((0, 0), |(i, acc), width| (i + 1, acc.max(width))) |
29 | } |
30 | } |
31 | |
32 | /// Returns a string width. |
33 | pub fn string_width(text: &str) -> usize { |
34 | #[cfg (not(feature = "color" ))] |
35 | { |
36 | unicode_width::UnicodeWidthStr::width(self:text) |
37 | } |
38 | |
39 | #[cfg (feature = "color" )] |
40 | { |
41 | // we need to strip ansi because of terminal links |
42 | // and they're can't be stripped by ansi_str. |
43 | |
44 | ansitok::parse_ansi(text) |
45 | .filter(|e| e.kind() == ansitok::ElementKind::Text) |
46 | .map(|e| &text[e.start()..e.end()]) |
47 | .map(unicode_width::UnicodeWidthStr::width) |
48 | .sum() |
49 | } |
50 | } |
51 | |
52 | /// Returns a max string width of a line. |
53 | pub fn string_width_multiline(text: &str) -> usize { |
54 | #[cfg (not(feature = "color" ))] |
55 | { |
56 | text.lines() |
57 | .map(unicode_width::UnicodeWidthStr::width) |
58 | .max() |
59 | .unwrap_or(default:0) |
60 | } |
61 | |
62 | #[cfg (feature = "color" )] |
63 | { |
64 | text.lines().map(string_width).max().unwrap_or(0) |
65 | } |
66 | } |
67 | |
68 | /// Calculates a number of lines. |
69 | pub fn count_lines(s: &str) -> usize { |
70 | if s.is_empty() { |
71 | return 1; |
72 | } |
73 | |
74 | bytecount::count(haystack:s.as_bytes(), needle:b' \n' ) + 1 |
75 | } |
76 | |
77 | /// Returns a list of tabs (`\t`) in a string.. |
78 | pub fn count_tabs(s: &str) -> usize { |
79 | bytecount::count(haystack:s.as_bytes(), needle:b' \t' ) |
80 | } |
81 | |
82 | /// Splits the string by lines. |
83 | #[cfg (feature = "std" )] |
84 | pub fn get_lines(text: &str) -> Lines<'_> { |
85 | #[cfg (not(feature = "color" ))] |
86 | { |
87 | // we call `split()` but not `lines()` in order to match colored implementation |
88 | // specifically how we treat a trailing '\n' character. |
89 | Lines { |
90 | inner: text.split(' \n' ), |
91 | } |
92 | } |
93 | |
94 | #[cfg (feature = "color" )] |
95 | { |
96 | Lines { |
97 | inner: ansi_str::AnsiStr::ansi_split(text, " \n" ), |
98 | } |
99 | } |
100 | } |
101 | |
102 | /// Iterator over lines. |
103 | /// |
104 | /// In comparison to `std::str::Lines`, it treats trailing '\n' as a new line. |
105 | #[allow (missing_debug_implementations)] |
106 | #[cfg (feature = "std" )] |
107 | pub struct Lines<'a> { |
108 | #[cfg (not(feature = "color" ))] |
109 | inner: std::str::Split<'a, char>, |
110 | #[cfg (feature = "color" )] |
111 | inner: ansi_str::AnsiSplit<'a>, |
112 | } |
113 | #[cfg (feature = "std" )] |
114 | impl<'a> Iterator for Lines<'a> { |
115 | type Item = std::borrow::Cow<'a, str>; |
116 | |
117 | fn next(&mut self) -> Option<Self::Item> { |
118 | #[cfg (not(feature = "color" ))] |
119 | { |
120 | self.inner.next().map(std::borrow::Cow::Borrowed) |
121 | } |
122 | |
123 | #[cfg (feature = "color" )] |
124 | { |
125 | self.inner.next() |
126 | } |
127 | } |
128 | } |
129 | |
130 | #[cfg (feature = "std" )] |
131 | /// Replaces tabs in a string with a given width of spaces. |
132 | pub fn replace_tab(text: &str, n: usize) -> std::borrow::Cow<'_, str> { |
133 | if !text.contains(' \t' ) { |
134 | return std::borrow::Cow::Borrowed(text); |
135 | } |
136 | |
137 | // it's a general case which probably must be faster? |
138 | let replaced: String = if n == 4 { |
139 | text.replace(from:' \t' , to:" " ) |
140 | } else { |
141 | let mut text: String = text.to_owned(); |
142 | replace_tab_range(&mut text, n); |
143 | text |
144 | }; |
145 | |
146 | std::borrow::Cow::Owned(replaced) |
147 | } |
148 | |
149 | #[cfg (feature = "std" )] |
150 | fn replace_tab_range(cell: &mut String, n: usize) -> &str { |
151 | let mut skip = 0; |
152 | while let &Some(pos) = &cell[skip..].find(' \t' ) { |
153 | let pos = skip + pos; |
154 | |
155 | let is_escaped = pos > 0 && cell.get(pos - 1..pos) == Some(" \\" ); |
156 | if is_escaped { |
157 | skip = pos + 1; |
158 | } else if n == 0 { |
159 | cell.remove(pos); |
160 | skip = pos; |
161 | } else { |
162 | // I'am not sure which version is faster a loop of 'replace' |
163 | // or allacation of a string for replacement; |
164 | cell.replace_range(pos..=pos, &" " .repeat(n)); |
165 | skip = pos + 1; |
166 | } |
167 | |
168 | if cell.is_empty() || skip >= cell.len() { |
169 | break; |
170 | } |
171 | } |
172 | |
173 | cell |
174 | } |
175 | |
176 | #[cfg (test)] |
177 | mod tests { |
178 | use super::*; |
179 | |
180 | #[test ] |
181 | fn string_width_emojie_test() { |
182 | // ...emojis such as “joy”, which normally take up two columns when printed in a terminal |
183 | // https://github.com/mgeisler/textwrap/pull/276 |
184 | assert_eq!(string_width("🎩" ), 2); |
185 | assert_eq!(string_width("Rust 💕" ), 7); |
186 | assert_eq!(string_width_multiline("Go 👍 \nC 😎" ), 5); |
187 | } |
188 | |
189 | #[cfg (feature = "color" )] |
190 | #[test ] |
191 | fn colored_string_width_test() { |
192 | use owo_colors::OwoColorize; |
193 | assert_eq!(string_width(&"hello world" .red().to_string()), 11); |
194 | assert_eq!( |
195 | string_width_multiline(&"hello \nworld" .blue().to_string()), |
196 | 5 |
197 | ); |
198 | assert_eq!(string_width(" \u{1b}[34m0 \u{1b}[0m" ), 1); |
199 | assert_eq!(string_width(&"0" .red().to_string()), 1); |
200 | } |
201 | |
202 | #[test ] |
203 | fn count_lines_test() { |
204 | assert_eq!( |
205 | count_lines(" \u{1b}[37mnow is the time for all good men \n\u{1b}[0m" ), |
206 | 2 |
207 | ); |
208 | assert_eq!(count_lines("now is the time for all good men \n" ), 2); |
209 | } |
210 | |
211 | #[cfg (feature = "color" )] |
212 | #[test ] |
213 | fn string_width_multinline_for_link() { |
214 | assert_eq!( |
215 | string_width_multiline( |
216 | " \u{1b}]8;;file:///home/nushell/asd.zip \u{1b}\\asd.zip \u{1b}]8;; \u{1b}\\" |
217 | ), |
218 | 7 |
219 | ); |
220 | } |
221 | |
222 | #[cfg (feature = "color" )] |
223 | #[test ] |
224 | fn string_width_for_link() { |
225 | assert_eq!( |
226 | string_width(" \u{1b}]8;;file:///home/nushell/asd.zip \u{1b}\\asd.zip \u{1b}]8;; \u{1b}\\" ), |
227 | 7 |
228 | ); |
229 | } |
230 | |
231 | #[cfg (feature = "std" )] |
232 | #[test ] |
233 | fn string_dimension_test() { |
234 | assert_eq!( |
235 | string_dimension(" \u{1b}[37mnow is the time for all good men \n\u{1b}[0m" ), |
236 | { |
237 | #[cfg (feature = "color" )] |
238 | { |
239 | (2, 32) |
240 | } |
241 | #[cfg (not(feature = "color" ))] |
242 | { |
243 | (2, 36) |
244 | } |
245 | } |
246 | ); |
247 | assert_eq!( |
248 | string_dimension("now is the time for all good men \n" ), |
249 | (2, 32) |
250 | ); |
251 | assert_eq!(string_dimension("asd" ), (1, 3)); |
252 | assert_eq!(string_dimension("" ), (1, 0)); |
253 | } |
254 | |
255 | #[cfg (feature = "std" )] |
256 | #[test ] |
257 | fn replace_tab_test() { |
258 | assert_eq!(replace_tab("123 \t\tabc \t" , 3), "123 abc " ); |
259 | |
260 | assert_eq!(replace_tab(" \t" , 0), "" ); |
261 | assert_eq!(replace_tab(" \t" , 3), " " ); |
262 | assert_eq!(replace_tab("123 \tabc" , 3), "123 abc" ); |
263 | assert_eq!(replace_tab("123 \tabc \tzxc" , 0), "123abczxc" ); |
264 | |
265 | assert_eq!(replace_tab(" \\t" , 0), " \\t" ); |
266 | assert_eq!(replace_tab(" \\t" , 4), " \\t" ); |
267 | assert_eq!(replace_tab("123 \\tabc" , 0), "123 \\tabc" ); |
268 | assert_eq!(replace_tab("123 \\tabc" , 4), "123 \\tabc" ); |
269 | } |
270 | } |
271 | |