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 pub ascii: bool,
80}
81
82impl Box {
83 pub fn new(template: &str, ascii: bool) -> Option<Box> {
93 let lines: Vec<&str> = template.lines().collect();
94 if lines.len() < 8 || lines[..8].iter().any(|line| line.chars().count() != 4) {
95 return None;
96 }
97 let mut parsed = Box::parse(template, ascii);
98 parsed.ascii = ascii;
99 Some(parsed)
100 }
101
102 const fn parse(template: &str, ascii: bool) -> Box {
103 let rows = split_rows(template);
104 Box {
105 top_left: rows[0][0],
106 top: rows[0][1],
107 top_divider: rows[0][2],
108 top_right: rows[0][3],
109
110 head_left: rows[1][0],
111 head_vertical: rows[1][2],
112 head_right: rows[1][3],
113
114 head_row_left: rows[2][0],
115 head_row_horizontal: rows[2][1],
116 head_row_cross: rows[2][2],
117 head_row_right: rows[2][3],
118
119 mid_left: rows[3][0],
120 mid_vertical: rows[3][2],
121 mid_right: rows[3][3],
122
123 row_left: rows[4][0],
124 row_horizontal: rows[4][1],
125 row_cross: rows[4][2],
126 row_right: rows[4][3],
127
128 foot_row_left: rows[5][0],
129 foot_row_horizontal: rows[5][1],
130 foot_row_cross: rows[5][2],
131 foot_row_right: rows[5][3],
132
133 foot_left: rows[6][0],
134 foot_vertical: rows[6][2],
135 foot_right: rows[6][3],
136
137 bottom_left: rows[7][0],
138 bottom: rows[7][1],
139 bottom_divider: rows[7][2],
140 bottom_right: rows[7][3],
141
142 ascii,
143 }
144 }
145
146 pub fn get_top(&self, widths: &[usize], edge: bool) -> String {
149 let mut parts = String::new();
150 if edge {
151 parts.push(self.top_left);
152 }
153 let last = widths.len().saturating_sub(1);
154 for (index, &width) in widths.iter().enumerate() {
155 for _ in 0..width {
156 parts.push(self.top);
157 }
158 if index != last {
159 parts.push(self.top_divider);
160 }
161 }
162 if edge {
163 parts.push(self.top_right);
164 }
165 parts
166 }
167
168 pub fn get_row(&self, widths: &[usize], level: RowLevel, edge: bool) -> String {
171 let (left, horizontal, cross, right) = match level {
172 RowLevel::Head => (
173 self.head_row_left,
174 self.head_row_horizontal,
175 self.head_row_cross,
176 self.head_row_right,
177 ),
178 RowLevel::Row => (
179 self.row_left,
180 self.row_horizontal,
181 self.row_cross,
182 self.row_right,
183 ),
184 RowLevel::Foot => (
185 self.foot_row_left,
186 self.foot_row_horizontal,
187 self.foot_row_cross,
188 self.foot_row_right,
189 ),
190 RowLevel::Mid => (self.mid_left, ' ', self.mid_vertical, self.mid_right),
191 };
192 let mut parts = String::new();
193 if edge {
194 parts.push(left);
195 }
196 let last = widths.len().saturating_sub(1);
197 for (index, &width) in widths.iter().enumerate() {
198 for _ in 0..width {
199 parts.push(horizontal);
200 }
201 if index != last {
202 parts.push(cross);
203 }
204 }
205 if edge {
206 parts.push(right);
207 }
208 parts
209 }
210
211 pub fn substitute(&self, legacy_windows: bool, safe: bool, ascii_only: bool) -> Box {
221 let mut result = *self;
222 if legacy_windows && safe {
223 result = legacy_windows_substitution(result);
224 }
225 if ascii_only && !result.ascii {
226 result = ASCII;
227 }
228 result
229 }
230
231 pub fn substitute_for(&self, options: &crate::console::ConsoleOptions, safe: bool) -> Box {
235 self.substitute(options.legacy_windows, safe, options.ascii_only())
236 }
237
238 pub fn is_ascii(&self) -> bool {
244 self.ascii
245 }
246
247 pub fn get_plain_headed_box(&self) -> Box {
251 match *self {
252 b if b == HEAVY_HEAD || b == SQUARE_DOUBLE_HEAD => SQUARE,
253 b if b == MINIMAL_DOUBLE_HEAD || b == MINIMAL_HEAVY_HEAD => MINIMAL,
254 b if b == ASCII_DOUBLE_HEAD => ASCII2,
255 other => other,
256 }
257 }
258
259 pub fn get_bottom(&self, widths: &[usize], edge: bool) -> String {
262 let mut parts = String::new();
263 if edge {
264 parts.push(self.bottom_left);
265 }
266 let last = widths.len().saturating_sub(1);
267 for (index, &width) in widths.iter().enumerate() {
268 for _ in 0..width {
269 parts.push(self.bottom);
270 }
271 if index != last {
272 parts.push(self.bottom_divider);
273 }
274 }
275 if edge {
276 parts.push(self.bottom_right);
277 }
278 parts
279 }
280}
281
282const fn split_rows(template: &str) -> [[char; 4]; 8] {
284 let bytes = template.as_bytes();
285 let mut rows = [[' '; 4]; 8];
286 let mut i = 0usize; let mut row = 0usize;
290 let mut col = 0usize;
291 while i < bytes.len() {
292 let (ch, width) = next_char(bytes, i);
293 if ch == '\n' {
294 row += 1;
295 col = 0;
296 i += width;
297 continue;
298 }
299 if row < 8 && col < 4 {
300 rows[row][col] = ch;
301 }
302 col += 1;
303 i += width;
304 }
305 rows
306}
307
308const fn next_char(bytes: &[u8], i: usize) -> (char, usize) {
311 let b0 = bytes[i];
312 if b0 < 0x80 {
313 (b0 as char, 1)
314 } else if b0 >> 5 == 0b110 {
315 let cp = ((b0 as u32 & 0x1f) << 6) | (bytes[i + 1] as u32 & 0x3f);
316 (char_from_u32(cp), 2)
317 } else if b0 >> 4 == 0b1110 {
318 let cp = ((b0 as u32 & 0x0f) << 12)
319 | ((bytes[i + 1] as u32 & 0x3f) << 6)
320 | (bytes[i + 2] as u32 & 0x3f);
321 (char_from_u32(cp), 3)
322 } else {
323 let cp = ((b0 as u32 & 0x07) << 18)
324 | ((bytes[i + 1] as u32 & 0x3f) << 12)
325 | ((bytes[i + 2] as u32 & 0x3f) << 6)
326 | (bytes[i + 3] as u32 & 0x3f);
327 (char_from_u32(cp), 4)
328 }
329}
330
331const fn char_from_u32(cp: u32) -> char {
332 match char::from_u32(cp) {
333 Some(c) => c,
334 None => '\u{fffd}',
335 }
336}
337
338fn legacy_windows_substitution(b: Box) -> Box {
340 if b == ROUNDED || b == HEAVY || b == HEAVY_EDGE || b == HEAVY_HEAD {
341 SQUARE
342 } else if b == MINIMAL_HEAVY_HEAD {
343 MINIMAL
344 } else if b == SIMPLE_HEAVY {
345 SIMPLE
346 } else {
347 b
348 }
349}
350
351pub const ASCII: Box = Box::parse("+--+\n| ||\n|-+|\n| ||\n|-+|\n|-+|\n| ||\n+--+\n", true);
354
355pub const SQUARE: Box = Box::parse("┌─┬┐\n│ ││\n├─┼┤\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘\n", false);
356
357pub const ROUNDED: Box = Box::parse("╭─┬╮\n│ ││\n├─┼┤\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n╰─┴╯\n", false);
358
359pub const HEAVY: Box = Box::parse("┏━┳┓\n┃ ┃┃\n┣━╋┫\n┃ ┃┃\n┣━╋┫\n┣━╋┫\n┃ ┃┃\n┗━┻┛\n", false);
360
361pub const HEAVY_HEAD: Box = Box::parse("┏━┳┓\n┃ ┃┃\n┡━╇┩\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘\n", false);
363
364pub const DOUBLE: Box = Box::parse("╔═╦╗\n║ ║║\n╠═╬╣\n║ ║║\n╠═╬╣\n╠═╬╣\n║ ║║\n╚═╩╝\n", false);
365
366pub const MINIMAL: Box = Box::parse(" ╷ \n │ \n╶─┼╴\n │ \n╶─┼╴\n╶─┼╴\n │ \n ╵ \n", false);
367
368pub const ASCII2: Box = Box::parse("+-++\n| ||\n+-++\n| ||\n+-++\n+-++\n| ||\n+-++\n", true);
369
370pub const ASCII_DOUBLE_HEAD: Box =
371 Box::parse("+-++\n| ||\n+=++\n| ||\n+-++\n+-++\n| ||\n+-++\n", true);
372
373pub const SQUARE_DOUBLE_HEAD: Box =
374 Box::parse("┌─┬┐\n│ ││\n╞═╪╡\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘\n", false);
375
376pub const MINIMAL_HEAVY_HEAD: Box =
377 Box::parse(" ╷ \n │ \n╺━┿╸\n │ \n╶─┼╴\n╶─┼╴\n │ \n ╵ \n", false);
378
379pub const MINIMAL_DOUBLE_HEAD: Box =
380 Box::parse(" ╷ \n │ \n ═╪ \n │ \n ─┼ \n ─┼ \n │ \n ╵ \n", false);
381
382pub const NONE: Box = Box::parse(" \n \n \n \n \n \n \n \n", true);
386
387pub const SIMPLE: Box = Box::parse(" \n \n ── \n \n \n ── \n \n \n", false);
389
390pub const SIMPLE_HEAD: Box = Box::parse(" \n \n ── \n \n \n \n \n \n", false);
391
392pub const SIMPLE_HEAVY: Box = Box::parse(" \n \n ━━ \n \n \n ━━ \n \n \n", false);
393
394pub const HORIZONTALS: Box = Box::parse(" ── \n \n ── \n \n ── \n ── \n \n ── \n", false);
395
396pub const HEAVY_EDGE: Box = Box::parse("┏━┯┓\n┃ │┃\n┠─┼┨\n┃ │┃\n┠─┼┨\n┠─┼┨\n┃ │┃\n┗━┷┛\n", false);
397
398pub const DOUBLE_EDGE: Box = Box::parse("╔═╤╗\n║ │║\n╟─┼╢\n║ │║\n╟─┼╢\n╟─┼╢\n║ │║\n╚═╧╝\n", false);
399
400pub const MARKDOWN: Box = Box::parse(" \n| ||\n|-||\n| ||\n|-||\n|-||\n| ||\n \n", true);
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn rounded_corners() {
409 assert_eq!(ROUNDED.top_left, '╭');
410 assert_eq!(ROUNDED.top_right, '╮');
411 assert_eq!(ROUNDED.bottom_left, '╰');
412 assert_eq!(ROUNDED.bottom_right, '╯');
413 assert_eq!(ROUNDED.top, '─');
414 assert_eq!(ROUNDED.mid_left, '│');
415 assert_eq!(ROUNDED.mid_right, '│');
416 }
417
418 #[test]
419 fn square_and_ascii() {
420 assert_eq!(SQUARE.top_left, '┌');
421 assert_eq!(ASCII.top_left, '+');
422 assert_eq!(ASCII.top, '-');
423 assert_eq!(ASCII.mid_left, '|');
424 }
425
426 #[test]
427 fn get_top_and_bottom_single_column() {
428 assert_eq!(ROUNDED.get_top(&[3], true), "╭───╮");
429 assert_eq!(ROUNDED.get_bottom(&[3], true), "╰───╯");
430 assert_eq!(ROUNDED.get_top(&[3], false), "───");
432 assert_eq!(SQUARE.get_row(&[2, 2], RowLevel::Head, false), "──┼──");
433 }
434
435 #[test]
436 fn additional_boxes_parse() {
437 assert_eq!(SIMPLE.top_left, ' ');
439 assert_eq!(SIMPLE.head_row_horizontal, '─');
440 assert_eq!(SIMPLE_HEAVY.head_row_horizontal, '━');
441 assert_eq!(MARKDOWN.mid_left, '|');
442 assert_eq!(MARKDOWN.head_row_horizontal, '-');
443 assert_eq!(DOUBLE_EDGE.top_left, '╔');
444 assert_eq!(DOUBLE_EDGE.mid_vertical, '│');
445 assert_eq!(HEAVY_EDGE.top_left, '┏');
446 assert_eq!(SQUARE_DOUBLE_HEAD.head_row_horizontal, '═');
447 assert_eq!(ASCII2.head_row_cross, '+');
448 }
449
450 #[test]
451 fn plain_headed_boxes() {
452 assert_eq!(HEAVY_HEAD.get_plain_headed_box(), SQUARE);
453 assert_eq!(SQUARE_DOUBLE_HEAD.get_plain_headed_box(), SQUARE);
454 assert_eq!(MINIMAL_DOUBLE_HEAD.get_plain_headed_box(), MINIMAL);
455 assert_eq!(MINIMAL_HEAVY_HEAD.get_plain_headed_box(), MINIMAL);
456 assert_eq!(ASCII_DOUBLE_HEAD.get_plain_headed_box(), ASCII2);
457 assert_eq!(ROUNDED.get_plain_headed_box(), ROUNDED);
458 }
459
460 #[test]
461 fn substitute_legacy_and_ascii() {
462 assert_eq!(ROUNDED.substitute(true, true, false), SQUARE);
464 assert_eq!(HEAVY.substitute(true, true, false), SQUARE);
465 assert_eq!(HEAVY_HEAD.substitute(true, true, false), SQUARE);
466 assert_eq!(DOUBLE.substitute(true, true, false), DOUBLE);
467 assert_eq!(SQUARE.substitute(true, true, false), SQUARE);
468 assert_eq!(ROUNDED.substitute(true, false, false), ROUNDED);
470 assert_eq!(ROUNDED.substitute(false, true, true), ASCII);
472 assert_eq!(DOUBLE.substitute(false, true, true), ASCII);
473 assert_eq!(ASCII.substitute(false, true, true), ASCII);
474 assert_eq!(ROUNDED.substitute(false, true, false), ROUNDED);
476 }
477}