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