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