1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum RowLevel {
14 Head,
16 Row,
18 Foot,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct Box {
37 pub top_left: char,
38 pub top: char,
39 pub top_divider: char,
40 pub top_right: char,
41
42 pub head_left: char,
43 pub head_vertical: char,
44 pub head_right: char,
45
46 pub head_row_left: char,
47 pub head_row_horizontal: char,
48 pub head_row_cross: char,
49 pub head_row_right: char,
50
51 pub mid_left: char,
52 pub mid_vertical: char,
53 pub mid_right: char,
54
55 pub row_left: char,
56 pub row_horizontal: char,
57 pub row_cross: char,
58 pub row_right: char,
59
60 pub foot_row_left: char,
61 pub foot_row_horizontal: char,
62 pub foot_row_cross: char,
63 pub foot_row_right: char,
64
65 pub foot_left: char,
66 pub foot_vertical: char,
67 pub foot_right: char,
68
69 pub bottom_left: char,
70 pub bottom: char,
71 pub bottom_divider: char,
72 pub bottom_right: char,
73}
74
75impl Box {
76 const fn parse(template: &str) -> Box {
81 let rows = split_rows(template);
82 Box {
83 top_left: rows[0][0],
84 top: rows[0][1],
85 top_divider: rows[0][2],
86 top_right: rows[0][3],
87
88 head_left: rows[1][0],
89 head_vertical: rows[1][2],
90 head_right: rows[1][3],
91
92 head_row_left: rows[2][0],
93 head_row_horizontal: rows[2][1],
94 head_row_cross: rows[2][2],
95 head_row_right: rows[2][3],
96
97 mid_left: rows[3][0],
98 mid_vertical: rows[3][2],
99 mid_right: rows[3][3],
100
101 row_left: rows[4][0],
102 row_horizontal: rows[4][1],
103 row_cross: rows[4][2],
104 row_right: rows[4][3],
105
106 foot_row_left: rows[5][0],
107 foot_row_horizontal: rows[5][1],
108 foot_row_cross: rows[5][2],
109 foot_row_right: rows[5][3],
110
111 foot_left: rows[6][0],
112 foot_vertical: rows[6][2],
113 foot_right: rows[6][3],
114
115 bottom_left: rows[7][0],
116 bottom: rows[7][1],
117 bottom_divider: rows[7][2],
118 bottom_right: rows[7][3],
119 }
120 }
121
122 pub fn get_top(&self, widths: &[usize], edge: bool) -> String {
125 let mut parts = String::new();
126 if edge {
127 parts.push(self.top_left);
128 }
129 let last = widths.len().saturating_sub(1);
130 for (index, &width) in widths.iter().enumerate() {
131 for _ in 0..width {
132 parts.push(self.top);
133 }
134 if index != last {
135 parts.push(self.top_divider);
136 }
137 }
138 if edge {
139 parts.push(self.top_right);
140 }
141 parts
142 }
143
144 pub fn get_row(&self, widths: &[usize], level: RowLevel, edge: bool) -> String {
147 let (left, horizontal, cross, right) = match level {
148 RowLevel::Head => (
149 self.head_row_left,
150 self.head_row_horizontal,
151 self.head_row_cross,
152 self.head_row_right,
153 ),
154 RowLevel::Row => (
155 self.row_left,
156 self.row_horizontal,
157 self.row_cross,
158 self.row_right,
159 ),
160 RowLevel::Foot => (
161 self.foot_row_left,
162 self.foot_row_horizontal,
163 self.foot_row_cross,
164 self.foot_row_right,
165 ),
166 };
167 let mut parts = String::new();
168 if edge {
169 parts.push(left);
170 }
171 let last = widths.len().saturating_sub(1);
172 for (index, &width) in widths.iter().enumerate() {
173 for _ in 0..width {
174 parts.push(horizontal);
175 }
176 if index != last {
177 parts.push(cross);
178 }
179 }
180 if edge {
181 parts.push(right);
182 }
183 parts
184 }
185
186 pub fn substitute(&self, legacy_windows: bool, safe: bool, ascii_only: bool) -> Box {
194 let mut result = *self;
195 if legacy_windows && safe && (result == ROUNDED || result == HEAVY || result == HEAVY_HEAD)
196 {
197 result = SQUARE;
198 }
199 if ascii_only && result != ASCII {
200 result = ASCII;
201 }
202 result
203 }
204
205 pub fn get_bottom(&self, widths: &[usize], edge: bool) -> String {
208 let mut parts = String::new();
209 if edge {
210 parts.push(self.bottom_left);
211 }
212 let last = widths.len().saturating_sub(1);
213 for (index, &width) in widths.iter().enumerate() {
214 for _ in 0..width {
215 parts.push(self.bottom);
216 }
217 if index != last {
218 parts.push(self.bottom_divider);
219 }
220 }
221 if edge {
222 parts.push(self.bottom_right);
223 }
224 parts
225 }
226}
227
228const fn split_rows(template: &str) -> [[char; 4]; 8] {
230 let bytes = template.as_bytes();
231 let mut rows = [[' '; 4]; 8];
232 let mut i = 0usize; let mut row = 0usize;
236 let mut col = 0usize;
237 while i < bytes.len() {
238 let (ch, width) = next_char(bytes, i);
239 if ch == '\n' {
240 row += 1;
241 col = 0;
242 i += width;
243 continue;
244 }
245 if row < 8 && col < 4 {
246 rows[row][col] = ch;
247 }
248 col += 1;
249 i += width;
250 }
251 rows
252}
253
254const fn next_char(bytes: &[u8], i: usize) -> (char, usize) {
257 let b0 = bytes[i];
258 if b0 < 0x80 {
259 (b0 as char, 1)
260 } else if b0 >> 5 == 0b110 {
261 let cp = ((b0 as u32 & 0x1f) << 6) | (bytes[i + 1] as u32 & 0x3f);
262 (char_from_u32(cp), 2)
263 } else if b0 >> 4 == 0b1110 {
264 let cp = ((b0 as u32 & 0x0f) << 12)
265 | ((bytes[i + 1] as u32 & 0x3f) << 6)
266 | (bytes[i + 2] as u32 & 0x3f);
267 (char_from_u32(cp), 3)
268 } else {
269 let cp = ((b0 as u32 & 0x07) << 18)
270 | ((bytes[i + 1] as u32 & 0x3f) << 12)
271 | ((bytes[i + 2] as u32 & 0x3f) << 6)
272 | (bytes[i + 3] as u32 & 0x3f);
273 (char_from_u32(cp), 4)
274 }
275}
276
277const fn char_from_u32(cp: u32) -> char {
278 match char::from_u32(cp) {
279 Some(c) => c,
280 None => '\u{fffd}',
281 }
282}
283
284pub const ASCII: Box = Box::parse("+--+\n| ||\n|-+|\n| ||\n|-+|\n|-+|\n| ||\n+--+\n");
287
288pub const SQUARE: Box = Box::parse("┌─┬┐\n│ ││\n├─┼┤\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘\n");
289
290pub const ROUNDED: Box = Box::parse("╭─┬╮\n│ ││\n├─┼┤\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n╰─┴╯\n");
291
292pub const HEAVY: Box = Box::parse("┏━┳┓\n┃ ┃┃\n┣━╋┫\n┃ ┃┃\n┣━╋┫\n┣━╋┫\n┃ ┃┃\n┗━┻┛\n");
293
294pub const HEAVY_HEAD: Box = Box::parse("┏━┳┓\n┃ ┃┃\n┡━╇┩\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘\n");
296
297pub const DOUBLE: Box = Box::parse("╔═╦╗\n║ ║║\n╠═╬╣\n║ ║║\n╠═╬╣\n╠═╬╣\n║ ║║\n╚═╩╝\n");
298
299pub const MINIMAL: Box = Box::parse(" ╷ \n │ \n╶─┼╴\n │ \n╶─┼╴\n╶─┼╴\n │ \n ╵ \n");
300
301pub const ASCII2: Box = Box::parse("+-++\n| ||\n+-++\n| ||\n+-++\n+-++\n| ||\n+-++\n");
302
303pub const ASCII_DOUBLE_HEAD: Box = Box::parse("+-++\n| ||\n+=++\n| ||\n+-++\n+-++\n| ||\n+-++\n");
304
305pub const SQUARE_DOUBLE_HEAD: Box = Box::parse("┌─┬┐\n│ ││\n╞═╪╡\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘\n");
306
307pub const MINIMAL_HEAVY_HEAD: Box = Box::parse(" ╷ \n │ \n╺━┿╸\n │ \n╶─┼╴\n╶─┼╴\n │ \n ╵ \n");
308
309pub const MINIMAL_DOUBLE_HEAD: Box = Box::parse(" ╷ \n │ \n ═╪ \n │ \n ─┼ \n ─┼ \n │ \n ╵ \n");
310
311pub const NONE: Box = Box::parse(" \n \n \n \n \n \n \n \n");
313
314pub const SIMPLE: Box = Box::parse(" \n \n ── \n \n \n ── \n \n \n");
316
317pub const SIMPLE_HEAD: Box = Box::parse(" \n \n ── \n \n \n \n \n \n");
318
319pub const SIMPLE_HEAVY: Box = Box::parse(" \n \n ━━ \n \n \n ━━ \n \n \n");
320
321pub const HORIZONTALS: Box = Box::parse(" ── \n \n ── \n \n ── \n ── \n \n ── \n");
322
323pub const HEAVY_EDGE: Box = Box::parse("┏━┯┓\n┃ │┃\n┠─┼┨\n┃ │┃\n┠─┼┨\n┠─┼┨\n┃ │┃\n┗━┷┛\n");
324
325pub const DOUBLE_EDGE: Box = Box::parse("╔═╤╗\n║ │║\n╟─┼╢\n║ │║\n╟─┼╢\n╟─┼╢\n║ │║\n╚═╧╝\n");
326
327pub const MARKDOWN: Box = Box::parse(" \n| ||\n|-||\n| ||\n|-||\n|-||\n| ||\n \n");
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn rounded_corners() {
336 assert_eq!(ROUNDED.top_left, '╭');
337 assert_eq!(ROUNDED.top_right, '╮');
338 assert_eq!(ROUNDED.bottom_left, '╰');
339 assert_eq!(ROUNDED.bottom_right, '╯');
340 assert_eq!(ROUNDED.top, '─');
341 assert_eq!(ROUNDED.mid_left, '│');
342 assert_eq!(ROUNDED.mid_right, '│');
343 }
344
345 #[test]
346 fn square_and_ascii() {
347 assert_eq!(SQUARE.top_left, '┌');
348 assert_eq!(ASCII.top_left, '+');
349 assert_eq!(ASCII.top, '-');
350 assert_eq!(ASCII.mid_left, '|');
351 }
352
353 #[test]
354 fn get_top_and_bottom_single_column() {
355 assert_eq!(ROUNDED.get_top(&[3], true), "╭───╮");
356 assert_eq!(ROUNDED.get_bottom(&[3], true), "╰───╯");
357 assert_eq!(ROUNDED.get_top(&[3], false), "───");
359 assert_eq!(SQUARE.get_row(&[2, 2], RowLevel::Head, false), "──┼──");
360 }
361
362 #[test]
363 fn additional_boxes_parse() {
364 assert_eq!(SIMPLE.top_left, ' ');
366 assert_eq!(SIMPLE.head_row_horizontal, '─');
367 assert_eq!(SIMPLE_HEAVY.head_row_horizontal, '━');
368 assert_eq!(MARKDOWN.mid_left, '|');
369 assert_eq!(MARKDOWN.head_row_horizontal, '-');
370 assert_eq!(DOUBLE_EDGE.top_left, '╔');
371 assert_eq!(DOUBLE_EDGE.mid_vertical, '│');
372 assert_eq!(HEAVY_EDGE.top_left, '┏');
373 assert_eq!(SQUARE_DOUBLE_HEAD.head_row_horizontal, '═');
374 assert_eq!(ASCII2.head_row_cross, '+');
375 }
376
377 #[test]
378 fn substitute_legacy_and_ascii() {
379 assert_eq!(ROUNDED.substitute(true, true, false), SQUARE);
381 assert_eq!(HEAVY.substitute(true, true, false), SQUARE);
382 assert_eq!(HEAVY_HEAD.substitute(true, true, false), SQUARE);
383 assert_eq!(DOUBLE.substitute(true, true, false), DOUBLE);
384 assert_eq!(SQUARE.substitute(true, true, false), SQUARE);
385 assert_eq!(ROUNDED.substitute(true, false, false), ROUNDED);
387 assert_eq!(ROUNDED.substitute(false, true, true), ASCII);
389 assert_eq!(DOUBLE.substitute(false, true, true), ASCII);
390 assert_eq!(ASCII.substitute(false, true, true), ASCII);
391 assert_eq!(ROUNDED.substitute(false, true, false), ROUNDED);
393 }
394}