mirror of
https://github.com/ruvnet/RuView
synced 2026-08-06 19:51:43 +00:00
Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
+408
@@ -0,0 +1,408 @@
|
||||
//! MathML generation from mathematical AST
|
||||
//!
|
||||
//! This module converts mathematical AST nodes to MathML (Mathematical Markup Language)
|
||||
//! XML format for rendering in web browsers and applications.
|
||||
|
||||
use crate::math::ast::{BinaryOp, BracketType, LargeOpType, MathExpr, MathNode, UnaryOp};
|
||||
|
||||
/// MathML generator for mathematical expressions
|
||||
pub struct MathMLGenerator {
|
||||
/// Use presentation MathML (true) or content MathML (false)
|
||||
presentation: bool,
|
||||
}
|
||||
|
||||
impl MathMLGenerator {
|
||||
/// Create a new MathML generator (presentation mode)
|
||||
pub fn new() -> Self {
|
||||
Self { presentation: true }
|
||||
}
|
||||
|
||||
/// Create a content MathML generator
|
||||
pub fn content() -> Self {
|
||||
Self {
|
||||
presentation: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate MathML string from a mathematical expression
|
||||
pub fn generate(&self, expr: &MathExpr) -> String {
|
||||
let content = self.generate_node(&expr.root);
|
||||
format!(
|
||||
r#"<math xmlns="http://www.w3.org/1998/Math/MathML">{}</math>"#,
|
||||
content
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate MathML for a single node
|
||||
fn generate_node(&self, node: &MathNode) -> String {
|
||||
match node {
|
||||
MathNode::Symbol { value, .. } => {
|
||||
format!("<mi>{}</mi>", escape_xml(value))
|
||||
}
|
||||
|
||||
MathNode::Number { value, .. } => {
|
||||
format!("<mn>{}</mn>", escape_xml(value))
|
||||
}
|
||||
|
||||
MathNode::Binary { op, left, right } => {
|
||||
let left_ml = self.generate_node(left);
|
||||
let right_ml = self.generate_node(right);
|
||||
let op_ml = self.binary_op_to_mathml(op);
|
||||
|
||||
format!("<mrow>{}<mo>{}</mo>{}</mrow>", left_ml, op_ml, right_ml)
|
||||
}
|
||||
|
||||
MathNode::Unary { op, operand } => {
|
||||
let op_ml = self.unary_op_to_mathml(op);
|
||||
let operand_ml = self.generate_node(operand);
|
||||
|
||||
format!("<mrow><mo>{}</mo>{}</mrow>", op_ml, operand_ml)
|
||||
}
|
||||
|
||||
MathNode::Fraction {
|
||||
numerator,
|
||||
denominator,
|
||||
} => {
|
||||
let num_ml = self.generate_node(numerator);
|
||||
let den_ml = self.generate_node(denominator);
|
||||
|
||||
format!("<mfrac>{}{}</mfrac>", num_ml, den_ml)
|
||||
}
|
||||
|
||||
MathNode::Radical { index, radicand } => {
|
||||
let rad_ml = self.generate_node(radicand);
|
||||
|
||||
if let Some(idx) = index {
|
||||
let idx_ml = self.generate_node(idx);
|
||||
format!("<mroot>{}{}</mroot>", rad_ml, idx_ml)
|
||||
} else {
|
||||
format!("<msqrt>{}</msqrt>", rad_ml)
|
||||
}
|
||||
}
|
||||
|
||||
MathNode::Script {
|
||||
base,
|
||||
subscript,
|
||||
superscript,
|
||||
} => {
|
||||
let base_ml = self.generate_node(base);
|
||||
|
||||
match (subscript, superscript) {
|
||||
(Some(sub), Some(sup)) => {
|
||||
let sub_ml = self.generate_node(sub);
|
||||
let sup_ml = self.generate_node(sup);
|
||||
format!("<msubsup>{}{}{}</msubsup>", base_ml, sub_ml, sup_ml)
|
||||
}
|
||||
(Some(sub), None) => {
|
||||
let sub_ml = self.generate_node(sub);
|
||||
format!("<msub>{}{}</msub>", base_ml, sub_ml)
|
||||
}
|
||||
(None, Some(sup)) => {
|
||||
let sup_ml = self.generate_node(sup);
|
||||
format!("<msup>{}{}</msup>", base_ml, sup_ml)
|
||||
}
|
||||
(None, None) => base_ml,
|
||||
}
|
||||
}
|
||||
|
||||
MathNode::Function { name, argument } => {
|
||||
let name_ml = format!("<mi>{}</mi>", escape_xml(name));
|
||||
let arg_ml = self.generate_node(argument);
|
||||
|
||||
format!("<mrow>{}<mo>⁡</mo>{}</mrow>", name_ml, arg_ml)
|
||||
}
|
||||
|
||||
MathNode::Matrix { rows, bracket_type } => {
|
||||
let mut content = String::new();
|
||||
|
||||
for row in rows {
|
||||
content.push_str("<mtr>");
|
||||
for elem in row {
|
||||
content.push_str("<mtd>");
|
||||
content.push_str(&self.generate_node(elem));
|
||||
content.push_str("</mtd>");
|
||||
}
|
||||
content.push_str("</mtr>");
|
||||
}
|
||||
|
||||
let (open, close) = self.bracket_to_mathml(*bracket_type);
|
||||
|
||||
format!(
|
||||
"<mrow><mo>{}</mo><mtable>{}</mtable><mo>{}</mo></mrow>",
|
||||
open, content, close
|
||||
)
|
||||
}
|
||||
|
||||
MathNode::Group {
|
||||
content,
|
||||
bracket_type,
|
||||
} => {
|
||||
let content_ml = self.generate_node(content);
|
||||
let (open, close) = self.bracket_to_mathml(*bracket_type);
|
||||
|
||||
if *bracket_type == BracketType::None {
|
||||
content_ml
|
||||
} else {
|
||||
format!(
|
||||
"<mrow><mo>{}</mo>{}<mo>{}</mo></mrow>",
|
||||
open, content_ml, close
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MathNode::LargeOp {
|
||||
op_type,
|
||||
lower,
|
||||
upper,
|
||||
content,
|
||||
} => {
|
||||
let op_ml = self.large_op_to_mathml(op_type);
|
||||
let content_ml = self.generate_node(content);
|
||||
|
||||
match (lower, upper) {
|
||||
(Some(low), Some(up)) => {
|
||||
let low_ml = self.generate_node(low);
|
||||
let up_ml = self.generate_node(up);
|
||||
format!(
|
||||
"<mrow><munderover><mo>{}</mo>{}{}</munderover>{}</mrow>",
|
||||
op_ml, low_ml, up_ml, content_ml
|
||||
)
|
||||
}
|
||||
(Some(low), None) => {
|
||||
let low_ml = self.generate_node(low);
|
||||
format!(
|
||||
"<mrow><munder><mo>{}</mo>{}</munder>{}</mrow>",
|
||||
op_ml, low_ml, content_ml
|
||||
)
|
||||
}
|
||||
(None, Some(up)) => {
|
||||
let up_ml = self.generate_node(up);
|
||||
format!(
|
||||
"<mrow><mover><mo>{}</mo>{}</mover>{}</mrow>",
|
||||
op_ml, up_ml, content_ml
|
||||
)
|
||||
}
|
||||
(None, None) => {
|
||||
format!("<mrow><mo>{}</mo>{}</mrow>", op_ml, content_ml)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MathNode::Sequence { elements } => {
|
||||
let mut content = String::new();
|
||||
for (i, elem) in elements.iter().enumerate() {
|
||||
if i > 0 {
|
||||
content.push_str("<mo>,</mo>");
|
||||
}
|
||||
content.push_str(&self.generate_node(elem));
|
||||
}
|
||||
format!("<mrow>{}</mrow>", content)
|
||||
}
|
||||
|
||||
MathNode::Text { content } => {
|
||||
format!("<mtext>{}</mtext>", escape_xml(content))
|
||||
}
|
||||
|
||||
MathNode::Empty => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert binary operator to MathML
|
||||
fn binary_op_to_mathml(&self, op: &BinaryOp) -> String {
|
||||
match op {
|
||||
BinaryOp::Add => "+".to_string(),
|
||||
BinaryOp::Subtract => "−".to_string(),
|
||||
BinaryOp::Multiply => "×".to_string(),
|
||||
BinaryOp::Divide => "÷".to_string(),
|
||||
BinaryOp::Power => "^".to_string(),
|
||||
BinaryOp::Equal => "=".to_string(),
|
||||
BinaryOp::NotEqual => "≠".to_string(),
|
||||
BinaryOp::Less => "<".to_string(),
|
||||
BinaryOp::Greater => ">".to_string(),
|
||||
BinaryOp::LessEqual => "≤".to_string(),
|
||||
BinaryOp::GreaterEqual => "≥".to_string(),
|
||||
BinaryOp::ApproxEqual => "≈".to_string(),
|
||||
BinaryOp::Equivalent => "≡".to_string(),
|
||||
BinaryOp::Similar => "∼".to_string(),
|
||||
BinaryOp::Congruent => "≅".to_string(),
|
||||
BinaryOp::Proportional => "∝".to_string(),
|
||||
BinaryOp::Custom(s) => s.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert unary operator to MathML
|
||||
fn unary_op_to_mathml(&self, op: &UnaryOp) -> String {
|
||||
match op {
|
||||
UnaryOp::Plus => "+".to_string(),
|
||||
UnaryOp::Minus => "−".to_string(),
|
||||
UnaryOp::Not => "¬".to_string(),
|
||||
UnaryOp::Custom(s) => s.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert large operator to MathML
|
||||
fn large_op_to_mathml(&self, op: &LargeOpType) -> &'static str {
|
||||
match op {
|
||||
LargeOpType::Sum => "∑",
|
||||
LargeOpType::Product => "∏",
|
||||
LargeOpType::Integral => "∫",
|
||||
LargeOpType::DoubleIntegral => "∬",
|
||||
LargeOpType::TripleIntegral => "∭",
|
||||
LargeOpType::ContourIntegral => "∮",
|
||||
LargeOpType::Union => "⋃",
|
||||
LargeOpType::Intersection => "⋂",
|
||||
LargeOpType::Coproduct => "∐",
|
||||
LargeOpType::DirectSum => "⊕",
|
||||
LargeOpType::Custom(_) => "∑", // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert bracket type to MathML delimiters
|
||||
fn bracket_to_mathml(&self, bracket_type: BracketType) -> (&'static str, &'static str) {
|
||||
match bracket_type {
|
||||
BracketType::Parentheses => ("(", ")"),
|
||||
BracketType::Brackets => ("[", "]"),
|
||||
BracketType::Braces => ("{", "}"),
|
||||
BracketType::AngleBrackets => ("⟨", "⟩"),
|
||||
BracketType::Vertical => ("|", "|"),
|
||||
BracketType::DoubleVertical => ("‖", "‖"),
|
||||
BracketType::Floor => ("⌊", "⌋"),
|
||||
BracketType::Ceiling => ("⌈", "⌉"),
|
||||
BracketType::None => ("", ""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MathMLGenerator {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape XML special characters
|
||||
fn escape_xml(s: &str) -> String {
|
||||
s.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_number() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Number {
|
||||
value: "42".to_string(),
|
||||
is_decimal: false,
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = MathMLGenerator::new();
|
||||
let result = gen.generate(&expr);
|
||||
assert!(result.contains("<mn>42</mn>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_symbol() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Symbol {
|
||||
value: "x".to_string(),
|
||||
unicode: Some('x'),
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = MathMLGenerator::new();
|
||||
let result = gen.generate(&expr);
|
||||
assert!(result.contains("<mi>x</mi>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binary_add() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Binary {
|
||||
op: BinaryOp::Add,
|
||||
left: Box::new(MathNode::Number {
|
||||
value: "1".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
right: Box::new(MathNode::Number {
|
||||
value: "2".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = MathMLGenerator::new();
|
||||
let result = gen.generate(&expr);
|
||||
assert!(result.contains("<mrow>"));
|
||||
assert!(result.contains("<mo>+</mo>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fraction() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Fraction {
|
||||
numerator: Box::new(MathNode::Number {
|
||||
value: "1".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
denominator: Box::new(MathNode::Number {
|
||||
value: "2".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = MathMLGenerator::new();
|
||||
let result = gen.generate(&expr);
|
||||
assert!(result.contains("<mfrac>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sqrt() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Radical {
|
||||
index: None,
|
||||
radicand: Box::new(MathNode::Number {
|
||||
value: "2".to_string(),
|
||||
is_decimal: false,
|
||||
}),
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = MathMLGenerator::new();
|
||||
let result = gen.generate(&expr);
|
||||
assert!(result.contains("<msqrt>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_superscript() {
|
||||
let expr = MathExpr::new(
|
||||
MathNode::Script {
|
||||
base: Box::new(MathNode::Symbol {
|
||||
value: "x".to_string(),
|
||||
unicode: Some('x'),
|
||||
}),
|
||||
subscript: None,
|
||||
superscript: Some(Box::new(MathNode::Number {
|
||||
value: "2".to_string(),
|
||||
is_decimal: false,
|
||||
})),
|
||||
},
|
||||
1.0,
|
||||
);
|
||||
let gen = MathMLGenerator::new();
|
||||
let result = gen.generate(&expr);
|
||||
assert!(result.contains("<msup>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xml_escaping() {
|
||||
assert_eq!(escape_xml("a < b"), "a < b");
|
||||
assert_eq!(escape_xml("x & y"), "x & y");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user