Skip to main content

math_core_renderer_internal/
ast.rs

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