use crate::build_common::make_span;
use crate::build_html::DomType;
use crate::build_mathml::{make_row, make_text};
use crate::define_function::{FunctionContext, FunctionDefSpec, FunctionPropSpec};
use crate::delimiter::{left_right_delim, size_to_max_height, sized_delim};
use crate::dom_tree::HtmlDomNode;
use crate::macros::MacroContextInterface as _;
use crate::mathml_tree::{MathDomNode, MathNode, MathNodeType};
use crate::options::Options;
use crate::parser::parse_node::{
AnyParseNode, NodeType, ParseNode, ParseNodeDelimsizing, ParseNodeLeftRight,
ParseNodeLeftRightRight, ParseNodeMiddle, check_symbol_node_type,
};
use crate::types::{ArgType, ParseError};
use crate::units::make_em;
use crate::{KatexContext, build_html, build_mathml};
use core::slice;
use phf::phf_map;
const DELIMITER_SIZES: phf::Map<&'static str, (&'static str, u8)> = phf_map! {
"\\bigl" => ("mopen", 1),
"\\Bigl" => ("mopen", 2),
"\\biggl" => ("mopen", 3),
"\\Biggl" => ("mopen", 4),
"\\bigr" => ("mclose", 1),
"\\Bigr" => ("mclose", 2),
"\\biggr" => ("mclose", 3),
"\\Biggr" => ("mclose", 4),
"\\bigm" => ("mrel", 1),
"\\Bigm" => ("mrel", 2),
"\\biggm" => ("mrel", 3),
"\\Biggm" => ("mrel", 4),
"\\big" => ("mord", 1),
"\\Big" => ("mord", 2),
"\\bigg" => ("mord", 3),
"\\Bigg" => ("mord", 4),
};
static DELIMITERS: phf::Set<&'static str> = phf::phf_set! {
"(", "\\lparen", ")", "\\rparen",
"[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\u{230a}", "\u{230b}",
"\\lceil", "\\rceil", "\u{2308}", "\u{2309}",
"<", ">", "\\langle", "\u{27e8}", "\\rangle", "\u{27e9}", "\\lt", "\\gt",
"\\lvert", "\\rvert", "\\lVert", "\\rVert",
"\\lgroup", "\\rgroup", "\u{27ee}", "\u{27ef}",
"\\lmoustache", "\\rmoustache", "\u{23b0}", "\u{23b1}",
"/", "\\backslash",
"|", "\\vert", "\\|", "\\Vert",
"\\uparrow", "\\Uparrow",
"\\downarrow", "\\Downarrow",
"\\updownarrow", "\\Updownarrow",
".",
};
fn check_delimiter(
delim: Option<&AnyParseNode>,
context: &FunctionContext,
) -> Result<String, ParseError> {
let sym_delim = check_symbol_node_type(delim);
if let Some(node_type) = sym_delim {
if let Some(node) = delim
&& let Some(text) = node.text()
&& DELIMITERS.contains(text)
{
return Ok(text.to_owned());
}
return Err(ParseError::new(format!(
"Invalid delimiter '{}' after '{}'",
node_type, context.func_name
)));
}
Err(ParseError::new(format!(
"Invalid delimiter type after '{}'",
context.func_name
)))
}
pub fn define_delimsizing(ctx: &mut KatexContext) {
let names: Vec<&str> = DELIMITER_SIZES.keys().copied().collect();
ctx.define_function(FunctionDefSpec {
node_type: Some(NodeType::Delimsizing),
names: &names,
props: FunctionPropSpec {
num_args: 1,
arg_types: Some(vec![ArgType::Primitive]),
..Default::default()
},
handler: Some(|context: FunctionContext, args, _opt_args| {
let delim_text = check_delimiter(args.first(), &context)?;
let (mclass, size) = DELIMITER_SIZES[&context.func_name];
Ok(ParseNode::Delimsizing(ParseNodeDelimsizing {
mode: context.parser.mode,
loc: context.loc(),
size,
mclass: mclass.to_owned(),
delim: delim_text,
}))
}),
html_builder: Some(delimsizing_html_builder),
mathml_builder: Some(delimsizing_mathml_builder),
});
}
pub fn define_leftright(ctx: &mut KatexContext) {
ctx.define_function(FunctionDefSpec {
node_type: Some(NodeType::LeftRight),
names: &["\\left"],
props: FunctionPropSpec {
num_args: 1,
primitive: true,
..Default::default()
},
handler: Some(|context: FunctionContext, args, _opt_args| {
let delim_text = check_delimiter(args.first(), &context)?;
let loc = context.loc();
let parser = context.parser;
parser.leftright_depth += 1.0;
let body = parser.parse_expression(false, None)?;
parser.leftright_depth -= 1.0;
parser.expect("\\right", false)?;
let Some(AnyParseNode::LeftRightRight(right_node)) =
parser.parse_function(None, None)?
else {
return Err(ParseError::new("Expected \\right after \\left"));
};
Ok(ParseNode::LeftRight(ParseNodeLeftRight {
mode: parser.mode,
loc,
body,
left: delim_text,
right: right_node.delim.clone(),
right_color: right_node.color,
}))
}),
html_builder: Some(leftright_html_builder),
mathml_builder: Some(leftright_mathml_builder),
});
ctx.define_function(FunctionDefSpec {
node_type: Some(NodeType::LeftRightRight),
names: &["\\right"],
props: FunctionPropSpec {
num_args: 1,
primitive: true,
..Default::default()
},
handler: Some(|context: FunctionContext, args, _opt_args| {
let delim_text = check_delimiter(args.first(), &context)?;
let color = context.parser.gullet.macros_mut().get("\\current@color");
let color_str = if let Some(color_val) = color {
if let Some(s) = color_val.as_str() {
Some(s.to_owned())
} else {
return Err(ParseError::new(
"\\current@color set to non-string in \\right",
));
}
} else {
None
};
Ok(ParseNode::LeftRightRight(ParseNodeLeftRightRight {
mode: context.parser.mode,
loc: context.loc(),
delim: delim_text,
color: color_str,
}))
}),
html_builder: None,
mathml_builder: None,
});
}
pub fn define_middle(ctx: &mut KatexContext) {
ctx.define_function(FunctionDefSpec {
node_type: Some(NodeType::Middle),
names: &["\\middle"],
props: FunctionPropSpec {
num_args: 1,
primitive: true,
..Default::default()
},
handler: Some(|context: FunctionContext, args, _opt_args| {
if context.parser.leftright_depth == 0.0 {
return Err(ParseError::new("\\middle without preceding \\left"));
}
let delim_text = check_delimiter(args.first(), &context)?;
Ok(ParseNode::Middle(ParseNodeMiddle {
mode: context.parser.mode,
loc: context.loc(),
delim: delim_text,
}))
}),
html_builder: Some(middle_html_builder),
mathml_builder: Some(middle_mathml_builder),
});
}
fn delimsizing_html_builder(
node: &ParseNode,
options: &Options,
ctx: &KatexContext,
) -> Result<HtmlDomNode, ParseError> {
let ParseNode::Delimsizing(group) = node else {
return Err(ParseError::new("Expected Delimsizing node"));
};
if group.delim == "." {
return Ok(make_span(vec![group.mclass.clone()], vec![], None, None).into());
}
Ok(sized_delim(
ctx,
&group.delim,
usize::from(group.size),
options,
group.mode,
slice::from_ref(&group.mclass),
)?
.into())
}
fn delimsizing_mathml_builder(
node: &ParseNode,
_options: &Options,
ctx: &KatexContext,
) -> Result<MathDomNode, ParseError> {
let ParseNode::Delimsizing(group) = node else {
return Err(ParseError::new("Expected Delimsizing node"));
};
let children = if group.delim == "." {
vec![]
} else {
vec![make_text(&group.delim, group.mode, None, &ctx.symbols).into()]
};
let mut node = MathNode::builder()
.node_type(MathNodeType::Mo)
.children(children)
.build();
if group.mclass == "mopen" || group.mclass == "mclose" {
node.set_attribute("fence", "true");
} else {
node.set_attribute("fence", "false");
}
node.set_attribute("stretchy", "true");
let size_em = make_em(size_to_max_height(group.size));
node.set_attribute("minsize", size_em.clone());
node.set_attribute("maxsize", size_em);
Ok(MathDomNode::Math(node))
}
fn leftright_html_builder(
node: &ParseNode,
options: &Options,
ctx: &KatexContext,
) -> Result<HtmlDomNode, ParseError> {
let ParseNode::LeftRight(group) = node else {
return Err(ParseError::new("Expected LeftRight node"));
};
let inner = build_html::build_expression(
ctx,
&group.body,
options,
build_html::GroupType::True,
(Some(DomType::Mopen), Some(DomType::Mclose)),
)?;
let mut inner_height: f64 = 0.0;
let mut inner_depth: f64 = 0.0;
let mut had_middle = false;
for item in &inner {
if let HtmlDomNode::DomSpan(item) = item
&& item.is_middle.is_some()
{
had_middle = true;
} else {
inner_height = inner_height.max(item.height());
inner_depth = inner_depth.max(item.depth());
}
}
let scale = options.size_multiplier;
inner_height *= scale;
inner_depth *= scale;
let mut inner_modified = inner;
let left_delim = if group.left == "." {
build_html::make_null_delimiter(options, &["mopen".to_owned()])
} else {
left_right_delim(
ctx,
&group.left,
inner_height,
inner_depth,
options,
group.mode,
&["mopen".to_owned()],
)?
};
inner_modified.insert(0, left_delim.into());
if had_middle {
for i in 1..inner_modified.len() {
if let HtmlDomNode::DomSpan(inner_modified) = &mut inner_modified[i]
&& let Some((delim, options)) = &inner_modified.is_middle
{
*inner_modified = left_right_delim(
ctx,
delim,
inner_height,
inner_depth,
options,
group.mode,
&[],
)?;
}
}
}
let right_delim = if group.right == "." {
build_html::make_null_delimiter(options, &["mclose".to_owned()])
} else {
let mut right_options = options;
let maybe_option;
if let Some(color) = &group.right_color {
maybe_option = options.with_color(color.clone());
right_options = &maybe_option;
}
left_right_delim(
ctx,
&group.right,
inner_height,
inner_depth,
right_options,
group.mode,
&["mclose".to_owned()],
)?
};
inner_modified.push(right_delim.into());
Ok(make_span(
vec!["minner".to_owned()],
inner_modified,
Some(options),
None,
)
.into())
}
fn leftright_mathml_builder(
node: &ParseNode,
options: &Options,
ctx: &KatexContext,
) -> Result<MathDomNode, ParseError> {
let ParseNode::LeftRight(group) = node else {
return Err(ParseError::new("Expected LeftRight node"));
};
let inner = build_mathml::build_expression(ctx, &group.body, options, None)?;
let mut children = vec![];
if group.left != "." {
let mut left_node = MathNode::builder()
.node_type(MathNodeType::Mo)
.children(vec![
make_text(&group.left, group.mode, None, &ctx.symbols).into(),
])
.build();
left_node.set_attribute("fence", "true");
children.push(MathDomNode::Math(left_node));
}
children.extend(inner);
if group.right != "." {
let mut right_node = MathNode::builder()
.node_type(MathNodeType::Mo)
.children(vec![
make_text(&group.right, group.mode, None, &ctx.symbols).into(),
])
.build();
right_node.set_attribute("fence", "true");
if let Some(color) = &group.right_color {
right_node.set_attribute("mathcolor", color);
}
children.push(MathDomNode::Math(right_node));
}
Ok(make_row(&children))
}
fn middle_html_builder(
node: &ParseNode,
options: &Options,
ctx: &KatexContext,
) -> Result<HtmlDomNode, ParseError> {
let ParseNode::Middle(group) = node else {
return Err(ParseError::new("Expected Middle node"));
};
let mut middle_delim = if group.delim == "." {
build_html::make_null_delimiter(options, &[])
} else {
sized_delim(ctx, &group.delim, 1, options, group.mode, &[])?
};
middle_delim.is_middle = Some((group.delim.clone(), options.clone()));
Ok(middle_delim.into())
}
fn middle_mathml_builder(
node: &ParseNode,
_options: &Options,
ctx: &KatexContext,
) -> Result<MathDomNode, ParseError> {
let ParseNode::Middle(group) = node else {
return Err(ParseError::new("Expected Middle node"));
};
let text = if group.delim == "\\vert" || group.delim == "|" {
"|"
} else {
&group.delim
};
let text_node = make_text(text, group.mode, None, &ctx.symbols);
let mut middle_node = MathNode::builder()
.node_type(MathNodeType::Mo)
.children(vec![text_node.into()])
.build();
middle_node.set_attribute("fence", "true");
middle_node.set_attribute("lspace", "0.05em");
middle_node.set_attribute("rspace", "0.05em");
Ok(MathDomNode::Math(middle_node))
}