1use std::borrow::Cow;
2
3use crate::{
4 grid::config::SpannedConfig, grid::dimension::SpannedGridDimension, grid::records::Records,
5};
6
7pub(crate) fn get_table_widths<R: Records>(records: R, cfg: &SpannedConfig) -> Vec<usize> {
8 SpannedGridDimension::width(records, cfg)
9}
10
11pub(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
20fn 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.
31pub(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"))]
59pub(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.
79pub(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")]
110pub(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)]
167mod 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