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