1use alloc::borrow::Cow;
2use alloc::string::String;
3use core::fmt::Write;
4use core::num::NonZeroU16;
5
6use bitflags::bitflags;
7use kstring::KString;
8use percent_encoding::percent_encode;
9#[cfg(feature = "serde")]
10use serde::{Deserialize, Serialize};
11
12use crate::FxHashMap;
13
14use crate::attribute::RowAttrs;
15use crate::escaping::{EscapeHtml, FRAGMENT_SAFE};
16use crate::fmt::new_line_and_indent;
17use crate::itoa::append_u8_as_hex;
18use crate::length::{Length, LengthSet, LengthUnit, LengthValue};
19use crate::symbol::MathMLOperator;
20use crate::table::{
21 Alignment, ArraySpec, BORDER_TOP_DASHED, BORDER_TOP_SOLID, ColumnAlignment, ColumnGenerator,
22 LineType, RIGHT_ALIGN, RowLabelInfo,
23};
24use crate::{
25 attribute::{
26 FracAttr, HtmlTextSize, HtmlTextStyle, LetterAttr, MathSpacing, Notation, OpAttrs, Size,
27 Style,
28 },
29 super_char::SuperChar,
30};
31
32#[derive(Clone, Copy, Debug)]
36#[cfg_attr(feature = "serde", derive(Serialize))]
37pub struct AHref<'arena> {
38 pub href: &'arena str,
39 pub text: &'arena str,
40}
41
42#[derive(Clone, Copy, Debug)]
44#[cfg_attr(feature = "serde", derive(Serialize))]
45pub struct MultiscriptPair<'arena> {
46 pub sub: &'arena Node<'arena>,
47 pub sup: &'arena Node<'arena>,
48}
49
50#[derive(Debug)]
52#[cfg_attr(feature = "serde", derive(Serialize))]
53pub enum Node<'arena> {
54 Number(&'arena str),
56 IdentifierChar(SuperChar, LetterAttr),
58 IdentifierStr(&'arena str),
60 Operator {
62 op: MathMLOperator,
63 attrs: OpAttrs,
64 size: Option<Size>,
65 left: Option<MathSpacing>,
66 right: Option<MathSpacing>,
67 },
68 PseudoOp {
70 force_movable_limits: bool,
71 left: Option<MathSpacing>,
72 right: Option<MathSpacing>,
73 name: &'arena str,
74 },
75 Space(Length),
77 Sub {
79 target: &'arena Node<'arena>,
80 symbol: &'arena Node<'arena>,
81 },
82 Sup {
84 target: &'arena Node<'arena>,
85 symbol: &'arena Node<'arena>,
86 },
87 SubSup {
89 target: &'arena Node<'arena>,
90 sub: &'arena Node<'arena>,
91 sup: &'arena Node<'arena>,
92 },
93 OverAccent(MathMLOperator, OpAttrs, &'arena Node<'arena>),
95 UnderAccent(MathMLOperator, OpAttrs, &'arena Node<'arena>),
97 Over {
99 symbol: &'arena Node<'arena>,
100 target: &'arena Node<'arena>,
101 },
102 Under {
104 symbol: &'arena Node<'arena>,
105 target: &'arena Node<'arena>,
106 },
107 UnderOver {
109 target: &'arena Node<'arena>,
110 under: &'arena Node<'arena>,
111 over: &'arena Node<'arena>,
112 },
113 Sqrt(&'arena Node<'arena>),
115 Root(&'arena Node<'arena>, &'arena Node<'arena>),
117 Frac {
119 num: &'arena Node<'arena>,
121 denom: &'arena Node<'arena>,
123 lt_value: LengthValue,
125 lt_unit: LengthUnit,
126 attr: Option<FracAttr>,
127 },
128 Row {
130 nodes: &'arena [&'arena Node<'arena>],
131 attrs: RowAttrs,
132 },
133 Padded {
135 node: &'arena Node<'arena>,
136 width_0: bool,
137 height_0: bool,
138 left: Option<MathSpacing>,
139 right: Option<MathSpacing>,
140 voffset: Option<&'arena LengthSet>,
141 },
142 Phantom { node: &'arena Node<'arena> },
144 Text {
147 text_style: Option<HtmlTextStyle>,
148 text_size: Option<HtmlTextSize>,
149 text: &'arena str,
150 },
151 AHref(&'arena AHref<'arena>),
154 EqRef(&'arena str),
157 Table {
159 align: Alignment,
160 style: Option<Style>,
161 border_top: Option<LineType>,
164 content: &'arena [&'arena Node<'arena>],
165 },
166 EquationArray {
168 align: Alignment,
169 last_row_info: Option<&'arena RowLabelInfo<'arena>>,
170 content: &'arena [&'arena Node<'arena>],
171 },
172 MultLine {
174 num_rows: NonZeroU16,
175 initial_shove: Option<ColumnAlignment>,
176 last_row_info: Option<&'arena RowLabelInfo<'arena>>,
177 content: &'arena [&'arena Node<'arena>],
178 },
179 Array {
181 style: Option<Style>,
182 array_spec: &'arena ArraySpec<'arena>,
183 content: &'arena [&'arena Node<'arena>],
184 },
185 ColumnSeparator,
187 RowSeparator {
189 label_info: Option<&'arena RowLabelInfo<'arena>>,
190 border_top: Option<LineType>,
191 shove: Option<ColumnAlignment>,
192 },
193 Enclose {
195 content: &'arena Node<'arena>,
196 notation: Notation,
197 },
198 Multiscripts {
202 base: &'arena Node<'arena>,
203 pre: &'arena &'arena [MultiscriptPair<'arena>],
204 post: &'arena &'arena [MultiscriptPair<'arena>],
205 },
206 UnknownCommand(&'arena str),
210}
211
212#[cfg(target_arch = "wasm32")]
213static_assertions::assert_eq_size!(Node<'_>, [usize; 4]);
214
215macro_rules! writeln_indent {
216 ($self:ident, $indent:expr, $($tail:tt)+) => {
217 new_line_and_indent(&mut $self.s, $indent, $self.indentation);
218 write!($self.s, $($tail)+)?
219 };
220}
221
222impl Node<'_> {
223 pub const EMPTY_ROW: Self = Self::Row {
224 nodes: &[],
225 attrs: RowAttrs::DEFAULT,
226 };
227}
228
229#[derive(Debug, Clone)]
230#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
231#[cfg_attr(feature = "serde", serde(default, rename_all = "kebab-case"))]
232pub struct CssClassNames {
233 pub unknown_command: Cow<'static, str>,
235}
236
237impl Default for CssClassNames {
238 fn default() -> Self {
239 Self {
240 unknown_command: "math-core-unknown-cmd".into(),
241 }
242 }
243}
244
245#[derive(Debug, Clone, Copy)]
246#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
247#[cfg_attr(feature = "serde", serde(untagged))]
248pub enum Indentation {
249 Keyword(IndentKeyword),
250 Spaces(usize),
251}
252
253impl Default for Indentation {
254 fn default() -> Self {
255 Indentation::Spaces(4)
256 }
257}
258
259impl Indentation {
260 pub fn tab() -> Self {
261 Indentation::Keyword(IndentKeyword::Tab)
262 }
263}
264
265#[derive(Debug, Clone, Copy, Default)]
266#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
267#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
268#[non_exhaustive]
269pub enum IndentKeyword {
270 #[default]
271 Tab,
272}
273
274#[derive(Debug)]
275pub struct Emitter<'state> {
276 s: String,
277 label_map: &'state FxHashMap<KString, KString>,
278 css_classes: &'state CssClassNames,
279 indentation: Indentation,
280 warnings: Warnings,
281 id_prefix: &'state str,
282}
283
284impl<'state> Emitter<'state> {
285 pub fn new(
286 s: String,
287 label_map: &'state FxHashMap<KString, KString>,
288 css_classes: &'state CssClassNames,
289 indentation: Indentation,
290 id_prefix: &'state str,
291 ) -> Self {
292 Self {
293 s,
294 label_map,
295 css_classes,
296 indentation,
297 warnings: Warnings::new(),
298 id_prefix,
299 }
300 }
301
302 pub fn emit(&mut self, node: &Node<'_>, base_indent: usize) -> core::fmt::Result {
303 let child_indent = if base_indent > 0 {
305 base_indent.saturating_add(1)
306 } else {
307 0
308 };
309
310 new_line_and_indent(&mut self.s, base_indent, self.indentation);
312
313 match *node {
314 Node::Number(number) => {
315 write!(self.s, "<mn>{number}</mn>")?;
316 }
317 Node::IdentifierChar(letter, attr) => {
318 let is_upright = matches!(attr, LetterAttr::ForcedUpright);
319 let write_mrow = if is_upright {
321 write!(self.s, "<mrow><mspace/><mi mathvariant=\"normal\">")?;
322 true
323 } else if letter.try_as_char().is_none() {
324 write!(self.s, "<mrow><mspace/><mi>")?;
326 true
327 } else {
328 write!(self.s, "<mi>")?;
329 false
330 };
331 write!(self.s, "{letter}</mi>")?;
332 if write_mrow {
333 write!(self.s, "</mrow>")?;
334 }
335 }
336 Node::Operator {
337 op,
338 attrs,
339 left,
340 right,
341 size,
342 } => {
343 emit_operator_attributes(&mut self.s, attrs, left, right)?;
344 if let Some(size) = size {
345 write!(
346 self.s,
347 " minsize=\"{}\" maxsize=\"{}\"",
348 <&str>::from(size),
349 <&str>::from(size),
350 )?;
351 }
352 write!(self.s, ">{op}</mo>")?;
353 }
354 Node::PseudoOp {
355 force_movable_limits,
356 left,
357 right,
358 name,
359 } => {
360 let attrs = if force_movable_limits {
361 OpAttrs::FORCE_MOVABLE_LIMITS
362 } else {
363 OpAttrs::empty()
364 };
365 emit_operator_attributes(&mut self.s, attrs, left, right)?;
366 write!(self.s, ">{name}</mo>")?;
367 }
368 Node::IdentifierStr(letters) => {
369 debug_assert!(
372 letters.chars().count() > 1,
373 "single-letter IdentifierStr should be IdentifierChar"
374 );
375 write!(
376 self.s,
377 "<mrow><mspace/><mi>{}</mi></mrow>",
378 EscapeHtml(letters)
379 )?;
380 }
381 Node::Text {
382 text_style,
383 text_size,
384 text: letters,
385 } => {
386 write!(self.s, "<mtext")?;
387 if let Some(size) = text_size {
388 write!(self.s, " style=\"font-size:{}\"", <&str>::from(size))?;
389 }
390 let (open, close) = match text_style {
391 None => ("", ""),
392 Some(HtmlTextStyle::Bold) => ("<b>", "</b>"),
393 Some(HtmlTextStyle::Italic) => ("<i>", "</i>"),
394 Some(HtmlTextStyle::BoldItalic) => ("<b><i>", "</i></b>"),
395 Some(HtmlTextStyle::Emphasis) => ("<em>", "</em>"),
396 Some(HtmlTextStyle::Typewriter) => ("<code>", "</code>"),
397 Some(HtmlTextStyle::SmallCaps) => {
398 ("<span style=\"font-variant-caps: small-caps\">", "</span>")
399 }
400 Some(HtmlTextStyle::SansSerif) => {
401 ("<span class=\"math-core-sans-serif-font\">", "</span>")
402 }
403 Some(HtmlTextStyle::Serif) => {
404 ("<span class=\"math-core-serif-font\">", "</span>")
405 }
406 Some(HtmlTextStyle::Strikethrough) => ("<s>", "</s>"),
407 Some(HtmlTextStyle::Underline) => ("<u>", "</u>"),
408 };
409 write!(self.s, ">{open}{}{close}</mtext>", EscapeHtml(letters))?;
410 }
411 Node::Space(space) => {
412 write!(self.s, "<mspace ")?;
413
414 if space.is_negative() {
415 write!(self.s, "style=\"margin-left:")?;
416 space.push_to_string(&mut self.s);
417 write!(self.s, ";")?;
418 } else {
419 write!(self.s, "width=\"")?;
420 space.push_to_string(&mut self.s);
421 if matches!(space.unit, LengthUnit::Rem) {
423 write!(self.s, "\" style=\"width:")?;
424 space.push_to_string(&mut self.s);
425 }
426 }
427
428 write!(self.s, "\"/>")?;
429 }
430 ref node @ (Node::Sub {
432 symbol: second,
433 target: first,
434 }
435 | Node::Sup {
436 symbol: second,
437 target: first,
438 }
439 | Node::Over {
440 symbol: second,
441 target: first,
442 }
443 | Node::Under {
444 symbol: second,
445 target: first,
446 }
447 | Node::Root(second, first)) => {
448 let (open, close) = match node {
449 Node::Sub { .. } => ("<msub>", "</msub>"),
450 Node::Sup { .. } => ("<msup>", "</msup>"),
451 Node::Over { .. } => ("<mover>", "</mover>"),
452 Node::Under { .. } => ("<munder>", "</munder>"),
453 Node::Root(_, _) => ("<mroot>", "</mroot>"),
454 _ => unreachable!(),
456 };
457 write!(self.s, "{open}")?;
458 self.emit(first, child_indent)?;
459 self.emit(second, child_indent)?;
460 writeln_indent!(self, base_indent, "{close}");
461 }
462 ref node @ (Node::SubSup {
464 target: first,
465 sub: second,
466 sup: third,
467 }
468 | Node::UnderOver {
469 target: first,
470 under: second,
471 over: third,
472 }) => {
473 let (open, close) = match node {
474 Node::SubSup { .. } => ("<msubsup>", "</msubsup>"),
475 Node::UnderOver { .. } => ("<munderover>", "</munderover>"),
476 _ => unreachable!(),
478 };
479 write!(self.s, "{open}")?;
480 self.emit(first, child_indent)?;
481 self.emit(second, child_indent)?;
482 self.emit(third, child_indent)?;
483 writeln_indent!(self, base_indent, "{close}");
484 }
485 Node::Multiscripts { base, pre, post } => {
486 write!(self.s, "<mmultiscripts>")?;
487 self.emit(base, child_indent)?;
488 for &MultiscriptPair { sub, sup } in *post {
489 self.emit(sub, child_indent)?;
490 self.emit(sup, child_indent)?;
491 }
492 if !pre.is_empty() {
493 writeln_indent!(self, child_indent, "<mprescripts/>");
494 for &MultiscriptPair { sub, sup } in *pre {
495 self.emit(sub, child_indent)?;
496 self.emit(sup, child_indent)?;
497 }
498 }
499
500 writeln_indent!(self, base_indent, "</mmultiscripts>");
501 }
502 ref
503 node @ (Node::OverAccent(op, attr, target) | Node::UnderAccent(op, attr, target)) => {
504 let (open, close) = match node {
505 Node::OverAccent(_, _, _) => ("<mover accent=\"true\">", "</mover>"),
506 Node::UnderAccent(_, _, _) => ("<munder accentunder=\"true\">", "</munder>"),
507 _ => unreachable!(),
509 };
510 write!(self.s, "{open}")?;
511 self.emit(target, child_indent)?;
512 writeln_indent!(self, child_indent, "<mo");
513 attr.write_to(&mut self.s);
514 write!(self.s, ">{op}</mo>")?;
515 writeln_indent!(self, base_indent, "{close}");
516 }
517 Node::Sqrt(content) => {
518 write!(self.s, "<msqrt>")?;
519 self.emit(content, child_indent)?;
520 writeln_indent!(self, base_indent, "</msqrt>");
521 }
522 Node::Frac {
523 num,
524 denom: den,
525 lt_value: line_length,
526 lt_unit: line_unit,
527 attr,
528 } => {
529 write!(self.s, "<mfrac")?;
530 let lt = Length::from_parts(line_length, line_unit);
531 if let Some(lt) = lt {
532 write!(self.s, " linethickness=\"")?;
533 lt.push_to_string(&mut self.s);
534 write!(self.s, "\"")?;
535 }
536 if let Some(style) = attr {
537 write!(self.s, "{}", <&str>::from(style))?;
538 }
539 write!(self.s, ">")?;
540 self.emit(num, child_indent)?;
541 self.emit(den, child_indent)?;
542 writeln_indent!(self, base_indent, "</mfrac>");
543 }
544 Node::Row {
545 nodes,
546 attrs:
547 RowAttrs {
548 color,
549 style,
550 math_shift_compact,
551 },
552 } => {
553 write!(self.s, "<mrow")?;
554
555 if color.is_some() || math_shift_compact {
556 write!(self.s, " style=\"")?;
557 if let Some((r, g, b)) = color {
558 write!(self.s, "color:#")?;
559 append_u8_as_hex(&mut self.s, r);
560 append_u8_as_hex(&mut self.s, g);
561 append_u8_as_hex(&mut self.s, b);
562 write!(self.s, ";")?;
563 }
564 if math_shift_compact {
565 write!(self.s, "math-shift:compact;")?;
566 }
567 write!(self.s, "\"")?;
568 }
569
570 if let Some(style) = style {
571 write!(self.s, "{}", <&str>::from(style))?;
572 }
573
574 write!(self.s, ">")?;
575
576 if nodes.is_empty() {
577 write!(self.s, "</mrow>")?;
578 } else {
579 for node in nodes {
580 self.emit(node, child_indent)?;
581 }
582 writeln_indent!(self, base_indent, "</mrow>");
583 }
584 }
585 Node::Padded {
586 node,
587 width_0,
588 height_0,
589 left,
590 right,
591 voffset,
592 } => {
593 write!(self.s, "<mpadded")?;
594 if let Some(voffset) = voffset {
595 let mut voffset_iter = voffset.iter();
596 if let Some(voffset_length) = voffset_iter.next() {
601 write!(self.s, r#" voffset=""#)?;
602 voffset_length.push_to_string(&mut self.s);
603 write!(self.s, r#"""#)?;
604 }
605 for voffset_length in voffset_iter {
606 write!(self.s, r#"><mpadded voffset=""#)?;
607 voffset_length.push_to_string(&mut self.s);
608 write!(self.s, r#"""#)?;
609 }
610 }
611 if width_0 {
612 write!(self.s, r#" width="0""#)?;
613 }
614 if height_0 {
615 write!(self.s, r#" height="0""#)?;
616 }
617 if left.is_some() || right.is_some() {
618 write!(self.s, " style=\"")?;
619 if let Some(left) = left {
620 write!(self.s, "padding-left:{};", <&str>::from(left))?;
621 }
622 if let Some(right) = right {
623 write!(self.s, "padding-right:{};", <&str>::from(right))?;
624 }
625 write!(self.s, "\"")?;
626 }
627 write!(self.s, ">")?;
628 self.emit(node, child_indent)?;
629 writeln_indent!(self, base_indent, "</mpadded>");
630 if let Some(voffset) = voffset {
631 for _ in voffset.iter().skip(1) {
632 write!(self.s, r#"</mpadded>"#)?;
633 }
634 }
635 }
636 Node::Phantom { node } => {
637 write!(self.s, "<mphantom>")?;
638 self.emit(node, child_indent)?;
639 writeln_indent!(self, base_indent, "</mphantom>");
640 }
641 Node::Table {
642 content,
643 align,
644 style,
645 border_top,
646 } => {
647 let mtd_opening = ColumnGenerator::new_predefined(align);
648
649 write!(self.s, "<mtable")?;
650 match border_top {
652 Some(LineType::Solid) => write!(self.s, " style=\"{BORDER_TOP_SOLID}\"")?,
653 Some(LineType::Dashed) => write!(self.s, " style=\"{BORDER_TOP_DASHED}\"")?,
654 None => (),
655 }
656 if let Some(style) = style {
657 write!(self.s, "{}", <&str>::from(style))?;
658 }
659 write!(self.s, ">")?;
660 self.emit_table(base_indent, child_indent, content, mtd_opening, None, None)?;
661 }
662 ref node @ (Node::EquationArray {
663 last_row_info,
664 content,
665 ..
666 }
667 | Node::MultLine {
668 last_row_info,
669 content,
670 ..
671 }) => {
672 let (mtd_opening, numbering_cols) = match *node {
673 Node::EquationArray { align, .. } => {
674 (ColumnGenerator::new_predefined(align), NumberColums::Wide)
675 }
676 Node::MultLine {
677 num_rows,
678 initial_shove,
679 ..
680 } => (
681 ColumnGenerator::new_multline(num_rows, initial_shove),
682 NumberColums::Narrow,
683 ),
684 _ => unreachable!(),
685 };
686
687 write!(
688 &mut self.s,
689 r#"<mtable displaystyle="true" scriptlevel="0" style="width: 100%">"#
690 )?;
691 self.emit_table(
692 base_indent,
693 child_indent,
694 content,
695 mtd_opening,
696 Some(numbering_cols),
697 last_row_info,
698 )?;
699 }
700 Node::Array {
701 style,
702 content,
703 array_spec,
704 } => {
705 let mtd_opening = ColumnGenerator::new_custom(array_spec);
706 write!(self.s, "<mtable")?;
707 if array_spec.border_left.is_some() || array_spec.border_top.is_some() {
710 write!(self.s, " style=\"")?;
711 match array_spec.border_left {
712 Some(LineType::Solid) => {
713 write!(self.s, "border-left: 0.05em solid currentcolor;")?;
714 }
715 Some(LineType::Dashed) => {
716 write!(self.s, "border-left: 0.05em dashed currentcolor;")?;
717 }
718 None => (),
719 }
720 match array_spec.border_top {
721 Some(LineType::Solid) => write!(self.s, "{BORDER_TOP_SOLID}")?,
722 Some(LineType::Dashed) => write!(self.s, "{BORDER_TOP_DASHED}")?,
723 None => (),
724 }
725 write!(self.s, "\"")?;
726 }
727 if let Some(style) = style {
728 write!(self.s, "{}", <&str>::from(style))?;
729 }
730 write!(self.s, ">")?;
731 self.emit_table(base_indent, child_indent, content, mtd_opening, None, None)?;
732 }
733 Node::RowSeparator { .. } | Node::ColumnSeparator => {
734 if cfg!(debug_assertions) {
736 panic!("ColumnSeparator node should be handled in emit_table");
737 }
738 }
739 Node::Enclose { content, notation } => {
740 write!(self.s, "<menclose notation=\"")?;
741 let mut first = true;
742 if notation.contains(Notation::BOX) {
743 write!(self.s, "box")?;
744 first = false;
745 }
746 if notation.contains(Notation::ACTUARIAL) {
747 if !first {
748 write!(self.s, " ")?;
749 }
750 write!(self.s, "actuarial")?;
751 first = false;
752 }
753 if notation.contains(Notation::PHASOR_ANGLE) {
754 if !first {
755 write!(self.s, " ")?;
756 }
757 write!(self.s, "phasorangle")?;
758 first = false;
759 }
760 if notation.contains(Notation::UP_DIAGONAL) {
761 if !first {
762 write!(self.s, " ")?;
763 }
764 write!(self.s, "updiagonalstrike")?;
765 first = false;
766 }
767 if notation.contains(Notation::DOWN_DIAGONAL) {
768 if !first {
769 write!(self.s, " ")?;
770 }
771 write!(self.s, "downdiagonalstrike")?;
772 }
773 write!(self.s, "\">")?;
774 self.emit(content, child_indent)?;
775 if notation.contains(Notation::PHASOR_ANGLE) {
776 writeln_indent!(
777 self,
778 child_indent,
779 "<mrow class=\"menclose-phasorangle\"></mrow>"
780 );
781 }
782 if notation.contains(Notation::UP_DIAGONAL) {
783 writeln_indent!(
784 self,
785 child_indent,
786 "<mrow class=\"menclose-updiagonalstrike\"></mrow>"
787 );
788 }
789 if notation.contains(Notation::DOWN_DIAGONAL) {
790 writeln_indent!(
791 self,
792 child_indent,
793 "<mrow class=\"menclose-downdiagonalstrike\"></mrow>"
794 );
795 }
796 writeln_indent!(self, base_indent, "</menclose>");
797 }
798 Node::AHref(&AHref { href, text }) => {
799 write!(
800 self.s,
801 r#"<mtext><a href="{}">{}</a></mtext>"#,
802 EscapeHtml(href),
803 EscapeHtml(text)
804 )?;
805 }
806 Node::EqRef(label) => {
807 let tag: &str = match self.label_map.get(label) {
808 Some(tag) => tag,
809 None => {
810 self.warnings.set_undefined_references();
811 "??"
812 }
813 };
814 write!(
815 self.s,
816 r##"<mtext><a href="#{id_prefix}{id}">({text})</a></mtext>"##,
817 id_prefix = self.id_prefix,
818 id = percent_encode(label.as_bytes(), FRAGMENT_SAFE),
819 text = EscapeHtml(tag),
820 )?;
821 }
822 Node::UnknownCommand(cmd_name) => {
823 self.warnings.set_unknown_commands();
824 write!(
825 self.s,
826 r#"<merror class="{}"><mtext>\{cmd_name}</mtext></merror>"#,
827 self.css_classes.unknown_command
828 )?;
829 }
830 }
831 Ok(())
832 }
833
834 fn emit_table(
835 &mut self,
836 base_indent: usize,
837 child_indent: usize,
838 content: &[&Node<'_>],
839 mut col_gen: ColumnGenerator,
840 numbering_cols: Option<NumberColums>,
841 last_row_info: Option<&RowLabelInfo>,
842 ) -> Result<(), core::fmt::Error> {
843 let child_indent2 = if base_indent > 0 {
844 child_indent.saturating_add(1)
845 } else {
846 0
847 };
848 let child_indent3 = if base_indent > 0 {
849 child_indent2.saturating_add(1)
850 } else {
851 0
852 };
853 writeln_indent!(self, child_indent, "<mtr>");
854 if let Some(numbering_cols) = numbering_cols {
855 numbering_cols.initial_dummy_column(&mut self.s, child_indent2, self.indentation)?;
856 }
857 col_gen.write_next_mtd(&mut self.s, child_indent2, self.indentation)?;
858 for node in content {
859 match **node {
860 Node::ColumnSeparator => {
861 writeln_indent!(self, child_indent2, "</mtd>");
862 col_gen.write_next_mtd(&mut self.s, child_indent2, self.indentation)?;
863 }
864 Node::RowSeparator {
865 label_info,
866 border_top,
867 shove,
868 } => {
869 writeln_indent!(self, child_indent2, "</mtd>");
870 if let Some(numbering_cols) = numbering_cols {
871 write_equation_num(
872 &mut self.s,
873 child_indent2,
874 child_indent3,
875 label_info,
876 numbering_cols,
877 self.indentation,
878 self.id_prefix,
879 )?;
880 }
881 writeln_indent!(self, child_indent, "</mtr>");
882 writeln_indent!(self, child_indent, "<mtr>");
883 if let Some(numbering_cols) = numbering_cols {
884 numbering_cols.initial_dummy_column(
885 &mut self.s,
886 child_indent2,
887 self.indentation,
888 )?;
889 }
890 col_gen.start_new_row(border_top, shove);
891 col_gen.write_next_mtd(&mut self.s, child_indent2, self.indentation)?;
892 }
893 _ => {
894 self.emit(node, child_indent3)?;
895 }
896 }
897 }
898 writeln_indent!(self, child_indent2, "</mtd>");
899 if let Some(numbering_cols) = numbering_cols {
900 write_equation_num(
901 &mut self.s,
902 child_indent2,
903 child_indent3,
904 last_row_info,
905 numbering_cols,
906 self.indentation,
907 self.id_prefix,
908 )?;
909 }
910 writeln_indent!(self, child_indent, "</mtr>");
911 writeln_indent!(self, base_indent, "</mtable>");
912 Ok(())
913 }
914
915 #[must_use]
916 pub fn into_string(self) -> String {
917 self.s
918 }
919
920 #[must_use]
921 pub fn warnings(&self) -> Warnings {
922 self.warnings
923 }
924}
925
926fn emit_operator_attributes(
927 s: &mut String,
928 attrs: OpAttrs,
929 left: Option<MathSpacing>,
930 right: Option<MathSpacing>,
931) -> core::fmt::Result {
932 s.push_str("<mo");
933 attrs.write_to(s);
934 match (left, right) {
935 (Some(left), Some(right)) => {
936 write!(
937 s,
938 " lspace=\"{}\" rspace=\"{}\"",
939 <&str>::from(left),
940 <&str>::from(right)
941 )?;
942 }
943 (Some(left), None) => {
944 write!(s, " lspace=\"{}\"", <&str>::from(left))?;
945 }
946 (None, Some(right)) => {
947 write!(s, " rspace=\"{}\"", <&str>::from(right))?;
948 }
949 _ => {}
950 }
951 Ok(())
952}
953
954#[derive(Clone, Copy, Debug, PartialEq, Eq)]
955enum NumberColums {
956 Narrow,
957 Wide,
958}
959
960impl NumberColums {
961 fn dummy_column_opening(
962 self,
963 s: &mut String,
964 child_indent2: usize,
965 indentation: Indentation,
966 ) -> Result<(), core::fmt::Error> {
967 match self {
968 NumberColums::Narrow => {
969 new_line_and_indent(s, child_indent2, indentation);
970 write!(s, r#"<mtd style="width: 7.5%"#)?;
971 }
972 NumberColums::Wide => {
973 new_line_and_indent(s, child_indent2, indentation);
974 write!(s, r#"<mtd style="width: 50%"#)?;
975 }
976 }
977 Ok(())
978 }
979
980 #[inline]
982 fn initial_dummy_column(
983 self,
984 s: &mut String,
985 child_indent2: usize,
986 indentation: Indentation,
987 ) -> Result<(), core::fmt::Error> {
988 self.dummy_column_opening(s, child_indent2, indentation)?;
989 write!(s, "\"></mtd>")?;
990 Ok(())
991 }
992}
993
994fn write_equation_num(
995 s: &mut String,
996 child_indent2: usize,
997 child_indent3: usize,
998 label_info: Option<&RowLabelInfo>,
999 numbering_cols: NumberColums,
1000 indentation: Indentation,
1001 id_prefix: &str,
1002) -> Result<(), core::fmt::Error> {
1003 numbering_cols.dummy_column_opening(s, child_indent2, indentation)?;
1004 if let Some(label_info) = label_info {
1005 write!(s, r#";{RIGHT_ALIGN}""#)?;
1006 if let Some(link_target) = label_info.link_target {
1007 write!(
1008 s,
1009 r#" id="{id_prefix}{id}">"#,
1010 id = percent_encode(link_target.as_bytes(), FRAGMENT_SAFE)
1011 )?;
1012 } else {
1013 write!(s, ">")?;
1014 }
1015 new_line_and_indent(s, child_indent3, indentation);
1016 let tag = EscapeHtml(label_info.tag.text);
1017 if label_info.tag.parenthesized {
1018 write!(s, "<mtext>({tag})</mtext>")?;
1019 } else {
1020 write!(s, "<mtext>{tag}</mtext>")?;
1021 }
1022 new_line_and_indent(s, child_indent2, indentation);
1023 write!(s, "</mtd>")?;
1024 } else {
1025 write!(s, "\"></mtd>")?;
1026 }
1027 Ok(())
1028}
1029
1030#[derive(Debug, Clone, Copy)]
1031pub struct Warnings {
1032 inner: InnerWarnings,
1033}
1034
1035impl Warnings {
1036 fn new() -> Self {
1037 Self {
1038 inner: InnerWarnings::empty(),
1039 }
1040 }
1041 fn set_undefined_references(&mut self) {
1042 self.inner.insert(InnerWarnings::UNDEFINED_REFERENCES);
1043 }
1044 fn set_unknown_commands(&mut self) {
1045 self.inner.insert(InnerWarnings::UNKNOWN_COMMANDS);
1046 }
1047 pub fn has_undefined_references(&self) -> bool {
1048 self.inner.contains(InnerWarnings::UNDEFINED_REFERENCES)
1049 }
1050 pub fn has_unknown_commands(&self) -> bool {
1051 self.inner.contains(InnerWarnings::UNKNOWN_COMMANDS)
1052 }
1053 pub fn has_any(&self) -> bool {
1054 !self.inner.is_empty()
1055 }
1056}
1057
1058bitflags! {
1059 #[repr(transparent)]
1060 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1061 struct InnerWarnings: u8 {
1062 const UNDEFINED_REFERENCES = 1;
1063 const UNKNOWN_COMMANDS = 1 << 1;
1064 }
1065}
1066
1067#[cfg(test)]
1068mod tests {
1069 use super::super::symbol;
1070 use super::super::table::{ColumnAlignment, ColumnSpecEntry, EquationTag};
1071 use super::*;
1072
1073 const WORD: usize = std::mem::size_of::<usize>();
1074
1075 #[test]
1076 fn test_struct_sizes() {
1077 assert!(std::mem::size_of::<Node>() <= 4 * WORD, "size of Node");
1078 }
1079
1080 pub fn render<'a, 'b>(node: &'a Node<'b>) -> String
1081 where
1082 'a: 'b,
1083 {
1084 let output = String::new();
1085 let label_map = FxHashMap::default();
1086 let css_classes = CssClassNames::default();
1087 let mut emitter = Emitter::new(
1088 output,
1089 &label_map,
1090 &css_classes,
1091 Indentation::default(),
1092 "test-id-prefix-",
1093 );
1094 emitter.emit(node, 0).unwrap();
1095 emitter.into_string()
1096 }
1097
1098 #[test]
1099 fn render_number() {
1100 assert_eq!(render(&Node::Number("3.14")), "<mn>3.14</mn>");
1101 }
1102
1103 #[test]
1104 fn render_single_letter_ident() {
1105 assert_eq!(
1106 render(&Node::IdentifierChar('x'.into(), LetterAttr::Default)),
1107 "<mi>x</mi>"
1108 );
1109 assert_eq!(
1110 render(&Node::IdentifierChar('Γ'.into(), LetterAttr::ForcedUpright)),
1111 "<mrow><mspace/><mi mathvariant=\"normal\">Γ</mi></mrow>"
1112 );
1113 assert_eq!(
1114 render(&Node::IdentifierChar('𝑥'.into(), LetterAttr::Default)),
1115 "<mi>𝑥</mi>"
1116 );
1117 }
1118
1119 #[test]
1120 fn render_operator_with_spacing() {
1121 assert_eq!(
1122 render(&Node::Operator {
1123 op: symbol::COLON.as_op(),
1124 attrs: OpAttrs::empty(),
1125 left: Some(MathSpacing::FourMu),
1126 right: Some(MathSpacing::FourMu),
1127 size: None,
1128 }),
1129 "<mo lspace=\"0.2222em\" rspace=\"0.2222em\">:</mo>"
1130 );
1131 assert_eq!(
1132 render(&Node::Operator {
1133 op: symbol::COLON.as_op(),
1134 attrs: OpAttrs::empty(),
1135 left: Some(MathSpacing::FourMu),
1136 right: Some(MathSpacing::Zero),
1137 size: None,
1138 }),
1139 "<mo lspace=\"0.2222em\" rspace=\"0\">:</mo>"
1140 );
1141 assert_eq!(
1142 render(&Node::Operator {
1143 op: symbol::IDENTICAL_TO.as_op(),
1144 attrs: OpAttrs::empty(),
1145 left: Some(MathSpacing::Zero),
1146 right: None,
1147 size: None,
1148 }),
1149 "<mo lspace=\"0\">≡</mo>"
1150 );
1151 assert_eq!(
1152 render(&Node::Operator {
1153 op: symbol::PLUS_SIGN.as_op(),
1154 attrs: OpAttrs::FORM_PREFIX,
1155 left: None,
1156 right: None,
1157 size: None,
1158 }),
1159 "<mo form=\"prefix\">+</mo>"
1160 );
1161 assert_eq!(
1162 render(&Node::Operator {
1163 op: symbol::N_ARY_SUMMATION.as_op(),
1164 attrs: OpAttrs::NO_MOVABLE_LIMITS,
1165 left: None,
1166 right: None,
1167 size: None,
1168 }),
1169 "<mo movablelimits=\"false\">∑</mo>"
1170 );
1171 }
1172
1173 #[test]
1174 fn render_pseudo_operator() {
1175 assert_eq!(
1176 render(&Node::PseudoOp {
1177 force_movable_limits: false,
1178 left: Some(MathSpacing::ThreeMu),
1179 right: Some(MathSpacing::ThreeMu),
1180 name: "sin"
1181 }),
1182 "<mo lspace=\"0.1667em\" rspace=\"0.1667em\">sin</mo>"
1183 );
1184 }
1185
1186 #[test]
1187 fn render_collected_letters() {
1188 assert_eq!(
1189 render(&Node::IdentifierStr("sin")),
1190 "<mrow><mspace/><mi>sin</mi></mrow>"
1191 );
1192 }
1193
1194 #[test]
1195 fn render_space() {
1196 assert_eq!(
1197 render(&Node::Space(Length::new(1.0, LengthUnit::Em))),
1198 "<mspace width=\"1em\"/>"
1199 );
1200 }
1201
1202 #[test]
1203 fn render_subscript() {
1204 assert_eq!(
1205 render(&Node::Sub {
1206 target: &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1207 symbol: &Node::Number("2"),
1208 }),
1209 "<msub><mi>x</mi><mn>2</mn></msub>"
1210 );
1211 }
1212
1213 #[test]
1214 fn render_superscript() {
1215 assert_eq!(
1216 render(&Node::Sup {
1217 target: &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1218 symbol: &Node::Number("2"),
1219 }),
1220 "<msup><mi>x</mi><mn>2</mn></msup>"
1221 );
1222 }
1223
1224 #[test]
1225 fn render_sub_sup() {
1226 assert_eq!(
1227 render(&Node::SubSup {
1228 target: &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1229 sub: &Node::Number("1"),
1230 sup: &Node::Number("2"),
1231 }),
1232 "<msubsup><mi>x</mi><mn>1</mn><mn>2</mn></msubsup>"
1233 );
1234 }
1235
1236 #[test]
1237 fn render_over_op() {
1238 assert_eq!(
1239 render(&Node::OverAccent(
1240 symbol::MACRON.as_op(),
1241 OpAttrs::STRETCHY_FALSE,
1242 &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1243 )),
1244 "<mover accent=\"true\"><mi>x</mi><mo stretchy=\"false\">¯</mo></mover>"
1245 );
1246 assert_eq!(
1247 render(&Node::OverAccent(
1248 symbol::OVERLINE.as_op(),
1249 OpAttrs::empty(),
1250 &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1251 )),
1252 "<mover accent=\"true\"><mi>x</mi><mo>‾</mo></mover>"
1253 );
1254 }
1255
1256 #[test]
1257 fn render_under_op() {
1258 assert_eq!(
1259 render(&Node::UnderAccent(
1260 symbol::COMBINING_LOW_LINE.as_op(),
1261 OpAttrs::empty(),
1262 &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1263 )),
1264 "<munder accentunder=\"true\"><mi>x</mi><mo>\u{332}</mo></munder>"
1265 );
1266 }
1267
1268 #[test]
1269 fn render_overset() {
1270 assert_eq!(
1271 render(&Node::Over {
1272 symbol: &Node::Operator {
1273 op: symbol::EXCLAMATION_MARK.as_op(),
1274 attrs: OpAttrs::empty(),
1275 left: None,
1276 right: None,
1277 size: None,
1278 },
1279 target: &Node::Operator {
1280 op: symbol::EQUALS_SIGN.as_op(),
1281 attrs: OpAttrs::empty(),
1282 left: None,
1283 right: None,
1284 size: None,
1285 },
1286 }),
1287 "<mover><mo>=</mo><mo>!</mo></mover>"
1288 );
1289 }
1290
1291 #[test]
1292 fn render_underset() {
1293 assert_eq!(
1294 render(&Node::Under {
1295 symbol: &Node::IdentifierChar('θ'.into(), LetterAttr::Default),
1296 target: &Node::PseudoOp {
1297 force_movable_limits: true,
1298 left: Some(MathSpacing::ThreeMu),
1299 right: Some(MathSpacing::ThreeMu),
1300 name: "min",
1301 },
1302 }),
1303 "<munder><mo movablelimits=\"true\" lspace=\"0.1667em\" rspace=\"0.1667em\">min</mo><mi>θ</mi></munder>"
1304 );
1305 }
1306
1307 #[test]
1308 fn render_under_over() {
1309 assert_eq!(
1310 render(&Node::UnderOver {
1311 target: &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1312 under: &Node::Number("1"),
1313 over: &Node::Number("2"),
1314 }),
1315 "<munderover><mi>x</mi><mn>1</mn><mn>2</mn></munderover>"
1316 );
1317 }
1318
1319 #[test]
1320 fn render_sqrt() {
1321 assert_eq!(
1322 render(&Node::Sqrt(&Node::IdentifierChar(
1323 'x'.into(),
1324 LetterAttr::Default
1325 ))),
1326 "<msqrt><mi>x</mi></msqrt>"
1327 );
1328 }
1329
1330 #[test]
1331 fn render_root() {
1332 assert_eq!(
1333 render(&Node::Root(
1334 &Node::Number("3"),
1335 &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1336 )),
1337 "<mroot><mi>x</mi><mn>3</mn></mroot>"
1338 );
1339 }
1340
1341 #[test]
1342 fn render_frac() {
1343 let num = &Node::Number("1");
1344 let denom = &Node::Number("2");
1345 let (lt_value, lt_unit) = Length::none().into_parts();
1346 assert_eq!(
1347 render(&Node::Frac {
1348 num,
1349 denom,
1350 lt_value,
1351 lt_unit,
1352 attr: None,
1353 }),
1354 "<mfrac><mn>1</mn><mn>2</mn></mfrac>"
1355 );
1356 assert_eq!(
1357 render(&Node::Frac {
1358 num,
1359 denom,
1360 lt_value,
1361 lt_unit,
1362 attr: Some(FracAttr::DisplayStyleTrue),
1363 }),
1364 "<mfrac displaystyle=\"true\"><mn>1</mn><mn>2</mn></mfrac>"
1365 );
1366 assert_eq!(
1367 render(&Node::Frac {
1368 num,
1369 denom,
1370 lt_value,
1371 lt_unit,
1372 attr: Some(FracAttr::DisplayStyleFalse),
1373 }),
1374 "<mfrac displaystyle=\"false\"><mn>1</mn><mn>2</mn></mfrac>"
1375 );
1376 let (lt_value, lt_unit) = Length::new(-1.0, LengthUnit::Rem).into_parts();
1377 assert_eq!(
1378 render(&Node::Frac {
1379 num,
1380 denom,
1381 lt_value,
1382 lt_unit,
1383 attr: None,
1384 }),
1385 "<mfrac linethickness=\"-1rem\"><mn>1</mn><mn>2</mn></mfrac>"
1386 );
1387 assert_eq!(
1388 render(&Node::Frac {
1389 num,
1390 denom,
1391 lt_value: LengthValue(1.0),
1392 lt_unit: LengthUnit::Em,
1393 attr: None,
1394 }),
1395 "<mfrac linethickness=\"1em\"><mn>1</mn><mn>2</mn></mfrac>"
1396 );
1397 assert_eq!(
1398 render(&Node::Frac {
1399 num,
1400 denom,
1401 lt_value: LengthValue(-1.0),
1402 lt_unit: LengthUnit::Ex,
1403 attr: None,
1404 }),
1405 "<mfrac linethickness=\"-1ex\"><mn>1</mn><mn>2</mn></mfrac>"
1406 );
1407 let (lt_value, lt_unit) = Length::new(2.0, LengthUnit::Rem).into_parts();
1408 assert_eq!(
1409 render(&Node::Frac {
1410 num,
1411 denom,
1412 lt_value,
1413 lt_unit,
1414 attr: None,
1415 }),
1416 "<mfrac linethickness=\"2rem\"><mn>1</mn><mn>2</mn></mfrac>"
1417 );
1418 let (lt_value, lt_unit) = Length::zero().into_parts();
1419 assert_eq!(
1420 render(&Node::Frac {
1421 num,
1422 denom,
1423 lt_value,
1424 lt_unit,
1425 attr: Some(FracAttr::DisplayStyleTrue),
1426 }),
1427 "<mfrac linethickness=\"0\" displaystyle=\"true\"><mn>1</mn><mn>2</mn></mfrac>"
1428 );
1429 }
1430
1431 #[test]
1432 fn render_row() {
1433 let nodes = &[
1434 &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1435 &Node::Operator {
1436 op: symbol::EQUALS_SIGN.as_op(),
1437 attrs: OpAttrs::empty(),
1438 left: None,
1439 right: None,
1440 size: None,
1441 },
1442 &Node::Number("1"),
1443 ];
1444
1445 assert_eq!(
1446 render(&Node::Row {
1447 nodes,
1448 attrs: RowAttrs {
1449 style: Some(Style::Display),
1450 ..RowAttrs::DEFAULT
1451 }
1452 }),
1453 "<mrow displaystyle=\"true\" scriptlevel=\"0\"><mi>x</mi><mo>=</mo><mn>1</mn></mrow>"
1454 );
1455
1456 assert_eq!(
1457 render(&Node::Row {
1458 nodes,
1459 attrs: RowAttrs {
1460 color: Some((0, 0, 0)),
1461 ..RowAttrs::DEFAULT
1462 }
1463 }),
1464 "<mrow style=\"color:#000000;\"><mi>x</mi><mo>=</mo><mn>1</mn></mrow>"
1465 );
1466 }
1467
1468 #[test]
1469 fn render_padded() {
1470 assert_eq!(
1471 render(&Node::Padded {
1472 node: &Node::Number("x"),
1473 width_0: true,
1474 height_0: false,
1475 left: None,
1476 right: Some(MathSpacing::FourMu),
1477 voffset: None,
1478 }),
1479 "<mpadded width=\"0\" style=\"padding-right:0.2222em;\"><mn>x</mn></mpadded>"
1480 );
1481 assert_eq!(
1482 render(&Node::Padded {
1483 node: &Node::Number("x"),
1484 width_0: true,
1485 height_0: false,
1486 left: None,
1487 right: Some(MathSpacing::FourMu),
1488 voffset: Length::from_parts(LengthValue(1.0), LengthUnit::Em)
1489 .map(LengthSet::from)
1490 .as_ref(),
1491 }),
1492 "<mpadded voffset=\"1em\" width=\"0\" style=\"padding-right:0.2222em;\"><mn>x</mn></mpadded>"
1493 );
1494 let a = Length::from_parts(LengthValue(1.0), LengthUnit::Em).unwrap();
1495 let b = Length::from_parts(LengthValue(2.0), LengthUnit::Rem).unwrap();
1496 let c = Length::from_parts(LengthValue(3.0), LengthUnit::Ex).unwrap();
1497 let voffset = LengthSet::from(a) + LengthSet::from(b) + LengthSet::from(c);
1498 assert_eq!(
1499 render(&Node::Padded {
1500 node: &Node::Number("x"),
1501 width_0: true,
1502 height_0: false,
1503 left: None,
1504 right: Some(MathSpacing::FourMu),
1505 voffset: Some(&voffset),
1506 }),
1507 "<mpadded voffset=\"2rem\"><mpadded voffset=\"1em\"><mpadded voffset=\"3ex\" width=\"0\" style=\"padding-right:0.2222em;\"><mn>x</mn></mpadded></mpadded></mpadded>"
1508 );
1509 }
1510
1511 #[test]
1512 fn render_phantom() {
1513 assert_eq!(
1514 render(&Node::Phantom {
1515 node: &Node::Number("x")
1516 }),
1517 "<mphantom><mn>x</mn></mphantom>"
1518 );
1519 }
1520
1521 #[test]
1522 fn render_eqref() {
1523 assert_eq!(
1524 render(&Node::EqRef("thing")),
1525 "<mtext><a href=\"#test-id-prefix-thing\">(??)</a></mtext>"
1526 );
1527 }
1528
1529 #[test]
1530 fn render_sized_operator() {
1531 assert_eq!(
1532 render(&Node::Operator {
1533 op: symbol::LEFT_PARENTHESIS.as_op(),
1534 attrs: OpAttrs::empty(),
1535 size: Some(Size::Scale1),
1536 left: None,
1537 right: None,
1538 }),
1539 "<mo minsize=\"1.2em\" maxsize=\"1.2em\">(</mo>"
1540 );
1541 assert_eq!(
1542 render(&Node::Operator {
1543 op: symbol::SOLIDUS.as_op(),
1544 attrs: OpAttrs::STRETCHY_TRUE | OpAttrs::SYMMETRIC_TRUE,
1545 size: Some(Size::Scale3),
1546 left: Some(MathSpacing::Zero),
1547 right: Some(MathSpacing::Zero),
1548 }),
1549 "<mo stretchy=\"true\" symmetric=\"true\" lspace=\"0\" rspace=\"0\" minsize=\"2.047em\" maxsize=\"2.047em\">/</mo>"
1550 );
1551 }
1552
1553 #[test]
1554 fn render_text() {
1555 assert_eq!(
1556 render(&Node::Text {
1557 text_style: None,
1558 text_size: None,
1559 text: "hello"
1560 }),
1561 "<mtext>hello</mtext>"
1562 );
1563 }
1564
1565 #[test]
1566 fn render_table() {
1567 let nodes = [
1568 &Node::Number("1"),
1569 &Node::ColumnSeparator,
1570 &Node::Number("2"),
1571 &Node::RowSeparator {
1572 label_info: None,
1573 border_top: None,
1574 shove: None,
1575 },
1576 &Node::Number("3"),
1577 &Node::ColumnSeparator,
1578 &Node::Number("4"),
1579 ];
1580
1581 assert_eq!(
1582 render(&Node::Table {
1583 content: &nodes,
1584 align: Alignment::Centered,
1585 style: None,
1586 border_top: None,
1587 }),
1588 "<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>"
1589 );
1590 }
1591
1592 #[test]
1593 fn render_equation_array() {
1594 let nodes = [
1595 &Node::Number("1"),
1596 &Node::ColumnSeparator,
1597 &Node::Number("2"),
1598 &Node::RowSeparator {
1599 label_info: Some(&RowLabelInfo {
1600 tag: EquationTag {
1601 text: "1",
1602 parenthesized: true,
1603 },
1604 link_target: None,
1605 }),
1606 border_top: None,
1607 shove: None,
1608 },
1609 &Node::Number("3"),
1610 &Node::ColumnSeparator,
1611 &Node::Number("4"),
1612 ];
1613
1614 let info_with_tag = RowLabelInfo {
1615 tag: EquationTag {
1616 text: "2",
1617 parenthesized: true,
1618 },
1619 link_target: None,
1620 };
1621 assert_eq!(
1622 render(&Node::EquationArray {
1623 content: &nodes,
1624 align: Alignment::Centered,
1625 last_row_info: Some(&info_with_tag),
1626 }),
1627 "<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>"
1628 );
1629
1630 assert_eq!(
1631 render(&Node::EquationArray {
1632 content: &nodes,
1633 align: Alignment::Centered,
1634 last_row_info: None,
1635 }),
1636 "<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>"
1637 );
1638 }
1639
1640 #[test]
1641 fn render_equation_array_with_link_target() {
1642 let nodes = [
1643 &Node::Number("1"),
1644 &Node::ColumnSeparator,
1645 &Node::Number("2"),
1646 &Node::RowSeparator {
1647 label_info: Some(&RowLabelInfo {
1648 tag: EquationTag {
1649 text: "1",
1650 parenthesized: true,
1651 },
1652 link_target: Some("eq:1"),
1653 }),
1654 border_top: None,
1655 shove: None,
1656 },
1657 &Node::Number("3"),
1658 &Node::ColumnSeparator,
1659 &Node::Number("4"),
1660 ];
1661
1662 let info_with_tag = RowLabelInfo {
1663 tag: EquationTag {
1664 text: "2",
1665 parenthesized: true,
1666 },
1667 link_target: Some("eq:2"),
1668 };
1669 assert_eq!(
1670 render(&Node::EquationArray {
1671 content: &nodes,
1672 align: Alignment::Centered,
1673 last_row_info: Some(&info_with_tag),
1674 }),
1675 "<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;\" id=\"test-id-prefix-eq:1\"><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;\" id=\"test-id-prefix-eq:2\"><mtext>(2)</mtext></mtd></mtr></mtable>"
1676 );
1677
1678 assert_eq!(
1679 render(&Node::EquationArray {
1680 content: &nodes,
1681 align: Alignment::Centered,
1682 last_row_info: None,
1683 }),
1684 "<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;\" id=\"test-id-prefix-eq:1\"><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>"
1685 );
1686 }
1687
1688 #[test]
1689 fn render_array() {
1690 let nodes = [
1691 &Node::Number("1"),
1692 &Node::ColumnSeparator,
1693 &Node::Number("2"),
1694 &Node::RowSeparator {
1695 label_info: None,
1696 border_top: None,
1697 shove: None,
1698 },
1699 &Node::Number("3"),
1700 &Node::ColumnSeparator,
1701 &Node::Number("4"),
1702 ];
1703
1704 assert_eq!(
1705 render(&Node::Array {
1706 style: None,
1707 content: &nodes,
1708 array_spec: &ArraySpec {
1709 border_left: None,
1710 border_top: None,
1711 is_sub: false,
1712 column_spec: &[
1713 ColumnSpecEntry::WithContent {
1714 alignment: ColumnAlignment::LeftJustified,
1715 border_right: None
1716 },
1717 ColumnSpecEntry::WithContent {
1718 alignment: ColumnAlignment::Centered,
1719 border_right: None
1720 },
1721 ],
1722 },
1723 }),
1724 "<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>"
1725 );
1726 }
1727
1728 #[test]
1729 fn render_array_with_hlines() {
1730 let nodes = [
1733 &Node::Number("1"),
1734 &Node::ColumnSeparator,
1735 &Node::Number("2"),
1736 &Node::RowSeparator {
1737 label_info: None,
1738 border_top: Some(LineType::Dashed),
1739 shove: None,
1740 },
1741 &Node::Number("3"),
1742 &Node::ColumnSeparator,
1743 &Node::Number("4"),
1744 ];
1745
1746 assert_eq!(
1747 render(&Node::Array {
1748 style: None,
1749 content: &nodes,
1750 array_spec: &ArraySpec {
1751 border_left: None,
1752 border_top: Some(LineType::Solid),
1753 is_sub: false,
1754 column_spec: &[
1755 ColumnSpecEntry::WithContent {
1756 alignment: ColumnAlignment::Centered,
1757 border_right: None
1758 },
1759 ColumnSpecEntry::WithContent {
1760 alignment: ColumnAlignment::Centered,
1761 border_right: None
1762 },
1763 ],
1764 },
1765 }),
1766 "<mtable style=\"border-top: 0.05em solid currentcolor;\"><mtr><mtd><mn>1</mn></mtd><mtd><mn>2</mn></mtd></mtr><mtr><mtd style=\"border-top: 0.05em dashed currentcolor;\"><mn>3</mn></mtd><mtd style=\"border-top: 0.05em dashed currentcolor;\"><mn>4</mn></mtd></mtr></mtable>"
1767 );
1768 }
1769
1770 #[test]
1771 fn render_table_with_hlines() {
1772 let nodes = [
1775 &Node::Number("1"),
1776 &Node::ColumnSeparator,
1777 &Node::Number("2"),
1778 &Node::RowSeparator {
1779 label_info: None,
1780 border_top: Some(LineType::Dashed),
1781 shove: None,
1782 },
1783 &Node::Number("3"),
1784 &Node::ColumnSeparator,
1785 &Node::Number("4"),
1786 ];
1787
1788 assert_eq!(
1789 render(&Node::Table {
1790 content: &nodes,
1791 align: Alignment::Centered,
1792 style: None,
1793 border_top: Some(LineType::Solid),
1794 }),
1795 "<mtable style=\"border-top: 0.05em solid currentcolor;\"><mtr><mtd><mn>1</mn></mtd><mtd><mn>2</mn></mtd></mtr><mtr><mtd style=\"border-top: 0.05em dashed currentcolor;\"><mn>3</mn></mtd><mtd style=\"border-top: 0.05em dashed currentcolor;\"><mn>4</mn></mtd></mtr></mtable>"
1796 );
1797 }
1798
1799 #[test]
1800 fn render_multiscript() {
1801 assert_eq!(
1802 render(&Node::Multiscripts {
1803 base: &Node::IdentifierChar('x'.into(), LetterAttr::Default),
1804 pre: &const {
1805 &[MultiscriptPair {
1806 sub: &Node::Number("1"),
1807 sup: &Node::EMPTY_ROW,
1808 }]
1809 },
1810 post: &const { &[] },
1811 }),
1812 "<mmultiscripts><mi>x</mi><mprescripts/><mn>1</mn><mrow></mrow></mmultiscripts>"
1813 );
1814 }
1815
1816 #[test]
1817 fn render_text_transform() {
1818 assert_eq!(
1819 render(&Node::IdentifierChar('a'.into(), LetterAttr::ForcedUpright)),
1820 "<mrow><mspace/><mi mathvariant=\"normal\">a</mi></mrow>"
1821 );
1822 assert_eq!(
1823 render(&Node::IdentifierChar('a'.into(), LetterAttr::ForcedUpright)),
1824 "<mrow><mspace/><mi mathvariant=\"normal\">a</mi></mrow>"
1825 );
1826 assert_eq!(
1827 render(&Node::IdentifierStr("abc")),
1828 "<mrow><mspace/><mi>abc</mi></mrow>"
1829 );
1830 assert_eq!(
1831 render(&Node::IdentifierChar('𝐚'.into(), LetterAttr::Default)),
1832 "<mi>𝐚</mi>"
1833 );
1834 assert_eq!(
1835 render(&Node::IdentifierChar('𝒂'.into(), LetterAttr::Default)),
1836 "<mi>𝒂</mi>"
1837 );
1838 assert_eq!(
1839 render(&Node::IdentifierStr("𝒂𝒃𝒄")),
1840 "<mrow><mspace/><mi>𝒂𝒃𝒄</mi></mrow>"
1841 );
1842 }
1843
1844 #[test]
1845 fn render_enclose() {
1846 let content = Node::Row {
1847 nodes: &[
1848 &Node::IdentifierChar('a'.into(), LetterAttr::Default),
1849 &Node::IdentifierChar('b'.into(), LetterAttr::Default),
1850 &Node::IdentifierChar('c'.into(), LetterAttr::Default),
1851 ],
1852 attrs: RowAttrs::DEFAULT,
1853 };
1854
1855 assert_eq!(
1856 render(&Node::Enclose {
1857 content: &content,
1858 notation: Notation::UP_DIAGONAL | Notation::DOWN_DIAGONAL
1859 }),
1860 "<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>"
1861 );
1862 }
1863
1864 #[test]
1865 fn fragment_encoding() {
1866 let ascii_samples = b"\x00\x01\x1F !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\x7F";
1867 let encoded = percent_encode(ascii_samples, FRAGMENT_SAFE).to_string();
1868 assert_eq!(
1869 encoded,
1870 "%00%01%1F%20!%22%23$%25&'()*+,-./09:;%3C=%3E?@AZ%5B%5C%5D%5E_%60az%7B%7C%7D~%7F"
1871 );
1872 }
1873}