1 | use std::borrow::Cow; |
2 | |
3 | use crate::{ |
4 | grid::config::SpannedConfig, grid::dimension::SpannedGridDimension, grid::records::Records, |
5 | }; |
6 | |
7 | pub(crate) fn get_table_widths<R: Records>(records: R, cfg: &SpannedConfig) -> Vec<usize> { |
8 | SpannedGridDimension::width(records, cfg) |
9 | } |
10 | |
11 | pub(crate) fn get_table_widths_with_total<R: Records>( |
12 | records: R, |
13 | cfg: &SpannedConfig, |
14 | ) -> (Vec<usize>, usize) { |
15 | let widths: Vec = SpannedGridDimension::width(records, cfg); |
16 | let total_width: usize = get_table_total_width(&widths, cfg); |
17 | (widths, total_width) |
18 | } |
19 | |
20 | fn get_table_total_width(list: &[usize], cfg: &SpannedConfig) -> usize { |
21 | let margin: Sides = cfg.get_margin(); |
22 | list.iter().sum::<usize>() |
23 | + cfg.count_vertical(count_columns:list.len()) |
24 | + margin.left.size |
25 | + margin.right.size |
26 | } |
27 | |
28 | /// The function cuts the string to a specific width. |
29 | /// |
30 | /// BE AWARE: width is expected to be in bytes. |
31 | pub(crate) fn cut_str(s: &str, width: usize) -> Cow<'_, str> { |
32 | #[cfg (feature = "color" )] |
33 | { |
34 | const REPLACEMENT: char = ' \u{FFFD}' ; |
35 | |
36 | let stripped = ansi_str::AnsiStr::ansi_strip(s); |
37 | let (length, count_unknowns, _) = split_at_pos(&stripped, width); |
38 | |
39 | let mut buf = ansi_str::AnsiStr::ansi_cut(s, ..length); |
40 | if count_unknowns > 0 { |
41 | let mut b = buf.into_owned(); |
42 | b.extend(std::iter::repeat(REPLACEMENT).take(count_unknowns)); |
43 | buf = Cow::Owned(b); |
44 | } |
45 | |
46 | buf |
47 | } |
48 | |
49 | #[cfg (not(feature = "color" ))] |
50 | { |
51 | cut_str_basic(s, width) |
52 | } |
53 | } |
54 | |
55 | /// The function cuts the string to a specific width. |
56 | /// |
57 | /// BE AWARE: width is expected to be in bytes. |
58 | #[cfg (not(feature = "color" ))] |
59 | pub(crate) fn cut_str_basic(s: &str, width: usize) -> Cow<'_, str> { |
60 | const REPLACEMENT: char = ' \u{FFFD}' ; |
61 | |
62 | let (length: usize, count_unknowns: usize, _) = split_at_pos(s, pos:width); |
63 | let buf: &str = &s[..length]; |
64 | if count_unknowns == 0 { |
65 | return Cow::Borrowed(buf); |
66 | } |
67 | |
68 | let mut buf: String = buf.to_owned(); |
69 | buf.extend(iter:std::iter::repeat(REPLACEMENT).take(count_unknowns)); |
70 | |
71 | Cow::Owned(buf) |
72 | } |
73 | |
74 | /// The function splits a string in the position and |
75 | /// returns a exact number of bytes before the position and in case of a split in an unicode grapheme |
76 | /// a width of a character which was tried to be splited in. |
77 | /// |
78 | /// BE AWARE: pos is expected to be in bytes. |
79 | pub(crate) fn split_at_pos(s: &str, pos: usize) -> (usize, usize, usize) { |
80 | let mut length: usize = 0; |
81 | let mut i: usize = 0; |
82 | for c: char in s.chars() { |
83 | if i == pos { |
84 | break; |
85 | }; |
86 | |
87 | let c_width = unicode_width::UnicodeWidthChar::width(self:c).unwrap_or_default(); |
88 | |
89 | // We cut the chars which takes more then 1 symbol to display, |
90 | // in order to archive the necessary width. |
91 | if i + c_width > pos { |
92 | let count: usize = pos - i; |
93 | return (length, count, c.len_utf8()); |
94 | } |
95 | |
96 | i += c_width; |
97 | length += c.len_utf8(); |
98 | } |
99 | |
100 | (length, 0, 0) |
101 | } |
102 | |
103 | /// Strip OSC codes from `s`. If `s` is a single OSC8 hyperlink, with no other text, then return |
104 | /// (s_with_all_hyperlinks_removed, Some(url)). If `s` does not meet this description, then return |
105 | /// (s_with_all_hyperlinks_removed, None). Any ANSI color sequences in `s` will be retained. See |
106 | /// <https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda> |
107 | /// |
108 | /// The function is based on Dan Davison <https://github.com/dandavison> delta <https://github.com/dandavison/delta> ansi library. |
109 | #[cfg (feature = "color" )] |
110 | pub(crate) fn strip_osc(text: &str) -> (String, Option<String>) { |
111 | #[derive (Debug)] |
112 | enum ExtractOsc8HyperlinkState { |
113 | ExpectOsc8Url, |
114 | ExpectFirstText, |
115 | ExpectMoreTextOrTerminator, |
116 | SeenOneHyperlink, |
117 | WillNotReturnUrl, |
118 | } |
119 | |
120 | use ExtractOsc8HyperlinkState::*; |
121 | |
122 | let mut url = None; |
123 | let mut state = ExpectOsc8Url; |
124 | let mut buf = String::with_capacity(text.len()); |
125 | |
126 | for el in ansitok::parse_ansi(text) { |
127 | match el.kind() { |
128 | ansitok::ElementKind::Osc => match state { |
129 | ExpectOsc8Url => { |
130 | url = Some(&text[el.start()..el.end()]); |
131 | state = ExpectFirstText; |
132 | } |
133 | ExpectMoreTextOrTerminator => state = SeenOneHyperlink, |
134 | _ => state = WillNotReturnUrl, |
135 | }, |
136 | ansitok::ElementKind::Sgr => buf.push_str(&text[el.start()..el.end()]), |
137 | ansitok::ElementKind::Csi => buf.push_str(&text[el.start()..el.end()]), |
138 | ansitok::ElementKind::Esc => {} |
139 | ansitok::ElementKind::Text => { |
140 | buf.push_str(&text[el.start()..el.end()]); |
141 | match state { |
142 | ExpectFirstText => state = ExpectMoreTextOrTerminator, |
143 | ExpectMoreTextOrTerminator => {} |
144 | _ => state = WillNotReturnUrl, |
145 | } |
146 | } |
147 | } |
148 | } |
149 | |
150 | match state { |
151 | WillNotReturnUrl => (buf, None), |
152 | _ => { |
153 | let url = url.and_then(|s| { |
154 | s.strip_prefix(" \x1b]8;;" ) |
155 | .and_then(|s| s.strip_suffix(' \x1b' )) |
156 | }); |
157 | if let Some(url) = url { |
158 | (buf, Some(url.to_string())) |
159 | } else { |
160 | (buf, None) |
161 | } |
162 | } |
163 | } |
164 | } |
165 | |
166 | #[cfg (test)] |
167 | mod tests { |
168 | use super::*; |
169 | |
170 | use crate::grid::util::string::string_width; |
171 | |
172 | #[cfg (feature = "color" )] |
173 | use owo_colors::{colors::Yellow, OwoColorize}; |
174 | |
175 | #[test ] |
176 | fn strip_test() { |
177 | assert_eq!(cut_str("123456" , 0), "" ); |
178 | assert_eq!(cut_str("123456" , 3), "123" ); |
179 | assert_eq!(cut_str("123456" , 10), "123456" ); |
180 | |
181 | assert_eq!(cut_str("a week ago" , 4), "a we" ); |
182 | |
183 | assert_eq!(cut_str("😳😳😳😳😳" , 0), "" ); |
184 | assert_eq!(cut_str("😳😳😳😳😳" , 3), "😳�" ); |
185 | assert_eq!(cut_str("😳😳😳😳😳" , 4), "😳😳" ); |
186 | assert_eq!(cut_str("😳😳😳😳😳" , 20), "😳😳😳😳😳" ); |
187 | |
188 | assert_eq!(cut_str("🏳️🏳️" , 0), "" ); |
189 | assert_eq!(cut_str("🏳️🏳️" , 1), "🏳" ); |
190 | assert_eq!(cut_str("🏳️🏳️" , 2), "🏳 \u{fe0f}🏳" ); |
191 | assert_eq!(string_width("🏳️🏳️" ), string_width("🏳 \u{fe0f}🏳" )); |
192 | |
193 | assert_eq!(cut_str("🎓" , 1), "�" ); |
194 | assert_eq!(cut_str("🎓" , 2), "🎓" ); |
195 | |
196 | assert_eq!(cut_str("🥿" , 1), "�" ); |
197 | assert_eq!(cut_str("🥿" , 2), "🥿" ); |
198 | |
199 | assert_eq!(cut_str("🩰" , 1), "�" ); |
200 | assert_eq!(cut_str("🩰" , 2), "🩰" ); |
201 | |
202 | assert_eq!(cut_str("👍🏿" , 1), "�" ); |
203 | assert_eq!(cut_str("👍🏿" , 2), "👍" ); |
204 | assert_eq!(cut_str("👍🏿" , 3), "👍�" ); |
205 | assert_eq!(cut_str("👍🏿" , 4), "👍🏿" ); |
206 | |
207 | assert_eq!(cut_str("🇻🇬" , 1), "🇻" ); |
208 | assert_eq!(cut_str("🇻🇬" , 2), "🇻🇬" ); |
209 | assert_eq!(cut_str("🇻🇬" , 3), "🇻🇬" ); |
210 | assert_eq!(cut_str("🇻🇬" , 4), "🇻🇬" ); |
211 | } |
212 | |
213 | #[cfg (feature = "color" )] |
214 | #[test ] |
215 | fn strip_color_test() { |
216 | let numbers = "123456" .red().on_bright_black().to_string(); |
217 | |
218 | assert_eq!(cut_str(&numbers, 0), " \u{1b}[31;100m \u{1b}[39m \u{1b}[49m" ); |
219 | assert_eq!( |
220 | cut_str(&numbers, 3), |
221 | " \u{1b}[31;100m123 \u{1b}[39m \u{1b}[49m" |
222 | ); |
223 | assert_eq!(cut_str(&numbers, 10), " \u{1b}[31;100m123456 \u{1b}[0m" ); |
224 | |
225 | let emojies = "😳😳😳😳😳" .red().on_bright_black().to_string(); |
226 | |
227 | assert_eq!(cut_str(&emojies, 0), " \u{1b}[31;100m \u{1b}[39m \u{1b}[49m" ); |
228 | assert_eq!( |
229 | cut_str(&emojies, 3), |
230 | " \u{1b}[31;100m😳 \u{1b}[39m \u{1b}[49m�" |
231 | ); |
232 | assert_eq!( |
233 | cut_str(&emojies, 4), |
234 | " \u{1b}[31;100m😳😳 \u{1b}[39m \u{1b}[49m" |
235 | ); |
236 | assert_eq!(cut_str(&emojies, 20), " \u{1b}[31;100m😳😳😳😳😳 \u{1b}[0m" ); |
237 | |
238 | let emojies = "🏳️🏳️" .red().on_bright_black().to_string(); |
239 | |
240 | assert_eq!(cut_str(&emojies, 0), " \u{1b}[31;100m \u{1b}[39m \u{1b}[49m" ); |
241 | assert_eq!(cut_str(&emojies, 1), " \u{1b}[31;100m🏳 \u{1b}[39m \u{1b}[49m" ); |
242 | assert_eq!( |
243 | cut_str(&emojies, 2), |
244 | " \u{1b}[31;100m🏳 \u{fe0f}🏳 \u{1b}[39m \u{1b}[49m" |
245 | ); |
246 | assert_eq!( |
247 | string_width(&emojies), |
248 | string_width(" \u{1b}[31;100m🏳 \u{fe0f}🏳 \u{1b}[39m \u{1b}[49m" ) |
249 | ); |
250 | } |
251 | |
252 | #[test ] |
253 | #[cfg (feature = "color" )] |
254 | fn test_color_strip() { |
255 | let s = "Collored string" |
256 | .fg::<Yellow>() |
257 | .on_truecolor(12, 200, 100) |
258 | .blink() |
259 | .to_string(); |
260 | assert_eq!( |
261 | cut_str(&s, 1), |
262 | " \u{1b}[5m \u{1b}[48;2;12;200;100m \u{1b}[33mC \u{1b}[25m \u{1b}[39m \u{1b}[49m" |
263 | ) |
264 | } |
265 | } |
266 | |