1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum RowLevel {
14 Head,
16 Row,
18 Foot,
20 Mid,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct Box {
40 pub top_left: char,
41 pub top: char,
42 pub top_divider: char,
43 pub top_right: char,
44
45 pub head_left: char,
46 pub head_vertical: char,
47 pub head_right: char,
48
49 pub head_row_left: char,
50 pub head_row_horizontal: char,
51 pub head_row_cross: char,
52 pub head_row_right: char,
53
54 pub mid_left: char,
55 pub mid_vertical: char,
56 pub mid_right: char,
57
58 pub row_left: char,
59 pub row_horizontal: char,
60 pub row_cross: char,
61 pub row_right: char,
62
63 pub foot_row_left: char,
64 pub foot_row_horizontal: char,
65 pub foot_row_cross: char,
66 pub foot_row_right: char,
67
68 pub foot_left: char,
69 pub foot_vertical: char,
70 pub foot_right: char,
71
72 pub bottom_left: char,
73 pub bottom: char,
74 pub bottom_divider: char,
75 pub bottom_right: char,
76}
77
78impl Box {
79 const fn parse(template: &str) -> Box {
84 let rows = split_rows(template);
85 Box {
86 top_left: rows[0][0],
87 top: rows[0][1],
88 top_divider: rows[0][2],
89 top_right: rows[0][3],
90
91 head_left: rows[1][0],
92 head_vertical: rows[1][2],
93 head_right: rows[1][3],
94
95 head_row_left: rows[2][0],
96 head_row_horizontal: rows[2][1],
97 head_row_cross: rows[2][2],
98 head_row_right: rows[2][3],
99
100 mid_left: rows[3][0],
101 mid_vertical: rows[3][2],
102 mid_right: rows[3][3],
103
104 row_left: rows[4][0],
105 row_horizontal: rows[4][1],
106 row_cross: rows[4][2],
107 row_right: rows[4][3],
108
109 foot_row_left: rows[5][0],
110 foot_row_horizontal: rows[5][1],
111 foot_row_cross: rows[5][2],
112 foot_row_right: rows[5][3],
113
114 foot_left: rows[6][0],
115 foot_vertical: rows[6][2],
116 foot_right: rows[6][3],
117
118 bottom_left: rows[7][0],
119 bottom: rows[7][1],
120 bottom_divider: rows[7][2],
121 bottom_right: rows[7][3],
122 }
123 }
124
125 pub fn get_top(&self, widths: &[usize], edge: bool) -> String {
128 let mut parts = String::new();
129 if edge {
130 parts.push(self.top_left);
131 }
132 let last = widths.len().saturating_sub(1);
133 for (index, &width) in widths.iter().enumerate() {
134 for _ in 0..width {
135 parts.push(self.top);
136 }
137 if index != last {
138 parts.push(self.top_divider);
139 }
140 }
141 if edge {
142 parts.push(self.top_right);
143 }
144 parts
145 }
146
147 pub fn get_row(&self, widths: &[usize], level: RowLevel, edge: bool) -> String {
150 let (left, horizontal, cross, right) = match level {
151 RowLevel::Head => (
152 self.head_row_left,
153 self.head_row_horizontal,
154 self.head_row_cross,
155 self.head_row_right,
156 ),
157 RowLevel::Row => (
158 self.row_left,
159 self.row_horizontal,
160 self.row_cross,
161 self.row_right,
162 ),
163 RowLevel::Foot => (
164 self.foot_row_left,
165 self.foot_row_horizontal,
166 self.foot_row_cross,
167 self.foot_row_right,
168 ),
169 RowLevel::Mid => (self.mid_left, ' ', self.mid_vertical, self.mid_right),
170 };
171 let mut parts = String::new();
172 if edge {
173 parts.push(left);
174 }
175 let last = widths.len().saturating_sub(1);
176 for (index, &width) in widths.iter().enumerate() {
177 for _ in 0..width {
178 parts.push(horizontal);
179 }
180 if index != last {
181 parts.push(cross);
182 }
183 }
184 if edge {
185 parts.push(right);
186 }
187 parts
188 }
189
190 pub fn substitute(&self, legacy_windows: bool, safe: bool, ascii_only: bool) -> Box {
198 let mut result = *self;
199 if legacy_windows && safe && (result == ROUNDED || result == HEAVY || result == HEAVY_HEAD)
200 {
201 result = SQUARE;
202 }
203 if ascii_only && !result.is_ascii() {
204 result = ASCII;
205 }
206 result
207 }
208
209 pub fn is_ascii(&self) -> bool {
214 [
215 self.top_left,
216 self.top,
217 self.top_divider,
218 self.top_right,
219 self.head_left,
220 self.head_vertical,
221 self.head_right,
222 self.head_row_left,
223 self.head_row_horizontal,
224 self.head_row_cross,
225 self.head_row_right,
226 self.mid_left,
227 self.mid_vertical,
228 self.mid_right,
229 self.row_left,
230 self.row_horizontal,
231 self.row_cross,
232 self.row_right,
233 self.foot_row_left,
234 self.foot_row_horizontal,
235 self.foot_row_cross,
236 self.foot_row_right,
237 self.foot_left,
238 self.foot_vertical,
239 self.foot_right,
240 self.bottom_left,
241 self.bottom,
242 self.bottom_divider,
243 self.bottom_right,
244 ]
245 .iter()
246 .all(char::is_ascii)
247 }
248
249 pub fn get_plain_headed_box(&self) -> Box {
253 match *self {
254 b if b == HEAVY_HEAD || b == SQUARE_DOUBLE_HEAD => SQUARE,
255 b if b == MINIMAL_DOUBLE_HEAD || b == MINIMAL_HEAVY_HEAD => MINIMAL,
256 b if b == ASCII_DOUBLE_HEAD => ASCII2,
257 other => other,
258 }
259 }
260
261 pub fn get_bottom(&self, widths: &[usize], edge: bool) -> String {
264 let mut parts = String::new();
265 if edge {
266 parts.push(self.bottom_left);
267 }
268 let last = widths.len().saturating_sub(1);
269 for (index, &width) in widths.iter().enumerate() {
270 for _ in 0..width {
271 parts.push(self.bottom);
272 }
273 if index != last {
274 parts.push(self.bottom_divider);
275 }
276 }
277 if edge {
278 parts.push(self.bottom_right);
279 }
280 parts
281 }
282}
283
284const fn split_rows(template: &str) -> [[char; 4]; 8] {
286 let bytes = template.as_bytes();
287 let mut rows = [[' '; 4]; 8];
288 let mut i = 0usize; let mut row = 0usize;
292 let mut col = 0usize;
293 while i < bytes.len() {
294 let (ch, width) = next_char(bytes, i);
295 if ch == '\n' {
296 row += 1;
297 col = 0;
298 i += width;
299 continue;
300 }
301 if row < 8 && col < 4 {
302 rows[row][col] = ch;
303 }
304 col += 1;
305 i += width;
306 }
307 rows
308}
309
310const fn next_char(bytes: &[u8], i: usize) -> (char, usize) {
313 let b0 = bytes[i];
314 if b0 < 0x80 {
315 (b0 as char, 1)
316 } else if b0 >> 5 == 0b110 {
317 let cp = ((b0 as u32 & 0x1f) << 6) | (bytes[i + 1] as u32 & 0x3f);
318 (char_from_u32(cp), 2)
319 } else if b0 >> 4 == 0b1110 {
320 let cp = ((b0 as u32 & 0x0f) << 12)
321 | ((bytes[i + 1] as u32 & 0x3f) << 6)
322 | (bytes[i + 2] as u32 & 0x3f);
323 (char_from_u32(cp), 3)
324 } else {
325 let cp = ((b0 as u32 & 0x07) << 18)
326 | ((bytes[i + 1] as u32 & 0x3f) << 12)
327 | ((bytes[i + 2] as u32 & 0x3f) << 6)
328 | (bytes[i + 3] as u32 & 0x3f);
329 (char_from_u32(cp), 4)
330 }
331}
332
333const fn char_from_u32(cp: u32) -> char {
334 match char::from_u32(cp) {
335 Some(c) => c,
336 None => '\u{fffd}',
337 }
338}
339
340pub const ASCII: Box = Box::parse("+--+\n| ||\n|-+|\n| ||\n|-+|\n|-+|\n| ||\n+--+\n");
343
344pub const SQUARE: Box = Box::parse("┌─┬┐\n│ ││\n├─┼┤\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘\n");
345
346pub const ROUNDED: Box = Box::parse("╭─┬╮\n│ ││\n├─┼┤\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n╰─┴╯\n");
347
348pub const HEAVY: Box = Box::parse("┏━┳┓\n┃ ┃┃\n┣━╋┫\n┃ ┃┃\n┣━╋┫\n┣━╋┫\n┃ ┃┃\n┗━┻┛\n");
349
350pub const HEAVY_HEAD: Box = Box::parse("┏━┳┓\n┃ ┃┃\n┡━╇┩\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘\n");
352
353pub const DOUBLE: Box = Box::parse("╔═╦╗\n║ ║║\n╠═╬╣\n║ ║║\n╠═╬╣\n╠═╬╣\n║ ║║\n╚═╩╝\n");
354
355pub const MINIMAL: Box = Box::parse(" ╷ \n │ \n╶─┼╴\n │ \n╶─┼╴\n╶─┼╴\n │ \n ╵ \n");
356
357pub const ASCII2: Box = Box::parse("+-++\n| ||\n+-++\n| ||\n+-++\n+-++\n| ||\n+-++\n");
358
359pub const ASCII_DOUBLE_HEAD: Box = Box::parse("+-++\n| ||\n+=++\n| ||\n+-++\n+-++\n| ||\n+-++\n");
360
361pub const SQUARE_DOUBLE_HEAD: Box = Box::parse("┌─┬┐\n│ ││\n╞═╪╡\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘\n");
362
363pub const MINIMAL_HEAVY_HEAD: Box = Box::parse(" ╷ \n │ \n╺━┿╸\n │ \n╶─┼╴\n╶─┼╴\n │ \n ╵ \n");
364
365pub const MINIMAL_DOUBLE_HEAD: Box = Box::parse(" ╷ \n │ \n ═╪ \n │ \n ─┼ \n ─┼ \n │ \n ╵ \n");
366
367pub const NONE: Box = Box::parse(" \n \n \n \n \n \n \n \n");
371
372pub const SIMPLE: Box = Box::parse(" \n \n ── \n \n \n ── \n \n \n");
374
375pub const SIMPLE_HEAD: Box = Box::parse(" \n \n ── \n \n \n \n \n \n");
376
377pub const SIMPLE_HEAVY: Box = Box::parse(" \n \n ━━ \n \n \n ━━ \n \n \n");
378
379pub const HORIZONTALS: Box = Box::parse(" ── \n \n ── \n \n ── \n ── \n \n ── \n");
380
381pub const HEAVY_EDGE: Box = Box::parse("┏━┯┓\n┃ │┃\n┠─┼┨\n┃ │┃\n┠─┼┨\n┠─┼┨\n┃ │┃\n┗━┷┛\n");
382
383pub const DOUBLE_EDGE: Box = Box::parse("╔═╤╗\n║ │║\n╟─┼╢\n║ │║\n╟─┼╢\n╟─┼╢\n║ │║\n╚═╧╝\n");
384
385pub const MARKDOWN: Box = Box::parse(" \n| ||\n|-||\n| ||\n|-||\n|-||\n| ||\n \n");
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn rounded_corners() {
394 assert_eq!(ROUNDED.top_left, '╭');
395 assert_eq!(ROUNDED.top_right, '╮');
396 assert_eq!(ROUNDED.bottom_left, '╰');
397 assert_eq!(ROUNDED.bottom_right, '╯');
398 assert_eq!(ROUNDED.top, '─');
399 assert_eq!(ROUNDED.mid_left, '│');
400 assert_eq!(ROUNDED.mid_right, '│');
401 }
402
403 #[test]
404 fn square_and_ascii() {
405 assert_eq!(SQUARE.top_left, '┌');
406 assert_eq!(ASCII.top_left, '+');
407 assert_eq!(ASCII.top, '-');
408 assert_eq!(ASCII.mid_left, '|');
409 }
410
411 #[test]
412 fn get_top_and_bottom_single_column() {
413 assert_eq!(ROUNDED.get_top(&[3], true), "╭───╮");
414 assert_eq!(ROUNDED.get_bottom(&[3], true), "╰───╯");
415 assert_eq!(ROUNDED.get_top(&[3], false), "───");
417 assert_eq!(SQUARE.get_row(&[2, 2], RowLevel::Head, false), "──┼──");
418 }
419
420 #[test]
421 fn additional_boxes_parse() {
422 assert_eq!(SIMPLE.top_left, ' ');
424 assert_eq!(SIMPLE.head_row_horizontal, '─');
425 assert_eq!(SIMPLE_HEAVY.head_row_horizontal, '━');
426 assert_eq!(MARKDOWN.mid_left, '|');
427 assert_eq!(MARKDOWN.head_row_horizontal, '-');
428 assert_eq!(DOUBLE_EDGE.top_left, '╔');
429 assert_eq!(DOUBLE_EDGE.mid_vertical, '│');
430 assert_eq!(HEAVY_EDGE.top_left, '┏');
431 assert_eq!(SQUARE_DOUBLE_HEAD.head_row_horizontal, '═');
432 assert_eq!(ASCII2.head_row_cross, '+');
433 }
434
435 #[test]
436 fn plain_headed_boxes() {
437 assert_eq!(HEAVY_HEAD.get_plain_headed_box(), SQUARE);
438 assert_eq!(SQUARE_DOUBLE_HEAD.get_plain_headed_box(), SQUARE);
439 assert_eq!(MINIMAL_DOUBLE_HEAD.get_plain_headed_box(), MINIMAL);
440 assert_eq!(MINIMAL_HEAVY_HEAD.get_plain_headed_box(), MINIMAL);
441 assert_eq!(ASCII_DOUBLE_HEAD.get_plain_headed_box(), ASCII2);
442 assert_eq!(ROUNDED.get_plain_headed_box(), ROUNDED);
443 }
444
445 #[test]
446 fn substitute_legacy_and_ascii() {
447 assert_eq!(ROUNDED.substitute(true, true, false), SQUARE);
449 assert_eq!(HEAVY.substitute(true, true, false), SQUARE);
450 assert_eq!(HEAVY_HEAD.substitute(true, true, false), SQUARE);
451 assert_eq!(DOUBLE.substitute(true, true, false), DOUBLE);
452 assert_eq!(SQUARE.substitute(true, true, false), SQUARE);
453 assert_eq!(ROUNDED.substitute(true, false, false), ROUNDED);
455 assert_eq!(ROUNDED.substitute(false, true, true), ASCII);
457 assert_eq!(DOUBLE.substitute(false, true, true), ASCII);
458 assert_eq!(ASCII.substitute(false, true, true), ASCII);
459 assert_eq!(ROUNDED.substitute(false, true, false), ROUNDED);
461 }
462}