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