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