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