use std::sync::OnceLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColDelim {
Paren,
Bracket,
Brace,
Ceil,
Floor,
Bar,
Null,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Delim {
Col(ColDelim),
Angle,
}
pub struct ColInfo {
pub spec: (char, char),
pub short: (char, char),
pub tall: [(char, char, char); 2],
pub vertex: Option<(char, char)>,
pub latex: (&'static str, &'static str),
}
pub static DELIM_NAMES: phf::Map<&'static str, char> = phf::phf_map! {
"lparen" => '(',
"rparen" => ')',
"lbrack" => '[',
"rbrack" => ']',
"lbrace" => '{',
"rbrace" => '}',
"langle" => '⟨',
"rangle" => '⟩',
"lceil" => '⌈',
"rceil" => '⌉',
"lfloor" => '⌊',
"rfloor" => '⌋',
"vert" | "mid" => '|',
"dot" | "none" => '.',
};
pub static DELIM_SPECS: phf::Map<char, (Delim, Option<bool>)> = phf::phf_map! {
'(' => (Delim::Col(ColDelim::Paren), Some(true)),
')' => (Delim::Col(ColDelim::Paren), Some(false)),
'[' => (Delim::Col(ColDelim::Bracket), Some(true)),
']' => (Delim::Col(ColDelim::Bracket), Some(false)),
'{' => (Delim::Col(ColDelim::Brace), Some(true)),
'}' => (Delim::Col(ColDelim::Brace), Some(false)),
'⌈' => (Delim::Col(ColDelim::Ceil), Some(true)),
'⌉' => (Delim::Col(ColDelim::Ceil), Some(false)),
'⌊' => (Delim::Col(ColDelim::Floor), Some(true)),
'⌋' => (Delim::Col(ColDelim::Floor), Some(false)),
'⟨' => (Delim::Angle, Some(true)),
'⟩' => (Delim::Angle, Some(false)),
'|' => (Delim::Col(ColDelim::Bar), None),
'.' => (Delim::Col(ColDelim::Null), None),
};
fn sorted(mut v: Vec<char>) -> Vec<char> {
v.sort_unstable();
v.dedup();
v
}
impl ColDelim {
pub const ALL: [ColDelim; 7] = [
ColDelim::Paren,
ColDelim::Bracket,
ColDelim::Brace,
ColDelim::Ceil,
ColDelim::Floor,
ColDelim::Bar,
ColDelim::Null,
];
#[rustfmt::skip]
pub const fn info(self) -> &'static ColInfo {
match self {
ColDelim::Paren => &ColInfo {
spec: ('(', ')'),
short: ('(', ')'),
tall: [('⎛', '⎜', '⎝'), ('⎞', '⎟', '⎠')],
vertex: None,
latex: ("(", ")"),
},
ColDelim::Bracket => &ColInfo {
spec: ('[', ']'),
short: ('[', ']'),
tall: [('⎡', '⎢', '⎣'), ('⎤', '⎥', '⎦')],
vertex: None,
latex: ("[", "]"),
},
ColDelim::Brace => &ColInfo {
spec: ('{', '}'),
short: ('{', '}'),
tall: [('⎧', '⎪', '⎩'), ('⎫', '⎪', '⎭')],
vertex: Some(('⎨', '⎬')),
latex: ("\\{", "\\}"),
},
ColDelim::Ceil => &ColInfo {
spec: ('⌈', '⌉'),
short: ('⌈', '⌉'),
tall: [('⎡', '⎢', '⎢'), ('⎤', '⎥', '⎥')],
vertex: None,
latex: ("\\lceil ", "\\rceil "),
},
ColDelim::Floor => &ColInfo {
spec: ('⌊', '⌋'),
short: ('⌊', '⌋'),
tall: [('⎢', '⎢', '⎣'), ('⎥', '⎥', '⎦')],
vertex: None,
latex: ("\\lfloor ", "\\rfloor "),
},
ColDelim::Bar => &ColInfo {
spec: ('|', '|'),
short: ('⎢', '⎥'),
tall: [('⎢', '⎢', '⎢'), ('⎥', '⎥', '⎥')],
vertex: None,
latex: ("|", "|"),
},
ColDelim::Null => &ColInfo {
spec: ('.', '.'),
short: ('┆', '┊'),
tall: [('┆', '┆', '┆'), ('┊', '┊', '┊')],
vertex: None,
latex: (".", "."),
},
}
}
pub fn spec(self, left: bool) -> char {
let (l, r) = self.info().spec;
if left { l } else { r }
}
pub const fn fuses(self) -> bool {
matches!(
self,
ColDelim::Paren | ColDelim::Bracket | ColDelim::Ceil | ColDelim::Floor | ColDelim::Bar
)
}
pub fn glyphs(self, left: bool) -> &'static [char] {
static SETS: OnceLock<[[Vec<char>; 2]; ColDelim::ALL.len()]> = OnceLock::new();
let sets = SETS.get_or_init(|| {
ColDelim::ALL.map(|d| {
[true, false].map(|left| {
let info = d.info();
let side = |p: (char, char)| if left { p.0 } else { p.1 };
let mut v = vec![side(info.short)];
v.extend(d.run_pieces(left));
sorted(v)
})
})
});
&sets[self as usize][usize::from(!left)]
}
pub fn run_pieces(self, left: bool) -> &'static [char] {
static SETS: OnceLock<[[Vec<char>; 2]; ColDelim::ALL.len()]> = OnceLock::new();
let sets = SETS.get_or_init(|| {
ColDelim::ALL.map(|d| {
[true, false].map(|left| {
let info = d.info();
let (t, e, b) = info.tall[usize::from(!left)];
let mut p = vec![t, e, b];
if let Some(vx) = info.vertex {
p.push(if left { vx.0 } else { vx.1 });
}
sorted(p)
})
})
});
&sets[self as usize][usize::from(!left)]
}
fn owners(c: char) -> usize {
static COUNTS: OnceLock<std::collections::HashMap<char, usize>> = OnceLock::new();
let counts = COUNTS.get_or_init(|| {
let mut m = std::collections::HashMap::new();
for d in ColDelim::ALL {
for left in [true, false] {
for &g in d.glyphs(left) {
*m.entry(g).or_insert(0) += 1;
}
}
}
m
});
counts.get(&c).copied().unwrap_or(0)
}
pub fn is_shared_piece(c: char, left: bool) -> bool {
ColDelim::ALL
.iter()
.filter(|d| d.glyphs(left).contains(&c))
.count()
> 1
}
pub fn side_shared_pieces() -> &'static [char] {
static SHARED: OnceLock<Vec<char>> = OnceLock::new();
SHARED.get_or_init(|| {
ColDelim::run_glyphs(true)
.iter()
.filter(|c| ColDelim::run_glyphs(false).contains(c))
.copied()
.collect()
})
}
pub fn run_glyphs(left: bool) -> &'static [char] {
static RUNS: OnceLock<[Vec<char>; 2]> = OnceLock::new();
let runs = RUNS.get_or_init(|| {
[true, false].map(|left| {
sorted(
ColDelim::ALL
.iter()
.flat_map(|d| d.run_pieces(left).iter().copied())
.collect(),
)
})
});
&runs[usize::from(!left)]
}
pub fn of_run(has: impl Fn(char) -> bool, left: bool) -> ColDelim {
for d in ColDelim::ALL {
if d.glyphs(left)
.iter()
.any(|&g| ColDelim::owners(g) == 1 && has(g))
{
return d;
}
}
let tall = |d: ColDelim| d.info().tall[usize::from(!left)];
let group: Vec<(ColDelim, (char, char, char))> = ColDelim::ALL
.into_iter()
.map(|d| (d, tall(d)))
.filter(|&(_, (_, e, _))| ColDelim::ALL.iter().filter(|&&o| tall(o).1 == e).count() > 1)
.collect();
let head = group
.iter()
.find_map(|&(_, (t, e, _))| (t != e).then_some(t));
let foot = group
.iter()
.find_map(|&(_, (_, e, b))| (b != e).then_some(b));
let (head_seen, foot_seen) = (head.is_some_and(&has), foot.is_some_and(&has));
group
.iter()
.find(|&&(_, (t, e, b))| (t != e) == head_seen && (b != e) == foot_seen)
.map(|&(d, _)| d)
.unwrap_or(ColDelim::Bar)
}
}
impl Delim {
pub const ALL: [Delim; 8] = [
Delim::Col(ColDelim::Paren),
Delim::Col(ColDelim::Bracket),
Delim::Col(ColDelim::Brace),
Delim::Col(ColDelim::Ceil),
Delim::Col(ColDelim::Floor),
Delim::Col(ColDelim::Bar),
Delim::Col(ColDelim::Null),
Delim::Angle,
];
pub fn col(self) -> Option<ColDelim> {
match self {
Delim::Col(c) => Some(c),
Delim::Angle => None,
}
}
pub fn of_spec(c: char) -> Option<(Delim, Option<bool>)> {
DELIM_SPECS.get(&c).copied()
}
pub fn of_spec_side(c: char, left: bool) -> Option<Delim> {
match Delim::of_spec(c)? {
(d, None) => Some(d),
(d, Some(side)) if side == left => Some(d),
_ => None,
}
}
pub fn spec(self, left: bool) -> char {
match self {
Delim::Col(c) => c.spec(left),
Delim::Angle => {
if left {
'⟨'
} else {
'⟩'
}
}
}
}
pub fn latex(self, left: bool) -> &'static str {
match self {
Delim::Col(c) => {
let (l, r) = c.info().latex;
if left { l } else { r }
}
Delim::Angle => {
if left {
"\\langle "
} else {
"\\rangle "
}
}
}
}
pub fn fuses(self) -> bool {
self.col().is_some_and(ColDelim::fuses)
}
pub fn glyphs(self, left: bool) -> &'static [char] {
static ANGLE: OnceLock<[Vec<char>; 2]> = OnceLock::new();
match self {
Delim::Col(c) => c.glyphs(left),
Delim::Angle => {
let a =
ANGLE.get_or_init(|| [true, false].map(|left| vec![Delim::Angle.spec(left)]));
&a[usize::from(!left)]
}
}
}
pub fn of_baseline_piece(c: char, left: bool) -> Option<Delim> {
Delim::ALL.into_iter().find(|d| match d {
Delim::Col(cd) => {
let info = cd.info();
let side = |p: (char, char)| if left { p.0 } else { p.1 };
match info.vertex {
Some(vx) => c == side(info.short) || c == side(vx),
None => cd.glyphs(left).contains(&c),
}
}
Delim::Angle => c == d.spec(left),
})
}
pub fn all_pieces() -> &'static [char] {
static PIECES: OnceLock<Vec<char>> = OnceLock::new();
PIECES.get_or_init(|| {
sorted(
Delim::ALL
.iter()
.flat_map(|d| [d.glyphs(true), d.glyphs(false)])
.flatten()
.copied()
.collect(),
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn delimiter_table_is_consistent() {
let mut seen_short = std::collections::HashSet::new();
for d in Delim::ALL {
let symmetric = d.spec(true) == d.spec(false);
for left in [true, false] {
let spec = d.spec(left);
let (row, side) = Delim::of_spec(spec).expect("a spec resolves");
assert_eq!(row, d, "{:?} resolves elsewhere", spec);
assert_eq!(side, (!symmetric).then_some(left));
assert_eq!(Delim::of_spec_side(spec, left), Some(d));
assert!(!d.latex(left).is_empty());
}
if !symmetric {
assert_eq!(Delim::of_spec_side(d.spec(false), true), None);
assert_eq!(Delim::of_spec_side(d.spec(true), false), None);
}
}
for cd in ColDelim::ALL {
let info = cd.info();
for g in [info.short.0, info.short.1] {
assert!(seen_short.insert(g), "{:?} is shared", g);
}
for (t, e, b) in info.tall {
assert!(
t == e || b == e || (t != e && b != e),
"{:?} has an unreadable column",
info.spec
);
}
}
for (&name, &c) in DELIM_NAMES.entries() {
assert!(Delim::of_spec(c).is_some(), "\\{} names no pair", name);
}
for (&c, &(d, _)) in DELIM_SPECS.entries() {
assert!(
d.spec(true) == c || d.spec(false) == c,
"{:?} is not a spec of {:?}",
c,
d
);
}
}
#[test]
fn run_classifiers_follow_the_table() {
for (c, want) in [
('⎢', Some(Delim::Col(ColDelim::Bracket))),
('⎡', Some(Delim::Col(ColDelim::Bracket))),
('⌈', Some(Delim::Col(ColDelim::Ceil))),
('(', Some(Delim::Col(ColDelim::Paren))),
('⎜', Some(Delim::Col(ColDelim::Paren))),
('⎨', Some(Delim::Col(ColDelim::Brace))),
('┆', Some(Delim::Col(ColDelim::Null))),
('⟨', Some(Delim::Angle)),
('⎧', None),
('⎪', None),
('|', None),
] {
assert_eq!(Delim::of_baseline_piece(c, true), want, "{:?}", c);
}
for left in [true, false] {
for cd in ColDelim::ALL {
let short = if left {
cd.info().short.0
} else {
cd.info().short.1
};
let run = ColDelim::run_glyphs(left);
if !matches!(cd, ColDelim::Null | ColDelim::Bar) {
assert!(!run.contains(&short), "{:?} in run", short);
}
}
}
let of = |glyphs: &'static str, left| ColDelim::of_run(|c| glyphs.contains(c), left);
assert_eq!(of("⎛⎜⎝", true), ColDelim::Paren);
assert_eq!(of("⎧⎨⎪⎩", true), ColDelim::Brace);
assert_eq!(of("⎡⎢⎣", true), ColDelim::Bracket);
assert_eq!(of("⎡⎢", true), ColDelim::Ceil);
assert_eq!(of("⎢⎣", true), ColDelim::Floor);
assert_eq!(of("⎢", true), ColDelim::Bar);
assert_eq!(of("⎪", true), ColDelim::Bar); assert_eq!(of("⎤⎥", false), ColDelim::Ceil);
assert_eq!(of("┊", false), ColDelim::Null);
}
}