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