1use std::sync::OnceLock;
7
8const TSV: &str = include_str!("../data/symbols.tsv");
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum SymbolKind {
13 Symbol,
15 Operator,
17 Container,
19 Modifier,
21}
22
23impl SymbolKind {
24 fn parse(s: &str) -> Option<Self> {
25 match s {
26 "Symbol" => Some(Self::Symbol),
27 "Operator" => Some(Self::Operator),
28 "Container" => Some(Self::Container),
29 "Modifier" => Some(Self::Modifier),
30 _ => None,
31 }
32 }
33
34 #[must_use]
36 pub fn as_str(self) -> &'static str {
37 match self {
38 Self::Symbol => "Symbol",
39 Self::Operator => "Operator",
40 Self::Container => "Container",
41 Self::Modifier => "Modifier",
42 }
43 }
44}
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct SymbolEntry {
59 pub glyph: &'static str,
61 pub category: &'static str,
63 pub kind: SymbolKind,
65 pub latex: &'static str,
67 pub description: &'static str,
69}
70
71impl SymbolEntry {
72 #[must_use]
74 pub fn command_name(&self) -> &str {
75 command_name(self.latex)
76 }
77}
78
79fn command_name(latex: &str) -> &str {
80 let t = latex.trim();
81 if let Some(rest) = t.strip_prefix('\\') {
82 let n = rest.chars().take_while(|c| c.is_ascii_alphabetic()).count();
83 if n > 0 {
84 return &rest[..n];
85 }
86 if let Some(c) = rest.chars().next() {
87 let end = c.len_utf8();
88 return &rest[..end];
89 }
90 }
91 t
92}
93
94fn parse_tsv(text: &'static str) -> Vec<SymbolEntry> {
95 let mut rows = Vec::new();
96 let mut lines = text.lines();
97 let header = lines.next().expect("symbols.tsv header");
98 assert_eq!(
99 header, "glyph\tcategory\tkind\tlatex\tdescription",
100 "symbols.tsv schema"
101 );
102 for line in lines {
103 if line.is_empty() {
104 continue;
105 }
106 let mut cols = line.split('\t');
107 let glyph = cols.next().expect("glyph");
108 let category = cols.next().expect("category");
109 let kind_s = cols.next().expect("kind");
110 let latex = cols.next().expect("latex");
111 let description = cols.next().unwrap_or("");
112 let kind = SymbolKind::parse(kind_s).expect("symbol kind");
113 rows.push(SymbolEntry {
114 glyph,
115 category,
116 kind,
117 latex,
118 description,
119 });
120 }
121 rows
122}
123
124fn catalog() -> &'static [SymbolEntry] {
125 static CAT: OnceLock<Vec<SymbolEntry>> = OnceLock::new();
126 CAT.get_or_init(|| parse_tsv(TSV)).as_slice()
127}
128
129#[must_use]
139pub fn symbols() -> &'static [SymbolEntry] {
140 catalog()
141}
142
143#[must_use]
164pub fn lookup(query: &str) -> Option<&'static SymbolEntry> {
165 let q = query.trim();
166 let q_name = command_name(q);
167 if let Some(e) = catalog().iter().find(|e| e.latex == q) {
168 return Some(e);
169 }
170 if let Some(e) = catalog()
171 .iter()
172 .find(|e| e.command_name() == q_name && is_bare_latex(e.latex, q_name))
173 {
174 return Some(e);
175 }
176 catalog()
177 .iter()
178 .find(|e| e.command_name() == q || e.command_name() == q_name)
179}
180
181#[must_use]
191pub fn glyph_char(query: &str) -> Option<char> {
192 let e = lookup(query)?;
193 let mut chars = e.glyph.chars();
194 match (chars.next(), chars.next()) {
195 (Some(c), None) => Some(c),
196 _ => None,
197 }
198}
199
200fn is_bare_latex(latex: &str, name: &str) -> bool {
201 let t = latex.trim();
202 t == name || t.strip_prefix('\\').is_some_and(|rest| rest == name)
203}
204
205#[must_use]
215pub fn category_count(category: &str) -> usize {
216 catalog().iter().filter(|e| e.category == category).count()
217}