use alloc::borrow::Cow;
use alloc::vec;
use crate::build_common::{
self, VEC_SVG_DATA, VListChild, VListElem, VListParam, make_span, make_v_list, static_svg,
};
use crate::build_mathml::make_text;
use crate::define_function::{FunctionDefSpec, FunctionPropSpec, normalize_argument};
use crate::dom_tree::HtmlDomNode;
use crate::mathml_tree::{MathDomNode, MathNode, MathNodeType};
use crate::options::Options;
use crate::parser::parse_node::{NodeType, ParseNode, ParseNodeAccent, ParseNodeTextOrd};
use crate::stretchy::{math_ml_node, svg_span};
use crate::types::ClassList;
use crate::types::{
ArgType, CssProperty, CssStyle, ErrorLocationProvider, Mode, ParseError, ParseErrorKind,
TokenText,
};
use crate::units::make_em;
use crate::{KatexContext, build_html, build_mathml};
use phf::phf_set;
static NON_STRETCHY_ACCENTS: phf::Set<&'static str> = phf_set! {
"\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
"\\check", "\\hat", "\\vec", "\\dot", "\\mathring"
};
const MATH_ACCENTS: &[&str] = &[
"\\acute",
"\\grave",
"\\ddot",
"\\tilde",
"\\bar",
"\\breve",
"\\check",
"\\hat",
"\\vec",
"\\dot",
"\\mathring",
"\\widecheck",
"\\widehat",
"\\widetilde",
"\\overrightarrow",
"\\overleftarrow",
"\\Overrightarrow",
"\\overleftrightarrow",
"\\overgroup",
"\\overlinesegment",
"\\overleftharpoon",
"\\overrightharpoon",
];
const TEXT_ACCENTS: &[&str] = &[
"\\'",
"\\`",
"\\^",
"\\~",
"\\=",
"\\u",
"\\.",
"\\\"",
"\\c",
"\\r",
"\\H",
"\\v",
"\\textcircled",
];
pub fn define_accent(ctx: &mut KatexContext) {
ctx.define_function(FunctionDefSpec {
node_type: Some(NodeType::Accent),
names: MATH_ACCENTS,
props: FunctionPropSpec {
num_args: 1,
..Default::default()
},
handler: Some(|context, args, _opt_args| {
let base = normalize_argument(&args[0]);
let is_stretchy = !NON_STRETCHY_ACCENTS.contains(context.func_name);
let is_shifty = !is_stretchy
|| context.func_name == "\\widehat"
|| context.func_name == "\\widetilde"
|| context.func_name == "\\widecheck";
Ok(ParseNode::Accent(Box::new(ParseNodeAccent {
mode: context.parser.mode,
loc: context.loc(),
label: context.func_name.to_owned(),
is_stretchy: Some(is_stretchy),
is_shifty: Some(is_shifty),
base: base.clone(),
})))
}),
html_builder: Some(html_builder),
mathml_builder: Some(mathml_builder),
});
ctx.define_function(FunctionDefSpec {
node_type: Some(NodeType::Accent),
names: TEXT_ACCENTS,
props: FunctionPropSpec {
num_args: 1,
allowed_in_text: true,
allowed_in_math: true,
arg_types: Some(vec![ArgType::Primitive]),
..Default::default()
},
handler: Some(|context, args, _opt_args| {
let base = args[0].clone();
let mode = if context.parser.mode == Mode::Math {
context.parser.settings.report_nonstrict(
"mathVsTextAccents",
&format!(
"LaTeX's accent {} works only in text mode",
context.func_name
),
context.token.map(|t| t as &dyn ErrorLocationProvider),
)?;
Mode::Text
} else {
context.parser.mode
};
Ok(ParseNode::Accent(Box::new(ParseNodeAccent {
mode,
loc: context.loc(),
label: context.func_name.to_owned(),
is_stretchy: Some(false),
is_shifty: Some(true),
base,
})))
}),
html_builder: Some(html_builder),
mathml_builder: Some(mathml_builder),
});
}
pub fn html_builder(
node: &ParseNode,
options: &Options,
ctx: &KatexContext,
) -> Result<HtmlDomNode, ParseError> {
let (group, base, supsub_group) = match node {
ParseNode::Accent(accent_node) => (accent_node, &accent_node.base, None),
ParseNode::SupSub(supsub) => {
if let Some(base) = &supsub.base
&& let ParseNode::Accent(accent) = &**base
{
let group = accent;
let base = &group.base;
let mut cloned = supsub.clone();
cloned.base = Some(Box::new(base.clone()));
let grp = ParseNode::SupSub(cloned);
let supsub_group = build_html::build_group(ctx, &grp, options, None)?;
(group, base, Some(supsub_group))
} else {
return Err(ParseError::new(ParseErrorKind::ExpectedSupSubBaseNode {
node: NodeType::Accent,
}));
}
}
_ => {
return Err(ParseError::new(ParseErrorKind::ExpectedNodeOrSupSub {
node: NodeType::Accent,
}));
}
};
let body = build_html::build_group(ctx, base, options, Some(&options.having_cramped_style()))?;
let must_shift = group.is_shifty.unwrap_or(false) && base.is_character_box()?;
let skew = if must_shift {
base_symbol_skew(&body)
} else {
0f64
};
let accent_below = group.label == "\\c";
let mut clearance = if accent_below {
body.height() + body.depth()
} else {
body.height().min(options.font_metrics().x_height)
};
let accent_body = if group.is_stretchy.unwrap_or(false) {
let accent_body = svg_span(&ParseNode::Accent(group.clone()), options)?;
let wrapper_style = (skew > 0.0).then(|| {
let mut style = CssStyle::with_capacity(2);
style.insert(
CssProperty::Width,
format!("calc(100% - {})", make_em(2.0 * skew)),
);
style.insert(CssProperty::MarginLeft, make_em(2.0 * skew));
style
});
let children = vec![
VListElem::builder().elem(body).build().into(),
VListElem::builder()
.elem(accent_body)
.wrapper_classes(ClassList::Static("svg-align"))
.maybe_wrapper_style(wrapper_style)
.build()
.into(),
];
make_v_list(VListParam::FirstBaseline { children }, options)?
} else {
let (accent, width): (HtmlDomNode, f64) = if group.label == "\\vec" {
(static_svg("vec", options)?.into(), VEC_SVG_DATA.0)
} else {
let ord = ParseNode::TextOrd(ParseNodeTextOrd {
mode: group.mode,
loc: group.loc.clone(),
text: TokenText::from(group.label.clone()),
});
let HtmlDomNode::Symbol(mut accent) = build_common::make_ord(ctx, &ord, options)?
else {
return Err(ParseError::new(ParseErrorKind::ExpectedSymbolNode {
context: "accent",
}));
};
accent.italic = 0.0;
if accent_below {
clearance += accent.depth;
}
let width = accent.width;
(accent.into(), width)
};
let mut accent_body = make_span("accent-body", vec![accent], None, None);
let accent_full = group.label == "\\textcircled";
if accent_full {
accent_body.classes.push("accent-full");
clearance = body.height();
}
let mut left = skew;
if !accent_full {
left -= width / 2.0;
}
accent_body.style.insert(CssProperty::Left, make_em(left));
if group.label == "\\textcircled" {
accent_body
.style
.insert(CssProperty::Top, ".2em".to_owned());
}
let children = vec![
VListElem::builder().elem(body).build().into(),
VListChild::Kern(build_common::VListKern { size: -clearance }),
VListElem::builder().elem(accent_body.into()).build().into(),
];
make_v_list(VListParam::FirstBaseline { children }, options)?
};
let accent_wrap: HtmlDomNode = make_span(
ClassList::Const(&["mord", "katex-accent"]),
vec![accent_body.into()],
Some(options),
None,
)
.into();
if let Some(mut supsub_group) = supsub_group {
let accent_wrap_height = accent_wrap.height();
if let HtmlDomNode::DomSpan(span) = &mut supsub_group {
if !span.children.is_empty() {
span.children[0] = accent_wrap;
}
span.height = span.height.max(accent_wrap_height);
if !span.classes.is_empty()
&& let Some(class) = span.classes.get_mut(0)
{
*class = Cow::Borrowed("mord");
}
}
Ok(supsub_group)
} else {
Ok(accent_wrap)
}
}
fn mathml_builder(
node: &ParseNode,
options: &Options,
ctx: &KatexContext,
) -> Result<MathDomNode, ParseError> {
let ParseNode::Accent(group) = node else {
return Err(ParseError::new(ParseErrorKind::ExpectedNode {
node: NodeType::Accent,
}));
};
let accent_node = if group.is_stretchy.unwrap_or(false) {
math_ml_node(&group.label)
} else {
let text_node = make_text(&group.label, group.mode, None, &ctx.symbols);
MathNode::builder()
.node_type(MathNodeType::Mo)
.children(vec![text_node.into()])
.build()
};
let base_group = build_mathml::build_group(ctx, &group.base, options)?;
let mut mover = MathNode::builder()
.node_type(MathNodeType::Mover)
.children(vec![base_group, MathDomNode::Math(accent_node)])
.build();
mover
.attributes
.insert("accent".to_owned(), "true".to_owned());
Ok(MathDomNode::Math(mover))
}
fn base_symbol_skew(node: &HtmlDomNode) -> f64 {
let children = match node {
HtmlDomNode::Symbol(symbol) => return symbol.skew,
HtmlDomNode::DomSpan(span) => &span.children,
HtmlDomNode::Anchor(anchor) => &anchor.children,
HtmlDomNode::Fragment(fragment) => &fragment.children,
_ => return 0.0,
};
if let [child] = children.as_slice() {
base_symbol_skew(child)
} else {
0.0
}
}