1use rowan::{TextRange, TextSize};
10
11use crate::syntax::{SyntaxElement, SyntaxKind, SyntaxNode, SyntaxToken};
12
13#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum MathClass {
16 #[default]
17 Ord,
18 Op,
19 Bin,
20 Rel,
21 Open,
22 Close,
23 Punct,
24 Fence,
25 Inner,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum DelimiterRole {
32 Open,
33 Close,
34 Fence,
35}
36
37#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
39pub struct MathAtomInfo {
40 pub class: MathClass,
41 pub delimiter: Option<DelimiterRole>,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub struct MathAtom {
47 pub range: TextRange,
48 pub class: MathClass,
49 pub delimiter: Option<DelimiterRole>,
50}
51
52const fn info(class: MathClass, delimiter: Option<DelimiterRole>) -> MathAtomInfo {
53 MathAtomInfo { class, delimiter }
54}
55
56type MathCommandMap = phf::Map<&'static str, MathAtomInfo>;
57
58include!(concat!(env!("OUT_DIR"), "/math_symbols.rs"));
59
60pub const NAMED_MATH_OPERATORS: &[&str] = &[
65 "arccos", "arcsin", "arctan", "arg", "cos", "cosh", "cot", "coth", "csc", "deg", "det", "dim",
66 "exp", "gcd", "hom", "inf", "ker", "lg", "lim", "liminf", "limsup", "ln", "log", "max", "min",
67 "Pr", "sec", "sin", "sinh", "sup", "tan", "tanh",
68];
69
70pub fn math_command_info(name: &str) -> MathAtomInfo {
74 curated_command_info(name)
75 .or_else(|| UNICODE_MATH_COMMANDS.get(name).copied())
76 .unwrap_or_default()
77}
78
79pub fn math_char_info(character: char) -> MathAtomInfo {
81 curated_char_info(character)
82 .or_else(|| {
83 UNICODE_MATH_CHARS
84 .binary_search_by_key(&character, |(candidate, _)| *candidate)
85 .ok()
86 .map(|index| UNICODE_MATH_CHARS[index].1)
87 })
88 .unwrap_or_default()
89}
90
91pub fn math_atoms(element: &SyntaxElement) -> MathAtoms<'_> {
94 match element {
95 SyntaxElement::Token(token) if token.kind() == SyntaxKind::WORD => MathAtoms {
96 inner: MathAtomsInner::Word {
97 text: token.text(),
98 start: token.text_range().start(),
99 offset: 0,
100 },
101 },
102 SyntaxElement::Token(token) => MathAtoms {
103 inner: MathAtomsInner::One(Some(atom(token.text_range(), token_info(token)))),
104 },
105 SyntaxElement::Node(node) => MathAtoms {
106 inner: MathAtomsInner::One(Some(atom(node.text_range(), node_info(node)))),
107 },
108 }
109}
110
111pub struct MathAtoms<'a> {
113 inner: MathAtomsInner<'a>,
114}
115
116enum MathAtomsInner<'a> {
117 Word {
118 text: &'a str,
119 start: TextSize,
120 offset: usize,
121 },
122 One(Option<MathAtom>),
123}
124
125impl Iterator for MathAtoms<'_> {
126 type Item = MathAtom;
127
128 fn next(&mut self) -> Option<Self::Item> {
129 match &mut self.inner {
130 MathAtomsInner::One(atom) => atom.take(),
131 MathAtomsInner::Word {
132 text,
133 start,
134 offset,
135 } => {
136 let character = text.get(*offset..)?.chars().next()?;
137 let len = character.len_utf8();
138 let atom_start = *start + TextSize::from(*offset as u32);
139 *offset += len;
140 let atom_end = *start + TextSize::from(*offset as u32);
141 let value = math_char_info(character);
142 Some(MathAtom {
143 range: TextRange::new(atom_start, atom_end),
144 class: value.class,
145 delimiter: value.delimiter,
146 })
147 }
148 }
149 }
150}
151
152fn atom(range: TextRange, value: MathAtomInfo) -> MathAtom {
153 MathAtom {
154 range,
155 class: value.class,
156 delimiter: value.delimiter,
157 }
158}
159
160fn token_info(token: &SyntaxToken) -> MathAtomInfo {
161 match token.kind() {
162 SyntaxKind::CONTROL_WORD | SyntaxKind::CONTROL_SYMBOL => token
163 .text()
164 .strip_prefix('\\')
165 .map_or_else(MathAtomInfo::default, math_command_info),
166 _ => {
167 let mut characters = token.text().chars();
168 match (characters.next(), characters.next()) {
169 (Some(character), None) => math_char_info(character),
170 _ => MathAtomInfo::default(),
171 }
172 }
173 }
174}
175
176fn node_info(node: &SyntaxNode) -> MathAtomInfo {
177 match node.kind() {
178 SyntaxKind::COMMAND => node
179 .children_with_tokens()
180 .filter_map(SyntaxElement::into_token)
181 .find(|token| {
182 matches!(
183 token.kind(),
184 SyntaxKind::CONTROL_WORD | SyntaxKind::CONTROL_SYMBOL
185 )
186 })
187 .as_ref()
188 .map_or_else(MathAtomInfo::default, token_info),
189 SyntaxKind::SCRIPTED => node
190 .children_with_tokens()
191 .find(|element| {
192 !matches!(
193 element.kind(),
194 SyntaxKind::WHITESPACE
195 | SyntaxKind::NEWLINE
196 | SyntaxKind::SUBSCRIPT
197 | SyntaxKind::SUPERSCRIPT
198 )
199 })
200 .and_then(|base| math_atoms(&base).next())
201 .map_or_else(MathAtomInfo::default, |base| {
202 info(base.class, base.delimiter)
203 }),
204 SyntaxKind::GROUP
205 | SyntaxKind::OPTIONAL
206 | SyntaxKind::LEFT_RIGHT
207 | SyntaxKind::ENVIRONMENT => info(MathClass::Inner, None),
208 _ => MathAtomInfo::default(),
209 }
210}
211
212fn curated_char_info(character: char) -> Option<MathAtomInfo> {
213 match character {
214 '*' | '-' => Some(info(MathClass::Bin, None)),
216 '!' => Some(info(MathClass::Close, None)),
218 '√' => Some(info(MathClass::Ord, None)),
219 '∛' | '∜' | '⟌' => Some(info(MathClass::Open, None)),
220 _ => None,
221 }
222}
223
224fn curated_command_info(name: &str) -> Option<MathAtomInfo> {
225 if NAMED_MATH_OPERATORS.contains(&name) {
226 return Some(info(MathClass::Op, None));
227 }
228 let value = match name {
229 "operatorname" => info(MathClass::Op, None),
231
232 "mathord" => info(MathClass::Ord, None),
235 "mathop" => info(MathClass::Op, None),
236 "mathbin" => info(MathClass::Bin, None),
237 "mathrel" => info(MathClass::Rel, None),
238 "mathopen" => info(MathClass::Open, None),
239 "mathclose" => info(MathClass::Close, None),
240 "mathpunct" => info(MathClass::Punct, None),
241 "mathinner" => info(MathClass::Inner, None),
242
243 "le" | "leq" | "ge" | "geq" | "ne" | "neq" | "equiv" | "approx" | "approxeq" | "sim"
246 | "simeq" | "cong" | "propto" | "asymp" | "doteq" | "models" | "vdash" | "dashv"
247 | "perp" | "parallel" | "mid" | "in" | "ni" | "notin" | "subset" | "subseteq"
248 | "subsetneq" | "supset" | "supseteq" | "supsetneq" | "sqsubseteq" | "sqsupseteq"
249 | "prec" | "preceq" | "succ" | "succeq" | "ll" | "gg" | "lll" | "ggg" | "to"
250 | "rightarrow" | "longrightarrow" | "Rightarrow" | "Longrightarrow" | "implies"
251 | "impliedby" | "iff" | "mapsto" | "longmapsto" | "leftarrow" | "Leftarrow" | "gets"
252 | "leftrightarrow" | "Leftrightarrow" | "Longleftrightarrow" | "hookrightarrow"
253 | "hookleftarrow" | "triangleq" | "coloneq" | "Coloneq" | "coloneqq" | "Coloneqq"
254 | "eqcolon" | "Eqcolon" | "eqqcolon" | "Eqqcolon" | "colonapprox" | "Colonapprox"
255 | "colonsim" | "Colonsim" | "lesssim" | "gtrsim" => info(MathClass::Rel, None),
256
257 "pm" | "mp" | "times" | "div" | "cdot" | "ast" | "star" | "circ" | "bullet" | "cup"
260 | "cap" | "uplus" | "sqcup" | "sqcap" | "vee" | "wedge" | "lor" | "land" | "oplus"
261 | "ominus" | "otimes" | "oslash" | "odot" | "setminus" | "amalg" | "diamond" | "wr"
262 | "dagger" | "ddagger" | "bigtriangleup" | "bigtriangledown" | "triangleleft"
263 | "triangleright" => info(MathClass::Bin, None),
264
265 "{" | "lvert" | "lVert" => info(MathClass::Open, Some(DelimiterRole::Open)),
267 "}" | "rvert" | "rVert" => info(MathClass::Close, Some(DelimiterRole::Close)),
268 "|" => info(MathClass::Fence, Some(DelimiterRole::Fence)),
269
270 "sqrt" | "cuberoot" | "fourthroot" | "longdivision" => info(MathClass::Open, None),
273 "mathexclam" => info(MathClass::Close, None),
274 _ => return None,
275 };
276 Some(value)
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::parser::parse;
283
284 fn root(source: &str) -> SyntaxNode {
285 SyntaxNode::new_root(parse(source).green)
286 }
287
288 #[test]
289 fn generated_and_curated_lookups_share_one_default() {
290 assert_eq!(UNICODE_MATH_COMMANDS.len(), 2448);
291 assert_eq!(math_command_info("nleq").class, MathClass::Rel);
292 assert_eq!(math_command_info("sin").class, MathClass::Op);
293 assert_eq!(math_command_info("bigtriangledown").class, MathClass::Bin);
294 assert_eq!(math_char_info('≤').class, MathClass::Rel);
295 assert_eq!(math_char_info(',').class, MathClass::Punct);
296 assert_eq!(math_command_info("vert").class, MathClass::Fence);
297 assert_eq!(math_char_info('-').class, MathClass::Bin);
298 assert_eq!(math_char_info('/').class, MathClass::Ord);
299 assert_eq!(
300 math_command_info("not-a-real-command"),
301 MathAtomInfo::default()
302 );
303 assert_eq!(math_char_info('🦀'), MathAtomInfo::default());
304 }
305
306 #[test]
307 fn delimiter_role_is_independent_of_spacing_class() {
308 assert_eq!(
309 math_command_info("langle").delimiter,
310 Some(DelimiterRole::Open)
311 );
312 assert_eq!(math_command_info("sqrt"), info(MathClass::Open, None));
313 assert_eq!(math_char_info('!'), info(MathClass::Close, None));
314 assert_eq!(math_char_info('√'), info(MathClass::Ord, None));
315 }
316
317 #[test]
318 fn word_atoms_have_exact_multibyte_source_spans() {
319 let tree = root("$a≤b$");
320 let word = tree
321 .descendants_with_tokens()
322 .filter_map(SyntaxElement::into_token)
323 .find(|token| token.text() == "a≤b")
324 .expect("coalesced math word");
325 let atoms: Vec<_> = math_atoms(&word.into()).collect();
326 assert_eq!(
327 atoms.iter().map(|atom| atom.class).collect::<Vec<_>>(),
328 [MathClass::Ord, MathClass::Rel, MathClass::Ord]
329 );
330 assert_eq!(
331 atoms.iter().map(|atom| atom.range).collect::<Vec<_>>(),
332 [
333 TextRange::new(1.into(), 2.into()),
334 TextRange::new(2.into(), 5.into()),
335 TextRange::new(5.into(), 6.into()),
336 ]
337 );
338 }
339
340 #[test]
341 fn commands_and_scripted_bases_are_single_atoms() {
342 let tree = root("$\\leq \\}^{1/2}$");
343 let command = tree
344 .descendants()
345 .find(|node| node.kind() == SyntaxKind::COMMAND)
346 .expect("relation command");
347 let command_atom = math_atoms(&command.clone().into()).next().unwrap();
348 assert_eq!(command_atom.class, MathClass::Rel);
349 assert_eq!(command_atom.range, command.text_range());
350
351 let scripted = tree
352 .descendants()
353 .find(|node| node.kind() == SyntaxKind::SCRIPTED)
354 .expect("scripted delimiter");
355 let scripted_atom = math_atoms(&scripted.clone().into()).next().unwrap();
356 assert_eq!(scripted_atom.delimiter, Some(DelimiterRole::Close));
357 assert_eq!(scripted_atom.range, scripted.text_range());
358 }
359
360 #[test]
361 fn structural_subformulas_are_inner_atoms() {
362 let tree = root("${x}$");
363 let group = tree
364 .descendants()
365 .find(|node| node.kind() == SyntaxKind::GROUP)
366 .expect("math group");
367 assert_eq!(
368 math_atoms(&group.into()).next().unwrap().class,
369 MathClass::Inner
370 );
371 }
372}