use crate::ParseError;
#[cfg(feature = "wasm")]
use crate::dom_tree::create_class;
use crate::tree::{DocumentFragment, VirtualNode};
use crate::units::make_em;
use crate::utils::escape_into;
#[cfg(feature = "wasm")]
use crate::web_context::WebContext;
use crate::{namespace::KeyMap, types::ClassList, types::CssStyle};
use bon::bon;
use core::fmt::{self, Debug, Write as _};
use strum::AsRefStr;
#[cfg(feature = "wasm")]
use wasm_bindgen::JsCast as _;
#[cfg(feature = "wasm")]
use wasm_bindgen::UnwrapThrowExt as _;
#[cfg(feature = "wasm")]
use web_sys;
fn map_fmt(result: fmt::Result) -> Result<(), ParseError> {
result.map_err(ParseError::from)
}
#[cfg(feature = "wasm")]
fn set_attribute(element: &web_sys::Element, name: &str, value: &str) {
element.set_attribute(name, value).unwrap_throw();
}
#[cfg(feature = "wasm")]
fn append_child(parent: &web_sys::Element, child: &web_sys::Node) {
parent.append_child(child).unwrap_throw();
}
#[cfg(feature = "wasm")]
fn create_element(ctx: &WebContext, name: &str) -> web_sys::Element {
ctx.document
.create_element_ns(Some("http://www.w3.org/1998/Math/MathML"), name)
.unwrap_throw()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, AsRefStr)]
#[strum(serialize_all = "lowercase")]
pub enum MathNodeType {
Math,
Annotation,
Semantics,
Mtext,
Mn,
Mo,
Mi,
Mspace,
Mover,
Munder,
Munderover,
Msup,
Msub,
Msubsup,
Mfrac,
Mroot,
Msqrt,
Mtable,
Mtr,
Mtd,
Mlabeledtr,
Mrow,
Menclose,
Mstyle,
Mpadded,
Mphantom,
Mglyph,
}
#[must_use]
pub fn get_space_character(width: f64) -> Option<String> {
if (0.05555..=0.05556).contains(&width) {
Some("\u{200a}".to_owned()) } else if (0.1666..=0.1667).contains(&width) {
Some("\u{2009}".to_owned()) } else if (0.2222..=0.2223).contains(&width) {
Some("\u{2005}".to_owned()) } else if (0.2777..=0.2778).contains(&width) {
Some("\u{2005}\u{200a}".to_owned()) }
else if (-0.05556..=-0.05555).contains(&width) {
Some("\u{200a}\u{2063}".to_owned()) } else if (-0.1667..=-0.1666).contains(&width) {
Some("\u{2009}\u{2063}".to_owned()) } else if (-0.2223..=-0.2222).contains(&width) {
Some("\u{205f}\u{2063}".to_owned()) } else if (-0.2778..=-0.2777).contains(&width) {
Some("\u{2005}\u{2063}".to_owned()) } else {
None
}
}
#[derive(Clone)]
pub enum MathDomNode {
Math(MathNode),
Text(TextNode),
Space(SpaceNode),
Fragment(Box<MathDomFragment>),
}
pub type MathDomFragment = DocumentFragment<MathDomNode>;
#[must_use]
pub fn make_fragment(children: Vec<MathDomNode>) -> MathDomFragment {
MathDomFragment {
children,
classes: ClassList::Empty,
depth: 0.0,
height: 0.0,
max_font_size: 0.0,
style: CssStyle::default(),
}
}
#[derive(Clone)]
pub struct MathNode {
pub node_type: MathNodeType,
pub attributes: KeyMap<String, String>,
pub children: Vec<MathDomNode>,
pub classes: ClassList,
}
impl Debug for MathNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MathNode")
.field("node_type", &self.node_type)
.field("attributes", &self.attributes)
.field(
"children",
&format_args!("{} children", self.children.len()),
)
.field("classes", &self.classes)
.finish()
}
}
#[bon]
impl MathNode {
#[builder]
pub fn new(
node_type: MathNodeType,
attributes: Option<KeyMap<String, String>>,
children: Option<Vec<MathDomNode>>,
classes: Option<ClassList>,
) -> Self {
Self {
node_type,
attributes: attributes.unwrap_or_default(),
children: children.unwrap_or_default(),
classes: classes.unwrap_or_default(),
}
}
#[must_use]
pub fn with_children(node_type: MathNodeType, children: Vec<MathDomNode>) -> Self {
Self {
node_type,
attributes: KeyMap::default(),
children,
classes: ClassList::Empty,
}
}
pub fn add_child(&mut self, child: MathDomNode) {
self.children.push(child);
}
pub fn set_attribute<K, V>(&mut self, key: K, value: V)
where
K: Into<String>,
V: Into<String>,
{
self.attributes.insert(key.into(), value.into());
}
fn to_text(&self) -> String {
self.children.iter().map(MathDomNode::to_text).collect()
}
}
impl VirtualNode for MathNode {
fn write_markup(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), ParseError> {
map_fmt(write!(fmt, "<{}", self.node_type.as_ref()))?;
if !self.classes.is_empty() {
map_fmt(fmt.write_str(" class=\""))?;
let mut first = true;
for class in &self.classes {
if !first {
map_fmt(fmt.write_char(' '))?;
}
first = false;
map_fmt(escape_into(fmt, class))?;
}
map_fmt(fmt.write_char('"'))?;
}
for (key, value) in &self.attributes {
map_fmt(write!(fmt, " {key}=\""))?;
map_fmt(escape_into(fmt, value))?;
map_fmt(fmt.write_char('"'))?;
}
map_fmt(fmt.write_char('>'))?;
for child in &self.children {
child.write_markup(fmt)?;
}
map_fmt(write!(fmt, "</{}>", self.node_type.as_ref()))?;
Ok(())
}
#[cfg(feature = "wasm")]
fn to_node(&self, ctx: &WebContext) -> web_sys::Node {
use wasm_bindgen::JsCast as _;
let element = create_element(ctx, self.node_type.as_ref());
for (key, value) in &self.attributes {
set_attribute(&element, key, value);
}
if !self.classes.is_empty() {
let class_str = create_class(&self.classes);
set_attribute(&element, "class", &class_str);
}
let mut i = 0;
while i < self.children.len() {
if let MathDomNode::Text(text_node) = &self.children[i] {
if i + 1 < self.children.len()
&& let MathDomNode::Text(next_text_node) = &self.children[i + 1]
{
let combined_text = format!("{}{}", text_node.text, next_text_node.text);
let text_node = ctx.document.create_text_node(&combined_text);
append_child(&element, &text_node);
i += 2; continue;
}
}
let child_node = self.children[i].to_node(ctx);
append_child(&element, &child_node);
i += 1;
}
element.unchecked_into::<web_sys::Node>()
}
}
#[derive(Debug, Clone)]
pub struct TextNode {
pub text: String,
}
impl TextNode {
fn to_text(&self) -> String {
self.text.clone()
}
}
impl VirtualNode for TextNode {
fn write_markup(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), ParseError> {
map_fmt(escape_into(fmt, &self.text))?;
Ok(())
}
#[cfg(feature = "wasm")]
fn to_node(&self, ctx: &WebContext) -> web_sys::Node {
ctx.document
.create_text_node(&self.text)
.unchecked_into::<web_sys::Node>()
}
}
#[derive(Debug, Clone)]
pub struct SpaceNode {
pub width: f64,
pub character: Option<String>,
}
impl SpaceNode {
#[must_use]
pub fn new(width: f64) -> Self {
let character = get_space_character(width);
Self { width, character }
}
fn to_text(&self) -> String {
self.character.clone().unwrap_or_else(|| " ".to_owned())
}
}
impl VirtualNode for SpaceNode {
fn write_markup(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), ParseError> {
if let Some(character) = &self.character {
map_fmt(fmt.write_str("<mtext>"))?;
map_fmt(escape_into(fmt, character))?;
map_fmt(fmt.write_str("</mtext>"))?;
} else {
let width = make_em(self.width);
map_fmt(fmt.write_str("<mspace width=\""))?;
map_fmt(fmt.write_str(&width))?;
map_fmt(fmt.write_str("\"/>"))?;
}
Ok(())
}
#[cfg(feature = "wasm")]
fn to_node(&self, ctx: &WebContext) -> web_sys::Node {
use wasm_bindgen::JsCast as _;
self.character.as_ref().map_or_else(
|| {
let element = create_element(ctx, "mspace");
set_attribute(&element, "width", &make_em(self.width));
element.unchecked_into::<web_sys::Node>()
},
|character| {
ctx.document
.create_text_node(character)
.unchecked_into::<web_sys::Node>()
},
)
}
}
impl MathDomNode {
pub fn to_text(&self) -> String {
match self {
Self::Math(node) => node.to_text(),
Self::Text(node) => node.to_text(),
Self::Space(node) => node.to_text(),
Self::Fragment(fragment) => fragment.children.iter().map(Self::to_text).collect(),
}
}
}
impl VirtualNode for MathDomNode {
fn write_markup(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), ParseError> {
match self {
Self::Math(node) => node.write_markup(fmt),
Self::Text(node) => node.write_markup(fmt),
Self::Space(node) => node.write_markup(fmt),
Self::Fragment(fragment) => fragment.write_markup(fmt),
}
}
#[cfg(feature = "wasm")]
fn to_node(&self, ctx: &WebContext) -> web_sys::Node {
match self {
Self::Math(node) => node.to_node(ctx),
Self::Text(node) => node.to_node(ctx),
Self::Space(node) => node.to_node(ctx),
Self::Fragment(fragment) => fragment.to_node(ctx),
}
}
}
impl MathDomNode {
#[must_use]
pub const fn as_math_node(&self) -> Option<&MathNode> {
match self {
Self::Math(node) => Some(node),
_ => None,
}
}
#[must_use]
pub const fn as_text_node(&self) -> Option<&TextNode> {
match self {
Self::Text(node) => Some(node),
_ => None,
}
}
#[must_use]
pub const fn as_space_node(&self) -> Option<&SpaceNode> {
match self {
Self::Space(node) => Some(node),
_ => None,
}
}
pub const fn as_math_node_mut(&mut self) -> Option<&mut MathNode> {
match self {
Self::Math(node) => Some(node),
_ => None,
}
}
pub const fn as_text_node_mut(&mut self) -> Option<&mut TextNode> {
match self {
Self::Text(node) => Some(node),
_ => None,
}
}
pub const fn as_space_node_mut(&mut self) -> Option<&mut SpaceNode> {
match self {
Self::Space(node) => Some(node),
_ => None,
}
}
#[must_use]
pub const fn as_fragment(&self) -> Option<&MathDomFragment> {
match self {
Self::Fragment(fragment) => Some(fragment),
_ => None,
}
}
pub const fn as_fragment_mut(&mut self) -> Option<&mut MathDomFragment> {
match self {
Self::Fragment(fragment) => Some(fragment),
_ => None,
}
}
}
impl From<MathNode> for MathDomNode {
fn from(node: MathNode) -> Self {
Self::Math(node)
}
}
impl From<TextNode> for MathDomNode {
fn from(node: TextNode) -> Self {
Self::Text(node)
}
}
impl From<SpaceNode> for MathDomNode {
fn from(node: SpaceNode) -> Self {
Self::Space(node)
}
}
impl From<MathDomFragment> for MathDomNode {
fn from(fragment: MathDomFragment) -> Self {
Self::Fragment(Box::new(fragment))
}
}