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