1use crate::node::{Node, NodeKind, StackKind};
10use crate::style::Style;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum AtomClass {
15 Ord,
17 Op,
19 Bin,
21 Rel,
23 Open,
25 Close,
27 Punct,
29 Inner,
31}
32
33pub const ATOM_CLASSES: [AtomClass; 8] = [
36 AtomClass::Ord,
37 AtomClass::Op,
38 AtomClass::Bin,
39 AtomClass::Rel,
40 AtomClass::Open,
41 AtomClass::Close,
42 AtomClass::Punct,
43 AtomClass::Inner,
44];
45
46impl AtomClass {
47 #[inline(always)]
49 #[must_use]
50 pub const fn index(self) -> usize {
51 match self {
52 Self::Ord => 0,
53 Self::Op => 1,
54 Self::Bin => 2,
55 Self::Rel => 3,
56 Self::Open => 4,
57 Self::Close => 5,
58 Self::Punct => 6,
59 Self::Inner => 7,
60 }
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum Spacing {
67 None,
69 Thin,
71 Med,
73 Thick,
75}
76
77impl Spacing {
78 #[inline(always)]
80 #[must_use]
81 pub const fn mu(self) -> i32 {
82 match self {
83 Self::None => 0,
84 Self::Thin => 3,
85 Self::Med => 4,
86 Self::Thick => 5,
87 }
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum PairSpacing {
94 Always(Spacing),
96 DisplayTextOnly(Spacing),
99 Impossible,
102}
103
104use PairSpacing::{Always as A, DisplayTextOnly as P, Impossible as X};
105use Spacing::{Med, None as N0, Thick, Thin};
106
107pub const SPACING_TABLE: [[PairSpacing; 8]; 8] = [
111 [
114 A(N0),
115 A(Thin),
116 P(Med),
117 P(Thick),
118 A(N0),
119 A(N0),
120 A(N0),
121 P(Thin),
122 ],
123 [A(Thin), A(Thin), X, P(Thick), A(N0), A(N0), A(N0), P(Thin)],
125 [P(Med), P(Med), X, X, P(Med), X, X, P(Med)],
127 [
129 P(Thick),
130 P(Thick),
131 X,
132 A(N0),
133 P(Thick),
134 A(N0),
135 A(N0),
136 P(Thick),
137 ],
138 [A(N0), A(N0), X, A(N0), A(N0), A(N0), A(N0), A(N0)],
140 [
142 A(N0),
143 A(Thin),
144 P(Med),
145 P(Thick),
146 A(N0),
147 A(N0),
148 A(N0),
149 P(Thin),
150 ],
151 [
153 P(Thin),
154 P(Thin),
155 X,
156 P(Thin),
157 P(Thin),
158 P(Thin),
159 P(Thin),
160 P(Thin),
161 ],
162 [
164 P(Thin),
165 A(Thin),
166 P(Med),
167 P(Thick),
168 P(Thin),
169 A(N0),
170 P(Thin),
171 P(Thin),
172 ],
173];
174
175#[inline(always)]
177#[must_use]
178pub const fn pair_spacing(left: AtomClass, right: AtomClass) -> PairSpacing {
179 SPACING_TABLE[left.index()][right.index()]
180}
181
182#[inline(always)]
187#[must_use]
188pub const fn spacing_in_style(left: AtomClass, right: AtomClass, style: Style) -> Spacing {
189 match pair_spacing(left, right) {
190 PairSpacing::Always(s) => s,
191 PairSpacing::DisplayTextOnly(s) => {
192 if style.is_script() {
193 Spacing::None
194 } else {
195 s
196 }
197 }
198 PairSpacing::Impossible => Spacing::None,
199 }
200}
201
202#[must_use]
205pub const fn char_class(ch: char) -> AtomClass {
206 match ch {
207 '+' | '−' | '-' | '∗' | '*' | '±' | '∓' | '×' | '⋅' | '÷' | '∘' | '∙' | '⊕' | '⊖' | '⊗'
208 | '⊘' | '⊙' | '∪' | '∩' | '∨' | '∧' | '∖' | '⋄' | '†' | '‡' | '⊎' | '⊔' | '⊓' | '≀'
209 | '⨿' | '⋆' | '◁' | '▷' => AtomClass::Bin,
210 '=' | '<' | '>' | ':' | '≤' | '≥' | '≠' | '≡' | '≈' | '∼' | '≃' | '≅' | '≐' | '∝' | '∈'
211 | '∋' | '∉' | '⊂' | '⊃' | '⊆' | '⊇' | '≪' | '≫' | '⊨' | '⊢' | '⊣' | '≍' | '∣' | '∥'
212 | '→' | '←' | '↔' | '⇒' | '⇐' | '⇔' | '⟶' | '⟵' | '⟷' | '⟹' | '⟸' | '⟺' | '↦' | '⟼'
213 | '↑' | '↓' | '↕' | '⇑' | '⇓' | '⇕' | '↗' | '↘' | '↙' | '↖' | '↪' | '↩' | '⇀' | '⇁'
214 | '↼' | '↽' | '⇌' => AtomClass::Rel,
215 '(' | '[' | '⟨' | '⌈' | '⌊' => AtomClass::Open,
216 ')' | ']' | '⟩' | '⌉' | '⌋' | '!' | '?' => AtomClass::Close,
217 ',' | ';' => AtomClass::Punct,
218 _ => AtomClass::Ord,
219 }
220}
221
222#[must_use]
227pub fn intrinsic_class(node: &Node) -> Option<AtomClass> {
228 match &node.kind {
229 NodeKind::Symbol { class, .. } => Some(*class),
230 NodeKind::BigOp { .. } | NodeKind::OpName { .. } => Some(AtomClass::Op),
231 NodeKind::Scripts { base, .. } => base
232 .as_deref()
233 .map_or(Some(AtomClass::Ord), intrinsic_class),
234 NodeKind::Frac { .. } | NodeKind::LeftRight { .. } => Some(AtomClass::Inner),
235 NodeKind::SizedDelim { class, .. } => Some(*class),
236 NodeKind::Radical { .. }
237 | NodeKind::Accent { .. }
238 | NodeKind::List(_)
239 | NodeKind::Text { .. }
240 | NodeKind::TextRun { .. }
241 | NodeKind::TextStyled { .. }
242 | NodeKind::MathIsland { .. }
243 | NodeKind::AlignBlock { .. }
244 | NodeKind::Environment { .. } => Some(AtomClass::Ord),
245 NodeKind::MathFont { body, .. } | NodeKind::Phantom { body, .. } => intrinsic_class(body),
246 NodeKind::Stack { kind, base, .. } => match kind {
247 StackKind::Stackrel => Some(AtomClass::Rel),
248 StackKind::Overset | StackKind::Underset => intrinsic_class(base),
249 },
250 NodeKind::XArrow { .. } => Some(AtomClass::Rel),
253 NodeKind::Fragment(kind) => match kind {
254 crate::node::FragmentKind::UnmatchedClose
255 | crate::node::FragmentKind::RedundantMathShift => None,
256 crate::node::FragmentKind::StrayRight(_) => Some(AtomClass::Close),
257 },
258 NodeKind::StyleChange(_)
259 | NodeKind::SizeChange(_)
260 | NodeKind::AlignChange(_)
261 | NodeKind::LineSpacing(_)
262 | NodeKind::ColorChange(_)
263 | NodeKind::Space(_)
264 | NodeKind::Tie
265 | NodeKind::Linebreak
266 | NodeKind::AlignTab => None,
267 }
268}
269
270#[must_use]
281pub fn classify_list(items: &[Node]) -> Vec<Option<AtomClass>> {
282 let mut classes: Vec<Option<AtomClass>> = items.iter().map(intrinsic_class).collect();
283 let mut prev_atom: Option<usize> = None;
284 for i in 0..classes.len() {
285 let Some(current) = classes[i] else { continue };
286 if current == AtomClass::Bin {
287 let degrade = match prev_atom {
288 None => true,
289 Some(p) => matches!(
290 classes[p],
291 Some(
292 AtomClass::Bin
293 | AtomClass::Op
294 | AtomClass::Rel
295 | AtomClass::Open
296 | AtomClass::Punct
297 )
298 ),
299 };
300 if degrade {
301 classes[i] = Some(AtomClass::Ord);
302 }
303 } else if matches!(
304 current,
305 AtomClass::Rel | AtomClass::Close | AtomClass::Punct
306 ) {
307 if let Some(p) = prev_atom {
308 if classes[p] == Some(AtomClass::Bin) {
309 classes[p] = Some(AtomClass::Ord);
310 }
311 }
312 }
313 prev_atom = Some(i);
314 }
315 if let Some(p) = prev_atom {
316 if classes[p] == Some(AtomClass::Bin) {
317 classes[p] = Some(AtomClass::Ord);
318 }
319 }
320 classes
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn spacing_table_diagonal_spot_checks() {
329 assert_eq!(pair_spacing(AtomClass::Ord, AtomClass::Op), A(Thin));
330 assert_eq!(pair_spacing(AtomClass::Bin, AtomClass::Bin), X);
331 assert_eq!(pair_spacing(AtomClass::Rel, AtomClass::Rel), A(N0));
332 assert_eq!(pair_spacing(AtomClass::Ord, AtomClass::Rel), P(Thick));
333 }
334
335 #[test]
336 fn script_styles_suppress_parenthesized_entries() {
337 assert_eq!(
338 spacing_in_style(AtomClass::Ord, AtomClass::Bin, Style::Text),
339 Med
340 );
341 assert_eq!(
342 spacing_in_style(AtomClass::Ord, AtomClass::Bin, Style::Script),
343 Spacing::None
344 );
345 assert_eq!(
346 spacing_in_style(AtomClass::Ord, AtomClass::Op, Style::ScriptScript),
347 Thin
348 );
349 }
350
351 #[test]
352 fn mu_values() {
353 assert_eq!(Spacing::Thin.mu(), 3);
354 assert_eq!(Spacing::Med.mu(), 4);
355 assert_eq!(Spacing::Thick.mu(), 5);
356 assert_eq!(Spacing::None.mu(), 0);
357 }
358}