use std::sync::LazyLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowLevel {
Head,
Row,
Foot,
Mid,
}
#[derive(Debug, Clone)]
pub struct BoxChars {
pub top_left: char,
pub top: char,
pub top_divider: char,
pub top_right: char,
pub head_left: char,
pub head_vertical: char,
pub head_right: char,
pub head_row_left: char,
pub head_row_horizontal: char,
pub head_row_cross: char,
pub head_row_right: char,
pub mid_left: char,
pub mid_vertical: char,
pub mid_right: char,
pub row_left: char,
pub row_horizontal: char,
pub row_cross: char,
pub row_right: char,
pub foot_row_left: char,
pub foot_row_horizontal: char,
pub foot_row_cross: char,
pub foot_row_right: char,
pub foot_left: char,
pub foot_vertical: char,
pub foot_right: char,
pub bottom_left: char,
pub bottom_char: char,
pub bottom_divider: char,
pub bottom_right: char,
pub ascii: bool,
}
impl BoxChars {
pub fn new(box_str: &str, ascii: bool) -> Self {
let lines: Vec<&str> = box_str.split('\n').collect();
assert_eq!(lines.len(), 8, "Box string must have exactly 8 lines");
let parse_line = |line: &str| -> Vec<char> {
let chars: Vec<char> = line.chars().collect();
assert_eq!(
chars.len(),
4,
"Each box line must have exactly 4 characters, got {} in {:?}",
chars.len(),
line,
);
chars
};
let top = parse_line(lines[0]);
let head = parse_line(lines[1]);
let head_row = parse_line(lines[2]);
let mid = parse_line(lines[3]);
let row = parse_line(lines[4]);
let foot_row = parse_line(lines[5]);
let foot = parse_line(lines[6]);
let bottom = parse_line(lines[7]);
Self {
top_left: top[0],
top: top[1],
top_divider: top[2],
top_right: top[3],
head_left: head[0],
head_vertical: head[2],
head_right: head[3],
head_row_left: head_row[0],
head_row_horizontal: head_row[1],
head_row_cross: head_row[2],
head_row_right: head_row[3],
mid_left: mid[0],
mid_vertical: mid[2],
mid_right: mid[3],
row_left: row[0],
row_horizontal: row[1],
row_cross: row[2],
row_right: row[3],
foot_row_left: foot_row[0],
foot_row_horizontal: foot_row[1],
foot_row_cross: foot_row[2],
foot_row_right: foot_row[3],
foot_left: foot[0],
foot_vertical: foot[2],
foot_right: foot[3],
bottom_left: bottom[0],
bottom_char: bottom[1],
bottom_divider: bottom[2],
bottom_right: bottom[3],
ascii,
}
}
pub fn get_top(&self, widths: &[usize]) -> String {
let mut s = String::new();
s.push(self.top_left);
for (i, &width) in widths.iter().enumerate() {
for _ in 0..width {
s.push(self.top);
}
if i < widths.len() - 1 {
s.push(self.top_divider);
}
}
s.push(self.top_right);
s
}
pub fn get_row(&self, widths: &[usize], level: RowLevel, edge: bool) -> String {
let (left, horizontal, cross, right) = match level {
RowLevel::Head => (
self.head_row_left,
self.head_row_horizontal,
self.head_row_cross,
self.head_row_right,
),
RowLevel::Row => (
self.row_left,
self.row_horizontal,
self.row_cross,
self.row_right,
),
RowLevel::Foot => (
self.foot_row_left,
self.foot_row_horizontal,
self.foot_row_cross,
self.foot_row_right,
),
RowLevel::Mid => (
self.mid_left,
self.row_horizontal,
self.mid_vertical,
self.mid_right,
),
};
let mut s = String::new();
if edge {
s.push(left);
}
for (i, &width) in widths.iter().enumerate() {
for _ in 0..width {
s.push(horizontal);
}
if i < widths.len() - 1 {
s.push(cross);
}
}
if edge {
s.push(right);
}
s
}
pub fn get_bottom(&self, widths: &[usize]) -> String {
let mut s = String::new();
s.push(self.bottom_left);
for (i, &width) in widths.iter().enumerate() {
for _ in 0..width {
s.push(self.bottom_char);
}
if i < widths.len() - 1 {
s.push(self.bottom_divider);
}
}
s.push(self.bottom_right);
s
}
pub fn substitute(&self, ascii_only: bool) -> &BoxChars {
if !ascii_only {
return self;
}
match (self.top_left, self.head_row_horizontal) {
('\u{256D}', '\u{2500}') => &SQUARE,
(' ', '\u{2501}') => {
if self.head_row_cross == '\u{253F}' {
&MINIMAL
} else {
&SIMPLE
}
}
('\u{250F}', '\u{2501}') => {
&SQUARE
}
('\u{250F}', '\u{2500}') => &SQUARE,
_ => self,
}
}
pub fn get_plain_headed_box(&self) -> &BoxChars {
match (self.top_left, self.head_row_horizontal) {
('\u{250F}', '\u{2501}') if self.head_row_cross == '\u{2547}' => &SQUARE,
('\u{250C}', '\u{2550}') => &SQUARE,
(' ', '\u{2550}') => &MINIMAL,
(' ', '\u{2501}') if self.head_row_cross == '\u{253F}' => &MINIMAL,
('+', '=') => &ASCII2,
_ => self,
}
}
}
pub static ASCII: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new("+--+\n| ||\n|-+|\n| ||\n|-+|\n|-+|\n| ||\n+--+", true));
pub static ASCII2: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new("+-++\n| ||\n+-++\n| ||\n+-++\n+-++\n| ||\n+-++", true));
pub static ASCII_DOUBLE_HEAD: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new("+-++\n| ||\n+=++\n| ||\n+-++\n+-++\n| ||\n+-++", true));
pub static SQUARE: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new("┌─┬┐\n│ ││\n├─┼┤\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘", false));
pub static SQUARE_DOUBLE_HEAD: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new("┌─┬┐\n│ ││\n╞═╪╡\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘", false));
pub static MINIMAL: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new(" ╷ \n │ \n╶─┼╴\n │ \n╶─┼╴\n╶─┼╴\n │ \n ╵ ", false));
pub static MINIMAL_HEAVY_HEAD: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new(" ╷ \n │ \n╺━┿╸\n │ \n╶─┼╴\n╶─┼╴\n │ \n ╵ ", false));
pub static MINIMAL_DOUBLE_HEAD: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new(" ╷ \n │ \n ═╪ \n │ \n ─┼ \n ─┼ \n │ \n ╵ ", false));
pub static SIMPLE: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new(" \n \n ── \n \n \n ── \n \n ", false));
pub static SIMPLE_HEAD: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new(" \n \n ── \n \n \n \n \n ", false));
pub static SIMPLE_HEAVY: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new(" \n \n ━━ \n \n \n ━━ \n \n ", false));
pub static HORIZONTALS: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new(" ── \n \n ── \n \n ── \n ── \n \n ── ", false));
pub static ROUNDED: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new("╭─┬╮\n│ ││\n├─┼┤\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n╰─┴╯", false));
pub static HEAVY: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new("┏━┳┓\n┃ ┃┃\n┣━╋┫\n┃ ┃┃\n┣━╋┫\n┣━╋┫\n┃ ┃┃\n┗━┻┛", false));
pub static HEAVY_EDGE: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new("┏━┯┓\n┃ │┃\n┠─┼┨\n┃ │┃\n┠─┼┨\n┠─┼┨\n┃ │┃\n┗━┷┛", false));
pub static HEAVY_HEAD: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new("┏━┳┓\n┃ ┃┃\n┡━╇┩\n│ ││\n├─┼┤\n├─┼┤\n│ ││\n└─┴┘", false));
pub static DOUBLE: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new("╔═╦╗\n║ ║║\n╠═╬╣\n║ ║║\n╠═╬╣\n╠═╬╣\n║ ║║\n╚═╩╝", false));
pub static DOUBLE_EDGE: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new("╔═╤╗\n║ │║\n╟─┼╢\n║ │║\n╟─┼╢\n╟─┼╢\n║ │║\n╚═╧╝", false));
pub static MARKDOWN: LazyLock<BoxChars> =
LazyLock::new(|| BoxChars::new(" \n| ||\n|-||\n| ||\n|-||\n|-||\n| ||\n ", true));
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ascii() {
let b = &*ASCII;
assert_eq!(b.top_left, '+');
assert_eq!(b.top, '-');
assert_eq!(b.top_divider, '-');
assert_eq!(b.top_right, '+');
assert_eq!(b.head_left, '|');
assert_eq!(b.head_vertical, '|');
assert_eq!(b.head_right, '|');
assert_eq!(b.head_row_left, '|');
assert_eq!(b.head_row_horizontal, '-');
assert_eq!(b.head_row_cross, '+');
assert_eq!(b.head_row_right, '|');
assert_eq!(b.bottom_left, '+');
assert_eq!(b.bottom_char, '-');
assert_eq!(b.bottom_divider, '-');
assert_eq!(b.bottom_right, '+');
assert!(b.ascii);
}
#[test]
fn test_parse_square() {
let b = &*SQUARE;
assert_eq!(b.top_left, '┌');
assert_eq!(b.top, '─');
assert_eq!(b.top_divider, '┬');
assert_eq!(b.top_right, '┐');
assert_eq!(b.head_left, '│');
assert_eq!(b.head_vertical, '│');
assert_eq!(b.head_right, '│');
assert_eq!(b.head_row_left, '├');
assert_eq!(b.head_row_horizontal, '─');
assert_eq!(b.head_row_cross, '┼');
assert_eq!(b.head_row_right, '┤');
assert_eq!(b.bottom_left, '└');
assert_eq!(b.bottom_char, '─');
assert_eq!(b.bottom_divider, '┴');
assert_eq!(b.bottom_right, '┘');
assert!(!b.ascii);
}
#[test]
fn test_parse_heavy() {
let b = &*HEAVY;
assert_eq!(b.top_left, '┏');
assert_eq!(b.top, '━');
assert_eq!(b.top_divider, '┳');
assert_eq!(b.top_right, '┓');
assert_eq!(b.head_row_cross, '╋');
assert!(!b.ascii);
}
#[test]
fn test_parse_double() {
let b = &*DOUBLE;
assert_eq!(b.top_left, '╔');
assert_eq!(b.top, '═');
assert_eq!(b.top_divider, '╦');
assert_eq!(b.top_right, '╗');
assert_eq!(b.bottom_left, '╚');
assert_eq!(b.bottom_char, '═');
assert_eq!(b.bottom_divider, '╩');
assert_eq!(b.bottom_right, '╝');
}
#[test]
fn test_parse_rounded() {
let b = &*ROUNDED;
assert_eq!(b.top_left, '╭');
assert_eq!(b.top_right, '╮');
assert_eq!(b.bottom_left, '╰');
assert_eq!(b.bottom_right, '╯');
}
#[test]
fn test_parse_all_19_boxes() {
let _ = &*ASCII;
let _ = &*ASCII2;
let _ = &*ASCII_DOUBLE_HEAD;
let _ = &*SQUARE;
let _ = &*SQUARE_DOUBLE_HEAD;
let _ = &*MINIMAL;
let _ = &*MINIMAL_HEAVY_HEAD;
let _ = &*MINIMAL_DOUBLE_HEAD;
let _ = &*SIMPLE;
let _ = &*SIMPLE_HEAD;
let _ = &*SIMPLE_HEAVY;
let _ = &*HORIZONTALS;
let _ = &*ROUNDED;
let _ = &*HEAVY;
let _ = &*HEAVY_EDGE;
let _ = &*HEAVY_HEAD;
let _ = &*DOUBLE;
let _ = &*DOUBLE_EDGE;
let _ = &*MARKDOWN;
}
#[test]
fn test_ascii_flag() {
assert!(ASCII.ascii);
assert!(ASCII2.ascii);
assert!(ASCII_DOUBLE_HEAD.ascii);
assert!(MARKDOWN.ascii);
assert!(!SQUARE.ascii);
assert!(!ROUNDED.ascii);
assert!(!HEAVY.ascii);
assert!(!DOUBLE.ascii);
}
#[test]
fn test_get_top_square() {
let top = SQUARE.get_top(&[5, 3]);
assert_eq!(top, "┌─────┬───┐");
}
#[test]
fn test_get_top_ascii() {
let top = ASCII.get_top(&[3, 4]);
assert_eq!(top, "+--------+");
}
#[test]
fn test_get_top_ascii2() {
let top = ASCII2.get_top(&[3, 4]);
assert_eq!(top, "+---+----+");
}
#[test]
fn test_get_top_single_column() {
let top = SQUARE.get_top(&[10]);
assert_eq!(top, "┌──────────┐");
}
#[test]
fn test_get_top_three_columns() {
let top = HEAVY.get_top(&[2, 3, 4]);
assert_eq!(top, "┏━━┳━━━┳━━━━┓");
}
#[test]
fn test_get_bottom_square() {
let bottom = SQUARE.get_bottom(&[5, 3]);
assert_eq!(bottom, "└─────┴───┘");
}
#[test]
fn test_get_bottom_heavy() {
let bottom = HEAVY.get_bottom(&[4, 4]);
assert_eq!(bottom, "┗━━━━┻━━━━┛");
}
#[test]
fn test_get_bottom_single_column() {
let bottom = DOUBLE.get_bottom(&[6]);
assert_eq!(bottom, "╚══════╝");
}
#[test]
fn test_get_row_head_square() {
let row = SQUARE.get_row(&[5, 3], RowLevel::Head, true);
assert_eq!(row, "├─────┼───┤");
}
#[test]
fn test_get_row_head_no_edge() {
let row = SQUARE.get_row(&[5, 3], RowLevel::Head, false);
assert_eq!(row, "─────┼───");
}
#[test]
fn test_get_row_body_square() {
let row = SQUARE.get_row(&[5, 3], RowLevel::Row, true);
assert_eq!(row, "├─────┼───┤");
}
#[test]
fn test_get_row_foot_square() {
let row = SQUARE.get_row(&[5, 3], RowLevel::Foot, true);
assert_eq!(row, "├─────┼───┤");
}
#[test]
fn test_get_row_head_heavy() {
let row = HEAVY.get_row(&[3, 3], RowLevel::Head, true);
assert_eq!(row, "┣━━━╋━━━┫");
}
#[test]
fn test_get_row_ascii() {
let row = ASCII2.get_row(&[4, 4], RowLevel::Head, true);
assert_eq!(row, "+----+----+");
}
#[test]
fn test_substitute_not_ascii() {
let b = SQUARE.substitute(false);
assert_eq!(b.top_left, '┌');
}
#[test]
fn test_substitute_rounded_to_square() {
let b = ROUNDED.substitute(true);
assert_eq!(b.top_left, '┌');
assert_eq!(b.bottom_left, '└');
}
#[test]
fn test_substitute_heavy_to_square() {
let b = HEAVY.substitute(true);
assert_eq!(b.top_left, '┌');
}
#[test]
fn test_substitute_heavy_edge_to_square() {
let b = HEAVY_EDGE.substitute(true);
assert_eq!(b.top_left, '┌');
}
#[test]
fn test_substitute_simple_heavy_to_simple() {
let b = SIMPLE_HEAVY.substitute(true);
assert_eq!(b.head_row_horizontal, '─');
}
#[test]
fn test_substitute_minimal_heavy_head_to_minimal() {
let b = MINIMAL_HEAVY_HEAD.substitute(true);
assert_eq!(b.head_row_horizontal, '─');
assert_eq!(b.head_row_cross, '┼');
}
#[test]
fn test_plain_headed_heavy_head() {
let b = HEAVY_HEAD.get_plain_headed_box();
assert_eq!(b.top_left, '┌');
assert_eq!(b.head_row_horizontal, '─');
}
#[test]
fn test_plain_headed_square_double_head() {
let b = SQUARE_DOUBLE_HEAD.get_plain_headed_box();
assert_eq!(b.top_left, '┌');
assert_eq!(b.head_row_horizontal, '─');
}
#[test]
fn test_plain_headed_minimal_double_head() {
let b = MINIMAL_DOUBLE_HEAD.get_plain_headed_box();
assert_eq!(b.head_row_horizontal, '─');
assert_eq!(b.head_row_cross, '┼');
}
#[test]
fn test_plain_headed_ascii_double_head() {
let b = ASCII_DOUBLE_HEAD.get_plain_headed_box();
assert_eq!(b.head_row_horizontal, '-');
assert_eq!(b.head_row_cross, '+');
}
#[test]
fn test_plain_headed_identity() {
let b = SQUARE.get_plain_headed_box();
assert_eq!(b.top_left, '┌');
assert_eq!(b.head_row_horizontal, '─');
}
#[test]
fn test_get_top_empty_widths() {
let top = SQUARE.get_top(&[]);
assert_eq!(top, "┌┐");
}
#[test]
fn test_get_bottom_empty_widths() {
let bottom = SQUARE.get_bottom(&[]);
assert_eq!(bottom, "└┘");
}
#[test]
fn test_get_row_empty_widths() {
let row = SQUARE.get_row(&[], RowLevel::Head, true);
assert_eq!(row, "├┤");
}
#[test]
fn test_get_top_zero_width_column() {
let top = SQUARE.get_top(&[0, 3]);
assert_eq!(top, "┌┬───┐");
}
#[test]
fn test_markdown_box() {
let b = &*MARKDOWN;
assert_eq!(b.head_left, '|');
assert_eq!(b.head_vertical, '|');
assert_eq!(b.head_right, '|');
assert_eq!(b.head_row_horizontal, '-');
assert_eq!(b.head_row_cross, '|');
assert!(b.ascii);
}
#[test]
#[should_panic(expected = "8 lines")]
fn test_bad_line_count() {
BoxChars::new("abc\ndef", false);
}
#[test]
#[should_panic(expected = "4 characters")]
fn test_bad_char_count() {
BoxChars::new("ab\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd\nabcd", false);
}
}