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