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