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