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