use core::fmt::Write as _;
use crate::ParseError;
use crate::namespace::KeyMap;
use bon::bon;
use phf::phf_map;
#[cfg(feature = "wasm")]
use web_sys;
use crate::mathml_tree::MathNode;
use crate::options::Options;
use crate::svg_geometry::PATH_MAP;
use crate::tree::{DocumentFragment, VirtualNode};
use crate::types::{CssProperty, CssStyle};
use crate::unicode::script_from_codepoint;
use crate::units::make_em;
use crate::utils::escape;
#[derive(Debug, Clone, PartialEq)]
pub struct Span<T> {
pub children: Vec<T>,
pub attributes: KeyMap<String, String>,
pub classes: Vec<String>,
pub height: f64,
pub depth: f64,
pub width: Option<f64>,
pub max_font_size: f64,
pub style: CssStyle,
pub is_middle: Option<(String, Options)>,
pub italic: Option<f64>,
}
#[bon]
impl<T> Span<T> {
#[builder]
#[expect(clippy::option_option)]
pub fn new(
#[builder(finish_fn)]
options: Option<&Options>,
children: Vec<T>,
attributes: Option<KeyMap<String, String>>,
classes: Option<Vec<String>>,
height: Option<f64>,
depth: Option<f64>,
width: Option<Option<f64>>,
max_font_size: Option<f64>,
style: Option<CssStyle>,
is_middle: Option<(String, Options)>,
) -> Self {
let mut span = Self {
children,
attributes: attributes.unwrap_or_default(),
classes: classes.unwrap_or_default(),
height: height.unwrap_or_default(),
depth: depth.unwrap_or_default(),
width: width.unwrap_or(None),
max_font_size: max_font_size.unwrap_or_default(),
style: style.unwrap_or_default(),
is_middle,
italic: None,
};
if let Some(options) = options {
init_node(&mut span.classes, &mut span.style, options);
}
span
}
}
#[derive(Debug, Clone)]
pub struct Anchor {
pub children: Vec<HtmlDomNode>,
pub attributes: KeyMap<String, String>,
pub classes: Vec<String>,
pub height: f64,
pub depth: f64,
pub max_font_size: f64,
pub style: CssStyle,
}
impl From<Anchor> for HtmlDomNode {
fn from(anchor: Anchor) -> Self {
Self::Anchor(anchor)
}
}
#[bon]
impl Anchor {
#[builder]
pub fn new(
#[builder(finish_fn)]
options: Option<&Options>,
children: Option<Vec<HtmlDomNode>>,
attributes: Option<KeyMap<String, String>>,
classes: Option<Vec<String>>,
height: Option<f64>,
depth: Option<f64>,
max_font_size: Option<f64>,
style: Option<CssStyle>,
) -> Self {
let mut anchor = Self {
children: children.unwrap_or_default(),
attributes: attributes.unwrap_or_default(),
classes: classes.unwrap_or_default(),
height: height.unwrap_or_default(),
depth: depth.unwrap_or_default(),
max_font_size: max_font_size.unwrap_or_default(),
style: style.unwrap_or_default(),
};
if let Some(options) = options {
init_node(&mut anchor.classes, &mut anchor.style, options);
}
anchor
}
}
impl Anchor {
#[must_use]
pub const fn new(
children: Vec<HtmlDomNode>,
attributes: KeyMap<String, String>,
classes: Vec<String>,
height: f64,
depth: f64,
max_font_size: f64,
style: CssStyle,
) -> Self {
Self {
children,
attributes,
classes,
height,
depth,
max_font_size,
style,
}
}
}
#[derive(Debug, Clone)]
pub struct Img {
pub src: String,
pub alt: String,
pub classes: Vec<String>,
pub height: f64,
pub depth: f64,
pub max_font_size: f64,
pub style: CssStyle,
}
impl Img {
#[must_use]
pub fn new(
src: String,
alt: String,
height: f64,
depth: f64,
max_font_size: f64,
style: CssStyle,
) -> Self {
Self {
src,
alt,
classes: vec!["mord".to_owned()],
height,
depth,
max_font_size,
style,
}
}
}
#[derive(Debug, Clone)]
pub struct SymbolNode {
pub text: String,
pub height: f64,
pub depth: f64,
pub italic: f64,
pub skew: f64,
pub width: f64,
pub max_font_size: f64,
pub classes: Vec<String>,
pub style: CssStyle,
}
impl From<SymbolNode> for HtmlDomNode {
fn from(symbol: SymbolNode) -> Self {
Self::Symbol(symbol)
}
}
const I_COMBINATIONS: phf::Map<&str, &str> = phf_map! {
"\u{ee}" => "\u{0131}\u{0302}",
"\u{ef}" => "\u{0131}\u{0308}",
"\u{ed}" => "\u{0131}\u{0301}",
"\u{ec}" => "\u{0131}\u{0300}",
};
#[bon]
impl SymbolNode {
#[builder]
pub fn new(
text: &str,
height: Option<f64>,
depth: Option<f64>,
italic: Option<f64>,
skew: Option<f64>,
width: Option<f64>,
max_font_size: Option<f64>,
classes: Option<Vec<String>>,
style: Option<CssStyle>,
) -> Self {
let mut classes = classes.unwrap_or_default();
if let Some(first_ch) = text.chars().next()
&& let Some(script) = script_from_codepoint(first_ch as u32)
{
classes.push(format!("{script}_fallback"));
}
let text = I_COMBINATIONS
.get(text)
.map_or_else(|| text.to_owned(), ToString::to_string);
Self {
text,
height: height.unwrap_or_default(),
depth: depth.unwrap_or_default(),
italic: italic.unwrap_or_default(),
skew: skew.unwrap_or_default(),
width: width.unwrap_or_default(),
max_font_size: max_font_size.unwrap_or_default(),
classes,
style: style.unwrap_or_default(),
}
}
}
pub type DomSpan = Span<HtmlDomNode>;
#[derive(Debug, Clone)]
pub enum HtmlDomNode {
DomSpan(Span<HtmlDomNode>),
Anchor(Anchor),
Img(Img),
Symbol(SymbolNode),
SvgNode(SvgNode),
MathML(MathNode),
Fragment(HtmlDomFragment),
}
impl From<Span<Self>> for HtmlDomNode {
fn from(span: Span<Self>) -> Self {
Self::DomSpan(span)
}
}
#[derive(Debug, Clone)]
pub enum SvgChildNode {
Path(PathNode),
Line(LineNode),
}
impl SvgChildNode {
pub fn to_markup(&self) -> Result<String, ParseError> {
match self {
Self::Path(path_node) => path_node.to_markup(),
Self::Line(line_node) => line_node.to_markup(),
}
}
#[cfg(feature = "wasm")]
#[must_use]
pub fn to_node(&self) -> web_sys::Node {
match self {
Self::Path(path_node) => path_node.to_node(),
Self::Line(line_node) => line_node.to_node(),
}
}
}
pub type HtmlDomFragment = DocumentFragment<HtmlDomNode>;
impl From<HtmlDomFragment> for HtmlDomNode {
fn from(fragment: HtmlDomFragment) -> Self {
Self::Fragment(fragment)
}
}
#[derive(Debug, Clone)]
pub struct SvgNode {
pub children: Vec<SvgChildNode>,
pub attributes: KeyMap<String, String>,
}
#[bon]
impl SvgNode {
#[builder]
pub fn new(
children: Vec<SvgChildNode>,
attributes: Option<KeyMap<String, String>>,
) -> Self {
Self {
children,
attributes: attributes.unwrap_or_default(),
}
}
}
pub fn create_class(classes: &[String]) -> String {
classes
.iter()
.filter(|cls| !cls.is_empty())
.map(String::as_str)
.collect::<Vec<&str>>()
.join(" ")
}
#[inline]
fn init_node(classes: &mut Vec<String>, style: &mut CssStyle, options: &Options) {
if options.style.is_tight() {
classes.push("mtight".to_owned());
}
if let Some(color) = options.get_color() {
style.insert(CssProperty::Color, color);
}
}
#[cfg(feature = "wasm")]
#[must_use]
pub fn to_node(node: &HtmlDomNode) -> web_sys::Node {
node.to_node()
}
pub fn to_markup(node: &HtmlDomNode) -> Result<String, ParseError> {
node.to_markup()
}
fn write_node_class(markup: &mut String, classes: &[String]) {
if !classes.is_empty() {
let _ = write!(markup, " class=\"{}\"", escape(&create_class(classes)));
}
}
fn write_node_style(markup: &mut String, style: &CssStyle) {
if !style.is_empty() {
let styles = style.iter().fold(String::new(), |mut s, (key, value)| {
let _ = write!(s, "{key}:{value};");
s
});
let _ = write!(markup, " style=\"{}\"", escape(&styles));
}
}
#[cfg(feature = "wasm")]
fn class_to_node(element: &web_sys::Element, classes: &[String]) {
if !classes.is_empty() {
let class_attr = create_class(classes);
element.set_attribute("class", &class_attr).unwrap();
}
}
#[cfg(feature = "wasm")]
fn style_to_node(element: &web_sys::Element, style: &CssStyle) {
if !style.is_empty() {
let styles = style.iter().fold(String::new(), |mut s, (key, value)| {
let _ = write!(s, "{key}:{value};");
s
});
element.set_attribute("style", &styles).unwrap();
}
}
impl<T: VirtualNode> VirtualNode for Span<T> {
fn to_markup(&self) -> Result<String, ParseError> {
let mut markup = String::from("<span");
write_node_class(&mut markup, &self.classes);
write_node_style(&mut markup, &self.style);
node_attributes_to_markup(&mut markup, &self.attributes)?;
markup.push('>');
for child in &self.children {
markup.push_str(&child.to_markup()?);
}
markup.push_str("</span>");
Ok(markup)
}
#[cfg(feature = "wasm")]
fn to_node(&self) -> web_sys::Node {
use wasm_bindgen::JsCast as _;
let document = web_sys::window().unwrap().document().unwrap();
let element = document.create_element("span").unwrap();
class_to_node(&element, &self.classes);
style_to_node(&element, &self.style);
node_attributes_to_node(&element, &self.attributes);
for child in &self.children {
let child_node = child.to_node();
element.append_child(&child_node).unwrap();
}
element.dyn_into::<web_sys::Node>().unwrap()
}
}
impl VirtualNode for Anchor {
fn to_markup(&self) -> Result<String, ParseError> {
let mut markup = String::from("<a");
write_node_class(&mut markup, &self.classes);
write_node_style(&mut markup, &self.style);
node_attributes_to_markup(&mut markup, &self.attributes)?;
markup.push('>');
for child in &self.children {
markup.push_str(&child.to_markup()?);
}
markup.push_str("</a>");
Ok(markup)
}
#[cfg(feature = "wasm")]
fn to_node(&self) -> web_sys::Node {
use wasm_bindgen::JsCast as _;
let document = web_sys::window().unwrap().document().unwrap();
let element = document.create_element("a").unwrap();
class_to_node(&element, &self.classes);
style_to_node(&element, &self.style);
node_attributes_to_node(&element, &self.attributes);
for child in &self.children {
let child_node = child.to_node();
element.append_child(&child_node).unwrap();
}
element.dyn_into::<web_sys::Node>().unwrap()
}
}
impl VirtualNode for Img {
fn to_markup(&self) -> Result<String, ParseError> {
let mut markup = format!(
"<img src=\"{}\" alt=\"{}\"",
escape(&self.src),
escape(&self.alt)
);
write_node_class(&mut markup, &self.classes);
write_node_style(&mut markup, &self.style);
markup.push_str("/>");
Ok(markup)
}
#[cfg(feature = "wasm")]
fn to_node(&self) -> web_sys::Node {
use wasm_bindgen::JsCast as _;
let document = web_sys::window().unwrap().document().unwrap();
let element = document.create_element("img").unwrap();
element.set_attribute("src", &self.src).unwrap();
element.set_attribute("alt", &self.alt).unwrap();
class_to_node(&element, &self.classes);
style_to_node(&element, &self.style);
element.dyn_into::<web_sys::Node>().unwrap()
}
}
fn symbol_node_style_str(italic: f64, style: &CssStyle) -> String {
let mut styles = String::new();
if italic > 0.0 {
let _ = write!(styles, "margin-right:{};", make_em(italic));
}
for (key, value) in style {
let _ = write!(styles, "{key}:{value};");
}
escape(&styles)
}
impl VirtualNode for SymbolNode {
fn to_markup(&self) -> Result<String, ParseError> {
let mut needs_span =
self.italic > 0.0 || !self.classes.is_empty() || !self.style.is_empty();
let mut markup = String::from("<span");
if !self.classes.is_empty() {
needs_span = true;
let _ = write!(
markup,
" class=\"{}\"",
escape(&create_class(&self.classes))
);
}
let styles = symbol_node_style_str(self.italic, &self.style);
if !styles.is_empty() {
needs_span = true;
let _ = write!(markup, " style=\"{styles}\"");
}
let escaped_text = escape(&self.text);
if needs_span {
markup.push('>');
markup.push_str(&escaped_text);
markup.push_str("</span>");
Ok(markup)
} else {
Ok(escaped_text)
}
}
#[cfg(feature = "wasm")]
fn to_node(&self) -> web_sys::Node {
use wasm_bindgen::JsCast as _;
let document = web_sys::window().unwrap().document().unwrap();
let needs_span = self.italic > 0.0 || !self.classes.is_empty() || !self.style.is_empty();
if needs_span {
let element = document.create_element("span").unwrap();
if !self.classes.is_empty() {
let class_attr = create_class(&self.classes);
element.set_attribute("class", &class_attr).unwrap();
}
let styles = symbol_node_style_str(self.italic, &self.style);
if !styles.is_empty() {
element.set_attribute("style", &styles).unwrap();
}
let text_node = document.create_text_node(&self.text);
element.append_child(&text_node).unwrap();
element.dyn_into::<web_sys::Node>().unwrap()
} else {
document
.create_text_node(&self.text)
.dyn_into::<web_sys::Node>()
.unwrap()
}
}
}
impl VirtualNode for SvgNode {
fn to_markup(&self) -> Result<String, ParseError> {
let mut markup = String::from("<svg xmlns=\"http://www.w3.org/2000/svg\"");
node_attributes_to_markup(&mut markup, &self.attributes)?;
markup.push('>');
for child in &self.children {
markup.push_str(&child.to_markup()?);
}
markup.push_str("</svg>");
Ok(markup)
}
#[cfg(feature = "wasm")]
fn to_node(&self) -> web_sys::Node {
use wasm_bindgen::JsCast as _;
let document = web_sys::window().unwrap().document().unwrap();
let element = document
.create_element_ns(Some("http://www.w3.org/2000/svg"), "svg")
.unwrap();
node_attributes_to_node(&element, &self.attributes);
for child in &self.children {
let child_node = child.to_node();
element.append_child(&child_node).unwrap();
}
element.dyn_into::<web_sys::Node>().unwrap()
}
}
impl VirtualNode for HtmlDomNode {
fn to_markup(&self) -> Result<String, ParseError> {
match self {
Self::DomSpan(span) => span.to_markup(),
Self::Anchor(anchor) => anchor.to_markup(),
Self::Img(img) => img.to_markup(),
Self::Symbol(symbol) => symbol.to_markup(),
Self::SvgNode(svg_node) => svg_node.to_markup(),
Self::MathML(math_node) => math_node.to_markup(),
Self::Fragment(fragment) => fragment.to_markup(),
}
}
#[cfg(feature = "wasm")]
fn to_node(&self) -> web_sys::Node {
match self {
Self::DomSpan(span) => span.to_node(),
Self::Anchor(anchor) => anchor.to_node(),
Self::Img(img) => img.to_node(),
Self::Symbol(symbol) => symbol.to_node(),
Self::SvgNode(svg_node) => svg_node.to_node(),
Self::MathML(math_node) => math_node.to_node(),
Self::Fragment(fragment) => fragment.to_node(),
}
}
}
impl HtmlDomNode {
#[must_use]
pub fn classes(&self) -> &[String] {
match self {
Self::DomSpan(span) => &span.classes,
Self::Anchor(anchor) => &anchor.classes,
Self::Img(img) => &img.classes,
Self::Symbol(symbol) => &symbol.classes,
Self::SvgNode(_) | Self::MathML { .. } => &[],
Self::Fragment(fragment) => &fragment.classes,
}
}
pub const fn classes_mut(&mut self) -> Option<&mut Vec<String>> {
match self {
Self::DomSpan(span) => Some(&mut span.classes),
Self::Anchor(anchor) => Some(&mut anchor.classes),
Self::Img(img) => Some(&mut img.classes),
Self::Symbol(symbol) => Some(&mut symbol.classes),
Self::SvgNode(_) | Self::MathML { .. } => None,
Self::Fragment(fragment) => Some(&mut fragment.classes),
}
}
#[must_use]
pub const fn height(&self) -> f64 {
match self {
Self::DomSpan(span) => span.height,
Self::Anchor(anchor) => anchor.height,
Self::Img(img) => img.height,
Self::Symbol(symbol) => symbol.height,
Self::SvgNode(_) | Self::MathML { .. } => 0.0,
Self::Fragment(fragment) => fragment.height,
}
}
pub const fn height_mut(&mut self) -> Option<&mut f64> {
match self {
Self::DomSpan(span) => Some(&mut span.height),
Self::Anchor(anchor) => Some(&mut anchor.height),
Self::Img(img) => Some(&mut img.height),
Self::Symbol(symbol) => Some(&mut symbol.height),
Self::SvgNode(_) | Self::MathML { .. } => None,
Self::Fragment(fragment) => Some(&mut fragment.height),
}
}
#[must_use]
pub const fn depth(&self) -> f64 {
match self {
Self::DomSpan(span) => span.depth,
Self::Anchor(anchor) => anchor.depth,
Self::Img(img) => img.depth,
Self::Symbol(symbol) => symbol.depth,
Self::SvgNode(_) | Self::MathML { .. } => 0.0,
Self::Fragment(fragment) => fragment.depth,
}
}
pub const fn depth_mut(&mut self) -> Option<&mut f64> {
match self {
Self::DomSpan(span) => Some(&mut span.depth),
Self::Anchor(anchor) => Some(&mut anchor.depth),
Self::Img(img) => Some(&mut img.depth),
Self::Symbol(symbol) => Some(&mut symbol.depth),
Self::SvgNode(_) | Self::MathML { .. } => None,
Self::Fragment(fragment) => Some(&mut fragment.depth),
}
}
#[must_use]
pub const fn max_font_size(&self) -> f64 {
match self {
Self::DomSpan(span) => span.max_font_size,
Self::Anchor(anchor) => anchor.max_font_size,
Self::Img(img) => img.max_font_size,
Self::Symbol(symbol) => symbol.max_font_size,
Self::SvgNode(_) | Self::MathML { .. } => 0.0,
Self::Fragment(fragment) => fragment.max_font_size,
}
}
pub const fn max_font_size_mut(&mut self) -> Option<&mut f64> {
match self {
Self::DomSpan(span) => Some(&mut span.max_font_size),
Self::Anchor(anchor) => Some(&mut anchor.max_font_size),
Self::Img(img) => Some(&mut img.max_font_size),
Self::Symbol(symbol) => Some(&mut symbol.max_font_size),
Self::SvgNode(_) | Self::MathML { .. } => None,
Self::Fragment(fragment) => Some(&mut fragment.max_font_size),
}
}
#[must_use]
pub const fn width(&self) -> Option<f64> {
match self {
Self::DomSpan(span) => span.width,
Self::Anchor(_)
| Self::Img(_)
| Self::SvgNode(_)
| Self::MathML { .. }
| Self::Fragment(_) => None,
Self::Symbol(symbol) => Some(symbol.width),
}
}
#[must_use]
pub const fn style(&self) -> Option<&CssStyle> {
match self {
Self::DomSpan(span) => Some(&span.style),
Self::Anchor(anchor) => Some(&anchor.style),
Self::Img(img) => Some(&img.style),
Self::Symbol(symbol) => Some(&symbol.style),
Self::Fragment(fragment) => Some(&fragment.style),
Self::SvgNode(_) | Self::MathML { .. } => None,
}
}
pub const fn style_mut(&mut self) -> Option<&mut CssStyle> {
match self {
Self::DomSpan(span) => Some(&mut span.style),
Self::Anchor(anchor) => Some(&mut anchor.style),
Self::Img(img) => Some(&mut img.style),
Self::Symbol(symbol) => Some(&mut symbol.style),
Self::SvgNode(_) | Self::MathML { .. } => None,
Self::Fragment(fragment) => Some(&mut fragment.style),
}
}
#[must_use]
pub fn has_class(&self, class_name: &str) -> bool {
self.classes().iter().any(|cls| cls == class_name)
}
#[must_use]
pub const fn attributes(&self) -> Option<&KeyMap<String, String>> {
match self {
Self::DomSpan(span) => Some(&span.attributes),
Self::Anchor(anchor) => Some(&anchor.attributes),
Self::Img(_) | Self::Symbol(_) | Self::Fragment(_) => None,
Self::SvgNode(svg_node) => Some(&svg_node.attributes),
Self::MathML(mathml) => Some(&mathml.attributes),
}
}
}
#[derive(Debug, Clone)]
pub struct PathNode {
pub path_name: String,
pub alternate: Option<String>,
}
impl VirtualNode for PathNode {
fn to_markup(&self) -> Result<String, ParseError> {
let path_data = self.alternate.as_ref().map_or_else(
|| {
PATH_MAP
.get(&self.path_name)
.map_or_else(String::new, |s| escape(s))
},
|alt| escape(alt),
);
Ok(format!("<path d=\"{path_data}\"/>"))
}
#[cfg(feature = "wasm")]
fn to_node(&self) -> web_sys::Node {
use wasm_bindgen::JsCast as _;
let document = web_sys::window().unwrap().document().unwrap();
let element = document
.create_element_ns(Some("http://www.w3.org/2000/svg"), "path")
.unwrap();
let path_data = self.alternate.as_ref().map_or_else(
|| {
PATH_MAP
.get(&self.path_name)
.map_or_else(String::new, |s| (*s).to_owned())
},
Clone::clone,
);
element.set_attribute("d", &path_data).unwrap();
element.dyn_into::<web_sys::Node>().unwrap()
}
}
#[derive(Debug, Clone)]
pub struct LineNode {
pub attributes: KeyMap<String, String>,
}
fn node_attributes_to_markup(
markup: &mut String,
attributes: &KeyMap<String, String>,
) -> Result<(), ParseError> {
for (attr, value) in attributes {
if !attr.is_empty() {
if attr.contains(|c: char| {
c.is_whitespace() || "\"'>/=".contains(c) || ('\x00'..='\x1f').contains(&c)
}) {
return Err(ParseError::new(format!("Invalid attribute name: '{attr}'")));
}
let _ = write!(markup, " {}=\"{}\"", attr, escape(value));
}
}
Ok(())
}
#[cfg(feature = "wasm")]
fn node_attributes_to_node(element: &web_sys::Element, attributes: &KeyMap<String, String>) {
for (attr, value) in attributes {
if !attr.is_empty() {
if attr.contains(|c: char| {
c.is_whitespace() || "\"'>/=".contains(c) || ('\x00'..='\x1f').contains(&c)
}) {
continue;
}
element.set_attribute(attr, value).unwrap();
}
}
}
impl VirtualNode for LineNode {
fn to_markup(&self) -> Result<String, ParseError> {
let mut markup = String::from("<line");
node_attributes_to_markup(&mut markup, &self.attributes)?;
markup.push_str("/>");
Ok(markup)
}
#[cfg(feature = "wasm")]
fn to_node(&self) -> web_sys::Node {
use wasm_bindgen::JsCast as _;
let document = web_sys::window().unwrap().document().unwrap();
let element = document
.create_element_ns(Some("http://www.w3.org/2000/svg"), "line")
.unwrap();
node_attributes_to_node(&element, &self.attributes);
element.dyn_into::<web_sys::Node>().unwrap()
}
}