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