use std::fmt::Write;
use std::num::NonZeroU16;
#[cfg(feature = "serde")]
use serde::Serialize;
use crate::attribute::{
FracAttr, HtmlTextStyle, LetterAttr, MathSpacing, Notation, OpAttrs, RowAttr, Size, Style,
};
use crate::fmt::new_line_and_indent;
use crate::itoa::append_u8_as_hex;
use crate::length::{Length, LengthUnit, LengthValue};
use crate::symbol::MathMLOperator;
use crate::table::{Alignment, ArraySpec, ColumnGenerator, LineType, RIGHT_ALIGN};
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(Serialize))]
pub enum Node<'arena> {
Number(&'arena str),
IdentifierChar(char, LetterAttr),
IdentifierStr(&'arena str),
Operator {
op: MathMLOperator,
attrs: OpAttrs,
size: Option<Size>,
left: Option<MathSpacing>,
right: Option<MathSpacing>,
},
PseudoOp {
attrs: OpAttrs,
left: Option<MathSpacing>,
right: Option<MathSpacing>,
name: &'arena str,
},
Space(Length),
Sub {
target: &'arena Node<'arena>,
symbol: &'arena Node<'arena>,
},
Sup {
target: &'arena Node<'arena>,
symbol: &'arena Node<'arena>,
},
SubSup {
target: &'arena Node<'arena>,
sub: &'arena Node<'arena>,
sup: &'arena Node<'arena>,
},
OverAccent(MathMLOperator, OpAttrs, &'arena Node<'arena>),
UnderAccent(MathMLOperator, OpAttrs, &'arena Node<'arena>),
Over {
symbol: &'arena Node<'arena>,
target: &'arena Node<'arena>,
},
Under {
symbol: &'arena Node<'arena>,
target: &'arena Node<'arena>,
},
UnderOver {
target: &'arena Node<'arena>,
under: &'arena Node<'arena>,
over: &'arena Node<'arena>,
},
Sqrt(&'arena Node<'arena>),
Root(&'arena Node<'arena>, &'arena Node<'arena>),
Frac {
num: &'arena Node<'arena>,
denom: &'arena Node<'arena>,
lt_value: LengthValue,
lt_unit: LengthUnit,
attr: Option<FracAttr>,
},
Row {
nodes: &'arena [&'arena Node<'arena>],
attr: Option<RowAttr>,
},
Text(Option<HtmlTextStyle>, &'arena str),
Table {
align: Alignment,
style: Option<Style>,
content: &'arena [&'arena Node<'arena>],
},
EquationArray {
align: Alignment,
last_tag: Option<NonZeroU16>,
content: &'arena [&'arena Node<'arena>],
},
MultLine {
num_rows: NonZeroU16,
last_equation_num: Option<NonZeroU16>,
content: &'arena [&'arena Node<'arena>],
},
Array {
style: Option<Style>,
array_spec: &'arena ArraySpec<'arena>,
content: &'arena [&'arena Node<'arena>],
},
ColumnSeparator,
RowSeparator {
tag: Option<NonZeroU16>,
link_target: Option<&'arena str>,
},
Enclose {
content: &'arena Node<'arena>,
notation: Notation,
},
Slashed(&'arena Node<'arena>),
Multiscript {
base: &'arena Node<'arena>,
sub: Option<&'arena Node<'arena>>,
sup: Option<&'arena Node<'arena>>,
},
UnknownCommand(&'arena str),
HardcodedMathML(&'static str),
}
#[cfg(target_arch = "wasm32")]
static_assertions::assert_eq_size!(Node<'_>, [usize; 4]);
macro_rules! writeln_indent {
($buf:expr, $indent:expr, $($tail:tt)+) => {
new_line_and_indent($buf, $indent);
write!($buf, $($tail)+)?
};
}
impl Node<'_> {
pub fn emit(&self, s: &mut String, base_indent: usize) -> std::fmt::Result {
let child_indent = if base_indent > 0 {
base_indent.saturating_add(1)
} else {
0
};
new_line_and_indent(s, base_indent);
match self {
Node::Number(number) => {
write!(s, "<mn>{number}</mn>")?;
}
Node::IdentifierChar(letter, attr) => {
let is_upright = matches!(attr, LetterAttr::ForcedUpright);
if is_upright {
write!(s, "<mrow><mspace/><mi mathvariant=\"normal\">")?;
} else {
write!(s, "<mi>")?;
}
let c = *letter;
write!(s, "{c}</mi>")?;
if is_upright {
write!(s, "</mrow>")?;
}
}
Node::Operator {
op,
attrs,
left,
right,
size,
} => {
emit_operator_attributes(s, *attrs, *left, *right)?;
if let Some(size) = size {
write!(
s,
" minsize=\"{}\" maxsize=\"{}\"",
<&str>::from(size),
<&str>::from(size),
)?;
}
write!(s, ">{}</mo>", char::from(op))?;
}
Node::PseudoOp {
attrs,
left,
right,
name,
} => {
emit_operator_attributes(s, *attrs, *left, *right)?;
write!(s, ">{name}</mo>")?;
}
node @ (Node::IdentifierStr(letters) | Node::Text(_, letters)) => {
let (open, close) = match node {
Node::IdentifierStr(_) => {
debug_assert!(
letters.chars().count() > 1,
"single-letter IdentifierStr should be IdentifierChar"
);
("<mrow><mspace/><mi>", "</mi></mrow>")
}
Node::Text(text_style, _) => match text_style {
None => ("<mtext>", "</mtext>"),
Some(HtmlTextStyle::Bold) => ("<mtext><b>", "</b></mtext>"),
Some(HtmlTextStyle::Italic) => ("<mtext><i>", "</i></mtext>"),
Some(HtmlTextStyle::BoldItalic) => ("<mtext><b><i>", "</i></b></mtext>"),
Some(HtmlTextStyle::Emphasis) => ("<mtext><em>", "</em></mtext>"),
Some(HtmlTextStyle::Typewriter) => ("<mtext><code>", "</code></mtext>"),
Some(HtmlTextStyle::SmallCaps) => (
"<mtext><span style=\"font-variant-caps: small-caps\">",
"</span></mtext>",
),
Some(HtmlTextStyle::SansSerif) => (
"<mtext><span class=\"math-core-sans-serif-font\">",
"</span></mtext>",
),
Some(HtmlTextStyle::Serif) => (
"<mtext><span class=\"math-core-serif-font\">",
"</span></mtext>",
),
},
_ => unreachable!(),
};
write!(s, "{open}{letters}{close}")?;
}
Node::Space(space) => {
write!(s, "<mspace width=\"")?;
space.push_to_string(s);
if matches!(space.unit, LengthUnit::Rem) {
write!(s, "\" style=\"width:")?;
space.push_to_string(s);
}
write!(s, "\"/>")?;
}
node @ (Node::Sub {
symbol: second,
target: first,
}
| Node::Sup {
symbol: second,
target: first,
}
| Node::Over {
symbol: second,
target: first,
}
| Node::Under {
symbol: second,
target: first,
}
| Node::Root(second, first)) => {
let (open, close) = match node {
Node::Sub { .. } => ("<msub>", "</msub>"),
Node::Sup { .. } => ("<msup>", "</msup>"),
Node::Over { .. } => ("<mover>", "</mover>"),
Node::Under { .. } => ("<munder>", "</munder>"),
Node::Root(_, _) => ("<mroot>", "</mroot>"),
_ => unreachable!(),
};
write!(s, "{open}")?;
first.emit(s, child_indent)?;
second.emit(s, child_indent)?;
writeln_indent!(s, base_indent, "{close}");
}
node @ (Node::SubSup {
target: first,
sub: second,
sup: third,
}
| Node::UnderOver {
target: first,
under: second,
over: third,
}) => {
let (open, close) = match node {
Node::SubSup { .. } => ("<msubsup>", "</msubsup>"),
Node::UnderOver { .. } => ("<munderover>", "</munderover>"),
_ => unreachable!(),
};
write!(s, "{open}")?;
first.emit(s, child_indent)?;
second.emit(s, child_indent)?;
third.emit(s, child_indent)?;
writeln_indent!(s, base_indent, "{close}");
}
Node::Multiscript { base, sub, sup } => {
write!(s, "<mmultiscripts>")?;
base.emit(s, child_indent)?;
writeln_indent!(s, child_indent, "<mprescripts/>");
if let Some(sub) = sub {
sub.emit(s, child_indent)?;
} else {
writeln_indent!(s, child_indent, "<mrow></mrow>");
}
if let Some(sup) = sup {
sup.emit(s, child_indent)?;
} else {
writeln_indent!(s, child_indent, "<mrow></mrow>");
}
writeln_indent!(s, base_indent, "</mmultiscripts>");
}
node @ (Node::OverAccent(op, attr, target) | Node::UnderAccent(op, attr, target)) => {
let (open, close) = match node {
Node::OverAccent(_, _, _) => ("<mover accent=\"true\">", "</mover>"),
Node::UnderAccent(_, _, _) => ("<munder accentunder=\"true\">", "</munder>"),
_ => unreachable!(),
};
write!(s, "{open}")?;
target.emit(s, child_indent)?;
writeln_indent!(s, child_indent, "<mo");
attr.write_to(s);
write!(s, ">{}</mo>", char::from(op))?;
writeln_indent!(s, base_indent, "{close}");
}
Node::Sqrt(content) => {
write!(s, "<msqrt>")?;
content.emit(s, child_indent)?;
writeln_indent!(s, base_indent, "</msqrt>");
}
Node::Frac {
num,
denom: den,
lt_value: line_length,
lt_unit: line_unit,
attr,
} => {
write!(s, "<mfrac")?;
let lt = Length::from_parts(*line_length, *line_unit);
if let Some(lt) = lt {
write!(s, " linethickness=\"")?;
lt.push_to_string(s);
write!(s, "\"")?;
}
if let Some(style) = attr {
write!(s, "{}", <&str>::from(style))?;
}
write!(s, ">")?;
num.emit(s, child_indent)?;
den.emit(s, child_indent)?;
writeln_indent!(s, base_indent, "</mfrac>");
}
Node::Row { nodes, attr: style } => {
match style {
None => {
write!(s, "<mrow>")?;
}
Some(RowAttr::Style(style)) => {
write!(s, "<mrow{}>", <&str>::from(style))?;
}
Some(RowAttr::Color(r, g, b)) => {
write!(s, "<mrow style=\"color:#")?;
append_u8_as_hex(s, *r);
append_u8_as_hex(s, *g);
append_u8_as_hex(s, *b);
write!(s, ";\">")?;
}
}
for node in *nodes {
node.emit(s, child_indent)?;
}
writeln_indent!(s, base_indent, "</mrow>");
}
Node::Slashed(node) => match node {
Node::IdentifierChar(x, attr) => {
if matches!(attr, LetterAttr::ForcedUpright) {
write!(s, "<mi mathvariant=\"normal\">{x}̸</mi>")?;
} else {
write!(s, "<mi>{x}̸</mi>")?;
}
}
Node::Operator { op, .. } => {
write!(s, "<mo>{}̸</mo>", char::from(op))?;
}
n => n.emit(s, base_indent)?,
},
Node::Table {
content,
align,
style,
} => {
let mtd_opening = ColumnGenerator::new_predefined(*align);
write!(s, "<mtable")?;
if let Some(style) = style {
write!(s, "{}", <&str>::from(style))?;
}
write!(s, ">")?;
emit_table(
s,
base_indent,
child_indent,
content,
mtd_opening,
None,
None,
)?;
}
node @ (Node::EquationArray {
last_tag: last_equation_num,
content,
..
}
| Node::MultLine {
last_equation_num,
content,
..
}) => {
let (mtd_opening, numbering_cols) = match node {
Node::EquationArray { align, .. } => {
(ColumnGenerator::new_predefined(*align), NumberColums::Wide)
}
Node::MultLine { num_rows, .. } => (
ColumnGenerator::new_multline(*num_rows),
NumberColums::Narrow,
),
_ => unreachable!(),
};
write!(
s,
r#"<mtable displaystyle="true" scriptlevel="0" style="width: 100%">"#
)?;
emit_table(
s,
base_indent,
child_indent,
content,
mtd_opening,
Some(numbering_cols),
last_equation_num.as_ref().copied(),
)?;
}
Node::Array {
style,
content,
array_spec,
} => {
let mtd_opening = ColumnGenerator::new_custom(array_spec);
write!(s, "<mtable")?;
match array_spec.beginning_line {
Some(LineType::Solid) => {
write!(s, " style=\"border-left: 0.05em solid currentcolor\"")?;
}
Some(LineType::Dashed) => {
write!(s, " style=\"border-left: 0.05em dashed currentcolor\"")?;
}
_ => (),
}
if let Some(style) = style {
write!(s, "{}", <&str>::from(style))?;
}
write!(s, ">")?;
emit_table(
s,
base_indent,
child_indent,
content,
mtd_opening,
None,
None,
)?;
}
Node::RowSeparator { .. } | Node::ColumnSeparator => {
if cfg!(debug_assertions) {
panic!("ColumnSeparator node should be handled in emit_table");
}
}
Node::Enclose { content, notation } => {
let notation = *notation;
write!(s, "<menclose notation=\"")?;
let mut first = true;
if notation.contains(Notation::UP_DIAGONAL) {
write!(s, "updiagonalstrike")?;
first = false;
}
if notation.contains(Notation::DOWN_DIAGONAL) {
if !first {
write!(s, " ")?;
}
write!(s, "downdiagonalstrike")?;
}
if notation.contains(Notation::HORIZONTAL) {
if !first {
write!(s, " ")?;
}
write!(s, "horizontalstrike")?;
}
write!(s, "\">")?;
content.emit(s, child_indent)?;
if notation.contains(Notation::UP_DIAGONAL) {
writeln_indent!(
s,
child_indent,
"<mrow class=\"menclose-updiagonalstrike\"></mrow>"
);
}
if notation.contains(Notation::DOWN_DIAGONAL) {
writeln_indent!(
s,
child_indent,
"<mrow class=\"menclose-downdiagonalstrike\"></mrow>"
);
}
if notation.contains(Notation::HORIZONTAL) {
writeln_indent!(
s,
child_indent,
"<mrow class=\"menclose-horizontalstrike\"></mrow>"
);
}
writeln_indent!(s, base_indent, "</menclose>");
}
Node::UnknownCommand(cmd_name) => {
write!(s, "<mtext style=\"color:#b22222\">\\{cmd_name}</mtext>")?;
}
Node::HardcodedMathML(mathml) => {
write!(s, "{mathml}")?;
}
}
Ok(())
}
}
fn emit_operator_attributes(
s: &mut String,
attrs: OpAttrs,
left: Option<MathSpacing>,
right: Option<MathSpacing>,
) -> std::fmt::Result {
s.push_str("<mo");
attrs.write_to(s);
match (left, right) {
(Some(left), Some(right)) => {
write!(
s,
" lspace=\"{}\" rspace=\"{}\"",
<&str>::from(left),
<&str>::from(right)
)?;
}
(Some(left), None) => {
write!(s, " lspace=\"{}\"", <&str>::from(left))?;
}
(None, Some(right)) => {
write!(s, " rspace=\"{}\"", <&str>::from(right))?;
}
_ => {}
}
Ok(())
}
#[derive(Clone, Copy)]
enum NumberColums {
Narrow,
Wide,
}
impl NumberColums {
fn dummy_column_opening(
self,
s: &mut String,
child_indent2: usize,
) -> Result<(), std::fmt::Error> {
match self {
NumberColums::Narrow => {
writeln_indent!(s, child_indent2, r#"<mtd style="width: 7.5%"#);
}
NumberColums::Wide => {
writeln_indent!(s, child_indent2, r#"<mtd style="width: 50%"#);
}
}
Ok(())
}
#[inline]
fn initial_dummy_column(
self,
s: &mut String,
child_indent2: usize,
) -> Result<(), std::fmt::Error> {
self.dummy_column_opening(s, child_indent2)?;
write!(s, "\"></mtd>")?;
Ok(())
}
}
fn emit_table(
s: &mut String,
base_indent: usize,
child_indent: usize,
content: &[&Node<'_>],
mut col_gen: ColumnGenerator,
numbering_cols: Option<NumberColums>,
last_tag: Option<NonZeroU16>,
) -> Result<(), std::fmt::Error> {
let child_indent2 = if base_indent > 0 {
child_indent.saturating_add(1)
} else {
0
};
let child_indent3 = if base_indent > 0 {
child_indent2.saturating_add(1)
} else {
0
};
writeln_indent!(s, child_indent, "<mtr>");
if let Some(numbering_cols) = numbering_cols {
numbering_cols.initial_dummy_column(s, child_indent2)?;
}
col_gen.write_next_mtd(s, child_indent2)?;
for node in content {
match node {
Node::ColumnSeparator => {
writeln_indent!(s, child_indent2, "</mtd>");
col_gen.write_next_mtd(s, child_indent2)?;
}
Node::RowSeparator { tag, link_target } => {
writeln_indent!(s, child_indent2, "</mtd>");
if let Some(numbering_cols) = numbering_cols {
write_equation_num(
s,
child_indent2,
child_indent3,
tag.as_ref().copied(),
*link_target,
numbering_cols,
)?;
}
writeln_indent!(s, child_indent, "</mtr>");
writeln_indent!(s, child_indent, "<mtr>");
if let Some(numbering_cols) = numbering_cols {
numbering_cols.initial_dummy_column(s, child_indent2)?;
}
col_gen.reset_to_new_row();
col_gen.write_next_mtd(s, child_indent2)?;
}
node => {
node.emit(s, child_indent3)?;
}
}
}
writeln_indent!(s, child_indent2, "</mtd>");
if let Some(numbering_cols) = numbering_cols {
write_equation_num(
s,
child_indent2,
child_indent3,
last_tag,
None,
numbering_cols,
)?;
}
writeln_indent!(s, child_indent, "</mtr>");
writeln_indent!(s, base_indent, "</mtable>");
Ok(())
}
fn write_equation_num(
s: &mut String,
child_indent2: usize,
child_indent3: usize,
tag: Option<NonZeroU16>,
link_target: Option<&str>,
numbering_cols: NumberColums,
) -> Result<(), std::fmt::Error> {
numbering_cols.dummy_column_opening(s, child_indent2)?;
if let Some(tag) = tag {
write!(s, r#";{RIGHT_ALIGN}""#)?;
if let Some(link_target) = link_target {
write!(s, r#" id="{link_target}">"#)?;
} else {
write!(s, ">")?;
}
writeln_indent!(s, child_indent3, "<mtext>({})</mtext>", tag);
writeln_indent!(s, child_indent2, "</mtd>");
} else {
write!(s, "\"></mtd>")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::super::symbol;
use super::super::table::{ColumnAlignment, ColumnSpec};
use super::*;
const WORD: usize = std::mem::size_of::<usize>();
#[test]
fn test_struct_sizes() {
assert!(std::mem::size_of::<Node>() <= 4 * WORD, "size of Node");
}
pub fn render<'a, 'b>(node: &'a Node<'b>) -> String
where
'a: 'b,
{
let mut output = String::new();
node.emit(&mut output, 0).unwrap();
output
}
#[test]
fn render_number() {
assert_eq!(render(&Node::Number("3.14")), "<mn>3.14</mn>");
}
#[test]
fn render_single_letter_ident() {
assert_eq!(
render(&Node::IdentifierChar('x', LetterAttr::Default)),
"<mi>x</mi>"
);
assert_eq!(
render(&Node::IdentifierChar('Γ', LetterAttr::ForcedUpright)),
"<mrow><mspace/><mi mathvariant=\"normal\">Γ</mi></mrow>"
);
assert_eq!(
render(&Node::IdentifierChar('𝑥', LetterAttr::Default)),
"<mi>𝑥</mi>"
);
}
#[test]
fn render_operator_with_spacing() {
assert_eq!(
render(&Node::Operator {
op: symbol::COLON.as_op(),
attrs: OpAttrs::empty(),
left: Some(MathSpacing::FourMu),
right: Some(MathSpacing::FourMu),
size: None,
}),
"<mo lspace=\"0.2222em\" rspace=\"0.2222em\">:</mo>"
);
assert_eq!(
render(&Node::Operator {
op: symbol::COLON.as_op(),
attrs: OpAttrs::empty(),
left: Some(MathSpacing::FourMu),
right: Some(MathSpacing::Zero),
size: None,
}),
"<mo lspace=\"0.2222em\" rspace=\"0\">:</mo>"
);
assert_eq!(
render(&Node::Operator {
op: symbol::IDENTICAL_TO.as_op(),
attrs: OpAttrs::empty(),
left: Some(MathSpacing::Zero),
right: None,
size: None,
}),
"<mo lspace=\"0\">≡</mo>"
);
assert_eq!(
render(&Node::Operator {
op: symbol::PLUS_SIGN.as_op(),
attrs: OpAttrs::FORM_PREFIX,
left: None,
right: None,
size: None,
}),
"<mo form=\"prefix\">+</mo>"
);
assert_eq!(
render(&Node::Operator {
op: symbol::N_ARY_SUMMATION.as_op(),
attrs: OpAttrs::NO_MOVABLE_LIMITS,
left: None,
right: None,
size: None,
}),
"<mo movablelimits=\"false\">∑</mo>"
);
}
#[test]
fn render_pseudo_operator() {
assert_eq!(
render(&Node::PseudoOp {
attrs: OpAttrs::empty(),
left: Some(MathSpacing::ThreeMu),
right: Some(MathSpacing::ThreeMu),
name: "sin"
}),
"<mo lspace=\"0.1667em\" rspace=\"0.1667em\">sin</mo>"
);
}
#[test]
fn render_collected_letters() {
assert_eq!(
render(&Node::IdentifierStr("sin")),
"<mrow><mspace/><mi>sin</mi></mrow>"
);
}
#[test]
fn render_space() {
assert_eq!(
render(&Node::Space(Length::new(1.0, LengthUnit::Em))),
"<mspace width=\"1em\"/>"
);
}
#[test]
fn render_subscript() {
assert_eq!(
render(&Node::Sub {
target: &Node::IdentifierChar('x', LetterAttr::Default),
symbol: &Node::Number("2"),
}),
"<msub><mi>x</mi><mn>2</mn></msub>"
);
}
#[test]
fn render_superscript() {
assert_eq!(
render(&Node::Sup {
target: &Node::IdentifierChar('x', LetterAttr::Default),
symbol: &Node::Number("2"),
}),
"<msup><mi>x</mi><mn>2</mn></msup>"
);
}
#[test]
fn render_sub_sup() {
assert_eq!(
render(&Node::SubSup {
target: &Node::IdentifierChar('x', LetterAttr::Default),
sub: &Node::Number("1"),
sup: &Node::Number("2"),
}),
"<msubsup><mi>x</mi><mn>1</mn><mn>2</mn></msubsup>"
);
}
#[test]
fn render_over_op() {
assert_eq!(
render(&Node::OverAccent(
symbol::MACRON,
OpAttrs::STRETCHY_FALSE,
&Node::IdentifierChar('x', LetterAttr::Default),
)),
"<mover accent=\"true\"><mi>x</mi><mo stretchy=\"false\">¯</mo></mover>"
);
assert_eq!(
render(&Node::OverAccent(
symbol::OVERLINE,
OpAttrs::empty(),
&Node::IdentifierChar('x', LetterAttr::Default),
)),
"<mover accent=\"true\"><mi>x</mi><mo>‾</mo></mover>"
);
}
#[test]
fn render_under_op() {
assert_eq!(
render(&Node::UnderAccent(
symbol::LOW_LINE.as_op(),
OpAttrs::empty(),
&Node::IdentifierChar('x', LetterAttr::Default),
)),
"<munder accentunder=\"true\"><mi>x</mi><mo>_</mo></munder>"
);
}
#[test]
fn render_overset() {
assert_eq!(
render(&Node::Over {
symbol: &Node::Operator {
op: symbol::EXCLAMATION_MARK,
attrs: OpAttrs::empty(),
left: None,
right: None,
size: None,
},
target: &Node::Operator {
op: symbol::EQUALS_SIGN.as_op(),
attrs: OpAttrs::empty(),
left: None,
right: None,
size: None,
},
}),
"<mover><mo>=</mo><mo>!</mo></mover>"
);
}
#[test]
fn render_underset() {
assert_eq!(
render(&Node::Under {
symbol: &Node::IdentifierChar('θ', LetterAttr::Default),
target: &Node::PseudoOp {
attrs: OpAttrs::FORCE_MOVABLE_LIMITS,
left: Some(MathSpacing::ThreeMu),
right: Some(MathSpacing::ThreeMu),
name: "min",
},
}),
"<munder><mo movablelimits=\"true\" lspace=\"0.1667em\" rspace=\"0.1667em\">min</mo><mi>θ</mi></munder>"
);
}
#[test]
fn render_under_over() {
assert_eq!(
render(&Node::UnderOver {
target: &Node::IdentifierChar('x', LetterAttr::Default),
under: &Node::Number("1"),
over: &Node::Number("2"),
}),
"<munderover><mi>x</mi><mn>1</mn><mn>2</mn></munderover>"
);
}
#[test]
fn render_sqrt() {
assert_eq!(
render(&Node::Sqrt(&Node::IdentifierChar('x', LetterAttr::Default))),
"<msqrt><mi>x</mi></msqrt>"
);
}
#[test]
fn render_root() {
assert_eq!(
render(&Node::Root(
&Node::Number("3"),
&Node::IdentifierChar('x', LetterAttr::Default),
)),
"<mroot><mi>x</mi><mn>3</mn></mroot>"
);
}
#[test]
fn render_frac() {
let num = &Node::Number("1");
let denom = &Node::Number("2");
let (lt_value, lt_unit) = Length::none().into_parts();
assert_eq!(
render(&Node::Frac {
num,
denom,
lt_value,
lt_unit,
attr: None,
}),
"<mfrac><mn>1</mn><mn>2</mn></mfrac>"
);
assert_eq!(
render(&Node::Frac {
num,
denom,
lt_value,
lt_unit,
attr: Some(FracAttr::DisplayStyleTrue),
}),
"<mfrac displaystyle=\"true\"><mn>1</mn><mn>2</mn></mfrac>"
);
assert_eq!(
render(&Node::Frac {
num,
denom,
lt_value,
lt_unit,
attr: Some(FracAttr::DisplayStyleFalse),
}),
"<mfrac displaystyle=\"false\"><mn>1</mn><mn>2</mn></mfrac>"
);
let (lt_value, lt_unit) = Length::new(-1.0, LengthUnit::Rem).into_parts();
assert_eq!(
render(&Node::Frac {
num,
denom,
lt_value,
lt_unit,
attr: None,
}),
"<mfrac linethickness=\"-1rem\"><mn>1</mn><mn>2</mn></mfrac>"
);
assert_eq!(
render(&Node::Frac {
num,
denom,
lt_value: LengthValue(1.0),
lt_unit: LengthUnit::Em,
attr: None,
}),
"<mfrac linethickness=\"1em\"><mn>1</mn><mn>2</mn></mfrac>"
);
assert_eq!(
render(&Node::Frac {
num,
denom,
lt_value: LengthValue(-1.0),
lt_unit: LengthUnit::Ex,
attr: None,
}),
"<mfrac linethickness=\"-1ex\"><mn>1</mn><mn>2</mn></mfrac>"
);
let (lt_value, lt_unit) = Length::new(2.0, LengthUnit::Rem).into_parts();
assert_eq!(
render(&Node::Frac {
num,
denom,
lt_value,
lt_unit,
attr: None,
}),
"<mfrac linethickness=\"2rem\"><mn>1</mn><mn>2</mn></mfrac>"
);
let (lt_value, lt_unit) = Length::zero().into_parts();
assert_eq!(
render(&Node::Frac {
num,
denom,
lt_value,
lt_unit,
attr: Some(FracAttr::DisplayStyleTrue),
}),
"<mfrac linethickness=\"0\" displaystyle=\"true\"><mn>1</mn><mn>2</mn></mfrac>"
);
}
#[test]
fn render_row() {
let nodes = &[
&Node::IdentifierChar('x', LetterAttr::Default),
&Node::Operator {
op: symbol::EQUALS_SIGN.as_op(),
attrs: OpAttrs::empty(),
left: None,
right: None,
size: None,
},
&Node::Number("1"),
];
assert_eq!(
render(&Node::Row {
nodes,
attr: Some(RowAttr::Style(Style::Display))
}),
"<mrow displaystyle=\"true\" scriptlevel=\"0\"><mi>x</mi><mo>=</mo><mn>1</mn></mrow>"
);
assert_eq!(
render(&Node::Row {
nodes,
attr: Some(RowAttr::Color(0, 0, 0))
}),
"<mrow style=\"color:#000000;\"><mi>x</mi><mo>=</mo><mn>1</mn></mrow>"
);
}
#[test]
fn render_hardcoded_mathml() {
assert_eq!(render(&Node::HardcodedMathML("<mi>hi</mi>")), "<mi>hi</mi>");
}
#[test]
fn render_sized_operator() {
assert_eq!(
render(&Node::Operator {
op: symbol::LEFT_PARENTHESIS.as_op(),
attrs: OpAttrs::empty(),
size: Some(Size::Scale1),
left: None,
right: None,
}),
"<mo minsize=\"1.2em\" maxsize=\"1.2em\">(</mo>"
);
assert_eq!(
render(&Node::Operator {
op: symbol::SOLIDUS.as_op(),
attrs: OpAttrs::STRETCHY_TRUE | OpAttrs::SYMMETRIC_TRUE,
size: Some(Size::Scale3),
left: Some(MathSpacing::Zero),
right: Some(MathSpacing::Zero),
}),
"<mo stretchy=\"true\" symmetric=\"true\" lspace=\"0\" rspace=\"0\" minsize=\"2.047em\" maxsize=\"2.047em\">/</mo>"
);
}
#[test]
fn render_text() {
assert_eq!(render(&Node::Text(None, "hello")), "<mtext>hello</mtext>");
}
#[test]
fn render_table() {
let nodes = [
&Node::Number("1"),
&Node::ColumnSeparator,
&Node::Number("2"),
&Node::RowSeparator {
tag: None,
link_target: None,
},
&Node::Number("3"),
&Node::ColumnSeparator,
&Node::Number("4"),
];
assert_eq!(
render(&Node::Table {
content: &nodes,
align: Alignment::Centered,
style: None,
}),
"<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>"
);
}
#[test]
fn render_equation_array() {
let nodes = [
&Node::Number("1"),
&Node::ColumnSeparator,
&Node::Number("2"),
&Node::RowSeparator {
tag: NonZeroU16::new(1),
link_target: None,
},
&Node::Number("3"),
&Node::ColumnSeparator,
&Node::Number("4"),
];
assert_eq!(
render(&Node::EquationArray {
content: &nodes,
align: Alignment::Centered,
last_tag: NonZeroU16::new(2),
}),
"<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>"
);
assert_eq!(
render(&Node::EquationArray {
content: &nodes,
align: Alignment::Centered,
last_tag: None,
}),
"<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>"
);
}
#[test]
fn render_array() {
let nodes = [
&Node::Number("1"),
&Node::ColumnSeparator,
&Node::Number("2"),
&Node::RowSeparator {
tag: None,
link_target: None,
},
&Node::Number("3"),
&Node::ColumnSeparator,
&Node::Number("4"),
];
assert_eq!(
render(&Node::Array {
style: None,
content: &nodes,
array_spec: &ArraySpec {
beginning_line: None,
is_sub: false,
column_spec: &[
ColumnSpec::WithContent(ColumnAlignment::LeftJustified, None),
ColumnSpec::WithContent(ColumnAlignment::Centered, None),
],
},
}),
"<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>"
);
}
#[test]
fn render_slashed() {
assert_eq!(
render(&Node::Slashed(&Node::IdentifierChar(
'x',
LetterAttr::Default,
))),
"<mi>x̸</mi>"
);
}
#[test]
fn render_multiscript() {
assert_eq!(
render(&Node::Multiscript {
base: &Node::IdentifierChar('x', LetterAttr::Default),
sub: Some(&Node::Number("1")),
sup: None,
}),
"<mmultiscripts><mi>x</mi><mprescripts/><mn>1</mn><mrow></mrow></mmultiscripts>"
);
}
#[test]
fn render_text_transform() {
assert_eq!(
render(&Node::IdentifierChar('a', LetterAttr::ForcedUpright)),
"<mrow><mspace/><mi mathvariant=\"normal\">a</mi></mrow>"
);
assert_eq!(
render(&Node::IdentifierChar('a', LetterAttr::ForcedUpright)),
"<mrow><mspace/><mi mathvariant=\"normal\">a</mi></mrow>"
);
assert_eq!(
render(&Node::IdentifierStr("abc")),
"<mrow><mspace/><mi>abc</mi></mrow>"
);
assert_eq!(
render(&Node::IdentifierChar('𝐚', LetterAttr::Default)),
"<mi>𝐚</mi>"
);
assert_eq!(
render(&Node::IdentifierChar('𝒂', LetterAttr::Default)),
"<mi>𝒂</mi>"
);
assert_eq!(
render(&Node::IdentifierStr("𝒂𝒃𝒄")),
"<mrow><mspace/><mi>𝒂𝒃𝒄</mi></mrow>"
);
}
#[test]
fn render_enclose() {
let content = Node::Row {
nodes: &[
&Node::IdentifierChar('a', LetterAttr::Default),
&Node::IdentifierChar('b', LetterAttr::Default),
&Node::IdentifierChar('c', LetterAttr::Default),
],
attr: None,
};
assert_eq!(
render(&Node::Enclose {
content: &content,
notation: Notation::UP_DIAGONAL | Notation::DOWN_DIAGONAL
}),
"<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>"
);
}
}