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