use crate::build_common::{make_span, try_combine_chars};
use crate::dom_tree::{DomSpan, HtmlDomNode};
use crate::options::Options;
use crate::parser::parse_node::AnyParseNode;
use crate::spacing_data::{SPACINGS, TIGHT_SPACINGS};
use crate::types::{CssProperty, ParseError};
use crate::units::make_em;
use crate::utils::OwnedOrMut;
use crate::{KatexContext, build_common};
use alloc::collections::VecDeque;
use core::mem;
use core::ops::DerefMut as _;
use core::str::FromStr as _;
use phf::phf_set;
use strum::{AsRefStr, EnumString, IntoDiscriminant as _};
const BIN_LEFT_CANCELLER: phf::Set<&str> =
phf_set!("leftmost", "mbin", "mopen", "mrel", "mop", "mpunct");
const BIN_RIGHT_CANCELLER: phf::Set<&str> = phf_set!("rightmost", "mrel", "mclose", "mpunct");
#[derive(Debug, Clone, Copy, PartialEq, Eq, AsRefStr, EnumString)]
#[strum(serialize_all = "lowercase")]
pub enum DomType {
Mord,
Mop,
Mbin,
Mrel,
Mopen,
Mclose,
Mpunct,
Minner,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GroupType {
False,
True,
Root,
}
impl GroupType {
#[must_use]
pub const fn is_real(&self) -> bool {
matches!(self, Self::True | Self::Root)
}
#[must_use]
pub const fn is_root(&self) -> bool {
matches!(self, Self::Root)
}
}
impl DomType {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Mord => "mord",
Self::Mop => "mop",
Self::Mbin => "mbin",
Self::Mrel => "mrel",
Self::Mopen => "mopen",
Self::Mclose => "mclose",
Self::Mpunct => "mpunct",
Self::Minner => "minner",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Side {
Left,
Right,
}
#[must_use]
pub fn make_null_delimiter(options: &Options, classes: &[String]) -> DomSpan {
let mut combined_classes =
Vec::with_capacity(classes.len() + 1 + options.base_sizing_classes().len());
combined_classes.extend_from_slice(classes);
combined_classes.push(String::from("nulldelimiter"));
combined_classes.extend(options.base_sizing_classes());
make_span(combined_classes, vec![], Some(options), None)
}
fn check_partial_group(node: &HtmlDomNode) -> Option<&Vec<HtmlDomNode>> {
match node {
HtmlDomNode::Fragment(fragment) => Some(&fragment.children),
HtmlDomNode::Anchor(anchor) => Some(&anchor.children),
HtmlDomNode::DomSpan(span) if span.classes.contains(&"enclosing".to_owned()) => {
Some(&span.children)
}
_ => None,
}
}
fn check_partial_group_mut(node: &mut HtmlDomNode) -> Option<&mut Vec<HtmlDomNode>> {
match node {
HtmlDomNode::Fragment(fragment) => Some(&mut fragment.children),
HtmlDomNode::Anchor(anchor) => Some(&mut anchor.children),
HtmlDomNode::DomSpan(span) if span.classes.contains(&"enclosing".to_owned()) => {
Some(&mut span.children)
}
_ => None,
}
}
fn get_outermost_node(node: &HtmlDomNode, side: Side) -> &HtmlDomNode {
if let Some(children) = check_partial_group(node)
&& !children.is_empty()
{
if side == Side::Right {
return get_outermost_node(&children[children.len() - 1], Side::Right);
}
return get_outermost_node(&children[0], Side::Left);
}
node
}
#[must_use]
pub fn get_type_of_dom_tree(node: &HtmlDomNode, side: Option<Side>) -> Option<DomType> {
let node = side.map_or(node, |side| get_outermost_node(node, side));
let dom_type = DomType::from_str(&node.classes()[0]);
dom_type.ok()
}
fn traverse_non_space_nodes(
ctx: &KatexContext,
nodes: &mut Vec<HtmlDomNode>,
callback: &mut impl FnMut(
&KatexContext,
&mut HtmlDomNode,
&mut HtmlDomNode,
) -> Result<Option<HtmlDomNode>, ParseError>,
prev_node: &mut HtmlDomNode,
next_node: &mut Option<HtmlDomNode>,
is_root: bool,
insertions: Option<&mut VecDeque<HtmlDomNode>>,
) -> Result<(), ParseError> {
let next_in_nodes = mem::take(next_node).is_some_and(|next| {
nodes.push(next);
true
});
let mut insertions = insertions.map_or_else(
|| OwnedOrMut::Owned {
idx: 0,
val: VecDeque::new(),
},
OwnedOrMut::Borrowed,
);
let mut prev_node = OwnedOrMut::Borrowed(prev_node);
let mut i = 0;
while i < nodes.len() {
let node = &mut nodes[i];
let partial_group = check_partial_group_mut(node);
if let Some(children) = partial_group {
traverse_non_space_nodes(
ctx,
children,
callback,
&mut prev_node,
&mut None,
is_root,
Some(&mut *insertions),
)?;
i += 1;
continue;
}
let nonspace = !node.has_class("mspace");
if nonspace {
let result = callback(ctx, node, &mut prev_node)?;
if let Some(new_node) = result {
insertions.deref_mut().push_back(new_node);
}
}
let to_be_prev = if nonspace {
Some(OwnedOrMut::Owned {
idx: i,
val: node.clone(),
})
} else if is_root && node.has_class("newline") {
Some(OwnedOrMut::Owned {
idx: i,
val: build_common::make_span(vec!["leftmost".to_owned()], vec![], None, None)
.into(),
})
} else {
None
};
if let Some(to_be_prev) = to_be_prev {
prev_node = to_be_prev;
}
if let OwnedOrMut::Owned { idx, val: arr } = &mut insertions {
i += arr.len();
nodes.splice((*idx + 1)..=(*idx), arr.drain(..));
*idx = i;
} else {
insertions = OwnedOrMut::Owned {
idx: i,
val: VecDeque::new(),
};
}
i += 1;
}
if let OwnedOrMut::Owned { idx, val: arr } = &mut insertions {
nodes.splice((*idx + 1)..=(*idx), arr.drain(..));
}
if next_in_nodes {
let next = nodes.pop();
*next_node = next;
}
Ok(())
}
pub fn build_expression(
ctx: &KatexContext,
expression: &[AnyParseNode],
options: &Options,
is_real_group: GroupType,
surrounding: (Option<DomType>, Option<DomType>),
) -> Result<Vec<HtmlDomNode>, ParseError> {
let mut groups: Vec<HtmlDomNode> = Vec::new();
for node in expression {
let output = build_group(ctx, node, options, None)?;
if let HtmlDomNode::Fragment(fragment) = output {
groups.extend(fragment.children);
} else {
groups.push(output);
}
}
try_combine_chars(&mut groups);
if !is_real_group.is_real() {
return Ok(groups);
}
let glue_options = if expression.len() == 1 {
if let AnyParseNode::Sizing(sizing) = &expression[0] {
options.having_size(sizing.size)
} else if let AnyParseNode::Styling(styling_node) = &expression[0] {
options.having_style(styling_node.style)
} else {
options.clone()
}
} else {
options.clone()
};
let mut dummy_prev = make_span(
vec![
surrounding
.0
.as_ref()
.map_or_else(|| "leftmost".to_owned(), |s| s.as_str().to_owned()),
],
vec![],
Some(options),
None,
)
.into();
let mut dummy_next = Some(
make_span(
vec![
surrounding
.1
.as_ref()
.map_or_else(|| "rightmost".to_owned(), |s| s.as_str().to_owned()),
],
vec![],
Some(options),
None,
)
.into(),
);
let is_root = is_real_group.is_root();
traverse_non_space_nodes(
ctx,
&mut groups,
&mut |_ctx: &KatexContext, node: &mut HtmlDomNode, prev: &mut HtmlDomNode| {
let prev_type = &prev.classes()[0];
let type_str = &node.classes()[0];
if prev_type == "mbin" && BIN_RIGHT_CANCELLER.contains(type_str) {
if let Some(classes) = prev.classes_mut()
&& !classes.is_empty()
{
"mord".clone_into(&mut classes[0]);
}
} else if type_str == "mbin" && BIN_LEFT_CANCELLER.contains(prev_type) {
if let Some(classes) = node.classes_mut()
&& !classes.is_empty()
{
"mord".clone_into(&mut classes[0]);
}
}
Ok(None)
},
&mut dummy_prev,
&mut dummy_next,
is_root,
None,
)?;
traverse_non_space_nodes(
ctx,
&mut groups,
&mut |ctx: &KatexContext, node: &mut HtmlDomNode, prev: &mut HtmlDomNode| {
let prev_type = get_type_of_dom_tree(prev, None);
let type_opt = get_type_of_dom_tree(node, None);
if let (Some(prev_type), Some(type_val)) = (prev_type, type_opt) {
let space = if node.has_class("mtight") {
TIGHT_SPACINGS
.get(prev_type.as_str())
.and_then(|inner| inner.get(type_val.as_str()))
} else {
SPACINGS
.get(prev_type.as_str())
.and_then(|inner| inner.get(type_val.as_str()))
};
if let Some(space) = space {
let glue = ctx.make_glue(space, &glue_options)?;
return Ok(Some(glue.into()));
}
}
Ok(None)
},
&mut dummy_prev,
&mut dummy_next,
is_root,
None,
)?;
Ok(groups)
}
pub fn build_group(
ctx: &KatexContext,
group: &AnyParseNode,
options: &Options,
base_options: Option<&Options>,
) -> Result<HtmlDomNode, ParseError> {
let group_type = group.discriminant();
let group_node = if let Some(builder) = ctx.html_group_builders.get(&group_type) {
builder(group, options, ctx)?
} else {
return Err(ParseError::new(format!(
"Got group of unknown type: {group_type:?}"
)));
};
if let Some(base_options) = base_options
&& options.size != base_options.size
{
let mut group_node = make_span(
options.sizing_classes(base_options),
vec![group_node],
Some(options),
None,
);
let multiplier = options.size_multiplier / base_options.size_multiplier;
group_node.height *= multiplier;
group_node.depth *= multiplier;
Ok(group_node.into())
} else {
Ok(group_node)
}
}
fn build_html_unbreakable(children: Vec<HtmlDomNode>, options: &Options) -> HtmlDomNode {
let mut body = make_span(vec!["base".to_owned()], children, Some(options), None);
let mut strut = make_span(vec!["strut".to_owned()], vec![], Some(options), None);
strut
.style
.insert(CssProperty::Height, make_em(body.height + body.depth));
if body.depth > 0.0 {
strut
.style
.insert(CssProperty::VerticalAlign, make_em(-body.depth));
}
body.children.insert(0, strut.into());
HtmlDomNode::DomSpan(body)
}
pub fn build_html(
ctx: &KatexContext,
tree: &[AnyParseNode],
options: &Options,
) -> Result<HtmlDomNode, ParseError> {
let mut tag = None;
let mut tree = tree;
if tree.len() == 1
&& let AnyParseNode::Tag(tag_node) = &tree[0]
{
tag = Some(&tag_node.tag);
tree = &tag_node.body;
}
let mut expression = build_expression(ctx, tree, options, GroupType::Root, (None, None))?;
let eqn_num = if expression.len() == 2
&& let Some(second) = expression.get(1)
&& second.has_class("tag")
{
expression.pop()
} else {
None
};
let mut children = Vec::new();
let mut parts = Vec::new();
let mut i = 0;
while i < expression.len() {
parts.push(expression[i].clone());
if expression[i].has_class("mbin")
|| expression[i].has_class("mrel")
|| expression[i].has_class("allowbreak")
{
let mut nobreak = false;
while i < expression.len() - 1
&& expression[i + 1].has_class("mspace")
&& !expression[i + 1].has_class("newline")
{
i += 1;
parts.push(expression[i].clone());
if expression[i].has_class("nobreak") {
nobreak = true;
}
}
if !nobreak {
children.push(build_html_unbreakable(parts, options));
parts = Vec::new();
}
} else if expression[i].has_class("newline") {
parts.pop();
if !parts.is_empty() {
children.push(build_html_unbreakable(parts, options));
parts = Vec::new();
}
children.push(expression[i].clone());
}
i += 1;
}
if !parts.is_empty() {
children.push(build_html_unbreakable(parts, options));
}
let tag_child_index = if let Some(tag_ref) = tag {
let tag_html = build_expression(ctx, tag_ref, options, GroupType::True, (None, None))?;
let mut unbreakable = build_html_unbreakable(tag_html, options);
if let HtmlDomNode::DomSpan(span) = &mut unbreakable {
span.classes = vec!["tag".to_owned()];
}
children.push(unbreakable);
Some(children.len() - 1)
} else {
if let Some(eqn_num) = eqn_num {
children.push(eqn_num);
}
None
};
let mut span = make_span(vec!["katex-html".to_owned()], children, Some(options), None);
span.attributes
.insert("aria-hidden".to_owned(), "true".to_owned());
if let Some(index) = tag_child_index
&& let Some(tag_child) = span.children.get_mut(index)
&& let HtmlDomNode::DomSpan(tag_span) = tag_child
&& let Some(strut_node) = tag_span.children.first_mut()
&& let HtmlDomNode::DomSpan(strut_span) = strut_node
{
let total_height = span.height + span.depth;
if total_height > 0.0 {
strut_span
.style
.insert(CssProperty::Height, make_em(total_height));
}
if span.depth > 0.0 {
strut_span
.style
.insert(CssProperty::VerticalAlign, make_em(-span.depth));
}
}
Ok(span.into())
}