math_core_renderer_internal/
ast.rs

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