use crate::build_common::{make_span, push_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::ClassList;
use crate::types::{CssProperty, ParseError, ParseErrorKind};
use crate::units::make_em;
use crate::{KatexContext, build_common};
use alloc::borrow::Cow;
use core::str::FromStr as _;
use phf::phf_set;
use strum::{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, 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<T: Into<ClassList>>(options: &Options, classes: T) -> DomSpan {
let mut combined_classes: ClassList = classes.into();
let base_classes = options.base_sizing_classes();
combined_classes.reserve(1 + base_classes.len());
combined_classes.push("nulldelimiter");
combined_classes.extend(base_classes);
make_span(combined_classes, vec![], None, 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") => 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") => {
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().first()?);
dom_type.ok()
}
fn container_ref_by_path<'a>(
nodes: &'a Vec<HtmlDomNode>,
path: &[usize],
) -> Option<&'a Vec<HtmlDomNode>> {
if path.is_empty() {
return Some(nodes);
}
let (last, rest) = path.split_last()?;
let parent = container_ref_by_path(nodes, rest)?;
let node = parent.get(*last)?;
check_partial_group(node)
}
fn container_mut_by_path<'a>(
nodes: &'a mut Vec<HtmlDomNode>,
path: &[usize],
) -> Option<&'a mut Vec<HtmlDomNode>> {
if path.is_empty() {
return Some(nodes);
}
let (last, rest) = path.split_last()?;
let parent = container_mut_by_path(nodes, rest)?;
let node = parent.get_mut(*last)?;
check_partial_group_mut(node)
}
fn container_len(nodes: &Vec<HtmlDomNode>, path: &[usize]) -> Option<usize> {
container_ref_by_path(nodes, path).map(Vec::len)
}
fn node_ref_by_path<'a>(nodes: &'a Vec<HtmlDomNode>, path: &[usize]) -> Option<&'a HtmlDomNode> {
let (last, rest) = path.split_last()?;
let container = container_ref_by_path(nodes, rest)?;
container.get(*last)
}
fn node_mut_by_path<'a>(
nodes: &'a mut Vec<HtmlDomNode>,
path: &[usize],
) -> Option<&'a mut HtmlDomNode> {
let (last, rest) = path.split_last()?;
let container = container_mut_by_path(nodes, rest)?;
container.get_mut(*last)
}
fn node_mut_in_slice<'a>(
slice: &'a mut [HtmlDomNode],
path: &[usize],
) -> Option<&'a mut HtmlDomNode> {
let (first, rest) = path.split_first()?;
if rest.is_empty() {
slice.get_mut(*first)
} else {
let children = check_partial_group_mut(slice.get_mut(*first)?)?;
node_mut_by_path(children, rest)
}
}
fn node_mut_in_slice_zero_first<'a>(
slice: &'a mut [HtmlDomNode],
path: &[usize],
) -> Option<&'a mut HtmlDomNode> {
let (_, rest) = path.split_first()?;
if rest.is_empty() {
slice.get_mut(0)
} else {
let children = check_partial_group_mut(slice.get_mut(0)?)?;
node_mut_by_path(children, rest)
}
}
fn two_nodes_mut_in_container<'a>(
container: &'a mut [HtmlDomNode],
left_path: &[usize],
right_path: &[usize],
) -> Option<(&'a mut HtmlDomNode, &'a mut HtmlDomNode)> {
debug_assert!(!left_path.is_empty() && !right_path.is_empty());
if left_path[0] == right_path[0] {
let children = check_partial_group_mut(container.get_mut(left_path[0])?)?;
return two_nodes_mut_by_path(children, &left_path[1..], &right_path[1..]);
}
if left_path[0] < right_path[0] {
let split_at = right_path[0];
if split_at > container.len() {
return None;
}
let (left_slice, right_slice) = container.split_at_mut(split_at);
let left_node = node_mut_in_slice(left_slice, left_path)?;
let right_node = node_mut_in_slice_zero_first(right_slice, right_path)?;
Some((left_node, right_node))
} else {
let split_at = left_path[0];
if split_at > container.len() {
return None;
}
let (left_slice, right_slice) = container.split_at_mut(split_at);
let right_node = node_mut_in_slice_zero_first(right_slice, left_path)?;
let left_node = node_mut_in_slice(left_slice, right_path)?;
Some((right_node, left_node))
}
}
fn two_nodes_mut_by_path<'a>(
nodes: &'a mut Vec<HtmlDomNode>,
left: &[usize],
right: &[usize],
) -> Option<(&'a mut HtmlDomNode, &'a mut HtmlDomNode)> {
debug_assert_ne!(left, right, "paths must reference distinct nodes");
let prefix_len = left
.iter()
.zip(right.iter())
.take_while(|(a, b)| a == b)
.count();
let container_path = &left[..prefix_len];
let container = container_mut_by_path(nodes, container_path)?;
two_nodes_mut_in_container(container, &left[prefix_len..], &right[prefix_len..])
}
fn insert_into_container<'a>(
nodes: &mut Vec<HtmlDomNode>,
parent_path: &'a [usize],
index: usize,
node: HtmlDomNode,
) -> (&'a [usize], usize) {
if let Some(container) = container_mut_by_path(nodes, parent_path) {
let insert_index = index.min(container.len());
container.insert(insert_index, node);
(parent_path, insert_index)
} else {
let insert_index = index.min(nodes.len());
nodes.insert(insert_index, node);
(&[], insert_index)
}
}
fn seek_last_nonspace(node: &HtmlDomNode, path: &mut Vec<usize>) -> bool {
if let Some(children) = check_partial_group(node) {
for idx in (0..children.len()).rev() {
path.push(idx);
if seek_last_nonspace(&children[idx], path) {
return true;
}
path.pop();
}
false
} else {
!node.has_class("mspace")
}
}
fn find_prev_nonspace_path(nodes: &Vec<HtmlDomNode>, path: &mut Vec<usize>) -> bool {
while let Some(last) = path.pop() {
if let Some(container) = container_ref_by_path(nodes, path) {
for idx in (0..last).rev() {
path.push(idx);
if seek_last_nonspace(&container[idx], path) {
return true;
}
path.pop();
}
}
}
false
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct NodePath(Vec<usize>);
impl NodePath {
const fn new(path: Vec<usize>) -> Self {
Self(path)
}
#[inline]
fn as_slice(&self) -> &[usize] {
&self.0
}
fn adjust_for_insert(&mut self, parent_path: &[usize], index: usize) {
if self.0.len() < parent_path.len() + 1 {
return;
}
if &self.0[..parent_path.len()] == parent_path {
let slot = parent_path.len();
if self.0[slot] >= index {
self.0[slot] += 1;
}
}
}
}
enum PrevNodeState {
Dummy,
Owned(Box<HtmlDomNode>),
Located(NodePath),
}
struct PrevTracker {
state: PrevNodeState,
}
impl PrevTracker {
const fn new() -> Self {
Self {
state: PrevNodeState::Dummy,
}
}
fn with_prev_and_current<F>(
&mut self,
root: &mut Vec<HtmlDomNode>,
dummy_prev: &mut HtmlDomNode,
current_path: &[usize],
f: &F,
) -> Result<Option<HtmlDomNode>, ParseError>
where
F: Fn(&mut HtmlDomNode, &mut HtmlDomNode) -> Result<Option<HtmlDomNode>, ParseError>,
{
match &mut self.state {
PrevNodeState::Dummy => {
let Some(current) = node_mut_by_path(root, current_path) else {
return Ok(None);
};
f(current, dummy_prev)
}
PrevNodeState::Owned(node) => {
let Some(current) = node_mut_by_path(root, current_path) else {
return Ok(None);
};
f(current, node)
}
PrevNodeState::Located(_) => {
let mut prev_path = current_path.to_vec();
if find_prev_nonspace_path(root, &mut prev_path) {
let prev_slice = prev_path.as_slice();
let nodes = two_nodes_mut_by_path(root, current_path, prev_slice);
self.state = PrevNodeState::Located(NodePath(prev_path));
if let Some((current, prev)) = nodes {
f(current, prev)
} else {
Ok(None)
}
} else {
self.state = PrevNodeState::Dummy;
let Some(current) = node_mut_by_path(root, current_path) else {
return Ok(None);
};
f(current, dummy_prev)
}
}
}
}
fn set_located(&mut self, path: NodePath) {
self.state = PrevNodeState::Located(path);
}
fn set_owned_dummy(&mut self, node: HtmlDomNode) {
self.state = PrevNodeState::Owned(Box::new(node));
}
fn prev_path(&self) -> Option<&[usize]> {
match &self.state {
PrevNodeState::Located(path) => Some(path.as_slice()),
_ => None,
}
}
fn adjust_for_insert(&mut self, parent_path: &[usize], index: usize) {
if let PrevNodeState::Located(path) = &mut self.state {
path.adjust_for_insert(parent_path, index);
}
}
fn adjust_for_insert_self(&mut self, index: usize, slot: usize) {
if let PrevNodeState::Located(path) = &mut self.state
&& path.0[slot] >= index
{
path.0[slot] += 1;
}
}
}
#[derive(Clone, Default)]
struct TraverseFrame {
path: Vec<usize>,
index: usize,
}
fn adjust_frame_paths(frames: &mut [TraverseFrame], parent_path: &[usize], index: usize) {
for frame in frames.iter_mut() {
if frame.path == parent_path {
if frame.index >= index {
frame.index += 1;
}
continue;
}
if frame.path.len() < parent_path.len() + 1 {
continue;
}
if frame.path[..parent_path.len()] == parent_path[..] {
let slot = parent_path.len();
if frame.path[slot] >= index {
frame.path[slot] += 1;
}
}
}
}
fn traverse_non_space_nodes<F>(
ctx: &KatexContext,
root: &mut Vec<HtmlDomNode>,
callback: &F,
prev: &mut PrevTracker,
next: &mut Option<HtmlDomNode>,
dummy_prev: &mut HtmlDomNode,
is_root: bool,
) -> Result<(), ParseError>
where
F: Fn(
&KatexContext,
&mut HtmlDomNode,
&mut HtmlDomNode,
) -> Result<Option<HtmlDomNode>, ParseError>,
{
let mut frames = vec![TraverseFrame::default()];
let appended_next = next.take().is_some_and(|next_node| {
root.push(next_node);
true
});
while let Some(frame_index) = frames.len().checked_sub(1) {
let Some(len) = container_len(root, &frames[frame_index].path) else {
frames.pop();
if frames.is_empty() {
break;
}
continue;
};
if frames[frame_index].index >= len {
frames.pop();
if frames.is_empty() {
break;
}
continue;
}
let current_index = frames[frame_index].index;
let mut current_path = frames[frame_index].path.clone();
current_path.push(current_index);
let container_path_len = current_path.len().saturating_sub(1);
let is_partial_group = {
let Some(node_ref) = node_ref_by_path(root, ¤t_path) else {
frames[frame_index].index = current_index + 1;
continue;
};
check_partial_group(node_ref).is_some()
};
if is_partial_group {
frames[frame_index].index = current_index + 1;
frames.push(TraverseFrame {
path: current_path,
index: 0,
});
continue;
}
let nonspace = {
let Some(node_ref) = node_ref_by_path(root, ¤t_path) else {
frames[frame_index].index = current_index + 1;
continue;
};
!node_ref.has_class("mspace")
};
let mut skip_inserted = 0usize;
if nonspace {
let result =
prev.with_prev_and_current(root, dummy_prev, ¤t_path, &|node, prev_node| {
callback(ctx, node, prev_node)
})?;
if let Some(new_node) = result {
if let Some(prev_path) = prev.prev_path() {
if let Some((last, parent)) = prev_path.split_last() {
let inserted_pos = *last + 1;
let container_path_slice = ¤t_path[..container_path_len];
let (actual_path, actual_index) =
insert_into_container(root, parent, inserted_pos, new_node);
let same_container = actual_path == container_path_slice;
adjust_frame_paths(&mut frames, actual_path, actual_index);
prev.adjust_for_insert_self(actual_index, actual_path.len());
if same_container && actual_index <= current_index {
skip_inserted += 1;
}
} else {
let container_path_slice = ¤t_path[..container_path_len];
let (actual_path, actual_index) =
insert_into_container(root, container_path_slice, 0, new_node);
let same_container = actual_path == container_path_slice;
adjust_frame_paths(&mut frames, actual_path, actual_index);
prev.adjust_for_insert(actual_path, actual_index);
if same_container && actual_index <= current_index {
skip_inserted += 1;
}
}
} else {
let container_path_slice = ¤t_path[..container_path_len];
let (actual_path, actual_index) =
insert_into_container(root, container_path_slice, 0, new_node);
let same_container = actual_path == container_path_slice;
adjust_frame_paths(&mut frames, actual_path, actual_index);
prev.adjust_for_insert(actual_path, actual_index);
if same_container && actual_index <= current_index {
skip_inserted += 1;
}
}
}
}
if skip_inserted > 0
&& let Some(last) = current_path.last_mut()
{
*last += skip_inserted;
}
if !nonspace && is_root {
let is_newline = node_ref_by_path(root, ¤t_path)
.is_some_and(|node_ref| node_ref.has_class("newline"));
if is_newline {
prev.set_owned_dummy(
build_common::make_span("leftmost", vec![], None, None).into(),
);
}
}
if nonspace {
prev.set_located(NodePath::new(current_path));
}
frames[frame_index].index = current_index + 1 + skip_inserted;
}
if appended_next && let Some(next_node) = root.pop() {
*next = Some(next_node);
}
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::with_capacity(expression.len());
for node in expression {
let output = build_group(ctx, node, options, None)?;
if let HtmlDomNode::Fragment(fragment) = output {
for child in fragment.children {
push_combine_chars(&mut groups, child);
}
} else {
push_combine_chars(&mut groups, output);
}
}
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(
surrounding
.0
.as_ref()
.map_or_else(|| "leftmost", |s| s.as_str()),
vec![],
Some(options),
None,
)
.into();
let mut dummy_next = Some(
make_span(
surrounding
.1
.as_ref()
.map_or_else(|| "rightmost", |s| s.as_str()),
vec![],
Some(options),
None,
)
.into(),
);
let is_root = is_real_group.is_root();
let mut prev_tracker = PrevTracker::new();
traverse_non_space_nodes(
ctx,
&mut groups,
&|_ctx: &KatexContext, node: &mut HtmlDomNode, prev: &mut HtmlDomNode| {
let Some(prev_type) = prev.classes().first() else {
return Ok(None);
};
let Some(type_str) = node.classes().first() else {
return Ok(None);
};
if prev_type == "mbin" && BIN_RIGHT_CANCELLER.contains(type_str) {
if let Some(classes) = prev.classes_mut()
&& let Some(class) = classes.get_mut(0)
{
*class = Cow::Borrowed("mord");
}
} else if type_str == "mbin" && BIN_LEFT_CANCELLER.contains(prev_type) {
if let Some(classes) = node.classes_mut()
&& let Some(class) = classes.get_mut(0)
{
*class = Cow::Borrowed("mord");
}
}
Ok(None)
},
&mut prev_tracker,
&mut dummy_next,
&mut dummy_prev,
is_root,
)?;
let mut prev_tracker = PrevTracker::new();
traverse_non_space_nodes(
ctx,
&mut groups,
&move |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 prev_tracker,
&mut dummy_next,
&mut dummy_prev,
is_root,
)?;
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(ParseErrorKind::UnknownGroupType {
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("base", children, Some(options), None);
let mut strut = make_span("strut", 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 iter = expression.into_iter().peekable();
while let Some(node) = iter.next() {
let is_break_candidate =
node.has_class("mbin") || node.has_class("mrel") || node.has_class("allowbreak");
let is_newline = node.has_class("newline");
parts.push(node);
if is_break_candidate {
let mut nobreak = false;
while let Some(next) =
iter.next_if(|n| n.has_class("mspace") && !n.has_class("newline"))
{
if next.has_class("nobreak") {
nobreak = true;
}
parts.push(next);
}
if !nobreak {
let mut chunk = Vec::with_capacity(parts.len());
chunk.append(&mut parts);
children.push(build_html_unbreakable(chunk, options));
}
} else if is_newline {
let newline = parts
.pop()
.ok_or_else(|| ParseError::new(ParseErrorKind::NewlineNodeNotFound))?;
if !parts.is_empty() {
let mut chunk = Vec::with_capacity(parts.len());
chunk.append(&mut parts);
children.push(build_html_unbreakable(chunk, options));
}
children.push(newline);
}
}
if !parts.is_empty() {
let mut chunk = Vec::with_capacity(parts.len());
chunk.append(&mut parts);
children.push(build_html_unbreakable(chunk, 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 = ClassList::Static("tag");
}
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("katex-html", 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())
}