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")]
9pub 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.
33pub 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.
53pub 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.
69pub 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..
78pub 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")]
84pub 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")]
107pub 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")]
114impl<'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.
132pub 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")]
150fn 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)]
177mod 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