Skip to main content

math_core_renderer_internal/
table.rs

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