Skip to main content

math_core_renderer_internal/
table.rs

1use alloc::string::String;
2use core::{fmt::Write, num::NonZeroU16};
3
4#[cfg(feature = "serde")]
5use serde::Serialize;
6
7use crate::ast::Indentation;
8use crate::fmt::new_line_and_indent;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
11#[cfg_attr(feature = "serde", derive(Serialize))]
12pub enum ColumnAlignment {
13    LeftJustified = 1,
14    Centered,
15    RightJustified,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
19#[cfg_attr(feature = "serde", derive(Serialize))]
20pub enum LineType {
21    Solid = 3,
22    Dashed = 4,
23}
24
25/// A column spec is the result of parsing a column specifier, like `{|c|l|}` for an array.
26/// Each entry can either be a column with content (with an alignment and an optional line to the
27/// right), or a column that is just a line (with no content).
28#[derive(Debug, Clone, Copy, PartialEq)]
29#[cfg_attr(feature = "serde", derive(Serialize))]
30pub enum ColumnSpecEntry {
31    WithContent {
32        alignment: ColumnAlignment,
33        border_right: Option<LineType>,
34    },
35    OnlyLine(LineType),
36}
37
38pub type ColumnSpec<'arena> = &'arena [ColumnSpecEntry];
39
40#[derive(Debug, PartialEq)]
41#[cfg_attr(feature = "serde", derive(Serialize))]
42pub struct ArraySpec<'arena> {
43    /// This field determines whether we need to draw a line to the left of the first column. If
44    /// `None`, no line is drawn. If `Some(LineType)`, a line of that type is drawn.
45    pub border_left: Option<LineType>,
46    /// This field determines whether we need to draw a line above the first row. If
47    /// `None`, no line is drawn. If `Some(LineType)`, a line of that type is drawn.
48    pub border_top: Option<LineType>,
49    /// `true` if this is a subarray (i.e., a `subarray` environment in LaTeX). Subarrays have
50    /// different padding rules.
51    pub is_sub: bool,
52    pub column_spec: ColumnSpec<'arena>,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq)]
56#[cfg_attr(feature = "serde", derive(Serialize))]
57pub enum Alignment {
58    Centered,
59    Cases,
60    Alternating,
61}
62
63/// Equation-number / label metadata for the last row of a numbered environment.
64///
65/// The last row of `align`, `gather`, `equation`, `multline`, etc. has no trailing
66/// `\\` row separator, so its tag (equation number) and link target (from `\label`)
67/// can't ride on a `Node::RowSeparator`. They're arena-allocated and passed
68/// through `Node::EquationArray` / `Node::MultLine` instead.
69#[derive(Debug)]
70#[cfg_attr(feature = "serde", derive(Serialize))]
71pub struct RowLabelInfo<'arena> {
72    pub tag: EquationTag<'arena>,
73    pub link_target: Option<&'arena str>,
74}
75
76/// The equation number (or custom text from `\tag`) shown next to an equation.
77#[derive(Debug, Clone, Copy, PartialEq)]
78#[cfg_attr(feature = "serde", derive(Serialize))]
79pub struct EquationTag<'arena> {
80    pub text: &'arena str,
81    /// `true` if the text should be surrounded by parentheses when rendered.
82    /// Tags set with `\tag*` are rendered without parentheses.
83    pub parenthesized: bool,
84}
85
86enum AlignmentType<'arena> {
87    Predefined(Alignment),
88    Custom(&'arena ArraySpec<'arena>),
89    MultLine(NonZeroU16),
90}
91
92const MTD_OPEN_STYLE: &str = "<mtd style=\"";
93const MTD_CLOSE_STYLE: &str = "\">";
94const LEFT_ALIGN: &str = "text-align: left;justify-items: start;";
95pub const RIGHT_ALIGN: &str = "text-align: right;justify-items: end;";
96const PADDING_RIGHT_ZERO: &str = "padding-right: 0;";
97const PADDING_LEFT_ZERO: &str = "padding-left: 0;";
98const PADDING_TOP_BOTTOM_ZERO: &str = "padding-top: 0;padding-bottom: 0;";
99const BORDER_RIGHT_SOLID: &str = "border-right: 0.05em solid currentcolor;";
100const BORDER_RIGHT_DASHED: &str = "border-right: 0.05em dashed currentcolor;";
101pub const BORDER_TOP_SOLID: &str = "border-top: 0.05em solid currentcolor;";
102pub const BORDER_TOP_DASHED: &str = "border-top: 0.05em dashed currentcolor;";
103const SIMPLE_CENTERED: &str = "<mtd>";
104
105pub struct ColumnGenerator<'arena> {
106    typ: AlignmentType<'arena>,
107    column_idx: usize,
108    row_idx: usize,
109    /// The top border (from `\hline`/`\hdashline`) applied to every cell of the current row.
110    /// MathML `<mtr>` borders aren't rendered by all browsers (notably Firefox), so the rule is
111    /// drawn per-cell instead.
112    row_border_top: Option<LineType>,
113    shove: Option<ColumnAlignment>,
114}
115
116impl<'arena> ColumnGenerator<'arena> {
117    pub fn new_predefined(align: Alignment) -> Self {
118        ColumnGenerator {
119            typ: AlignmentType::Predefined(align),
120            column_idx: 0,
121            row_idx: 0,
122            row_border_top: None,
123            shove: None,
124        }
125    }
126
127    pub fn new_custom(array_spec: &'arena ArraySpec<'arena>) -> Self {
128        ColumnGenerator {
129            typ: AlignmentType::Custom(array_spec),
130            column_idx: 0,
131            row_idx: 0,
132            row_border_top: None,
133            shove: None,
134        }
135    }
136
137    pub fn new_multline(num_rows: NonZeroU16, shove: Option<ColumnAlignment>) -> Self {
138        ColumnGenerator {
139            typ: AlignmentType::MultLine(num_rows),
140            column_idx: 0,
141            row_idx: 0,
142            row_border_top: None,
143            shove,
144        }
145    }
146
147    /// Start a new row in the table. This resets the column index to 0, increments the row index,
148    /// and sets the top border and shove for the new row.
149    pub fn start_new_row(&mut self, border_top: Option<LineType>, shove: Option<ColumnAlignment>) {
150        self.column_idx = 0;
151        self.row_idx += 1;
152        self.row_border_top = border_top;
153        self.shove = shove;
154    }
155
156    pub fn write_next_mtd(
157        &mut self,
158        s: &mut String,
159        indent_num: usize,
160        indentation: Indentation,
161    ) -> Result<(), core::fmt::Error> {
162        new_line_and_indent(s, indent_num, indentation);
163        let column_idx = self.column_idx;
164        self.column_idx += 1;
165        // Top border (from `\hline`/`\hdashline`) applied to every cell of the current row.
166        // When non-empty, the plain `<mtd>` fast paths are replaced by a styled cell, and the
167        // border is injected at the start of every other cell's style.
168        let border_top = match self.row_border_top {
169            None => "",
170            Some(LineType::Solid) => BORDER_TOP_SOLID,
171            Some(LineType::Dashed) => BORDER_TOP_DASHED,
172        };
173        match self.typ {
174            AlignmentType::Predefined(align) => {
175                let is_even = column_idx.is_multiple_of(2);
176                match align {
177                    Alignment::Cases => {
178                        write!(
179                            s,
180                            "{MTD_OPEN_STYLE}{border_top}{LEFT_ALIGN}{PADDING_RIGHT_ZERO}"
181                        )?;
182                        if !is_even {
183                            write!(s, "padding-left:1em;")?;
184                        }
185                        write!(s, "{MTD_CLOSE_STYLE}")?;
186                    }
187                    Alignment::Centered => {
188                        write_simple_mtd(s, border_top)?;
189                    }
190                    Alignment::Alternating => {
191                        write!(s, "{MTD_OPEN_STYLE}{border_top}")?;
192                        if is_even {
193                            write!(s, "{RIGHT_ALIGN}{PADDING_RIGHT_ZERO}")?;
194                        } else {
195                            write!(s, "{LEFT_ALIGN}{PADDING_LEFT_ZERO}")?;
196                        }
197                        write!(s, "{MTD_CLOSE_STYLE}")?;
198                    }
199                }
200            }
201            AlignmentType::Custom(array_spec) => {
202                static DEFAULT_COLUMN_SPEC: ColumnSpecEntry = ColumnSpecEntry::WithContent {
203                    alignment: ColumnAlignment::Centered,
204                    border_right: None,
205                };
206                let mut column_spec = array_spec
207                    .column_spec
208                    .get(column_idx)
209                    .unwrap_or(&DEFAULT_COLUMN_SPEC);
210                while let ColumnSpecEntry::OnlyLine(line_type) = column_spec {
211                    column_spec = array_spec
212                        .column_spec
213                        .get(self.column_idx)
214                        .unwrap_or(&DEFAULT_COLUMN_SPEC);
215                    self.column_idx += 1;
216                    write!(s, "{MTD_OPEN_STYLE}{border_top}")?;
217                    match line_type {
218                        LineType::Solid => {
219                            write!(s, "{BORDER_RIGHT_SOLID}")?;
220                        }
221                        LineType::Dashed => {
222                            write!(s, "{BORDER_RIGHT_DASHED}")?;
223                        }
224                    }
225                    if array_spec.is_sub {
226                        write!(s, "{PADDING_TOP_BOTTOM_ZERO}")?;
227                    }
228                    write!(s, "padding-left: 0.1em;padding-right: 0.1em;")?;
229                    write!(s, "\"></mtd>")?;
230                    new_line_and_indent(s, indent_num, indentation);
231                }
232                match column_spec {
233                    ColumnSpecEntry::WithContent {
234                        alignment,
235                        border_right,
236                    } => {
237                        if matches!(alignment, ColumnAlignment::Centered)
238                            && border_right.is_none()
239                            && !array_spec.is_sub
240                        {
241                            write_simple_mtd(s, border_top)?;
242                            return Ok(());
243                        }
244                        write!(s, "{MTD_OPEN_STYLE}{border_top}")?;
245                        match alignment {
246                            ColumnAlignment::LeftJustified => {
247                                write!(s, "{LEFT_ALIGN}")?;
248                            }
249                            ColumnAlignment::Centered => {}
250                            ColumnAlignment::RightJustified => {
251                                write!(s, "{RIGHT_ALIGN}")?;
252                            }
253                        }
254                        match border_right {
255                            Some(LineType::Solid) => {
256                                write!(s, "{BORDER_RIGHT_SOLID}")?;
257                            }
258                            Some(LineType::Dashed) => {
259                                write!(s, "{BORDER_RIGHT_DASHED}")?;
260                            }
261                            _ => {}
262                        }
263                        if array_spec.is_sub {
264                            write!(s, "{PADDING_TOP_BOTTOM_ZERO}")?;
265                        }
266                        write!(s, "{MTD_CLOSE_STYLE}")?;
267                    }
268                    ColumnSpecEntry::OnlyLine(_) => {}
269                }
270            }
271            AlignmentType::MultLine(num_rows) => {
272                let align = if let Some(shove) = self.shove {
273                    shove
274                } else {
275                    // Multline is left-aligned for the first row, right-aligned for the last row,
276                    // and centered for all other rows.
277                    let row_idx = self.row_idx;
278                    if row_idx == 0 {
279                        ColumnAlignment::LeftJustified
280                    } else if row_idx + 1 == (num_rows.get() as usize) {
281                        ColumnAlignment::RightJustified
282                    } else {
283                        ColumnAlignment::Centered
284                    }
285                };
286                match align {
287                    ColumnAlignment::LeftJustified => {
288                        write!(s, "{MTD_OPEN_STYLE}{LEFT_ALIGN}{MTD_CLOSE_STYLE}")?
289                    }
290                    ColumnAlignment::Centered => write_simple_mtd(s, "")?,
291                    ColumnAlignment::RightJustified => {
292                        write!(s, "{MTD_OPEN_STYLE}{RIGHT_ALIGN}{MTD_CLOSE_STYLE}")?
293                    }
294                }
295            }
296        }
297        Ok(())
298    }
299}
300
301/// Write a centered cell (`<mtd>`) with no other styling, adding a top border if one is set
302/// for the current row.
303fn write_simple_mtd(s: &mut String, border_top: &str) -> core::fmt::Result {
304    if border_top.is_empty() {
305        write!(s, "{SIMPLE_CENTERED}")
306    } else {
307        write!(s, "{MTD_OPEN_STYLE}{border_top}{MTD_CLOSE_STYLE}")
308    }
309}