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