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