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