Skip to main content

typst_library/math/ir/
multiline.rs

1use std::vec;
2
3use typst_syntax::Span;
4use unicode_math_class::MathClass;
5
6use super::item::{FencedBody, FencedItem, MathItem, RawMathItem, SharedFenceSizing};
7use crate::foundations::StyleChain;
8
9/// A row split at alignment points into grouped (single-item) columns.
10#[derive(Debug)]
11pub struct AlignedRow<'a>(Vec<MathItem<'a>>);
12
13impl<'a> AlignedRow<'a> {
14    /// Create a row from aligned columns.
15    pub(crate) fn new(columns: Vec<MathItem<'a>>) -> Self {
16        Self(columns)
17    }
18
19    /// The number of columns in this row.
20    pub fn len(&self) -> usize {
21        self.0.len()
22    }
23
24    /// Whether this row is empty.
25    pub fn is_empty(&self) -> bool {
26        self.0.is_empty()
27    }
28
29    /// Pad the row with empty group items until its length reaches `length`.
30    pub(super) fn pad_to(&mut self, length: usize, styles: StyleChain<'a>) {
31        while self.0.len() < length {
32            self.0.push(MathItem::wrap(Vec::new(), styles));
33        }
34    }
35
36    /// Returns an iterator over references to the math items.
37    pub fn iter(&self) -> std::slice::Iter<'_, MathItem<'a>> {
38        self.0.iter()
39    }
40
41    /// Returns a reference to the item at the given index.
42    pub fn get(&self, index: usize) -> Option<&MathItem<'a>> {
43        self.0.get(index)
44    }
45}
46
47impl<'a> IntoIterator for AlignedRow<'a> {
48    type Item = MathItem<'a>;
49    type IntoIter = vec::IntoIter<MathItem<'a>>;
50
51    fn into_iter(self) -> Self::IntoIter {
52        self.0.into_iter()
53    }
54}
55
56/// Splits a fenced item's body by alignment points and linebreaks into
57/// segments.
58pub(super) fn expand_multiline_fence<'a>(
59    rows: Vec<AlignedRow<'a>>,
60    mut open: Option<MathItem<'a>>,
61    mut close: Option<MathItem<'a>>,
62    styles: StyleChain<'a>,
63    span: Span,
64) -> Vec<RawMathItem<'a>> {
65    let nrows = rows.len();
66    let mut bodies = Vec::new();
67    let mut row_lengths = Vec::with_capacity(nrows);
68
69    for row in rows {
70        row_lengths.push(row.len());
71        bodies.extend(row);
72    }
73
74    let ncells: usize = row_lengths.iter().sum();
75    let sizing = SharedFenceSizing::new(bodies, styles);
76
77    let mut result = Vec::with_capacity((2 * ncells).saturating_sub(1));
78    let mut body_idx = 0;
79    for (row_idx, &ncols) in row_lengths.iter().enumerate() {
80        if row_idx > 0 {
81            result.push(RawMathItem::Linebreak);
82        }
83
84        for col_idx in 0..ncols {
85            if col_idx > 0 {
86                result.push(RawMathItem::Align);
87            }
88
89            let is_first = row_idx == 0 && col_idx == 0;
90            let is_last = row_idx + 1 == nrows && col_idx + 1 == ncols;
91            result.push(
92                FencedItem::create(
93                    open.take_if(|_| is_first),
94                    close.take_if(|_| is_last),
95                    FencedBody::shared(body_idx, sizing.clone()),
96                    true,
97                    styles,
98                    span,
99                )
100                .into(),
101            );
102
103            body_idx += 1;
104        }
105    }
106
107    result
108}
109
110/// Splits preprocessed items at alignment point markers into columns, marking
111/// items which should have their spacing moved in different columns of a
112/// (right-aligned, left-aligned) pair to the right-aligned column.
113pub(crate) fn split_at_align<'a, I>(items: I, styles: StyleChain<'a>) -> AlignedRow<'a>
114where
115    I: IntoIterator<Item = RawMathItem<'a>>,
116{
117    let mut cols = vec![vec![]];
118
119    let mut at_boundary = false;
120    for raw in items {
121        match raw {
122            RawMathItem::Align => {
123                cols.push(Vec::new());
124                at_boundary = true;
125            }
126            RawMathItem::Linebreak => unreachable!(),
127            RawMathItem::Item(mut item) => {
128                // If we just passed an alignment point, check if this item is
129                // semantically infix.
130                if at_boundary && !item.is_ignorant() {
131                    if cols.len().is_multiple_of(2)
132                        && matches!(item.class(), MathClass::Relation | MathClass::Binary)
133                        && let MathItem::Component(ref mut comp) = item
134                    {
135                        comp.props.align_form_infix = true;
136                    }
137
138                    at_boundary = false;
139                }
140
141                cols.last_mut().unwrap().push(item);
142            }
143        }
144    }
145
146    AlignedRow::new(cols.into_iter().map(|col| MathItem::wrap(col, styles)).collect())
147}