use crate::namespace::KeyMap;
use core::mem;
use strum::IntoDiscriminant as _;
use crate::ParseError;
use crate::build_common::{FONT_MAP, make_span};
use crate::context::KatexContext;
use crate::dom_tree::{DomSpan, HtmlDomNode};
use crate::font_metrics::get_character_metrics;
use crate::mathml_tree::{MathDomNode, MathNode, MathNodeType, TextNode};
use crate::options::{FontShape, FontWeight, Options};
use crate::parser::parse_node::AnyParseNode;
use crate::symbols::{Symbols, is_ligature};
use crate::types::ClassList;
use crate::types::Mode;
use crate::types::ParseErrorKind;
#[must_use]
pub fn make_text(text: &str, mode: Mode, options: Option<&Options>, symbols: &Symbols) -> TextNode {
let mut final_text = text.to_owned();
if let Some(char_info) = symbols.get(mode, text)
&& let Some(replace) = &char_info.replace
{
let char_code = text.chars().next().unwrap_or('\0') as u32;
if !(0x1D400..=0x1D7FF).contains(&char_code) {
let skip_replacement = options.is_some_and(|opts| {
let font_family = &opts.font_family;
let font = &opts.font;
let is_tt_font = (font_family.len() >= 6 && &font_family[4..6] == "tt")
|| (font.len() >= 6 && &font[4..6] == "tt");
is_ligature(text) && is_tt_font
});
if !skip_replacement {
final_text = replace.to_string();
}
}
}
TextNode { text: final_text }
}
#[must_use]
pub fn make_row(mut body: Vec<MathDomNode>) -> MathDomNode {
if body.len() == 1
&& let Some(node) = body.pop()
{
return node;
}
MathDomNode::Math(MathNode {
node_type: MathNodeType::Mrow,
attributes: KeyMap::default(),
children: body,
classes: ClassList::Empty,
})
}
pub fn get_variant(
ctx: &KatexContext,
group: &AnyParseNode,
options: &Options,
) -> Result<Option<&'static str>, ParseError> {
let Some(text) = group.text() else {
return Ok(None);
};
if text == "\\imath" || text == "\\jmath" {
return Ok(None);
}
if options.font_family == "texttt" {
return Ok(Some("monospace"));
} else if options.font_family == "textsf" {
return Ok(Some(match (&options.font_shape, &options.font_weight) {
(FontShape::TextIt, FontWeight::TextBf) => "sans-serif-bold-italic",
(FontShape::TextIt, _) => "sans-serif-italic",
(_, FontWeight::TextBf) => "bold-sans-serif",
_ => "sans-serif",
}));
} else if options.font_shape == FontShape::TextIt && options.font_weight == FontWeight::TextBf {
return Ok(Some("bold-italic"));
} else if options.font_shape == FontShape::TextIt {
return Ok(Some("italic"));
} else if options.font_weight == FontWeight::TextBf {
return Ok(Some("bold"));
}
let font = &options.font;
if font.is_empty() || font == "mathnormal" {
return Ok(None);
}
let mode = group.mode();
if let Some(result) = match font.as_str() {
"mathit" => Some("italic"),
"boldsymbol" => match group {
AnyParseNode::TextOrd(_) => Some("bold"),
_ => Some("bold-italic"),
},
"mathbf" => Some("bold"),
"mathbb" => Some("double-struck"),
"mathsfit" => Some("sans-serif-italic"),
"mathfrak" => Some("fraktur"),
"mathscr" | "mathcal" => {
Some("script")
}
"mathsf" => Some("sans-serif"),
"mathtt" => Some("monospace"),
_ => None,
} {
return Ok(Some(result));
}
let final_text = if let Some(char_info) = ctx.symbols.get(mode, text)
&& let Some(replaced) = char_info.replace
{
replaced.to_string()
} else {
text.to_owned()
};
if let Some(font_entry) = FONT_MAP.get(font)
&& let Some(final_char) = final_text.chars().next()
&& get_character_metrics(ctx, final_char, font_entry.font_name, mode)?.is_some()
{
return Ok(Some(font_entry.variant));
}
Ok(None)
}
fn is_number_punctuation(group: Option<&MathNode>) -> bool {
if let Some(node) = group {
if node.node_type == MathNodeType::Mi && node.children.len() == 1 {
if let Some(child) = node.children.first()
&& let MathDomNode::Text(text_node) = child
{
return text_node.text == ".";
}
} else if node.node_type == MathNodeType::Mo && node.children.len() == 1 {
let has_separator = node
.attributes
.get("separator")
.is_some_and(|s| s == "true");
let lspace = node.attributes.get("lspace").is_some_and(|s| s == "0em");
let rspace = node.attributes.get("rspace").is_some_and(|s| s == "0em");
if has_separator
&& lspace
&& rspace
&& let Some(child) = node.children.first()
&& let MathDomNode::Text(text_node) = child
{
return text_node.text == ",";
}
}
}
false
}
pub fn build_expression(
ctx: &KatexContext,
expression: &[AnyParseNode],
options: &Options,
is_ordgroup: Option<bool>,
) -> Result<Vec<MathDomNode>, ParseError> {
if expression.is_empty() {
return Ok(Vec::new());
}
if expression.len() == 1 {
let group = build_group(ctx, &expression[0], options)?;
if let Some(math_node) = group.as_math_node()
&& is_ordgroup.unwrap_or(false)
&& math_node.node_type == MathNodeType::Mo
{
let mut new_node = math_node.clone();
new_node
.attributes
.insert("lspace".to_owned(), "0em".to_owned());
new_node
.attributes
.insert("rspace".to_owned(), "0em".to_owned());
return Ok(vec![MathDomNode::Math(new_node)]);
}
return Ok(vec![group]);
}
let mut groups_enum: Vec<MathDomNode> = Vec::with_capacity(expression.len());
for node in expression {
let mut group = build_group(ctx, node, options)?;
if let Some(mut last_group) = groups_enum.pop() {
let mut repush_last = true;
let mut push_current = true;
if let (Some(last_math), Some(current_math)) =
(last_group.as_math_node_mut(), group.as_math_node_mut())
{
if current_math.node_type == MathNodeType::Mtext
&& last_math.node_type == MathNodeType::Mtext
{
let mathvariant_match = current_math.attributes.get("mathvariant")
== last_math.attributes.get("mathvariant");
if mathvariant_match {
last_math.children.append(&mut current_math.children);
push_current = false;
}
}
else if (is_number_punctuation(Some(&*current_math))
|| current_math.node_type == MathNodeType::Mn)
&& last_math.node_type == MathNodeType::Mn
{
last_math.children.append(&mut current_math.children);
push_current = false;
}
else if current_math.node_type == MathNodeType::Mn
&& is_number_punctuation(Some(&*last_math))
{
let prefix = mem::take(&mut last_math.children);
current_math.children.splice(0..0, prefix);
repush_last = false;
}
else if (current_math.node_type == MathNodeType::Msup
|| current_math.node_type == MathNodeType::Msub)
&& !current_math.children.is_empty()
&& (last_math.node_type == MathNodeType::Mn
|| is_number_punctuation(Some(&*last_math)))
{
if let Some(base) = current_math.children.first_mut()
&& let Some(base_math) = base.as_math_node_mut()
&& base_math.node_type == MathNodeType::Mn
{
let mut prefix = mem::take(&mut last_math.children);
prefix.append(&mut base_math.children);
base_math.children = prefix;
repush_last = false;
}
}
else if last_math.node_type == MathNodeType::Mi
&& last_math.children.len() == 1
&& let Some(last_child) = last_math.children.first()
&& let Some(text_node) = last_child.as_text_node()
&& text_node.text == "\u{0338}"
&& (current_math.node_type == MathNodeType::Mo
|| current_math.node_type == MathNodeType::Mi
|| current_math.node_type == MathNodeType::Mn)
&& let Some(child) = current_math.children.first_mut()
&& let Some(text_child) = child.as_text_node_mut()
&& !text_child.text.is_empty()
&& let Some(first_char) = text_child.text.chars().next()
{
let insert_pos = first_char.len_utf8();
text_child.text.insert(insert_pos, '\u{0338}');
repush_last = false;
}
}
if repush_last {
groups_enum.push(last_group);
}
if push_current {
groups_enum.push(group);
}
} else {
groups_enum.push(group);
}
}
Ok(groups_enum)
}
pub fn build_expression_row(
ctx: &KatexContext,
expression: &[AnyParseNode],
options: &Options,
is_ordgroup: Option<bool>,
) -> Result<MathDomNode, ParseError> {
let body = build_expression(ctx, expression, options, is_ordgroup)?;
Ok(make_row(body))
}
pub fn build_group(
ctx: &KatexContext,
group: &AnyParseNode,
options: &Options,
) -> Result<MathDomNode, ParseError> {
let group_type = group.discriminant();
ctx.mathml_group_builders.get(&group_type).map_or_else(
|| {
Err(ParseError::new(ParseErrorKind::UnknownGroupType {
group_type,
}))
},
|builder| builder(group, options, ctx),
)
}
pub fn build_mathml(
ctx: &KatexContext,
tree: &[AnyParseNode],
tex_expression: &str,
options: &Options,
is_display_mode: bool,
for_mathml_only: bool,
) -> Result<DomSpan, ParseError> {
let expression = build_expression(ctx, tree, options, None)?;
let expression_enum = expression;
let wrapper_enum = if expression_enum.len() == 1 {
if let Some(math_node) = expression_enum[0].as_math_node() {
if matches!(
math_node.node_type,
MathNodeType::Mrow | MathNodeType::Mtable
) {
expression_enum[0].clone()
} else {
MathDomNode::Math(MathNode {
node_type: MathNodeType::Mrow,
attributes: KeyMap::default(),
children: expression_enum.clone(),
classes: ClassList::Empty,
})
}
} else {
MathDomNode::Math(MathNode {
node_type: MathNodeType::Mrow,
attributes: KeyMap::default(),
children: expression_enum,
classes: ClassList::Empty,
})
}
} else {
MathDomNode::Math(MathNode {
node_type: MathNodeType::Mrow,
attributes: KeyMap::default(),
children: expression_enum,
classes: ClassList::Empty,
})
};
let annotation_enum = MathDomNode::Math(MathNode {
node_type: MathNodeType::Annotation,
attributes: KeyMap::default(),
children: vec![MathDomNode::Text(TextNode {
text: tex_expression.to_owned(),
})],
classes: ClassList::Empty,
});
let annotation_with_encoding = if let MathDomNode::Math(mut node) = annotation_enum {
node.attributes
.insert("encoding".to_owned(), "application/x-tex".to_owned());
MathDomNode::Math(node)
} else {
annotation_enum
};
let semantics_enum = MathDomNode::Math(MathNode {
node_type: MathNodeType::Semantics,
attributes: KeyMap::default(),
children: vec![wrapper_enum, annotation_with_encoding],
classes: ClassList::Empty,
});
let mut math_enum = MathDomNode::Math(MathNode {
node_type: MathNodeType::Math,
attributes: KeyMap::default(),
children: vec![semantics_enum],
classes: ClassList::Empty,
});
if let MathDomNode::Math(ref mut math_node) = math_enum {
math_node.attributes.insert(
"xmlns".to_owned(),
"http://www.w3.org/1998/Math/MathML".to_owned(),
);
if is_display_mode {
math_node
.attributes
.insert("display".to_owned(), "block".to_owned());
}
}
let math_node = if let MathDomNode::Math(node) = math_enum {
node
} else {
MathNode::builder().node_type(MathNodeType::Math).build()
};
let wrapper_class = if for_mathml_only {
"katex"
} else {
"katex-mathml"
};
Ok(make_span(
ClassList::Static(wrapper_class),
vec![HtmlDomNode::MathML(math_node)],
None,
None,
))
}