Skip to main content

typst_library/math/ir/
process.rs

1use std::iter;
2use std::ops::{Deref, DerefMut};
3
4use smallvec::SmallVec;
5use unicode_math_class::MathClass;
6
7use super::item::{MathItem, RawMathItem};
8use super::multiline::{AlignedRow, split_at_align};
9use crate::foundations::StyleChain;
10use crate::math::{MEDIUM, MathSize, THICK, THIN};
11
12/// The result of processing items for grouping.
13pub(crate) enum GroupResult<'a> {
14    /// Linebreaks weren't present and alignment points were pruned giving plain
15    /// items.
16    Flat(Vec<MathItem<'a>>),
17    /// Linebreaks were present, and items are split into padded rows and
18    /// alignment columns.
19    Multiline(Vec<AlignedRow<'a>>),
20}
21
22/// Processes raw items for grouping.
23///
24/// The `closing` parameter indicates whether a closing delimiter follows the
25/// items. The `pad` parameter indicates whether, when linebreaks are present,
26/// the resulting rows should be padded to have the same length. The `split`
27/// parameter indicates whether alignment points should split the items into
28/// columns, even when no linebreaks are present.
29pub(crate) fn process_group<'a, I>(
30    items: I,
31    styles: StyleChain<'a>,
32    closing: bool,
33    pad: bool,
34    split: bool,
35) -> GroupResult<'a>
36where
37    I: IntoIterator<Item = RawMathItem<'a>>,
38    I::IntoIter: ExactSizeIterator,
39{
40    let preprocessed = preprocess(items, closing, false);
41    if preprocessed.linebreaks > 0 || (split && preprocessed.has_align) {
42        let mut row = Vec::new();
43        let mut rows: Vec<_> = preprocessed
44            .items
45            .into_iter()
46            .chain(iter::once(RawMathItem::Linebreak))
47            .filter_map(|item| match item {
48                RawMathItem::Linebreak => Some(split_at_align(row.drain(..), styles)),
49                other => {
50                    row.push(other);
51                    None
52                }
53            })
54            .collect();
55
56        if pad {
57            let ncols = rows.iter().map(AlignedRow::len).max().unwrap_or_default();
58            for row in &mut rows {
59                row.pad_to(ncols, styles);
60            }
61        }
62
63        GroupResult::Multiline(rows)
64    } else {
65        GroupResult::Flat(
66            preprocessed
67                .items
68                .into_iter()
69                .filter(|item| !matches!(item, RawMathItem::Align))
70                .map(RawMathItem::into_item)
71                .collect::<Option<_>>()
72                .unwrap(),
73        )
74    }
75}
76
77/// The result of processing items for a table cell.
78pub(crate) struct TableCellResult<'a> {
79    /// Linebreaks stripped, and items split at alignment points.
80    pub sub_columns: AlignedRow<'a>,
81    /// Whether the original input contained any linebreaks.
82    pub had_linebreaks: bool,
83}
84
85/// Processes raw items for a table cell.
86pub(crate) fn process_table_cell<'a, I>(
87    items: I,
88    styles: StyleChain<'a>,
89) -> TableCellResult<'a>
90where
91    I: IntoIterator<Item = RawMathItem<'a>>,
92    I::IntoIter: ExactSizeIterator,
93{
94    let preprocessed = preprocess(items, false, true);
95    let sub_columns = if preprocessed.has_align {
96        split_at_align(preprocessed.items, styles)
97    } else {
98        AlignedRow::new(vec![MathItem::wrap(
99            preprocessed
100                .items
101                .into_iter()
102                .map(RawMathItem::into_item)
103                .collect::<Option<_>>()
104                .unwrap(),
105            styles,
106        )])
107    };
108    TableCellResult {
109        sub_columns,
110        had_linebreaks: preprocessed.had_linebreaks,
111    }
112}
113
114/// Internal result of the preprocessing logic.
115struct Preprocessed<'a> {
116    items: SmallVec<[RawMathItem<'a>; 8]>,
117    had_linebreaks: bool,
118    has_align: bool,
119    linebreaks: u32,
120}
121
122/// Takes the given [`RawMathItem`]s and processes the spacing between them.
123///
124/// The `closing` parameter indicates whether a closing delimiter follows the
125/// items. The `strip_linebreaks` parameter indicates whether linebreaks should
126/// be discarded.
127///
128/// The behavior of spacing around alignment points is subtle and differs from
129/// the `align` environment in amsmath. The current policy is:
130/// > always put the correct spacing between items separated by an alignment
131/// > point, and move the spacing between items in different columns of a
132/// > (right-aligned, left-aligned) pair to the right-aligned column
133///
134/// This is handled in the [`split_at_align`] function.
135fn preprocess<'a, I>(items: I, closing: bool, strip_linebreaks: bool) -> Preprocessed<'a>
136where
137    I: IntoIterator<Item = RawMathItem<'a>>,
138    I::IntoIter: ExactSizeIterator,
139{
140    let iter = items.into_iter();
141    let mut resolved = MathBuffer::with_capacity(iter.len());
142
143    let mut last: Option<usize> = None;
144    let mut space: Option<MathItem> = None;
145    let mut had_linebreaks = false;
146    let mut has_align = false;
147    let mut linebreaks: u32 = 0;
148
149    for item in iter {
150        match item {
151            // Tags don't affect layout.
152            RawMathItem::Item(MathItem::Tag(_)) => {
153                resolved.push(item);
154                continue;
155            }
156            // Keep space only if supported by spaced items.
157            RawMathItem::Item(MathItem::Space) => {
158                if last.is_some() {
159                    space = item.into_item();
160                }
161                continue;
162            }
163
164            // Explicit spacing disables automatic spacing.
165            RawMathItem::Item(MathItem::Spacing(width, font_size, weak)) => {
166                last = None;
167                space = None;
168
169                if weak {
170                    let Some(resolved_last) = resolved.last_mut() else {
171                        continue;
172                    };
173                    if let RawMathItem::Item(MathItem::Spacing(
174                        prev_width,
175                        prev_font_size,
176                        true,
177                    )) = resolved_last
178                    {
179                        if prev_width.at(*prev_font_size) < width.at(font_size) {
180                            *prev_width = width;
181                            *prev_font_size = font_size;
182                        }
183                        continue;
184                    }
185                }
186
187                resolved.push(item);
188                continue;
189            }
190
191            // Alignment points are resolved later.
192            RawMathItem::Align => {
193                has_align = true;
194                resolved.push(item);
195                continue;
196            }
197
198            // New line, new things.
199            RawMathItem::Linebreak => {
200                had_linebreaks = true;
201                if strip_linebreaks {
202                    continue;
203                }
204                linebreaks += 1;
205                resolved.push(item);
206                space = None;
207                last = None;
208                continue;
209            }
210
211            _ => {}
212        }
213
214        let mut item = item.into_item().unwrap();
215
216        // Convert variable operators into binary operators if something
217        // precedes them and they are not preceded by a operator or comparator.
218        if item.class() == MathClass::Vary
219            && let Some(RawMathItem::Item(prev)) = last.map(|i| &resolved[i])
220            && matches!(
221                prev.class(),
222                MathClass::Normal
223                    | MathClass::Alphabetic
224                    | MathClass::Closing
225                    | MathClass::Fence
226            )
227        {
228            item.set_class(MathClass::Binary);
229        }
230
231        // Insert spacing between the last and this non-ignorant item.
232        if !item.is_ignorant() {
233            if let Some(i) = last
234                && let RawMathItem::Item(ref mut prev) = resolved[i]
235                && let Some(s) = spacing(prev, space.take(), &mut item)
236            {
237                resolved.insert(i + 1, RawMathItem::Item(s));
238            }
239
240            last = Some(resolved.len());
241        }
242
243        resolved.push(RawMathItem::Item(item));
244    }
245
246    // Apply closing punctuation spacing if applicable.
247    if closing
248        && let Some(RawMathItem::Item(item)) = resolved.last_mut()
249        && item.rclass() == MathClass::Punctuation
250        && item.size().is_none_or(|s| s > MathSize::Script)
251    {
252        item.set_rspace(Some(THIN))
253    } else if let Some(idx) = resolved.last_index()
254        && let RawMathItem::Item(MathItem::Spacing(_, _, true)) = resolved.0[idx]
255    {
256        resolved.0.remove(idx);
257    }
258
259    // Strip final trailing linebreak.
260    if !closing
261        && let Some(idx) = resolved.last_index()
262        && matches!(resolved.0[idx], RawMathItem::Linebreak)
263    {
264        resolved.0.remove(idx);
265        linebreaks -= 1;
266    }
267
268    Preprocessed {
269        items: resolved.0,
270        had_linebreaks,
271        has_align,
272        linebreaks,
273    }
274}
275
276/// Computes the spacing between two adjacent math items.
277fn spacing<'a>(
278    l: &mut MathItem,
279    space: Option<MathItem<'a>>,
280    r: &mut MathItem,
281) -> Option<MathItem<'a>> {
282    use MathClass::*;
283
284    let script = |f: &MathItem| f.size().is_some_and(|s| s <= MathSize::Script);
285
286    match (l.rclass(), r.lclass()) {
287        // No spacing before punctuation; thin spacing after punctuation, unless
288        // in script size.
289        (_, Punctuation) => {}
290        (Punctuation, _) if !script(l) => l.set_rspace(Some(THIN)),
291
292        // No spacing after opening delimiters and before closing delimiters.
293        (Opening, _) | (_, Closing) => {}
294
295        // Thick spacing around relations, unless followed by a another relation
296        // or in script size.
297        (Relation, Relation) => {}
298        (Relation, _) if !script(l) => l.set_rspace(Some(THICK)),
299        (_, Relation) if !script(r) => r.set_lspace(Some(THICK)),
300
301        // Medium spacing around binary operators, unless in script size.
302        (Binary, _) if !script(l) => l.set_rspace(Some(MEDIUM)),
303        (_, Binary) if !script(r) => r.set_lspace(Some(MEDIUM)),
304
305        // Thin spacing around large operators, unless to the left of
306        // an opening delimiter. TeXBook, p170
307        (Large, Opening | Fence) => {}
308        (Large, _) => l.set_rspace(Some(THIN)),
309
310        (_, Large) => r.set_lspace(Some(THIN)),
311
312        // Spacing around spaced frames.
313        _ if (l.is_spaced() || r.is_spaced()) => return space,
314
315        _ => {}
316    };
317
318    None
319}
320
321/// A wrapper around `SmallVec<[RawMathItem; 8]>` that ignores ignorant items in
322/// some access methods.
323struct MathBuffer<'a>(SmallVec<[RawMathItem<'a>; 8]>);
324
325impl<'a> MathBuffer<'a> {
326    /// Creates a new buffer with the given capacity.
327    fn with_capacity(size: usize) -> Self {
328        Self(SmallVec::with_capacity(size))
329    }
330
331    /// Returns a mutable reference to the last non-ignorant item.
332    fn last_mut(&mut self) -> Option<&mut RawMathItem<'a>> {
333        self.0.iter_mut().rev().find(|i| !i.is_ignorant())
334    }
335
336    /// Returns the physical index of the last non-ignorant item.
337    fn last_index(&self) -> Option<usize> {
338        self.0.iter().rposition(|i| !i.is_ignorant())
339    }
340}
341
342impl<'a> Deref for MathBuffer<'a> {
343    type Target = SmallVec<[RawMathItem<'a>; 8]>;
344
345    fn deref(&self) -> &Self::Target {
346        &self.0
347    }
348}
349
350impl<'a> DerefMut for MathBuffer<'a> {
351    fn deref_mut(&mut self) -> &mut Self::Target {
352        &mut self.0
353    }
354}