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