Skip to main content

latex_rust/
symbols.rs

1//! Math-mode symbol catalog from `data/symbols.tsv`.
2//!
3//! Source tables live in `documents/`. Flutter UI columns are not loaded.
4//! Duplicate `\sqrt{}` rows were collapsed to one entry.
5
6use std::sync::OnceLock;
7
8const TSV: &str = include_str!("../data/symbols.tsv");
9
10/// How a catalog entry is used on a math keyboard / in the parser.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum SymbolKind {
13    /// Single glyph (`\alpha`, `\times`).
14    Symbol,
15    /// Operator (`\sum`, `\int`, `+`).
16    Operator,
17    /// Structure that takes a body (`\frac`, `\sqrt`, matrices).
18    Container,
19    /// Accent or modifier (`\hat`, `\vec`).
20    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    /// Catalog spelling.
35    #[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/// One row of the shipped symbol table.
47///
48/// # Examples
49///
50/// ```
51/// use latex_rust::lookup;
52///
53/// let e = lookup(r"\alpha").unwrap();
54/// assert_eq!(e.glyph, "α");
55/// assert_eq!(e.command_name(), "alpha");
56/// ```
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct SymbolEntry {
59    /// Typical rendered character (may be a placeholder for containers).
60    pub glyph: &'static str,
61    /// High-level group (`Greek`, `Calculus`, …).
62    pub category: &'static str,
63    /// Keyboard / parser kind.
64    pub kind: SymbolKind,
65    /// Raw LaTeX from the table (`\alpha`, `\frac{}{}`, `+`).
66    pub latex: &'static str,
67    /// Human description from the table.
68    pub description: &'static str,
69}
70
71impl SymbolEntry {
72    /// Control-sequence name without `\`, or the raw character for `+` / `=`.
73    #[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/// All shipped symbols, in table order.
130///
131/// # Examples
132///
133/// ```
134/// use latex_rust::symbols;
135///
136/// assert!(!symbols().is_empty());
137/// ```
138#[must_use]
139pub fn symbols() -> &'static [SymbolEntry] {
140    catalog()
141}
142
143/// Look up by raw table LaTeX (`\alpha`, `\frac{}{}`) or command name (`alpha`).
144///
145/// Bare commands (`\aleph`) win over composite rows (`\aleph_0`).
146///
147/// # Arguments
148///
149/// * `query` — control sequence with or without `\`, or a table `latex` cell.
150///
151/// # Returns
152///
153/// The catalog row, or `None` if `query` is not in the table.
154///
155/// # Examples
156///
157/// ```
158/// use latex_rust::lookup;
159///
160/// assert_eq!(lookup(r"\alpha").unwrap().glyph, "α");
161/// assert!(lookup(r"\notacommand").is_none());
162/// ```
163#[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/// Single-character catalog glyph for `query`, if the row is a lone code point.
182///
183/// # Examples
184///
185/// ```
186/// use latex_rust::glyph_char;
187///
188/// assert_eq!(glyph_char(r"\alpha"), Some('α'));
189/// ```
190#[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/// Number of entries in a category.
206///
207/// # Examples
208///
209/// ```
210/// use latex_rust::category_count;
211///
212/// assert!(category_count("Greek") > 0);
213/// ```
214#[must_use]
215pub fn category_count(category: &str) -> usize {
216    catalog().iter().filter(|e| e.category == category).count()
217}