Skip to main content

math_core_renderer_internal/
ast.rs

1use std::fmt::{self, Write};
2use std::num::NonZeroU16;
3
4#[cfg(feature = "serde")]
5use serde::Serialize;
6
7use crate::fmt::new_line_and_indent;
8use crate::itoa::append_u8_as_hex;
9use crate::length::{Length, LengthUnit, LengthValue};
10use crate::symbol::MathMLOperator;
11use crate::table::{Alignment, ArraySpec, ColumnGenerator, LineType, RIGHT_ALIGN, RowLabelInfo};
12use crate::{
13    attribute::{
14        FracAttr, HtmlTextStyle, LetterAttr, MathSpacing, Notation, OpAttrs, RowAttr, Size, Style,
15    },
16    super_char::SuperChar,
17};
18
19/// A wrapper around `str` to do HTML escaping
20/// in the `Display` impl.
21#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22#[cfg_attr(feature = "serde", derive(Serialize))]
23#[cfg_attr(feature = "serde", serde(transparent))]
24#[repr(transparent)]
25struct EscapeHtml<'a>(&'a str);
26
27impl fmt::Display for EscapeHtml<'_> {
28    #[inline]
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        for c in self.0.chars() {
31            match c {
32                '<' => write!(f, "&lt;")?,
33                '>' => write!(f, "&gt;")?,
34                '&' => write!(f, "&amp;")?,
35                '"' => write!(f, "&quot;")?,
36                '\'' => write!(f, "&#39;")?,
37                _ => write!(f, "{c}")?,
38            }
39        }
40        Ok(())
41    }
42}
43
44/// Stores the contents of [`Node::AHref`].
45/// Needs to be a separate `struct` to keep [`Node`]
46/// 4 words in size
47#[derive(Clone, Copy, Debug)]
48#[cfg_attr(feature = "serde", derive(Serialize))]
49pub struct AHref<'arena> {
50    pub href: &'arena str,
51    pub text: &'arena str,
52}
53
54/// AST node
55#[derive(Debug)]
56#[cfg_attr(feature = "serde", derive(Serialize))]
57pub enum Node<'arena> {
58    /// `<mn>...</mn>`
59    Number(&'arena str),
60    /// `<mi>...</mi>` for a [`SuperChar`].
61    IdentifierChar(SuperChar, LetterAttr),
62    /// `<mi>...</mi>` for a string.
63    IdentifierStr(&'arena str),
64    /// `<mo>...</mo>` for a single character.
65    Operator {
66        op: MathMLOperator,
67        attrs: OpAttrs,
68        size: Option<Size>,
69        left: Option<MathSpacing>,
70        right: Option<MathSpacing>,
71    },
72    /// `<mo>...</mo>` for a string.
73    PseudoOp {
74        attrs: OpAttrs,
75        left: Option<MathSpacing>,
76        right: Option<MathSpacing>,
77        name: &'arena str,
78    },
79    /// `<mspace width="..."/>`
80    Space(Length),
81    /// `<msub>...</msub>`
82    Sub {
83        target: &'arena Node<'arena>,
84        symbol: &'arena Node<'arena>,
85    },
86    /// `<msup>...</msup>`
87    Sup {
88        target: &'arena Node<'arena>,
89        symbol: &'arena Node<'arena>,
90    },
91    /// `<msubsup>...</msubsup>`
92    SubSup {
93        target: &'arena Node<'arena>,
94        sub: &'arena Node<'arena>,
95        sup: &'arena Node<'arena>,
96    },
97    /// `<mover accent="true">...</mover>`
98    OverAccent(MathMLOperator, OpAttrs, &'arena Node<'arena>),
99    /// `<munder accentunder="true">...</munder>`
100    UnderAccent(MathMLOperator, OpAttrs, &'arena Node<'arena>),
101    /// `<mover>...</mover>`
102    Over {
103        symbol: &'arena Node<'arena>,
104        target: &'arena Node<'arena>,
105    },
106    /// `<munder>...</munder>`
107    Under {
108        symbol: &'arena Node<'arena>,
109        target: &'arena Node<'arena>,
110    },
111    /// `<munderover>...</munderover>`
112    UnderOver {
113        target: &'arena Node<'arena>,
114        under: &'arena Node<'arena>,
115        over: &'arena Node<'arena>,
116    },
117    /// `<msqrt>...</msqrt>`
118    Sqrt(&'arena Node<'arena>),
119    /// `<mroot>...</mroot>`
120    Root(&'arena Node<'arena>, &'arena Node<'arena>),
121    /// `<mfrac>...</mfrac>`
122    Frac {
123        /// Numerator
124        num: &'arena Node<'arena>,
125        /// Denominator
126        denom: &'arena Node<'arena>,
127        /// Line thickness
128        lt_value: LengthValue,
129        lt_unit: LengthUnit,
130        attr: Option<FracAttr>,
131    },
132    /// `<mrow>...</mrow>`
133    Row {
134        nodes: &'arena [&'arena Node<'arena>],
135        attr: Option<RowAttr>,
136    },
137    /// `<mtext>...</mtext>`.
138    /// The `str` gets HTML-escaped.
139    Text(Option<HtmlTextStyle>, &'arena str),
140    /// `<mtext><a href="...">...</mtext>`.
141    /// The link and text get HTML-escaped.
142    AHref(&'arena AHref<'arena>),
143    /// `<mtable>...</mtable>` for matrices and similar constructs
144    Table {
145        align: Alignment,
146        style: Option<Style>,
147        content: &'arena [&'arena Node<'arena>],
148    },
149    /// `<mtable>...</mtable>` for equation arrays like the `align` environment
150    EquationArray {
151        align: Alignment,
152        last_row_info: Option<&'arena RowLabelInfo<'arena>>,
153        content: &'arena [&'arena Node<'arena>],
154    },
155    /// `<mtable>...</mtable>` for the `multline` environment
156    MultLine {
157        num_rows: NonZeroU16,
158        last_row_info: Option<&'arena RowLabelInfo<'arena>>,
159        content: &'arena [&'arena Node<'arena>],
160    },
161    /// `<mtable>...</mtable>` for arrays
162    Array {
163        style: Option<Style>,
164        array_spec: &'arena ArraySpec<'arena>,
165        content: &'arena [&'arena Node<'arena>],
166    },
167    /// `<mtd>...</mtd>`
168    ColumnSeparator,
169    /// `<mtr>...</mtr>`
170    RowSeparator {
171        tag: Option<NonZeroU16>,
172        link_target: Option<&'arena str>,
173    },
174    /// `<menclose>...</menclose>`
175    Enclose {
176        content: &'arena Node<'arena>,
177        notation: Notation,
178    },
179    Multiscript {
180        base: &'arena Node<'arena>,
181        sub: Option<&'arena Node<'arena>>,
182        sup: Option<&'arena Node<'arena>>,
183    },
184    /// This node is used for displaying unknown commands.
185    /// We are not using `<merror>` here because it's rendered with a red border and a yellow
186    /// background, which is not desirable for our use case. We'll just render it as `<mtext>`
187    /// with a custom style.
188    UnknownCommand(&'arena str),
189    HardcodedMathML(&'static str),
190}
191
192#[cfg(target_arch = "wasm32")]
193static_assertions::assert_eq_size!(Node<'_>, [usize; 4]);
194
195macro_rules! writeln_indent {
196    ($buf:expr, $indent:expr, $($tail:tt)+) => {
197        new_line_and_indent($buf, $indent);
198        write!($buf, $($tail)+)?
199    };
200}
201
202impl Node<'_> {
203    pub fn emit(&self, s: &mut String, base_indent: usize) -> std::fmt::Result {
204        // Compute the indentation for the children of the node.
205        let child_indent = if base_indent > 0 {
206            base_indent.saturating_add(1)
207        } else {
208            0
209        };
210
211        // Get the base indentation out of the way.
212        new_line_and_indent(s, base_indent);
213
214        match self {
215            Node::Number(number) => {
216                write!(s, "<mn>{number}</mn>")?;
217            }
218            Node::IdentifierChar(letter, attr) => {
219                let is_upright = matches!(attr, LetterAttr::ForcedUpright);
220                // Only set "mathvariant" if we are not transforming the letter.
221                let write_mrow;
222                if is_upright {
223                    write!(s, "<mrow><mspace/><mi mathvariant=\"normal\">")?;
224                    write_mrow = true;
225                } else if letter.try_as_char().is_none() {
226                    // check if multi-char
227                    write!(s, "<mrow><mspace/><mi>")?;
228                    write_mrow = true;
229                } else {
230                    write!(s, "<mi>")?;
231                    write_mrow = false;
232                }
233                let c = *letter;
234                write!(s, "{c}</mi>")?;
235                if write_mrow {
236                    write!(s, "</mrow>")?;
237                }
238            }
239            Node::Operator {
240                op,
241                attrs,
242                left,
243                right,
244                size,
245            } => {
246                emit_operator_attributes(s, *attrs, *left, *right)?;
247                if let Some(size) = size {
248                    write!(
249                        s,
250                        " minsize=\"{}\" maxsize=\"{}\"",
251                        <&str>::from(size),
252                        <&str>::from(size),
253                    )?;
254                }
255                write!(s, ">{op}</mo>")?;
256            }
257            Node::PseudoOp {
258                attrs,
259                left,
260                right,
261                name,
262            } => {
263                emit_operator_attributes(s, *attrs, *left, *right)?;
264                write!(s, ">{name}</mo>")?;
265            }
266            node @ (Node::IdentifierStr(letters) | Node::Text(_, letters)) => {
267                let (open, close) = match node {
268                    Node::IdentifierStr(_) => {
269                        // The "<mrow>" with "<mspace/>" is needed to prevent Firefox from adding
270                        // extra space around multi-letter identifiers.
271                        debug_assert!(
272                            letters.chars().count() > 1,
273                            "single-letter IdentifierStr should be IdentifierChar"
274                        );
275                        ("<mrow><mspace/><mi>", "</mi></mrow>")
276                    }
277                    Node::Text(text_style, _) => match text_style {
278                        None => ("<mtext>", "</mtext>"),
279                        Some(HtmlTextStyle::Bold) => ("<mtext><b>", "</b></mtext>"),
280                        Some(HtmlTextStyle::Italic) => ("<mtext><i>", "</i></mtext>"),
281                        Some(HtmlTextStyle::BoldItalic) => ("<mtext><b><i>", "</i></b></mtext>"),
282                        Some(HtmlTextStyle::Emphasis) => ("<mtext><em>", "</em></mtext>"),
283                        Some(HtmlTextStyle::Typewriter) => ("<mtext><code>", "</code></mtext>"),
284                        Some(HtmlTextStyle::SmallCaps) => (
285                            "<mtext><span style=\"font-variant-caps: small-caps\">",
286                            "</span></mtext>",
287                        ),
288                        Some(HtmlTextStyle::SansSerif) => (
289                            "<mtext><span class=\"math-core-sans-serif-font\">",
290                            "</span></mtext>",
291                        ),
292                        Some(HtmlTextStyle::Serif) => (
293                            "<mtext><span class=\"math-core-serif-font\">",
294                            "</span></mtext>",
295                        ),
296                        Some(HtmlTextStyle::Strikethrough) => ("<mtext><s>", "</s></mtext>"),
297                    },
298                    // Compiler is able to infer that this is unreachable.
299                    _ => unreachable!(),
300                };
301                write!(s, "{open}{}{close}", EscapeHtml(letters))?;
302            }
303            Node::Space(space) => {
304                write!(s, "<mspace width=\"")?;
305                space.push_to_string(s);
306                // Work-around for a Firefox bug that causes "rem" to not be processed correctly
307                if matches!(space.unit, LengthUnit::Rem) {
308                    write!(s, "\" style=\"width:")?;
309                    space.push_to_string(s);
310                }
311                write!(s, "\"/>")?;
312            }
313            // The following nodes have exactly two children.
314            node @ (Node::Sub {
315                symbol: second,
316                target: first,
317            }
318            | Node::Sup {
319                symbol: second,
320                target: first,
321            }
322            | Node::Over {
323                symbol: second,
324                target: first,
325            }
326            | Node::Under {
327                symbol: second,
328                target: first,
329            }
330            | Node::Root(second, first)) => {
331                let (open, close) = match node {
332                    Node::Sub { .. } => ("<msub>", "</msub>"),
333                    Node::Sup { .. } => ("<msup>", "</msup>"),
334                    Node::Over { .. } => ("<mover>", "</mover>"),
335                    Node::Under { .. } => ("<munder>", "</munder>"),
336                    Node::Root(_, _) => ("<mroot>", "</mroot>"),
337                    // Compiler is able to infer that this is unreachable.
338                    _ => unreachable!(),
339                };
340                write!(s, "{open}")?;
341                first.emit(s, child_indent)?;
342                second.emit(s, child_indent)?;
343                writeln_indent!(s, base_indent, "{close}");
344            }
345            // The following nodes have exactly three children.
346            node @ (Node::SubSup {
347                target: first,
348                sub: second,
349                sup: third,
350            }
351            | Node::UnderOver {
352                target: first,
353                under: second,
354                over: third,
355            }) => {
356                let (open, close) = match node {
357                    Node::SubSup { .. } => ("<msubsup>", "</msubsup>"),
358                    Node::UnderOver { .. } => ("<munderover>", "</munderover>"),
359                    // Compiler is able to infer that this is unreachable.
360                    _ => unreachable!(),
361                };
362                write!(s, "{open}")?;
363                first.emit(s, child_indent)?;
364                second.emit(s, child_indent)?;
365                third.emit(s, child_indent)?;
366                writeln_indent!(s, base_indent, "{close}");
367            }
368            Node::Multiscript { base, sub, sup } => {
369                write!(s, "<mmultiscripts>")?;
370                base.emit(s, child_indent)?;
371                writeln_indent!(s, child_indent, "<mprescripts/>");
372                if let Some(sub) = sub {
373                    sub.emit(s, child_indent)?;
374                } else {
375                    writeln_indent!(s, child_indent, "<mrow></mrow>");
376                }
377                if let Some(sup) = sup {
378                    sup.emit(s, child_indent)?;
379                } else {
380                    writeln_indent!(s, child_indent, "<mrow></mrow>");
381                }
382                writeln_indent!(s, base_indent, "</mmultiscripts>");
383            }
384            node @ (Node::OverAccent(op, attr, target) | Node::UnderAccent(op, attr, target)) => {
385                let (open, close) = match node {
386                    Node::OverAccent(_, _, _) => ("<mover accent=\"true\">", "</mover>"),
387                    Node::UnderAccent(_, _, _) => ("<munder accentunder=\"true\">", "</munder>"),
388                    // Compiler is able to infer that this is unreachable.
389                    _ => unreachable!(),
390                };
391                write!(s, "{open}")?;
392                target.emit(s, child_indent)?;
393                writeln_indent!(s, child_indent, "<mo");
394                attr.write_to(s);
395                write!(s, ">{op}</mo>")?;
396                writeln_indent!(s, base_indent, "{close}");
397            }
398            Node::Sqrt(content) => {
399                write!(s, "<msqrt>")?;
400                content.emit(s, child_indent)?;
401                writeln_indent!(s, base_indent, "</msqrt>");
402            }
403            Node::Frac {
404                num,
405                denom: den,
406                lt_value: line_length,
407                lt_unit: line_unit,
408                attr,
409            } => {
410                write!(s, "<mfrac")?;
411                let lt = Length::from_parts(*line_length, *line_unit);
412                if let Some(lt) = lt {
413                    write!(s, " linethickness=\"")?;
414                    lt.push_to_string(s);
415                    write!(s, "\"")?;
416                }
417                if let Some(style) = attr {
418                    write!(s, "{}", <&str>::from(style))?;
419                }
420                write!(s, ">")?;
421                num.emit(s, child_indent)?;
422                den.emit(s, child_indent)?;
423                writeln_indent!(s, base_indent, "</mfrac>");
424            }
425            Node::Row { nodes, attr: style } => {
426                match style {
427                    None => {
428                        write!(s, "<mrow>")?;
429                    }
430                    Some(RowAttr::Style(style)) => {
431                        write!(s, "<mrow{}>", <&str>::from(style))?;
432                    }
433                    Some(RowAttr::Color(r, g, b)) => {
434                        write!(s, "<mrow style=\"color:#")?;
435                        append_u8_as_hex(s, *r);
436                        append_u8_as_hex(s, *g);
437                        append_u8_as_hex(s, *b);
438                        write!(s, ";\">")?;
439                    }
440                }
441                for node in *nodes {
442                    node.emit(s, child_indent)?;
443                }
444                writeln_indent!(s, base_indent, "</mrow>");
445            }
446            Node::Table {
447                content,
448                align,
449                style,
450            } => {
451                let mtd_opening = ColumnGenerator::new_predefined(*align);
452
453                write!(s, "<mtable")?;
454                if let Some(style) = style {
455                    write!(s, "{}", <&str>::from(style))?;
456                }
457                write!(s, ">")?;
458                emit_table(
459                    s,
460                    base_indent,
461                    child_indent,
462                    content,
463                    mtd_opening,
464                    None,
465                    None,
466                )?;
467            }
468            node @ (Node::EquationArray {
469                last_row_info,
470                content,
471                ..
472            }
473            | Node::MultLine {
474                last_row_info,
475                content,
476                ..
477            }) => {
478                let (mtd_opening, numbering_cols) = match node {
479                    Node::EquationArray { align, .. } => {
480                        (ColumnGenerator::new_predefined(*align), NumberColums::Wide)
481                    }
482                    Node::MultLine { num_rows, .. } => (
483                        ColumnGenerator::new_multline(*num_rows),
484                        NumberColums::Narrow,
485                    ),
486                    _ => unreachable!(),
487                };
488
489                write!(
490                    s,
491                    r#"<mtable displaystyle="true" scriptlevel="0" style="width: 100%">"#
492                )?;
493                emit_table(
494                    s,
495                    base_indent,
496                    child_indent,
497                    content,
498                    mtd_opening,
499                    Some(numbering_cols),
500                    last_row_info.as_deref(),
501                )?;
502            }
503            Node::Array {
504                style,
505                content,
506                array_spec,
507            } => {
508                let mtd_opening = ColumnGenerator::new_custom(array_spec);
509                write!(s, "<mtable")?;
510                match array_spec.beginning_line {
511                    Some(LineType::Solid) => {
512                        write!(s, " style=\"border-left: 0.05em solid currentcolor\"")?;
513                    }
514                    Some(LineType::Dashed) => {
515                        write!(s, " style=\"border-left: 0.05em dashed currentcolor\"")?;
516                    }
517                    _ => (),
518                }
519                if let Some(style) = style {
520                    write!(s, "{}", <&str>::from(style))?;
521                }
522                write!(s, ">")?;
523                emit_table(
524                    s,
525                    base_indent,
526                    child_indent,
527                    content,
528                    mtd_opening,
529                    None,
530                    None,
531                )?;
532            }
533            Node::RowSeparator { .. } | Node::ColumnSeparator => {
534                // This should only appear in tables where it is handled in `emit_table`.
535                if cfg!(debug_assertions) {
536                    panic!("ColumnSeparator node should be handled in emit_table");
537                }
538            }
539            Node::Enclose { content, notation } => {
540                let notation = *notation;
541                write!(s, "<menclose notation=\"")?;
542                let mut first = true;
543                if notation.contains(Notation::UP_DIAGONAL) {
544                    write!(s, "updiagonalstrike")?;
545                    first = false;
546                }
547                if notation.contains(Notation::DOWN_DIAGONAL) {
548                    if !first {
549                        write!(s, " ")?;
550                    }
551                    write!(s, "downdiagonalstrike")?;
552                }
553                if notation.contains(Notation::HORIZONTAL) {
554                    if !first {
555                        write!(s, " ")?;
556                    }
557                    write!(s, "horizontalstrike")?;
558                }
559                write!(s, "\">")?;
560                content.emit(s, child_indent)?;
561                if notation.contains(Notation::UP_DIAGONAL) {
562                    writeln_indent!(
563                        s,
564                        child_indent,
565                        "<mrow class=\"menclose-updiagonalstrike\"></mrow>"
566                    );
567                }
568                if notation.contains(Notation::DOWN_DIAGONAL) {
569                    writeln_indent!(
570                        s,
571                        child_indent,
572                        "<mrow class=\"menclose-downdiagonalstrike\"></mrow>"
573                    );
574                }
575                if notation.contains(Notation::HORIZONTAL) {
576                    writeln_indent!(
577                        s,
578                        child_indent,
579                        "<mrow class=\"menclose-horizontalstrike\"></mrow>"
580                    );
581                }
582                writeln_indent!(s, base_indent, "</menclose>");
583            }
584            &Node::AHref(&AHref { href, text }) => {
585                write!(
586                    s,
587                    r#"<mtext><a href="{}">{}</a></mtext>"#,
588                    EscapeHtml(href),
589                    EscapeHtml(text)
590                )?;
591            }
592            Node::UnknownCommand(cmd_name) => {
593                write!(s, "<mtext style=\"color:#b22222\">\\{cmd_name}</mtext>")?;
594            }
595            Node::HardcodedMathML(mathml) => {
596                write!(s, "{mathml}")?;
597            }
598        }
599        Ok(())
600    }
601}
602
603fn emit_operator_attributes(
604    s: &mut String,
605    attrs: OpAttrs,
606    left: Option<MathSpacing>,
607    right: Option<MathSpacing>,
608) -> std::fmt::Result {
609    s.push_str("<mo");
610    attrs.write_to(s);
611    match (left, right) {
612        (Some(left), Some(right)) => {
613            write!(
614                s,
615                " lspace=\"{}\" rspace=\"{}\"",
616                <&str>::from(left),
617                <&str>::from(right)
618            )?;
619        }
620        (Some(left), None) => {
621            write!(s, " lspace=\"{}\"", <&str>::from(left))?;
622        }
623        (None, Some(right)) => {
624            write!(s, " rspace=\"{}\"", <&str>::from(right))?;
625        }
626        _ => {}
627    }
628    Ok(())
629}
630
631#[derive(Clone, Copy, Debug, PartialEq, Eq)]
632enum NumberColums {
633    Narrow,
634    Wide,
635}
636
637impl NumberColums {
638    fn dummy_column_opening(
639        self,
640        s: &mut String,
641        child_indent2: usize,
642    ) -> Result<(), std::fmt::Error> {
643        match self {
644            NumberColums::Narrow => {
645                writeln_indent!(s, child_indent2, r#"<mtd style="width: 7.5%"#);
646            }
647            NumberColums::Wide => {
648                writeln_indent!(s, child_indent2, r#"<mtd style="width: 50%"#);
649            }
650        }
651        Ok(())
652    }
653
654    /// Initial dummy column for equation numbering for keeping alignment.
655    #[inline]
656    fn initial_dummy_column(
657        self,
658        s: &mut String,
659        child_indent2: usize,
660    ) -> Result<(), std::fmt::Error> {
661        self.dummy_column_opening(s, child_indent2)?;
662        write!(s, "\"></mtd>")?;
663        Ok(())
664    }
665}
666
667fn emit_table(
668    s: &mut String,
669    base_indent: usize,
670    child_indent: usize,
671    content: &[&Node<'_>],
672    mut col_gen: ColumnGenerator,
673    numbering_cols: Option<NumberColums>,
674    last_row_info: Option<&RowLabelInfo>,
675) -> Result<(), std::fmt::Error> {
676    let child_indent2 = if base_indent > 0 {
677        child_indent.saturating_add(1)
678    } else {
679        0
680    };
681    let child_indent3 = if base_indent > 0 {
682        child_indent2.saturating_add(1)
683    } else {
684        0
685    };
686    writeln_indent!(s, child_indent, "<mtr>");
687    if let Some(numbering_cols) = numbering_cols {
688        numbering_cols.initial_dummy_column(s, child_indent2)?;
689    }
690    col_gen.write_next_mtd(s, child_indent2)?;
691    for node in content {
692        match node {
693            Node::ColumnSeparator => {
694                writeln_indent!(s, child_indent2, "</mtd>");
695                col_gen.write_next_mtd(s, child_indent2)?;
696            }
697            Node::RowSeparator { tag, link_target } => {
698                writeln_indent!(s, child_indent2, "</mtd>");
699                if let Some(numbering_cols) = numbering_cols {
700                    write_equation_num(
701                        s,
702                        child_indent2,
703                        child_indent3,
704                        tag.as_ref().copied(),
705                        *link_target,
706                        numbering_cols,
707                    )?;
708                }
709                writeln_indent!(s, child_indent, "</mtr>");
710                writeln_indent!(s, child_indent, "<mtr>");
711                if let Some(numbering_cols) = numbering_cols {
712                    numbering_cols.initial_dummy_column(s, child_indent2)?;
713                }
714                col_gen.reset_to_new_row();
715                col_gen.write_next_mtd(s, child_indent2)?;
716            }
717            node => {
718                node.emit(s, child_indent3)?;
719            }
720        }
721    }
722    writeln_indent!(s, child_indent2, "</mtd>");
723    if let Some(numbering_cols) = numbering_cols {
724        let (last_tag, last_link_target) = match last_row_info {
725            Some(info) => (info.tag, info.link_target),
726            None => (None, None),
727        };
728        write_equation_num(
729            s,
730            child_indent2,
731            child_indent3,
732            last_tag,
733            last_link_target,
734            numbering_cols,
735        )?;
736    }
737    writeln_indent!(s, child_indent, "</mtr>");
738    writeln_indent!(s, base_indent, "</mtable>");
739    Ok(())
740}
741
742fn write_equation_num(
743    s: &mut String,
744    child_indent2: usize,
745    child_indent3: usize,
746    tag: Option<NonZeroU16>,
747    link_target: Option<&str>,
748    numbering_cols: NumberColums,
749) -> Result<(), std::fmt::Error> {
750    numbering_cols.dummy_column_opening(s, child_indent2)?;
751    if let Some(tag) = tag {
752        write!(s, r#";{RIGHT_ALIGN}""#)?;
753        if let Some(link_target) = link_target {
754            write!(s, r#" id="{link_target}">"#)?;
755        } else {
756            write!(s, ">")?;
757        }
758        writeln_indent!(s, child_indent3, "<mtext>({})</mtext>", tag);
759        writeln_indent!(s, child_indent2, "</mtd>");
760    } else {
761        write!(s, "\"></mtd>")?;
762    }
763    Ok(())
764}
765
766#[cfg(test)]
767mod tests {
768    use super::super::symbol;
769    use super::super::table::{ColumnAlignment, ColumnSpec};
770    use super::*;
771
772    const WORD: usize = std::mem::size_of::<usize>();
773
774    #[test]
775    fn test_struct_sizes() {
776        assert!(std::mem::size_of::<Node>() <= 4 * WORD, "size of Node");
777    }
778
779    pub fn render<'a, 'b>(node: &'a Node<'b>) -> String
780    where
781        'a: 'b,
782    {
783        let mut output = String::new();
784        node.emit(&mut output, 0).unwrap();
785        output
786    }
787
788    #[test]
789    fn render_number() {
790        assert_eq!(render(&Node::Number("3.14")), "<mn>3.14</mn>");
791    }
792
793    #[test]
794    fn render_single_letter_ident() {
795        assert_eq!(
796            render(&Node::IdentifierChar('x'.into(), LetterAttr::Default)),
797            "<mi>x</mi>"
798        );
799        assert_eq!(
800            render(&Node::IdentifierChar('Γ'.into(), LetterAttr::ForcedUpright)),
801            "<mrow><mspace/><mi mathvariant=\"normal\">Γ</mi></mrow>"
802        );
803        assert_eq!(
804            render(&Node::IdentifierChar('𝑥'.into(), LetterAttr::Default)),
805            "<mi>𝑥</mi>"
806        );
807    }
808
809    #[test]
810    fn render_operator_with_spacing() {
811        assert_eq!(
812            render(&Node::Operator {
813                op: symbol::COLON.as_op(),
814                attrs: OpAttrs::empty(),
815                left: Some(MathSpacing::FourMu),
816                right: Some(MathSpacing::FourMu),
817                size: None,
818            }),
819            "<mo lspace=\"0.2222em\" rspace=\"0.2222em\">:</mo>"
820        );
821        assert_eq!(
822            render(&Node::Operator {
823                op: symbol::COLON.as_op(),
824                attrs: OpAttrs::empty(),
825                left: Some(MathSpacing::FourMu),
826                right: Some(MathSpacing::Zero),
827                size: None,
828            }),
829            "<mo lspace=\"0.2222em\" rspace=\"0\">:</mo>"
830        );
831        assert_eq!(
832            render(&Node::Operator {
833                op: symbol::IDENTICAL_TO.as_op(),
834                attrs: OpAttrs::empty(),
835                left: Some(MathSpacing::Zero),
836                right: None,
837                size: None,
838            }),
839            "<mo lspace=\"0\">≡</mo>"
840        );
841        assert_eq!(
842            render(&Node::Operator {
843                op: symbol::PLUS_SIGN.as_op(),
844                attrs: OpAttrs::FORM_PREFIX,
845                left: None,
846                right: None,
847                size: None,
848            }),
849            "<mo form=\"prefix\">+</mo>"
850        );
851        assert_eq!(
852            render(&Node::Operator {
853                op: symbol::N_ARY_SUMMATION.as_op(),
854                attrs: OpAttrs::NO_MOVABLE_LIMITS,
855                left: None,
856                right: None,
857                size: None,
858            }),
859            "<mo movablelimits=\"false\">∑</mo>"
860        );
861    }
862
863    #[test]
864    fn render_pseudo_operator() {
865        assert_eq!(
866            render(&Node::PseudoOp {
867                attrs: OpAttrs::empty(),
868                left: Some(MathSpacing::ThreeMu),
869                right: Some(MathSpacing::ThreeMu),
870                name: "sin"
871            }),
872            "<mo lspace=\"0.1667em\" rspace=\"0.1667em\">sin</mo>"
873        );
874    }
875
876    #[test]
877    fn render_collected_letters() {
878        assert_eq!(
879            render(&Node::IdentifierStr("sin")),
880            "<mrow><mspace/><mi>sin</mi></mrow>"
881        );
882    }
883
884    #[test]
885    fn render_space() {
886        assert_eq!(
887            render(&Node::Space(Length::new(1.0, LengthUnit::Em))),
888            "<mspace width=\"1em\"/>"
889        );
890    }
891
892    #[test]
893    fn render_subscript() {
894        assert_eq!(
895            render(&Node::Sub {
896                target: &Node::IdentifierChar('x'.into(), LetterAttr::Default),
897                symbol: &Node::Number("2"),
898            }),
899            "<msub><mi>x</mi><mn>2</mn></msub>"
900        );
901    }
902
903    #[test]
904    fn render_superscript() {
905        assert_eq!(
906            render(&Node::Sup {
907                target: &Node::IdentifierChar('x'.into(), LetterAttr::Default),
908                symbol: &Node::Number("2"),
909            }),
910            "<msup><mi>x</mi><mn>2</mn></msup>"
911        );
912    }
913
914    #[test]
915    fn render_sub_sup() {
916        assert_eq!(
917            render(&Node::SubSup {
918                target: &Node::IdentifierChar('x'.into(), LetterAttr::Default),
919                sub: &Node::Number("1"),
920                sup: &Node::Number("2"),
921            }),
922            "<msubsup><mi>x</mi><mn>1</mn><mn>2</mn></msubsup>"
923        );
924    }
925
926    #[test]
927    fn render_over_op() {
928        assert_eq!(
929            render(&Node::OverAccent(
930                symbol::MACRON,
931                OpAttrs::STRETCHY_FALSE,
932                &Node::IdentifierChar('x'.into(), LetterAttr::Default),
933            )),
934            "<mover accent=\"true\"><mi>x</mi><mo stretchy=\"false\">¯</mo></mover>"
935        );
936        assert_eq!(
937            render(&Node::OverAccent(
938                symbol::OVERLINE,
939                OpAttrs::empty(),
940                &Node::IdentifierChar('x'.into(), LetterAttr::Default),
941            )),
942            "<mover accent=\"true\"><mi>x</mi><mo>‾</mo></mover>"
943        );
944    }
945
946    #[test]
947    fn render_under_op() {
948        assert_eq!(
949            render(&Node::UnderAccent(
950                symbol::LOW_LINE.as_op(),
951                OpAttrs::empty(),
952                &Node::IdentifierChar('x'.into(), LetterAttr::Default),
953            )),
954            "<munder accentunder=\"true\"><mi>x</mi><mo>_</mo></munder>"
955        );
956    }
957
958    #[test]
959    fn render_overset() {
960        assert_eq!(
961            render(&Node::Over {
962                symbol: &Node::Operator {
963                    op: symbol::EXCLAMATION_MARK,
964                    attrs: OpAttrs::empty(),
965                    left: None,
966                    right: None,
967                    size: None,
968                },
969                target: &Node::Operator {
970                    op: symbol::EQUALS_SIGN.as_op(),
971                    attrs: OpAttrs::empty(),
972                    left: None,
973                    right: None,
974                    size: None,
975                },
976            }),
977            "<mover><mo>=</mo><mo>!</mo></mover>"
978        );
979    }
980
981    #[test]
982    fn render_underset() {
983        assert_eq!(
984            render(&Node::Under {
985                symbol: &Node::IdentifierChar('θ'.into(), LetterAttr::Default),
986                target: &Node::PseudoOp {
987                    attrs: OpAttrs::FORCE_MOVABLE_LIMITS,
988                    left: Some(MathSpacing::ThreeMu),
989                    right: Some(MathSpacing::ThreeMu),
990                    name: "min",
991                },
992            }),
993            "<munder><mo movablelimits=\"true\" lspace=\"0.1667em\" rspace=\"0.1667em\">min</mo><mi>θ</mi></munder>"
994        );
995    }
996
997    #[test]
998    fn render_under_over() {
999        assert_eq!(
1000            render(&Node::UnderOver {
1001                target: &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1002                under: &Node::Number("1"),
1003                over: &Node::Number("2"),
1004            }),
1005            "<munderover><mi>x</mi><mn>1</mn><mn>2</mn></munderover>"
1006        );
1007    }
1008
1009    #[test]
1010    fn render_sqrt() {
1011        assert_eq!(
1012            render(&Node::Sqrt(&Node::IdentifierChar(
1013                'x'.into(),
1014                LetterAttr::Default
1015            ))),
1016            "<msqrt><mi>x</mi></msqrt>"
1017        );
1018    }
1019
1020    #[test]
1021    fn render_root() {
1022        assert_eq!(
1023            render(&Node::Root(
1024                &Node::Number("3"),
1025                &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1026            )),
1027            "<mroot><mi>x</mi><mn>3</mn></mroot>"
1028        );
1029    }
1030
1031    #[test]
1032    fn render_frac() {
1033        let num = &Node::Number("1");
1034        let denom = &Node::Number("2");
1035        let (lt_value, lt_unit) = Length::none().into_parts();
1036        assert_eq!(
1037            render(&Node::Frac {
1038                num,
1039                denom,
1040                lt_value,
1041                lt_unit,
1042                attr: None,
1043            }),
1044            "<mfrac><mn>1</mn><mn>2</mn></mfrac>"
1045        );
1046        assert_eq!(
1047            render(&Node::Frac {
1048                num,
1049                denom,
1050                lt_value,
1051                lt_unit,
1052                attr: Some(FracAttr::DisplayStyleTrue),
1053            }),
1054            "<mfrac displaystyle=\"true\"><mn>1</mn><mn>2</mn></mfrac>"
1055        );
1056        assert_eq!(
1057            render(&Node::Frac {
1058                num,
1059                denom,
1060                lt_value,
1061                lt_unit,
1062                attr: Some(FracAttr::DisplayStyleFalse),
1063            }),
1064            "<mfrac displaystyle=\"false\"><mn>1</mn><mn>2</mn></mfrac>"
1065        );
1066        let (lt_value, lt_unit) = Length::new(-1.0, LengthUnit::Rem).into_parts();
1067        assert_eq!(
1068            render(&Node::Frac {
1069                num,
1070                denom,
1071                lt_value,
1072                lt_unit,
1073                attr: None,
1074            }),
1075            "<mfrac linethickness=\"-1rem\"><mn>1</mn><mn>2</mn></mfrac>"
1076        );
1077        assert_eq!(
1078            render(&Node::Frac {
1079                num,
1080                denom,
1081                lt_value: LengthValue(1.0),
1082                lt_unit: LengthUnit::Em,
1083                attr: None,
1084            }),
1085            "<mfrac linethickness=\"1em\"><mn>1</mn><mn>2</mn></mfrac>"
1086        );
1087        assert_eq!(
1088            render(&Node::Frac {
1089                num,
1090                denom,
1091                lt_value: LengthValue(-1.0),
1092                lt_unit: LengthUnit::Ex,
1093                attr: None,
1094            }),
1095            "<mfrac linethickness=\"-1ex\"><mn>1</mn><mn>2</mn></mfrac>"
1096        );
1097        let (lt_value, lt_unit) = Length::new(2.0, LengthUnit::Rem).into_parts();
1098        assert_eq!(
1099            render(&Node::Frac {
1100                num,
1101                denom,
1102                lt_value,
1103                lt_unit,
1104                attr: None,
1105            }),
1106            "<mfrac linethickness=\"2rem\"><mn>1</mn><mn>2</mn></mfrac>"
1107        );
1108        let (lt_value, lt_unit) = Length::zero().into_parts();
1109        assert_eq!(
1110            render(&Node::Frac {
1111                num,
1112                denom,
1113                lt_value,
1114                lt_unit,
1115                attr: Some(FracAttr::DisplayStyleTrue),
1116            }),
1117            "<mfrac linethickness=\"0\" displaystyle=\"true\"><mn>1</mn><mn>2</mn></mfrac>"
1118        );
1119    }
1120
1121    #[test]
1122    fn render_row() {
1123        let nodes = &[
1124            &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1125            &Node::Operator {
1126                op: symbol::EQUALS_SIGN.as_op(),
1127                attrs: OpAttrs::empty(),
1128                left: None,
1129                right: None,
1130                size: None,
1131            },
1132            &Node::Number("1"),
1133        ];
1134
1135        assert_eq!(
1136            render(&Node::Row {
1137                nodes,
1138                attr: Some(RowAttr::Style(Style::Display))
1139            }),
1140            "<mrow displaystyle=\"true\" scriptlevel=\"0\"><mi>x</mi><mo>=</mo><mn>1</mn></mrow>"
1141        );
1142
1143        assert_eq!(
1144            render(&Node::Row {
1145                nodes,
1146                attr: Some(RowAttr::Color(0, 0, 0))
1147            }),
1148            "<mrow style=\"color:#000000;\"><mi>x</mi><mo>=</mo><mn>1</mn></mrow>"
1149        );
1150    }
1151
1152    #[test]
1153    fn render_hardcoded_mathml() {
1154        assert_eq!(render(&Node::HardcodedMathML("<mi>hi</mi>")), "<mi>hi</mi>");
1155    }
1156
1157    #[test]
1158    fn render_sized_operator() {
1159        assert_eq!(
1160            render(&Node::Operator {
1161                op: symbol::LEFT_PARENTHESIS.as_op(),
1162                attrs: OpAttrs::empty(),
1163                size: Some(Size::Scale1),
1164                left: None,
1165                right: None,
1166            }),
1167            "<mo minsize=\"1.2em\" maxsize=\"1.2em\">(</mo>"
1168        );
1169        assert_eq!(
1170            render(&Node::Operator {
1171                op: symbol::SOLIDUS.as_op(),
1172                attrs: OpAttrs::STRETCHY_TRUE | OpAttrs::SYMMETRIC_TRUE,
1173                size: Some(Size::Scale3),
1174                left: Some(MathSpacing::Zero),
1175                right: Some(MathSpacing::Zero),
1176            }),
1177            "<mo stretchy=\"true\" symmetric=\"true\" lspace=\"0\" rspace=\"0\" minsize=\"2.047em\" maxsize=\"2.047em\">/</mo>"
1178        );
1179    }
1180
1181    #[test]
1182    fn render_text() {
1183        assert_eq!(render(&Node::Text(None, "hello")), "<mtext>hello</mtext>");
1184    }
1185
1186    #[test]
1187    fn render_table() {
1188        let nodes = [
1189            &Node::Number("1"),
1190            &Node::ColumnSeparator,
1191            &Node::Number("2"),
1192            &Node::RowSeparator {
1193                tag: None,
1194                link_target: None,
1195            },
1196            &Node::Number("3"),
1197            &Node::ColumnSeparator,
1198            &Node::Number("4"),
1199        ];
1200
1201        assert_eq!(
1202            render(&Node::Table {
1203                content: &nodes,
1204                align: Alignment::Centered,
1205                style: None,
1206            }),
1207            "<mtable><mtr><mtd><mn>1</mn></mtd><mtd><mn>2</mn></mtd></mtr><mtr><mtd><mn>3</mn></mtd><mtd><mn>4</mn></mtd></mtr></mtable>"
1208        );
1209    }
1210
1211    #[test]
1212    fn render_equation_array() {
1213        let nodes = [
1214            &Node::Number("1"),
1215            &Node::ColumnSeparator,
1216            &Node::Number("2"),
1217            &Node::RowSeparator {
1218                tag: NonZeroU16::new(1),
1219                link_target: None,
1220            },
1221            &Node::Number("3"),
1222            &Node::ColumnSeparator,
1223            &Node::Number("4"),
1224        ];
1225
1226        let info_with_tag = RowLabelInfo {
1227            tag: NonZeroU16::new(2),
1228            link_target: None,
1229        };
1230        assert_eq!(
1231            render(&Node::EquationArray {
1232                content: &nodes,
1233                align: Alignment::Centered,
1234                last_row_info: Some(&info_with_tag),
1235            }),
1236            "<mtable displaystyle=\"true\" scriptlevel=\"0\" style=\"width: 100%\"><mtr><mtd style=\"width: 50%\"></mtd><mtd><mn>1</mn></mtd><mtd><mn>2</mn></mtd><mtd style=\"width: 50%;text-align: right;justify-items: end;\"><mtext>(1)</mtext></mtd></mtr><mtr><mtd style=\"width: 50%\"></mtd><mtd><mn>3</mn></mtd><mtd><mn>4</mn></mtd><mtd style=\"width: 50%;text-align: right;justify-items: end;\"><mtext>(2)</mtext></mtd></mtr></mtable>"
1237        );
1238
1239        assert_eq!(
1240            render(&Node::EquationArray {
1241                content: &nodes,
1242                align: Alignment::Centered,
1243                last_row_info: None,
1244            }),
1245            "<mtable displaystyle=\"true\" scriptlevel=\"0\" style=\"width: 100%\"><mtr><mtd style=\"width: 50%\"></mtd><mtd><mn>1</mn></mtd><mtd><mn>2</mn></mtd><mtd style=\"width: 50%;text-align: right;justify-items: end;\"><mtext>(1)</mtext></mtd></mtr><mtr><mtd style=\"width: 50%\"></mtd><mtd><mn>3</mn></mtd><mtd><mn>4</mn></mtd><mtd style=\"width: 50%\"></mtd></mtr></mtable>"
1246        );
1247    }
1248
1249    #[test]
1250    fn render_array() {
1251        let nodes = [
1252            &Node::Number("1"),
1253            &Node::ColumnSeparator,
1254            &Node::Number("2"),
1255            &Node::RowSeparator {
1256                tag: None,
1257                link_target: None,
1258            },
1259            &Node::Number("3"),
1260            &Node::ColumnSeparator,
1261            &Node::Number("4"),
1262        ];
1263
1264        assert_eq!(
1265            render(&Node::Array {
1266                style: None,
1267                content: &nodes,
1268                array_spec: &ArraySpec {
1269                    beginning_line: None,
1270                    is_sub: false,
1271                    column_spec: &[
1272                        ColumnSpec::WithContent(ColumnAlignment::LeftJustified, None),
1273                        ColumnSpec::WithContent(ColumnAlignment::Centered, None),
1274                    ],
1275                },
1276            }),
1277            "<mtable><mtr><mtd style=\"text-align: left;justify-items: start;\"><mn>1</mn></mtd><mtd><mn>2</mn></mtd></mtr><mtr><mtd style=\"text-align: left;justify-items: start;\"><mn>3</mn></mtd><mtd><mn>4</mn></mtd></mtr></mtable>"
1278        );
1279    }
1280
1281    #[test]
1282    fn render_multiscript() {
1283        assert_eq!(
1284            render(&Node::Multiscript {
1285                base: &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1286                sub: Some(&Node::Number("1")),
1287                sup: None,
1288            }),
1289            "<mmultiscripts><mi>x</mi><mprescripts/><mn>1</mn><mrow></mrow></mmultiscripts>"
1290        );
1291    }
1292
1293    #[test]
1294    fn render_text_transform() {
1295        assert_eq!(
1296            render(&Node::IdentifierChar('a'.into(), LetterAttr::ForcedUpright)),
1297            "<mrow><mspace/><mi mathvariant=\"normal\">a</mi></mrow>"
1298        );
1299        assert_eq!(
1300            render(&Node::IdentifierChar('a'.into(), LetterAttr::ForcedUpright)),
1301            "<mrow><mspace/><mi mathvariant=\"normal\">a</mi></mrow>"
1302        );
1303        assert_eq!(
1304            render(&Node::IdentifierStr("abc")),
1305            "<mrow><mspace/><mi>abc</mi></mrow>"
1306        );
1307        assert_eq!(
1308            render(&Node::IdentifierChar('𝐚'.into(), LetterAttr::Default)),
1309            "<mi>𝐚</mi>"
1310        );
1311        assert_eq!(
1312            render(&Node::IdentifierChar('𝒂'.into(), LetterAttr::Default)),
1313            "<mi>𝒂</mi>"
1314        );
1315        assert_eq!(
1316            render(&Node::IdentifierStr("𝒂𝒃𝒄")),
1317            "<mrow><mspace/><mi>𝒂𝒃𝒄</mi></mrow>"
1318        );
1319    }
1320
1321    #[test]
1322    fn render_enclose() {
1323        let content = Node::Row {
1324            nodes: &[
1325                &Node::IdentifierChar('a'.into(), LetterAttr::Default),
1326                &Node::IdentifierChar('b'.into(), LetterAttr::Default),
1327                &Node::IdentifierChar('c'.into(), LetterAttr::Default),
1328            ],
1329            attr: None,
1330        };
1331
1332        assert_eq!(
1333            render(&Node::Enclose {
1334                content: &content,
1335                notation: Notation::UP_DIAGONAL | Notation::DOWN_DIAGONAL
1336            }),
1337            "<menclose notation=\"updiagonalstrike downdiagonalstrike\"><mrow><mi>a</mi><mi>b</mi><mi>c</mi></mrow><mrow class=\"menclose-updiagonalstrike\"></mrow><mrow class=\"menclose-downdiagonalstrike\"></mrow></menclose>"
1338        );
1339    }
1340}