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