Skip to main content

katex_parser/anvil/
spacing.rs

1//! TeX math spacing rules. Based on KaTeX spacingData.ts and TeXbook Ch.18.
2
3use crate::ast::AtomFamily;
4
5/// Concrete space representations for a rendering backend.
6/// Different backends (unicode text, HTML/CSS, LaTeX) provide their own spec.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct SpacingSpec {
9    pub thick: String,
10    pub medium: String,
11    pub thin: String,
12    pub operator: String,
13}
14
15/// Converts AtomFamily to the corresponding spacing type string.
16pub fn atom_family_name(family: AtomFamily) -> String {
17    match family {
18        AtomFamily::Mord => "mord",
19        AtomFamily::Mop => "mop",
20        AtomFamily::Mbin => "mbin",
21        AtomFamily::Mrel => "mrel",
22        AtomFamily::Mopen => "mopen",
23        AtomFamily::Mclose => "mclose",
24        AtomFamily::Mpunct => "mpunct",
25        AtomFamily::Minner => "minner",
26    }
27    .to_string()
28}
29
30/// Normal (display/text) spacing table between atom types.
31fn normal_spacing<'a>(left: &str, right: &str, spec: &'a SpacingSpec) -> Option<&'a str> {
32    match (left, right) {
33        ("mord", "mop") | ("mord", "mbig") => Some(&spec.thin),
34        ("mord", "mbin") => Some(&spec.medium),
35        ("mord", "mrel") => Some(&spec.thick),
36        ("mord", "minner") => Some(&spec.thin),
37        ("mop", "mord") => Some(&spec.operator),
38        ("mop", "mop") | ("mop", "mbig") => Some(&spec.operator),
39        ("mop", "mrel") => Some(&spec.thick),
40        ("mop", "minner") => Some(&spec.thin),
41        ("mbig", "mord") | ("mbig", "mop") | ("mbig", "mopen") | ("mbig", "minner") => {
42            Some(&spec.medium)
43        }
44        ("mbig", "mbin") => Some(&spec.medium),
45        ("mbig", "mrel") => Some(&spec.thick),
46        ("mbig", "mclose") | ("mbig", "mpunct") => Some(&spec.thin),
47        ("mbin", "mord") => Some(&spec.medium),
48        ("mbin", "mop") | ("mbin", "mbig") => Some(&spec.medium),
49        ("mbin", "mopen") => Some(&spec.medium),
50        ("mbin", "minner") => Some(&spec.medium),
51        ("mrel", "mord") => Some(&spec.thick),
52        ("mrel", "mop") | ("mrel", "mbig") => Some(&spec.thick),
53        ("mrel", "mopen") => Some(&spec.thick),
54        ("mrel", "minner") => Some(&spec.thick),
55        ("mclose", "mop") | ("mclose", "mbig") => Some(&spec.thin),
56        ("mclose", "mbin") => Some(&spec.medium),
57        ("mclose", "mrel") => Some(&spec.thick),
58        ("mclose", "minner") => Some(&spec.thin),
59        ("mpunct", "mord") => Some(&spec.thin),
60        ("mpunct", "mop") | ("mpunct", "mbig") => Some(&spec.thin),
61        ("mpunct", "mrel") => Some(&spec.thick),
62        ("mpunct", "mopen") => Some(&spec.thin),
63        ("mpunct", "mclose") => Some(&spec.thin),
64        ("mpunct", "mpunct") => Some(&spec.thin),
65        ("mpunct", "minner") => Some(&spec.thin),
66        ("minner", "mord") => Some(&spec.thin),
67        ("minner", "mop") | ("minner", "mbig") => Some(&spec.thin),
68        ("minner", "mbin") => Some(&spec.medium),
69        ("minner", "mrel") => Some(&spec.thick),
70        ("minner", "mopen") => Some(&spec.thin),
71        ("minner", "mpunct") => Some(&spec.thin),
72        ("minner", "minner") => Some(&spec.thin),
73        _ => None,
74    }
75}
76
77/// Tight (script/scriptscript) spacing table.
78fn tight_spacing<'a>(left: &str, right: &str, spec: &'a SpacingSpec) -> Option<&'a str> {
79    match (left, right) {
80        ("mord", "mop") => Some(&spec.thin),
81        ("mop", "mord") => Some(&spec.thin),
82        ("mop", "mop") => Some(&spec.thin),
83        ("mbig", "mord") | ("mbig", "mop") | ("mbig", "minner") => Some(&spec.thin),
84        ("mclose", "mop") => Some(&spec.thin),
85        ("minner", "mop") => Some(&spec.thin),
86        _ => None,
87    }
88}
89
90/// Look up spacing between two atom types.
91pub fn math_spacing(
92    left_type: &str,
93    right_type: &str,
94    tight: bool,
95    spec: &SpacingSpec,
96) -> Option<String> {
97    if tight {
98        tight_spacing(left_type, right_type, spec)
99    } else {
100        normal_spacing(left_type, right_type, spec)
101    }
102    .map(|s| s.to_string())
103}
104
105/// A renderable atom with its math class and baseline info.
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct SpacableItem {
108    pub atom_type: Option<String>,
109    pub text: String,
110    pub baseline: usize,
111}
112
113/// True when a type to the left of a binary atom cancels its binary spacing.
114/// TeXbook Rules 5-6.
115fn cancels_left_of_bin(ty: &str) -> bool {
116    matches!(ty, "leftmost" | "mbin" | "mopen" | "mrel" | "mop" | "mpunct" | "mbig")
117}
118
119/// True when a type to the right of a binary atom cancels its binary spacing.
120fn cancels_right_of_bin(ty: &str) -> bool {
121    matches!(ty, "rightmost" | "mrel" | "mclose" | "mpunct")
122}
123
124/// Apply bin cancellation: Mbin atoms become Mord in certain contexts.
125pub fn cancel_bin_atoms(items: Vec<SpacableItem>) -> Vec<SpacableItem> {
126    let n = items.len();
127    if n == 0 {
128        return items;
129    }
130    let mut items = items;
131
132    if items[0].atom_type.as_deref() == Some("mbin") {
133        items[0].atom_type = Some("mord".to_string());
134    }
135
136    for i in 1..n {
137        let prev_ty = items[i - 1].atom_type.clone();
138        let curr_ty = items[i].atom_type.clone();
139        match (prev_ty.as_deref(), curr_ty.as_deref()) {
140            (Some("mbin"), Some(right)) => {
141                if cancels_right_of_bin(right) {
142                    items[i - 1].atom_type = Some("mord".to_string());
143                }
144            }
145            (Some(left), Some("mbin"))
146                if cancels_left_of_bin(left) => {
147                    items[i].atom_type = Some("mord".to_string());
148                }
149            _ => (),
150        }
151    }
152
153    if n >= 1 && items[n - 1].atom_type.as_deref() == Some("mbin") {
154        items[n - 1].atom_type = Some("mord".to_string());
155    }
156
157    items
158}
159
160/// Join spacable items into a string, inserting spacing between adjacent
161/// non-space atoms according to the given `SpacingSpec`.
162pub fn join_with_spacing(items: &[SpacableItem], tight: bool, spec: &SpacingSpec) -> String {
163    let n = items.len();
164    if n == 0 {
165        return String::new();
166    }
167    if n == 1 {
168        return items[0].text.clone();
169    }
170
171    let mut result = items[0].text.clone();
172    for i in 1..n {
173        let prev = &items[i - 1];
174        let curr = &items[i];
175        match (&prev.atom_type, &curr.atom_type) {
176            (Some(left), Some(right)) => {
177                if let Some(space) = math_spacing(left, right, tight, spec) {
178                    result.push_str(&space);
179                }
180                result.push_str(&curr.text);
181            }
182            _ => result.push_str(&curr.text),
183        }
184    }
185    result
186}