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