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 = 0,
14    Centered = 1,
15    RightJustified = 2,
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}
114
115impl<'arena> ColumnGenerator<'arena> {
116    pub fn new_predefined(align: Alignment) -> Self {
117        ColumnGenerator {
118            typ: AlignmentType::Predefined(align),
119            column_idx: 0,
120            row_idx: 0,
121            row_border_top: None,
122        }
123    }
124
125    pub fn new_custom(array_spec: &'arena ArraySpec<'arena>) -> Self {
126        ColumnGenerator {
127            typ: AlignmentType::Custom(array_spec),
128            column_idx: 0,
129            row_idx: 0,
130            row_border_top: None,
131        }
132    }
133
134    pub fn new_multline(num_rows: NonZeroU16) -> Self {
135        ColumnGenerator {
136            typ: AlignmentType::MultLine(num_rows),
137            column_idx: 0,
138            row_idx: 0,
139            row_border_top: None,
140        }
141    }
142
143    pub fn reset_to_new_row(&mut self) {
144        self.column_idx = 0;
145        self.row_idx += 1;
146    }
147
148    /// Set the top border applied to each cell of the row that is about to be generated.
149    pub fn set_row_border_top(&mut self, border_top: Option<LineType>) {
150        self.row_border_top = border_top;
151    }
152
153    pub fn write_next_mtd(
154        &mut self,
155        s: &mut String,
156        indent_num: usize,
157        indentation: Indentation,
158    ) -> Result<(), core::fmt::Error> {
159        new_line_and_indent(s, indent_num, indentation);
160        let column_idx = self.column_idx;
161        self.column_idx += 1;
162        // Top border (from `\hline`/`\hdashline`) applied to every cell of the current row.
163        // When non-empty, the plain `<mtd>` fast paths are replaced by a styled cell, and the
164        // border is injected at the start of every other cell's style.
165        let border_top = match self.row_border_top {
166            None => "",
167            Some(LineType::Solid) => BORDER_TOP_SOLID,
168            Some(LineType::Dashed) => BORDER_TOP_DASHED,
169        };
170        match self.typ {
171            AlignmentType::Predefined(align) => {
172                let is_even = column_idx.is_multiple_of(2);
173                match align {
174                    Alignment::Cases => {
175                        write!(
176                            s,
177                            "{MTD_OPEN_STYLE}{border_top}{LEFT_ALIGN}{PADDING_RIGHT_ZERO}"
178                        )?;
179                        if !is_even {
180                            write!(s, "padding-left:1em;")?;
181                        }
182                        write!(s, "{MTD_CLOSE_STYLE}")?;
183                    }
184                    Alignment::Centered => {
185                        write_simple_mtd(s, border_top)?;
186                    }
187                    Alignment::Alternating => {
188                        write!(s, "{MTD_OPEN_STYLE}{border_top}")?;
189                        if is_even {
190                            write!(s, "{RIGHT_ALIGN}{PADDING_RIGHT_ZERO}")?;
191                        } else {
192                            write!(s, "{LEFT_ALIGN}{PADDING_LEFT_ZERO}")?;
193                        }
194                        write!(s, "{MTD_CLOSE_STYLE}")?;
195                    }
196                }
197            }
198            AlignmentType::Custom(array_spec) => {
199                static DEFAULT_COLUMN_SPEC: ColumnSpecEntry = ColumnSpecEntry::WithContent {
200                    alignment: ColumnAlignment::Centered,
201                    border_right: None,
202                };
203                let mut column_spec = array_spec
204                    .column_spec
205                    .get(column_idx)
206                    .unwrap_or(&DEFAULT_COLUMN_SPEC);
207                while let ColumnSpecEntry::OnlyLine(line_type) = column_spec {
208                    column_spec = array_spec
209                        .column_spec
210                        .get(self.column_idx)
211                        .unwrap_or(&DEFAULT_COLUMN_SPEC);
212                    self.column_idx += 1;
213                    write!(s, "{MTD_OPEN_STYLE}{border_top}")?;
214                    match line_type {
215                        LineType::Solid => {
216                            write!(s, "{BORDER_RIGHT_SOLID}")?;
217                        }
218                        LineType::Dashed => {
219                            write!(s, "{BORDER_RIGHT_DASHED}")?;
220                        }
221                    }
222                    if array_spec.is_sub {
223                        write!(s, "{PADDING_TOP_BOTTOM_ZERO}")?;
224                    }
225                    write!(s, "padding-left: 0.1em;padding-right: 0.1em;")?;
226                    write!(s, "\"></mtd>")?;
227                    new_line_and_indent(s, indent_num, indentation);
228                }
229                match column_spec {
230                    ColumnSpecEntry::WithContent {
231                        alignment,
232                        border_right,
233                    } => {
234                        if matches!(alignment, ColumnAlignment::Centered)
235                            && border_right.is_none()
236                            && !array_spec.is_sub
237                        {
238                            write_simple_mtd(s, border_top)?;
239                            return Ok(());
240                        }
241                        write!(s, "{MTD_OPEN_STYLE}{border_top}")?;
242                        match alignment {
243                            ColumnAlignment::LeftJustified => {
244                                write!(s, "{LEFT_ALIGN}")?;
245                            }
246                            ColumnAlignment::Centered => {}
247                            ColumnAlignment::RightJustified => {
248                                write!(s, "{RIGHT_ALIGN}")?;
249                            }
250                        }
251                        match border_right {
252                            Some(LineType::Solid) => {
253                                write!(s, "{BORDER_RIGHT_SOLID}")?;
254                            }
255                            Some(LineType::Dashed) => {
256                                write!(s, "{BORDER_RIGHT_DASHED}")?;
257                            }
258                            _ => {}
259                        }
260                        if array_spec.is_sub {
261                            write!(s, "{PADDING_TOP_BOTTOM_ZERO}")?;
262                        }
263                        write!(s, "{MTD_CLOSE_STYLE}")?;
264                    }
265                    ColumnSpecEntry::OnlyLine(_) => {}
266                }
267            }
268            AlignmentType::MultLine(num_rows) => {
269                let row_idx = self.row_idx;
270                // Multline is left-aligned for the first row, right-aligned for the last row,
271                // and centered for all other rows.
272                if row_idx == 0 {
273                    write!(
274                        s,
275                        "{MTD_OPEN_STYLE}{border_top}{LEFT_ALIGN}{MTD_CLOSE_STYLE}"
276                    )?;
277                } else if row_idx + 1 == (num_rows.get() as usize) {
278                    write!(
279                        s,
280                        "{MTD_OPEN_STYLE}{border_top}{RIGHT_ALIGN}{MTD_CLOSE_STYLE}"
281                    )?;
282                } else {
283                    write_simple_mtd(s, border_top)?;
284                }
285            }
286        }
287        Ok(())
288    }
289}
290
291/// Write a centered cell (`<mtd>`) with no other styling, adding a top border if one is set
292/// for the current row.
293fn write_simple_mtd(s: &mut String, border_top: &str) -> core::fmt::Result {
294    if border_top.is_empty() {
295        write!(s, "{SIMPLE_CENTERED}")
296    } else {
297        write!(s, "{MTD_OPEN_STYLE}{border_top}{MTD_CLOSE_STYLE}")
298    }
299}