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