1use bstr::ByteSlice;
5use owo_colors::Style;
6
7pub fn highlight_end(slice: &[u8]) -> usize {
11 let mut iter = slice.find_iter(b"\n");
13 match iter.next() {
14 Some(_) => {
15 match iter.next() {
16 Some(second) => second,
17 None => slice.len(),
19 }
20 }
21 None => slice.len(),
23 }
24}
25
26#[derive(Debug, Default, Clone)]
27pub(super) struct Styles {
28 pub(super) is_colorized: bool,
29 pub(super) count: Style,
30 pub(super) pass: Style,
31 pub(super) retry: Style,
32 pub(super) fail: Style,
33 pub(super) skip: Style,
34 pub(super) script_id: Style,
35 pub(super) list_styles: crate::list::Styles,
36}
37
38impl Styles {
39 pub(super) fn colorize(&mut self) {
40 self.is_colorized = true;
41 self.count = Style::new().bold();
42 self.pass = Style::new().green().bold();
43 self.retry = Style::new().magenta().bold();
44 self.fail = Style::new().red().bold();
45 self.skip = Style::new().yellow().bold();
46 self.script_id = Style::new().blue().bold();
47 self.list_styles.colorize();
48 }
49}
50
51pub(crate) const fn floor_char_boundary(s: &str, index: usize) -> usize {
54 if index >= s.len() {
55 s.len()
56 } else {
57 let mut i = index;
58 while i > 0 {
59 if is_utf8_char_boundary(s.as_bytes()[i]) {
60 break;
61 }
62 i -= 1;
63 }
64
65 debug_assert!(i >= index.saturating_sub(3));
67
68 i
69 }
70}
71
72#[inline]
73const fn is_utf8_char_boundary(b: u8) -> bool {
74 (b as i8) >= -0x40
76}
77
78pub(crate) fn print_lines_in_chunks(
92 text: &str,
93 max_chunk_bytes: usize,
94 mut callback: impl FnMut(&str),
95) {
96 let mut remaining = text;
97 while !remaining.is_empty() {
98 let max = floor_char_boundary(remaining, max_chunk_bytes);
101
102 let last_newline = remaining[..max].rfind('\n');
104
105 if let Some(index) = last_newline {
106 callback(&remaining[..=index]);
108 remaining = &remaining[index + 1..];
109 } else {
110 let next_newline = remaining[max..].find('\n');
112
113 if let Some(index) = next_newline {
114 callback(&remaining[..=index + max]);
116 remaining = &remaining[index + max + 1..];
117 } else {
118 callback(remaining);
120 remaining = "";
121 }
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_highlight_end() {
132 let tests: &[(&str, usize)] = &[
133 ("", 0),
134 ("\n", 1),
135 ("foo", 3),
136 ("foo\n", 4),
137 ("foo\nbar", 7),
138 ("foo\nbar\n", 7),
139 ("foo\nbar\nbaz", 7),
140 ("foo\nbar\nbaz\n", 7),
141 ("\nfoo\nbar\nbaz", 4),
142 ];
143
144 for (input, output) in tests {
145 assert_eq!(
146 highlight_end(input.as_bytes()),
147 *output,
148 "for input {input:?}"
149 );
150 }
151 }
152
153 #[test]
154 fn test_print_lines_in_chunks() {
155 let tests: &[(&str, &str, usize, &[&str])] = &[
156 ("empty string", "", 1024, &[]),
158 ("single line no newline", "hello", 1024, &["hello"]),
159 ("single line with newline", "hello\n", 1024, &["hello\n"]),
160 (
161 "multiple lines small",
162 "line1\nline2\nline3\n",
163 1024,
164 &["line1\nline2\nline3\n"],
165 ),
166 (
167 "breaks at newline",
168 "line1\nline2\nline3\n",
169 10,
170 &["line1\n", "line2\n", "line3\n"],
171 ),
172 (
173 "multiple lines per chunk",
174 "line1\nline2\nline3\n",
175 13,
176 &["line1\nline2\n", "line3\n"],
177 ),
178 (
179 "long line with newline",
180 &format!("{}\n", "a".repeat(2000)),
181 1024,
182 &[&format!("{}\n", "a".repeat(2000))],
183 ),
184 (
185 "long line no newline",
186 &"a".repeat(2000),
187 1024,
188 &[&"a".repeat(2000)],
189 ),
190 ("exact boundary", "123456789\n", 10, &["123456789\n"]),
191 (
192 "newline at boundary",
193 "12345678\nabcdefgh\n",
194 10,
195 &["12345678\n", "abcdefgh\n"],
196 ),
197 (
198 "utf8 emoji",
199 "hello๐\nworld\n",
200 10,
201 &["hello๐\n", "world\n"],
202 ),
203 (
204 "utf8 near boundary",
205 "1234๐\nabcd\n",
206 7,
207 &["1234๐\n", "abcd\n"],
208 ),
209 (
210 "consecutive newlines",
211 "line1\n\n\nline2\n",
212 10,
213 &["line1\n\n\n", "line2\n"],
214 ),
215 (
216 "no trailing newline",
217 "line1\nline2\nline3",
218 10,
219 &["line1\n", "line2\n", "line3"],
220 ),
221 (
222 "mixed lengths",
223 "short\nvery_long_line_that_exceeds_chunk_size\nmedium_line\nok\n",
224 20,
225 &[
226 "short\n",
227 "very_long_line_that_exceeds_chunk_size\n",
228 "medium_line\nok\n",
229 ],
230 ),
231 ];
232
233 for (description, input, chunk_size, expected) in tests {
234 let mut chunks = Vec::new();
235 print_lines_in_chunks(input, *chunk_size, |chunk| chunks.push(chunk.to_string()));
236 assert_eq!(
237 chunks,
238 expected.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
239 "test case: {}",
240 description
241 );
242 }
243 }
244}